blob: a01f62520a2e4fd8b93e91c274d7feee3bc01227 [file] [log] [blame]
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;
};
};