Spaces:
Runtime error
Runtime error
; | |
const List = require('../utils/List.cjs'); | |
const SyntaxError = require('./SyntaxError.cjs'); | |
const index = require('../tokenizer/index.cjs'); | |
const sequence = require('./sequence.cjs'); | |
const OffsetToLocation = require('../tokenizer/OffsetToLocation.cjs'); | |
const TokenStream = require('../tokenizer/TokenStream.cjs'); | |
const utils = require('../tokenizer/utils.cjs'); | |
const types = require('../tokenizer/types.cjs'); | |
const names = require('../tokenizer/names.cjs'); | |
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 | |
}; | |
} | |
function createParser(config) { | |
let source = ''; | |
let filename = '<unknown>'; | |
let needPositions = false; | |
let onParseError = NOOP; | |
let onParseErrorThrow = false; | |
const locationMap = new OffsetToLocation.OffsetToLocation(); | |
const parser = Object.assign(new TokenStream.TokenStream(), processConfig(config || {}), { | |
parseAtrulePrelude: true, | |
parseRulePrelude: true, | |
parseValue: true, | |
parseCustomProperty: false, | |
readSequence: sequence.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.List(); | |
}, | |
createSingleNodeList(node) { | |
return new List.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 !== types.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 utils.cmpChar(source, offset, charCode); | |
}, | |
cmpStr(offsetStart, offsetEnd, str) { | |
return utils.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(types.Function); | |
return name; | |
}, | |
consumeNumber(type) { | |
const number = source.substring(this.tokenStart, utils.consumeNumber(source, this.tokenStart)); | |
this.eat(type); | |
return number; | |
}, | |
eat(tokenType) { | |
if (this.tokenType !== tokenType) { | |
const tokenName = names[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 types.Ident: | |
// when identifier is expected but there is a function or url | |
if (this.tokenType === types.Function || this.tokenType === types.Url) { | |
offset = this.tokenEnd - 1; | |
message = 'Identifier is expected but function found'; | |
} else { | |
message = 'Identifier is expected'; | |
} | |
break; | |
case types.Hash: | |
if (this.isDelim(NUMBERSIGN)) { | |
this.next(); | |
offset++; | |
message = 'Name is expected'; | |
} | |
break; | |
case types.Percentage: | |
if (this.tokenType === types.Number) { | |
offset = this.tokenEnd; | |
message = 'Percent sign is expected'; | |
} | |
break; | |
} | |
this.error(message, offset); | |
} | |
this.next(); | |
}, | |
eatIdent(name) { | |
if (this.tokenType !== types.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(utils.findWhiteSpaceStart(source, source.length - 1)) | |
: locationMap.getLocation(this.tokenStart); | |
throw new SyntaxError.SyntaxError( | |
message || 'Unexpected input', | |
source, | |
location.offset, | |
location.line, | |
location.column | |
); | |
} | |
}); | |
const parse = function(source_, options) { | |
source = source_; | |
options = options || {}; | |
parser.setSource(source, index.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 === types.Comment) { | |
const loc = parser.getLocation(start, end); | |
const value = utils.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: SyntaxError.SyntaxError, | |
config: parser.config | |
}); | |
} | |
exports.createParser = createParser; | |