305 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			305 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 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;
 | |
|     };
 | |
| };
 |