You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
304 lines
9.5 KiB
304 lines
9.5 KiB
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; |
|
}; |
|
};
|
|
|