create.cjs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. 'use strict';
  2. const List = require('../utils/List.cjs');
  3. const SyntaxError = require('./SyntaxError.cjs');
  4. const index = require('../tokenizer/index.cjs');
  5. const sequence = require('./sequence.cjs');
  6. const OffsetToLocation = require('../tokenizer/OffsetToLocation.cjs');
  7. const TokenStream = require('../tokenizer/TokenStream.cjs');
  8. const utils = require('../tokenizer/utils.cjs');
  9. const types = require('../tokenizer/types.cjs');
  10. const names = require('../tokenizer/names.cjs');
  11. const NOOP = () => {};
  12. const EXCLAMATIONMARK = 0x0021; // U+0021 EXCLAMATION MARK (!)
  13. const NUMBERSIGN = 0x0023; // U+0023 NUMBER SIGN (#)
  14. const SEMICOLON = 0x003B; // U+003B SEMICOLON (;)
  15. const LEFTCURLYBRACKET = 0x007B; // U+007B LEFT CURLY BRACKET ({)
  16. const NULL = 0;
  17. function createParseContext(name) {
  18. return function() {
  19. return this[name]();
  20. };
  21. }
  22. function fetchParseValues(dict) {
  23. const result = Object.create(null);
  24. for (const name of Object.keys(dict)) {
  25. const item = dict[name];
  26. const fn = item.parse || item;
  27. if (fn) {
  28. result[name] = fn;
  29. }
  30. }
  31. return result;
  32. }
  33. function processConfig(config) {
  34. const parseConfig = {
  35. context: Object.create(null),
  36. features: Object.assign(Object.create(null), config.features),
  37. scope: Object.assign(Object.create(null), config.scope),
  38. atrule: fetchParseValues(config.atrule),
  39. pseudo: fetchParseValues(config.pseudo),
  40. node: fetchParseValues(config.node)
  41. };
  42. for (const [name, context] of Object.entries(config.parseContext)) {
  43. switch (typeof context) {
  44. case 'function':
  45. parseConfig.context[name] = context;
  46. break;
  47. case 'string':
  48. parseConfig.context[name] = createParseContext(context);
  49. break;
  50. }
  51. }
  52. return {
  53. config: parseConfig,
  54. ...parseConfig,
  55. ...parseConfig.node
  56. };
  57. }
  58. function createParser(config) {
  59. let source = '';
  60. let filename = '<unknown>';
  61. let needPositions = false;
  62. let onParseError = NOOP;
  63. let onParseErrorThrow = false;
  64. const locationMap = new OffsetToLocation.OffsetToLocation();
  65. const parser = Object.assign(new TokenStream.TokenStream(), processConfig(config || {}), {
  66. parseAtrulePrelude: true,
  67. parseRulePrelude: true,
  68. parseValue: true,
  69. parseCustomProperty: false,
  70. readSequence: sequence.readSequence,
  71. consumeUntilBalanceEnd: () => 0,
  72. consumeUntilLeftCurlyBracket(code) {
  73. return code === LEFTCURLYBRACKET ? 1 : 0;
  74. },
  75. consumeUntilLeftCurlyBracketOrSemicolon(code) {
  76. return code === LEFTCURLYBRACKET || code === SEMICOLON ? 1 : 0;
  77. },
  78. consumeUntilExclamationMarkOrSemicolon(code) {
  79. return code === EXCLAMATIONMARK || code === SEMICOLON ? 1 : 0;
  80. },
  81. consumeUntilSemicolonIncluded(code) {
  82. return code === SEMICOLON ? 2 : 0;
  83. },
  84. createList() {
  85. return new List.List();
  86. },
  87. createSingleNodeList(node) {
  88. return new List.List().appendData(node);
  89. },
  90. getFirstListNode(list) {
  91. return list && list.first;
  92. },
  93. getLastListNode(list) {
  94. return list && list.last;
  95. },
  96. parseWithFallback(consumer, fallback) {
  97. const startIndex = this.tokenIndex;
  98. try {
  99. return consumer.call(this);
  100. } catch (e) {
  101. if (onParseErrorThrow) {
  102. throw e;
  103. }
  104. this.skip(startIndex - this.tokenIndex);
  105. const fallbackNode = fallback.call(this);
  106. onParseErrorThrow = true;
  107. onParseError(e, fallbackNode);
  108. onParseErrorThrow = false;
  109. return fallbackNode;
  110. }
  111. },
  112. lookupNonWSType(offset) {
  113. let type;
  114. do {
  115. type = this.lookupType(offset++);
  116. if (type !== types.WhiteSpace && type !== types.Comment) {
  117. return type;
  118. }
  119. } while (type !== NULL);
  120. return NULL;
  121. },
  122. charCodeAt(offset) {
  123. return offset >= 0 && offset < source.length ? source.charCodeAt(offset) : 0;
  124. },
  125. substring(offsetStart, offsetEnd) {
  126. return source.substring(offsetStart, offsetEnd);
  127. },
  128. substrToCursor(start) {
  129. return this.source.substring(start, this.tokenStart);
  130. },
  131. cmpChar(offset, charCode) {
  132. return utils.cmpChar(source, offset, charCode);
  133. },
  134. cmpStr(offsetStart, offsetEnd, str) {
  135. return utils.cmpStr(source, offsetStart, offsetEnd, str);
  136. },
  137. consume(tokenType) {
  138. const start = this.tokenStart;
  139. this.eat(tokenType);
  140. return this.substrToCursor(start);
  141. },
  142. consumeFunctionName() {
  143. const name = source.substring(this.tokenStart, this.tokenEnd - 1);
  144. this.eat(types.Function);
  145. return name;
  146. },
  147. consumeNumber(type) {
  148. const number = source.substring(this.tokenStart, utils.consumeNumber(source, this.tokenStart));
  149. this.eat(type);
  150. return number;
  151. },
  152. eat(tokenType) {
  153. if (this.tokenType !== tokenType) {
  154. const tokenName = names[tokenType].slice(0, -6).replace(/-/g, ' ').replace(/^./, m => m.toUpperCase());
  155. let message = `${/[[\](){}]/.test(tokenName) ? `"${tokenName}"` : tokenName} is expected`;
  156. let offset = this.tokenStart;
  157. // tweak message and offset
  158. switch (tokenType) {
  159. case types.Ident:
  160. // when identifier is expected but there is a function or url
  161. if (this.tokenType === types.Function || this.tokenType === types.Url) {
  162. offset = this.tokenEnd - 1;
  163. message = 'Identifier is expected but function found';
  164. } else {
  165. message = 'Identifier is expected';
  166. }
  167. break;
  168. case types.Hash:
  169. if (this.isDelim(NUMBERSIGN)) {
  170. this.next();
  171. offset++;
  172. message = 'Name is expected';
  173. }
  174. break;
  175. case types.Percentage:
  176. if (this.tokenType === types.Number) {
  177. offset = this.tokenEnd;
  178. message = 'Percent sign is expected';
  179. }
  180. break;
  181. }
  182. this.error(message, offset);
  183. }
  184. this.next();
  185. },
  186. eatIdent(name) {
  187. if (this.tokenType !== types.Ident || this.lookupValue(0, name) === false) {
  188. this.error(`Identifier "${name}" is expected`);
  189. }
  190. this.next();
  191. },
  192. eatDelim(code) {
  193. if (!this.isDelim(code)) {
  194. this.error(`Delim "${String.fromCharCode(code)}" is expected`);
  195. }
  196. this.next();
  197. },
  198. getLocation(start, end) {
  199. if (needPositions) {
  200. return locationMap.getLocationRange(
  201. start,
  202. end,
  203. filename
  204. );
  205. }
  206. return null;
  207. },
  208. getLocationFromList(list) {
  209. if (needPositions) {
  210. const head = this.getFirstListNode(list);
  211. const tail = this.getLastListNode(list);
  212. return locationMap.getLocationRange(
  213. head !== null ? head.loc.start.offset - locationMap.startOffset : this.tokenStart,
  214. tail !== null ? tail.loc.end.offset - locationMap.startOffset : this.tokenStart,
  215. filename
  216. );
  217. }
  218. return null;
  219. },
  220. error(message, offset) {
  221. const location = typeof offset !== 'undefined' && offset < source.length
  222. ? locationMap.getLocation(offset)
  223. : this.eof
  224. ? locationMap.getLocation(utils.findWhiteSpaceStart(source, source.length - 1))
  225. : locationMap.getLocation(this.tokenStart);
  226. throw new SyntaxError.SyntaxError(
  227. message || 'Unexpected input',
  228. source,
  229. location.offset,
  230. location.line,
  231. location.column,
  232. locationMap.startLine,
  233. locationMap.startColumn
  234. );
  235. }
  236. });
  237. const parse = function(source_, options) {
  238. source = source_;
  239. options = options || {};
  240. parser.setSource(source, index.tokenize);
  241. locationMap.setSource(
  242. source,
  243. options.offset,
  244. options.line,
  245. options.column
  246. );
  247. filename = options.filename || '<unknown>';
  248. needPositions = Boolean(options.positions);
  249. onParseError = typeof options.onParseError === 'function' ? options.onParseError : NOOP;
  250. onParseErrorThrow = false;
  251. parser.parseAtrulePrelude = 'parseAtrulePrelude' in options ? Boolean(options.parseAtrulePrelude) : true;
  252. parser.parseRulePrelude = 'parseRulePrelude' in options ? Boolean(options.parseRulePrelude) : true;
  253. parser.parseValue = 'parseValue' in options ? Boolean(options.parseValue) : true;
  254. parser.parseCustomProperty = 'parseCustomProperty' in options ? Boolean(options.parseCustomProperty) : false;
  255. const { context = 'default', onComment } = options;
  256. if (context in parser.context === false) {
  257. throw new Error('Unknown context `' + context + '`');
  258. }
  259. if (typeof onComment === 'function') {
  260. parser.forEachToken((type, start, end) => {
  261. if (type === types.Comment) {
  262. const loc = parser.getLocation(start, end);
  263. const value = utils.cmpStr(source, end - 2, end, '*/')
  264. ? source.slice(start + 2, end - 2)
  265. : source.slice(start + 2, end);
  266. onComment(value, loc);
  267. }
  268. });
  269. }
  270. const ast = parser.context[context].call(parser, options);
  271. if (!parser.eof) {
  272. parser.error();
  273. }
  274. return ast;
  275. };
  276. return Object.assign(parse, {
  277. SyntaxError: SyntaxError.SyntaxError,
  278. config: parser.config
  279. });
  280. }
  281. exports.createParser = createParser;