create.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. var OffsetToLocation = require('../common/OffsetToLocation');
  2. var SyntaxError = require('../common/SyntaxError');
  3. var TokenStream = require('../common/TokenStream');
  4. var List = require('../common/List');
  5. var tokenize = require('../tokenizer');
  6. var constants = require('../tokenizer/const');
  7. var { findWhiteSpaceStart, cmpStr } = require('../tokenizer/utils');
  8. var sequence = require('./sequence');
  9. var noop = function() {};
  10. var TYPE = constants.TYPE;
  11. var NAME = constants.NAME;
  12. var WHITESPACE = TYPE.WhiteSpace;
  13. var COMMENT = TYPE.Comment;
  14. var IDENT = TYPE.Ident;
  15. var FUNCTION = TYPE.Function;
  16. var URL = TYPE.Url;
  17. var HASH = TYPE.Hash;
  18. var PERCENTAGE = TYPE.Percentage;
  19. var NUMBER = TYPE.Number;
  20. var NUMBERSIGN = 0x0023; // U+0023 NUMBER SIGN (#)
  21. var NULL = 0;
  22. function createParseContext(name) {
  23. return function() {
  24. return this[name]();
  25. };
  26. }
  27. function processConfig(config) {
  28. var parserConfig = {
  29. context: {},
  30. scope: {},
  31. atrule: {},
  32. pseudo: {}
  33. };
  34. if (config.parseContext) {
  35. for (var name in config.parseContext) {
  36. switch (typeof config.parseContext[name]) {
  37. case 'function':
  38. parserConfig.context[name] = config.parseContext[name];
  39. break;
  40. case 'string':
  41. parserConfig.context[name] = createParseContext(config.parseContext[name]);
  42. break;
  43. }
  44. }
  45. }
  46. if (config.scope) {
  47. for (var name in config.scope) {
  48. parserConfig.scope[name] = config.scope[name];
  49. }
  50. }
  51. if (config.atrule) {
  52. for (var name in config.atrule) {
  53. var atrule = config.atrule[name];
  54. if (atrule.parse) {
  55. parserConfig.atrule[name] = atrule.parse;
  56. }
  57. }
  58. }
  59. if (config.pseudo) {
  60. for (var name in config.pseudo) {
  61. var pseudo = config.pseudo[name];
  62. if (pseudo.parse) {
  63. parserConfig.pseudo[name] = pseudo.parse;
  64. }
  65. }
  66. }
  67. if (config.node) {
  68. for (var name in config.node) {
  69. parserConfig[name] = config.node[name].parse;
  70. }
  71. }
  72. return parserConfig;
  73. }
  74. module.exports = function createParser(config) {
  75. var parser = {
  76. scanner: new TokenStream(),
  77. locationMap: new OffsetToLocation(),
  78. filename: '<unknown>',
  79. needPositions: false,
  80. onParseError: noop,
  81. onParseErrorThrow: false,
  82. parseAtrulePrelude: true,
  83. parseRulePrelude: true,
  84. parseValue: true,
  85. parseCustomProperty: false,
  86. readSequence: sequence,
  87. createList: function() {
  88. return new List();
  89. },
  90. createSingleNodeList: function(node) {
  91. return new List().appendData(node);
  92. },
  93. getFirstListNode: function(list) {
  94. return list && list.first();
  95. },
  96. getLastListNode: function(list) {
  97. return list.last();
  98. },
  99. parseWithFallback: function(consumer, fallback) {
  100. var startToken = this.scanner.tokenIndex;
  101. try {
  102. return consumer.call(this);
  103. } catch (e) {
  104. if (this.onParseErrorThrow) {
  105. throw e;
  106. }
  107. var fallbackNode = fallback.call(this, startToken);
  108. this.onParseErrorThrow = true;
  109. this.onParseError(e, fallbackNode);
  110. this.onParseErrorThrow = false;
  111. return fallbackNode;
  112. }
  113. },
  114. lookupNonWSType: function(offset) {
  115. do {
  116. var type = this.scanner.lookupType(offset++);
  117. if (type !== WHITESPACE) {
  118. return type;
  119. }
  120. } while (type !== NULL);
  121. return NULL;
  122. },
  123. eat: function(tokenType) {
  124. if (this.scanner.tokenType !== tokenType) {
  125. var offset = this.scanner.tokenStart;
  126. var message = NAME[tokenType] + ' is expected';
  127. // tweak message and offset
  128. switch (tokenType) {
  129. case IDENT:
  130. // when identifier is expected but there is a function or url
  131. if (this.scanner.tokenType === FUNCTION || this.scanner.tokenType === URL) {
  132. offset = this.scanner.tokenEnd - 1;
  133. message = 'Identifier is expected but function found';
  134. } else {
  135. message = 'Identifier is expected';
  136. }
  137. break;
  138. case HASH:
  139. if (this.scanner.isDelim(NUMBERSIGN)) {
  140. this.scanner.next();
  141. offset++;
  142. message = 'Name is expected';
  143. }
  144. break;
  145. case PERCENTAGE:
  146. if (this.scanner.tokenType === NUMBER) {
  147. offset = this.scanner.tokenEnd;
  148. message = 'Percent sign is expected';
  149. }
  150. break;
  151. default:
  152. // when test type is part of another token show error for current position + 1
  153. // e.g. eat(HYPHENMINUS) will fail on "-foo", but pointing on "-" is odd
  154. if (this.scanner.source.charCodeAt(this.scanner.tokenStart) === tokenType) {
  155. offset = offset + 1;
  156. }
  157. }
  158. this.error(message, offset);
  159. }
  160. this.scanner.next();
  161. },
  162. consume: function(tokenType) {
  163. var value = this.scanner.getTokenValue();
  164. this.eat(tokenType);
  165. return value;
  166. },
  167. consumeFunctionName: function() {
  168. var name = this.scanner.source.substring(this.scanner.tokenStart, this.scanner.tokenEnd - 1);
  169. this.eat(FUNCTION);
  170. return name;
  171. },
  172. getLocation: function(start, end) {
  173. if (this.needPositions) {
  174. return this.locationMap.getLocationRange(
  175. start,
  176. end,
  177. this.filename
  178. );
  179. }
  180. return null;
  181. },
  182. getLocationFromList: function(list) {
  183. if (this.needPositions) {
  184. var head = this.getFirstListNode(list);
  185. var tail = this.getLastListNode(list);
  186. return this.locationMap.getLocationRange(
  187. head !== null ? head.loc.start.offset - this.locationMap.startOffset : this.scanner.tokenStart,
  188. tail !== null ? tail.loc.end.offset - this.locationMap.startOffset : this.scanner.tokenStart,
  189. this.filename
  190. );
  191. }
  192. return null;
  193. },
  194. error: function(message, offset) {
  195. var location = typeof offset !== 'undefined' && offset < this.scanner.source.length
  196. ? this.locationMap.getLocation(offset)
  197. : this.scanner.eof
  198. ? this.locationMap.getLocation(findWhiteSpaceStart(this.scanner.source, this.scanner.source.length - 1))
  199. : this.locationMap.getLocation(this.scanner.tokenStart);
  200. throw new SyntaxError(
  201. message || 'Unexpected input',
  202. this.scanner.source,
  203. location.offset,
  204. location.line,
  205. location.column
  206. );
  207. }
  208. };
  209. config = processConfig(config || {});
  210. for (var key in config) {
  211. parser[key] = config[key];
  212. }
  213. return function(source, options) {
  214. options = options || {};
  215. var context = options.context || 'default';
  216. var onComment = options.onComment;
  217. var ast;
  218. tokenize(source, parser.scanner);
  219. parser.locationMap.setSource(
  220. source,
  221. options.offset,
  222. options.line,
  223. options.column
  224. );
  225. parser.filename = options.filename || '<unknown>';
  226. parser.needPositions = Boolean(options.positions);
  227. parser.onParseError = typeof options.onParseError === 'function' ? options.onParseError : noop;
  228. parser.onParseErrorThrow = false;
  229. parser.parseAtrulePrelude = 'parseAtrulePrelude' in options ? Boolean(options.parseAtrulePrelude) : true;
  230. parser.parseRulePrelude = 'parseRulePrelude' in options ? Boolean(options.parseRulePrelude) : true;
  231. parser.parseValue = 'parseValue' in options ? Boolean(options.parseValue) : true;
  232. parser.parseCustomProperty = 'parseCustomProperty' in options ? Boolean(options.parseCustomProperty) : false;
  233. if (!parser.context.hasOwnProperty(context)) {
  234. throw new Error('Unknown context `' + context + '`');
  235. }
  236. if (typeof onComment === 'function') {
  237. parser.scanner.forEachToken((type, start, end) => {
  238. if (type === COMMENT) {
  239. const loc = parser.getLocation(start, end);
  240. const value = cmpStr(source, end - 2, end, '*/')
  241. ? source.slice(start + 2, end - 2)
  242. : source.slice(start + 2, end);
  243. onComment(value, loc);
  244. }
  245. });
  246. }
  247. ast = parser.context[context].call(parser, options);
  248. if (!parser.scanner.eof) {
  249. parser.error();
  250. }
  251. return ast;
  252. };
  253. };