| import { List } from '../utils/List.js'; |
| import { SyntaxError } from './SyntaxError.js'; |
| import { |
| tokenize, |
| OffsetToLocation, |
| TokenStream, |
| tokenNames, |
| |
| consumeNumber, |
| findWhiteSpaceStart, |
| cmpChar, |
| cmpStr, |
| |
| WhiteSpace, |
| Comment, |
| Ident, |
| Function as FunctionToken, |
| Url, |
| Hash, |
| Percentage, |
| Number as NumberToken |
| } from '../tokenizer/index.js'; |
| import { readSequence } from './sequence.js'; |
| |
| const NOOP = () => {}; |
| const EXCLAMATIONMARK = 0x0021; // U+0021 EXCLAMATION MARK (!) |
| const NUMBERSIGN = 0x0023; // U+0023 NUMBER SIGN (#) |
| const SEMICOLON = 0x003B; // U+003B SEMICOLON (;) |
| const LEFTCURLYBRACKET = 0x007B; // U+007B LEFT CURLY BRACKET ({) |
| const NULL = 0; |
| |
| function createParseContext(name) { |
| return function() { |
| return this[name](); |
| }; |
| } |
| |
| function fetchParseValues(dict) { |
| const result = Object.create(null); |
| |
| for (const name in dict) { |
| const item = dict[name]; |
| const fn = item.parse || item; |
| |
| if (fn) { |
| result[name] = fn; |
| } |
| } |
| |
| return result; |
| } |
| |
| function processConfig(config) { |
| const parseConfig = { |
| context: Object.create(null), |
| scope: Object.assign(Object.create(null), config.scope), |
| atrule: fetchParseValues(config.atrule), |
| pseudo: fetchParseValues(config.pseudo), |
| node: fetchParseValues(config.node) |
| }; |
| |
| for (const name in config.parseContext) { |
| switch (typeof config.parseContext[name]) { |
| case 'function': |
| parseConfig.context[name] = config.parseContext[name]; |
| break; |
| |
| case 'string': |
| parseConfig.context[name] = createParseContext(config.parseContext[name]); |
| break; |
| } |
| } |
| |
| return { |
| config: parseConfig, |
| ...parseConfig, |
| ...parseConfig.node |
| }; |
| } |
| |
| export function createParser(config) { |
| let source = ''; |
| let filename = '<unknown>'; |
| let needPositions = false; |
| let onParseError = NOOP; |
| let onParseErrorThrow = false; |
| |
| const locationMap = new OffsetToLocation(); |
| const parser = Object.assign(new TokenStream(), processConfig(config || {}), { |
| parseAtrulePrelude: true, |
| parseRulePrelude: true, |
| parseValue: true, |
| parseCustomProperty: false, |
| |
| readSequence, |
| |
| consumeUntilBalanceEnd: () => 0, |
| consumeUntilLeftCurlyBracket(code) { |
| return code === LEFTCURLYBRACKET ? 1 : 0; |
| }, |
| consumeUntilLeftCurlyBracketOrSemicolon(code) { |
| return code === LEFTCURLYBRACKET || code === SEMICOLON ? 1 : 0; |
| }, |
| consumeUntilExclamationMarkOrSemicolon(code) { |
| return code === EXCLAMATIONMARK || code === SEMICOLON ? 1 : 0; |
| }, |
| consumeUntilSemicolonIncluded(code) { |
| return code === SEMICOLON ? 2 : 0; |
| }, |
| |
| createList() { |
| return new List(); |
| }, |
| createSingleNodeList(node) { |
| return new List().appendData(node); |
| }, |
| getFirstListNode(list) { |
| return list && list.first; |
| }, |
| getLastListNode(list) { |
| return list && list.last; |
| }, |
| |
| parseWithFallback(consumer, fallback) { |
| const startToken = this.tokenIndex; |
| |
| try { |
| return consumer.call(this); |
| } catch (e) { |
| if (onParseErrorThrow) { |
| throw e; |
| } |
| |
| const fallbackNode = fallback.call(this, startToken); |
| |
| onParseErrorThrow = true; |
| onParseError(e, fallbackNode); |
| onParseErrorThrow = false; |
| |
| return fallbackNode; |
| } |
| }, |
| |
| lookupNonWSType(offset) { |
| let type; |
| |
| do { |
| type = this.lookupType(offset++); |
| if (type !== WhiteSpace) { |
| return type; |
| } |
| } while (type !== NULL); |
| |
| return NULL; |
| }, |
| |
| charCodeAt(offset) { |
| return offset >= 0 && offset < source.length ? source.charCodeAt(offset) : 0; |
| }, |
| substring(offsetStart, offsetEnd) { |
| return source.substring(offsetStart, offsetEnd); |
| }, |
| substrToCursor(start) { |
| return this.source.substring(start, this.tokenStart); |
| }, |
| |
| cmpChar(offset, charCode) { |
| return cmpChar(source, offset, charCode); |
| }, |
| cmpStr(offsetStart, offsetEnd, str) { |
| return cmpStr(source, offsetStart, offsetEnd, str); |
| }, |
| |
| consume(tokenType) { |
| const start = this.tokenStart; |
| |
| this.eat(tokenType); |
| |
| return this.substrToCursor(start); |
| }, |
| consumeFunctionName() { |
| const name = source.substring(this.tokenStart, this.tokenEnd - 1); |
| |
| this.eat(FunctionToken); |
| |
| return name; |
| }, |
| consumeNumber(type) { |
| const number = source.substring(this.tokenStart, consumeNumber(source, this.tokenStart)); |
| |
| this.eat(type); |
| |
| return number; |
| }, |
| |
| eat(tokenType) { |
| if (this.tokenType !== tokenType) { |
| const tokenName = tokenNames[tokenType].slice(0, -6).replace(/-/g, ' ').replace(/^./, m => m.toUpperCase()); |
| let message = `${/[[\](){}]/.test(tokenName) ? `"${tokenName}"` : tokenName} is expected`; |
| let offset = this.tokenStart; |
| |
| // tweak message and offset |
| switch (tokenType) { |
| case Ident: |
| // when identifier is expected but there is a function or url |
| if (this.tokenType === FunctionToken || this.tokenType === Url) { |
| offset = this.tokenEnd - 1; |
| message = 'Identifier is expected but function found'; |
| } else { |
| message = 'Identifier is expected'; |
| } |
| break; |
| |
| case Hash: |
| if (this.isDelim(NUMBERSIGN)) { |
| this.next(); |
| offset++; |
| message = 'Name is expected'; |
| } |
| break; |
| |
| case Percentage: |
| if (this.tokenType === NumberToken) { |
| offset = this.tokenEnd; |
| message = 'Percent sign is expected'; |
| } |
| break; |
| } |
| |
| this.error(message, offset); |
| } |
| |
| this.next(); |
| }, |
| eatIdent(name) { |
| if (this.tokenType !== Ident || this.lookupValue(0, name) === false) { |
| this.error(`Identifier "${name}" is expected`); |
| } |
| |
| this.next(); |
| }, |
| eatDelim(code) { |
| if (!this.isDelim(code)) { |
| this.error(`Delim "${String.fromCharCode(code)}" is expected`); |
| } |
| |
| this.next(); |
| }, |
| |
| getLocation(start, end) { |
| if (needPositions) { |
| return locationMap.getLocationRange( |
| start, |
| end, |
| filename |
| ); |
| } |
| |
| return null; |
| }, |
| getLocationFromList(list) { |
| if (needPositions) { |
| const head = this.getFirstListNode(list); |
| const tail = this.getLastListNode(list); |
| return locationMap.getLocationRange( |
| head !== null ? head.loc.start.offset - locationMap.startOffset : this.tokenStart, |
| tail !== null ? tail.loc.end.offset - locationMap.startOffset : this.tokenStart, |
| filename |
| ); |
| } |
| |
| return null; |
| }, |
| |
| error(message, offset) { |
| const location = typeof offset !== 'undefined' && offset < source.length |
| ? locationMap.getLocation(offset) |
| : this.eof |
| ? locationMap.getLocation(findWhiteSpaceStart(source, source.length - 1)) |
| : locationMap.getLocation(this.tokenStart); |
| |
| throw new SyntaxError( |
| message || 'Unexpected input', |
| source, |
| location.offset, |
| location.line, |
| location.column |
| ); |
| } |
| }); |
| |
| const parse = function(source_, options) { |
| source = source_; |
| options = options || {}; |
| |
| parser.setSource(source, tokenize); |
| locationMap.setSource( |
| source, |
| options.offset, |
| options.line, |
| options.column |
| ); |
| |
| filename = options.filename || '<unknown>'; |
| needPositions = Boolean(options.positions); |
| onParseError = typeof options.onParseError === 'function' ? options.onParseError : NOOP; |
| 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; |
| |
| const { context = 'default', onComment } = options; |
| |
| if (context in parser.context === false) { |
| throw new Error('Unknown context `' + context + '`'); |
| } |
| |
| if (typeof onComment === 'function') { |
| parser.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); |
| } |
| }); |
| } |
| |
| const ast = parser.context[context].call(parser, options); |
| |
| if (!parser.eof) { |
| parser.error(); |
| } |
| |
| return ast; |
| }; |
| |
| return Object.assign(parse, { |
| SyntaxError, |
| config: parser.config |
| }); |
| }; |