create.cjs 13 KB

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