You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

289 lines
9.0 KiB

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