"use strict";
|
|
|
|
var Parser = require("fastparse");
|
|
var regexpu = require("regexpu-core");
|
|
|
|
function unescape(str) {
|
|
return str.replace(/\\(.)/g, "$1");
|
|
}
|
|
|
|
function commentMatch(match, content) {
|
|
this.selector.nodes.push({
|
|
type: "comment",
|
|
content: content
|
|
});
|
|
}
|
|
|
|
function typeMatch(type) {
|
|
return function(match, name) {
|
|
this.selector.nodes.push({
|
|
type: type,
|
|
name: unescape(name)
|
|
});
|
|
};
|
|
}
|
|
|
|
function pseudoClassStartMatch(match, name) {
|
|
var newToken = {
|
|
type: "pseudo-class",
|
|
name: unescape(name),
|
|
content: ""
|
|
};
|
|
this.selector.nodes.push(newToken);
|
|
this.token = newToken;
|
|
this.brackets = 1;
|
|
return "inBrackets";
|
|
}
|
|
|
|
function nestedPseudoClassStartMatch(match, name, after) {
|
|
var newSelector = {
|
|
type: "selector",
|
|
nodes: []
|
|
};
|
|
var newToken = {
|
|
type: "nested-pseudo-class",
|
|
name: unescape(name),
|
|
nodes: [newSelector]
|
|
};
|
|
if(after) {
|
|
newSelector.before = after;
|
|
}
|
|
this.selector.nodes.push(newToken);
|
|
this.stack.push(this.root);
|
|
this.root = newToken;
|
|
this.selector = newSelector;
|
|
}
|
|
|
|
function nestedEnd(match, before) {
|
|
if(this.stack.length > 0) {
|
|
if(before) {
|
|
this.selector.after = before;
|
|
}
|
|
this.root = this.stack.pop();
|
|
this.selector = this.root.nodes[this.root.nodes.length - 1];
|
|
} else {
|
|
this.selector.nodes.push({
|
|
type: "invalid",
|
|
value: match
|
|
});
|
|
}
|
|
}
|
|
|
|
function operatorMatch(match, before, operator, after) {
|
|
var token = {
|
|
type: "operator",
|
|
operator: operator
|
|
};
|
|
if(before) {
|
|
token.before = before;
|
|
}
|
|
if(after) {
|
|
token.after = after;
|
|
}
|
|
this.selector.nodes.push(token);
|
|
}
|
|
|
|
function spacingMatch(match) {
|
|
this.selector.nodes.push({
|
|
type: "spacing",
|
|
value: match
|
|
});
|
|
}
|
|
|
|
function elementMatch(match, namespace, name) {
|
|
var newToken = {
|
|
type: "element",
|
|
name: unescape(name)
|
|
};
|
|
|
|
if(namespace) {
|
|
newToken.namespace = unescape(namespace.substr(0, namespace.length - 1));
|
|
}
|
|
this.selector.nodes.push(newToken);
|
|
}
|
|
|
|
function universalMatch(match, namespace) {
|
|
var newToken = {
|
|
type: "universal"
|
|
};
|
|
if(namespace) {
|
|
newToken.namespace = unescape(namespace.substr(0, namespace.length - 1));
|
|
}
|
|
this.selector.nodes.push(newToken);
|
|
}
|
|
|
|
function attributeMatch(match, content) {
|
|
this.selector.nodes.push({
|
|
type: "attribute",
|
|
content: content
|
|
});
|
|
}
|
|
|
|
function invalidMatch(match) {
|
|
this.selector.nodes.push({
|
|
type: "invalid",
|
|
value: match
|
|
});
|
|
}
|
|
|
|
function irrelevantSpacingStartMatch(match) {
|
|
this.selector.before = match;
|
|
}
|
|
|
|
function irrelevantSpacingEndMatch(match) {
|
|
this.selector.after = match;
|
|
}
|
|
|
|
function nextSelectorMatch(match, before, after) {
|
|
var newSelector = {
|
|
type: "selector",
|
|
nodes: []
|
|
};
|
|
if(before) {
|
|
this.selector.after = before;
|
|
}
|
|
if(after) {
|
|
newSelector.before = after;
|
|
}
|
|
this.root.nodes.push(newSelector);
|
|
this.selector = newSelector;
|
|
}
|
|
|
|
function addToCurrent(match) {
|
|
this.token.content += match;
|
|
}
|
|
|
|
function bracketStart(match) {
|
|
this.token.content += match;
|
|
this.brackets++;
|
|
}
|
|
|
|
function bracketEnd(match) {
|
|
if(--this.brackets === 0) {
|
|
return "selector";
|
|
}
|
|
this.token.content += match;
|
|
}
|
|
|
|
function getSelectors() {
|
|
// The assignment here is split to preserve the property enumeration order.
|
|
var selectors = {
|
|
"/\\*([\\s\\S]*?)\\*/": commentMatch
|
|
};
|
|
// https://www.w3.org/TR/CSS21/syndata.html#characters
|
|
// 4.1.3: identifiers (...) can contain only the characters [a-zA-Z0-9] and
|
|
// ISO 10646 characters U+00A0 and higher, plus the hyphen (-) and the underscore (_)
|
|
//
|
|
// 10ffff is the maximum allowed in current Unicode
|
|
selectors[regexpu("\\.((?:\\\\.|[A-Za-z_\\-\\u{00a0}-\\u{10ffff}])(?:\\\\.|[A-Za-z_\\-0-9\\u{00a0}-\\u{10ffff}])*)", "u")] = typeMatch("class");
|
|
selectors[regexpu("#((?:\\\\.|[A-Za-z_\\-\\u{00a0}-\\u{10ffff}])(?:\\\\.|[A-Za-z_\\-0-9\\u{00a0}-\\u{10ffff}])*)", "u")] = typeMatch("id");
|
|
var selectorsSecondHalf = {
|
|
":(not|matches|has|local|global)\\((\\s*)": nestedPseudoClassStartMatch,
|
|
":((?:\\\\.|[A-Za-z_\\-0-9])+)\\(": pseudoClassStartMatch,
|
|
":((?:\\\\.|[A-Za-z_\\-0-9])+)": typeMatch("pseudo-class"),
|
|
"::((?:\\\\.|[A-Za-z_\\-0-9])+)": typeMatch("pseudo-element"),
|
|
"(\\*\\|)((?:\\\\.|[A-Za-z_\\-0-9])+)": elementMatch,
|
|
"(\\*\\|)\\*": universalMatch,
|
|
"((?:\\\\.|[A-Za-z_\\-0-9])*\\|)?\\*": universalMatch,
|
|
"((?:\\\\.|[A-Za-z_\\-0-9])*\\|)?((?:\\\\.|[A-Za-z_\\-])(?:\\\\.|[A-Za-z_\\-0-9])*)": elementMatch,
|
|
"\\[([^\\]]+)\\]": attributeMatch,
|
|
"(\\s*)\\)": nestedEnd,
|
|
"(\\s*)((?:\\|\\|)|(?:>>)|[>+~])(\\s*)": operatorMatch,
|
|
"(\\s*),(\\s*)": nextSelectorMatch,
|
|
"\\s+$": irrelevantSpacingEndMatch,
|
|
"^\\s+": irrelevantSpacingStartMatch,
|
|
"\\s+": spacingMatch,
|
|
".": invalidMatch
|
|
};
|
|
var selector;
|
|
for (selector in selectorsSecondHalf) {
|
|
if (Object.prototype.hasOwnProperty.call(selectorsSecondHalf, selector)) {
|
|
selectors[selector] = selectorsSecondHalf[selector];
|
|
}
|
|
}
|
|
return selectors;
|
|
}
|
|
|
|
var parser = new Parser({
|
|
selector: getSelectors(),
|
|
inBrackets: {
|
|
"/\\*[\\s\\S]*?\\*/": addToCurrent,
|
|
"\"([^\\\\\"]|\\\\.)*\"": addToCurrent,
|
|
"'([^\\\\']|\\\\.)*'": addToCurrent,
|
|
"[^()'\"/]+": addToCurrent,
|
|
"\\(": bracketStart,
|
|
"\\)": bracketEnd,
|
|
".": addToCurrent
|
|
}
|
|
});
|
|
|
|
function parse(str) {
|
|
var selectorNode = {
|
|
type: "selector",
|
|
nodes: []
|
|
};
|
|
var rootNode = {
|
|
type: "selectors",
|
|
nodes: [
|
|
selectorNode
|
|
]
|
|
};
|
|
parser.parse("selector", str, {
|
|
stack: [],
|
|
root: rootNode,
|
|
selector: selectorNode
|
|
});
|
|
return rootNode;
|
|
}
|
|
|
|
module.exports = parse;
|