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 = ''; 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 || ''; 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 }); };