| 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 = require('../tokenizer/utils').findWhiteSpaceStart; | 
| var sequence = require('./sequence'); | 
| var noop = function() {}; | 
|   | 
| var TYPE = constants.TYPE; | 
| var NAME = constants.NAME; | 
| var WHITESPACE = TYPE.WhiteSpace; | 
| 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 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 + '`'); | 
|         } | 
|   | 
|         ast = parser.context[context].call(parser, options); | 
|   | 
|         if (!parser.scanner.eof) { | 
|             parser.error(); | 
|         } | 
|   | 
|         return ast; | 
|     }; | 
| }; |