create.js 12 KB

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