create.js 10 KB

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