| var OffsetToLocation = require('../common/OffsetToLocation'); |
| var SyntaxError = require('../common/SyntaxError'); |
| var TokenStream = require('../common/TokenStream'); |
| var List = require('../common/List'); |
| var tokenize = require('../tokenizer'); |
| var constants = require('../tokenizer/const'); |
| var { findWhiteSpaceStart, cmpStr } = require('../tokenizer/utils'); |
| var sequence = require('./sequence'); |
| var noop = function() {}; |
| |
| var TYPE = constants.TYPE; |
| var NAME = constants.NAME; |
| var WHITESPACE = TYPE.WhiteSpace; |
| var COMMENT = TYPE.Comment; |
| var IDENT = TYPE.Ident; |
| var FUNCTION = TYPE.Function; |
| var URL = TYPE.Url; |
| var HASH = TYPE.Hash; |
| var PERCENTAGE = TYPE.Percentage; |
| var NUMBER = TYPE.Number; |
| var NUMBERSIGN = 0x0023; // U+0023 NUMBER SIGN (#) |
| var NULL = 0; |
| |
| function createParseContext(name) { |
| return function() { |
| return this[name](); |
| }; |
| } |
| |
| function processConfig(config) { |
| var parserConfig = { |
| context: {}, |
| scope: {}, |
| atrule: {}, |
| pseudo: {} |
| }; |
| |
| if (config.parseContext) { |
| for (var name in config.parseContext) { |
| switch (typeof config.parseContext[name]) { |
| case 'function': |
| parserConfig.context[name] = config.parseContext[name]; |
| break; |
| |
| case 'string': |
| parserConfig.context[name] = createParseContext(config.parseContext[name]); |
| break; |
| } |
| } |
| } |
| |
| if (config.scope) { |
| for (var name in config.scope) { |
| parserConfig.scope[name] = config.scope[name]; |
| } |
| } |
| |
| if (config.atrule) { |
| for (var name in config.atrule) { |
| var atrule = config.atrule[name]; |
| |
| if (atrule.parse) { |
| parserConfig.atrule[name] = atrule.parse; |
| } |
| } |
| } |
| |
| if (config.pseudo) { |
| for (var name in config.pseudo) { |
| var pseudo = config.pseudo[name]; |
| |
| if (pseudo.parse) { |
| parserConfig.pseudo[name] = pseudo.parse; |
| } |
| } |
| } |
| |
| if (config.node) { |
| for (var name in config.node) { |
| parserConfig[name] = config.node[name].parse; |
| } |
| } |
| |
| return parserConfig; |
| } |
| |
| module.exports = function createParser(config) { |
| var parser = { |
| scanner: new TokenStream(), |
| locationMap: new OffsetToLocation(), |
| |
| filename: '<unknown>', |
| needPositions: false, |
| onParseError: noop, |
| onParseErrorThrow: false, |
| parseAtrulePrelude: true, |
| parseRulePrelude: true, |
| parseValue: true, |
| parseCustomProperty: false, |
| |
| readSequence: sequence, |
| |
| createList: function() { |
| return new List(); |
| }, |
| createSingleNodeList: function(node) { |
| return new List().appendData(node); |
| }, |
| getFirstListNode: function(list) { |
| return list && list.first(); |
| }, |
| getLastListNode: function(list) { |
| return list.last(); |
| }, |
| |
| parseWithFallback: function(consumer, fallback) { |
| var startToken = this.scanner.tokenIndex; |
| |
| try { |
| return consumer.call(this); |
| } catch (e) { |
| if (this.onParseErrorThrow) { |
| throw e; |
| } |
| |
| var fallbackNode = fallback.call(this, startToken); |
| |
| this.onParseErrorThrow = true; |
| this.onParseError(e, fallbackNode); |
| this.onParseErrorThrow = false; |
| |
| return fallbackNode; |
| } |
| }, |
| |
| lookupNonWSType: function(offset) { |
| do { |
| var type = this.scanner.lookupType(offset++); |
| if (type !== WHITESPACE) { |
| return type; |
| } |
| } while (type !== NULL); |
| |
| return NULL; |
| }, |
| |
| eat: function(tokenType) { |
| if (this.scanner.tokenType !== tokenType) { |
| var offset = this.scanner.tokenStart; |
| var message = NAME[tokenType] + ' is expected'; |
| |
| // tweak message and offset |
| switch (tokenType) { |
| case IDENT: |
| // when identifier is expected but there is a function or url |
| if (this.scanner.tokenType === FUNCTION || this.scanner.tokenType === URL) { |
| offset = this.scanner.tokenEnd - 1; |
| message = 'Identifier is expected but function found'; |
| } else { |
| message = 'Identifier is expected'; |
| } |
| break; |
| |
| case HASH: |
| if (this.scanner.isDelim(NUMBERSIGN)) { |
| this.scanner.next(); |
| offset++; |
| message = 'Name is expected'; |
| } |
| break; |
| |
| case PERCENTAGE: |
| if (this.scanner.tokenType === NUMBER) { |
| offset = this.scanner.tokenEnd; |
| message = 'Percent sign is expected'; |
| } |
| break; |
| |
| default: |
| // when test type is part of another token show error for current position + 1 |
| // e.g. eat(HYPHENMINUS) will fail on "-foo", but pointing on "-" is odd |
| if (this.scanner.source.charCodeAt(this.scanner.tokenStart) === tokenType) { |
| offset = offset + 1; |
| } |
| } |
| |
| this.error(message, offset); |
| } |
| |
| this.scanner.next(); |
| }, |
| |
| consume: function(tokenType) { |
| var value = this.scanner.getTokenValue(); |
| |
| this.eat(tokenType); |
| |
| return value; |
| }, |
| consumeFunctionName: function() { |
| var name = this.scanner.source.substring(this.scanner.tokenStart, this.scanner.tokenEnd - 1); |
| |
| this.eat(FUNCTION); |
| |
| return name; |
| }, |
| |
| getLocation: function(start, end) { |
| if (this.needPositions) { |
| return this.locationMap.getLocationRange( |
| start, |
| end, |
| this.filename |
| ); |
| } |
| |
| return null; |
| }, |
| getLocationFromList: function(list) { |
| if (this.needPositions) { |
| var head = this.getFirstListNode(list); |
| var tail = this.getLastListNode(list); |
| return this.locationMap.getLocationRange( |
| head !== null ? head.loc.start.offset - this.locationMap.startOffset : this.scanner.tokenStart, |
| tail !== null ? tail.loc.end.offset - this.locationMap.startOffset : this.scanner.tokenStart, |
| this.filename |
| ); |
| } |
| |
| return null; |
| }, |
| |
| error: function(message, offset) { |
| var location = typeof offset !== 'undefined' && offset < this.scanner.source.length |
| ? this.locationMap.getLocation(offset) |
| : this.scanner.eof |
| ? this.locationMap.getLocation(findWhiteSpaceStart(this.scanner.source, this.scanner.source.length - 1)) |
| : this.locationMap.getLocation(this.scanner.tokenStart); |
| |
| throw new SyntaxError( |
| message || 'Unexpected input', |
| this.scanner.source, |
| location.offset, |
| location.line, |
| location.column |
| ); |
| } |
| }; |
| |
| config = processConfig(config || {}); |
| for (var key in config) { |
| parser[key] = config[key]; |
| } |
| |
| return function(source, options) { |
| options = options || {}; |
| |
| var context = options.context || 'default'; |
| var onComment = options.onComment; |
| var ast; |
| |
| tokenize(source, parser.scanner); |
| parser.locationMap.setSource( |
| source, |
| options.offset, |
| options.line, |
| options.column |
| ); |
| |
| parser.filename = options.filename || '<unknown>'; |
| parser.needPositions = Boolean(options.positions); |
| parser.onParseError = typeof options.onParseError === 'function' ? options.onParseError : noop; |
| parser.onParseErrorThrow = false; |
| parser.parseAtrulePrelude = 'parseAtrulePrelude' in options ? Boolean(options.parseAtrulePrelude) : true; |
| parser.parseRulePrelude = 'parseRulePrelude' in options ? Boolean(options.parseRulePrelude) : true; |
| parser.parseValue = 'parseValue' in options ? Boolean(options.parseValue) : true; |
| parser.parseCustomProperty = 'parseCustomProperty' in options ? Boolean(options.parseCustomProperty) : false; |
| |
| if (!parser.context.hasOwnProperty(context)) { |
| throw new Error('Unknown context `' + context + '`'); |
| } |
| |
| if (typeof onComment === 'function') { |
| parser.scanner.forEachToken((type, start, end) => { |
| if (type === COMMENT) { |
| const loc = parser.getLocation(start, end); |
| const value = cmpStr(source, end - 2, end, '*/') |
| ? source.slice(start + 2, end - 2) |
| : source.slice(start + 2, end); |
| |
| onComment(value, loc); |
| } |
| }); |
| } |
| |
| ast = parser.context[context].call(parser, options); |
| |
| if (!parser.scanner.eof) { |
| parser.error(); |
| } |
| |
| return ast; |
| }; |
| }; |