336 lines
10 KiB
JavaScript
336 lines
10 KiB
JavaScript
'use strict';
|
|
|
|
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;
|