597 lines
15 KiB
JavaScript
597 lines
15 KiB
JavaScript
function CssSelectorParser() {
|
|
this.pseudos = {};
|
|
this.attrEqualityMods = {};
|
|
this.ruleNestingOperators = {};
|
|
this.substitutesEnabled = false;
|
|
}
|
|
|
|
CssSelectorParser.prototype.registerSelectorPseudos = function(name) {
|
|
for (var j = 0, len = arguments.length; j < len; j++) {
|
|
name = arguments[j];
|
|
this.pseudos[name] = 'selector';
|
|
}
|
|
return this;
|
|
};
|
|
|
|
CssSelectorParser.prototype.unregisterSelectorPseudos = function(name) {
|
|
for (var j = 0, len = arguments.length; j < len; j++) {
|
|
name = arguments[j];
|
|
delete this.pseudos[name];
|
|
}
|
|
return this;
|
|
};
|
|
|
|
CssSelectorParser.prototype.registerNumericPseudos = function(name) {
|
|
for (var j = 0, len = arguments.length; j < len; j++) {
|
|
name = arguments[j];
|
|
this.pseudos[name] = 'numeric';
|
|
}
|
|
return this;
|
|
};
|
|
|
|
CssSelectorParser.prototype.unregisterNumericPseudos = function(name) {
|
|
for (var j = 0, len = arguments.length; j < len; j++) {
|
|
name = arguments[j];
|
|
delete this.pseudos[name];
|
|
}
|
|
return this;
|
|
};
|
|
|
|
CssSelectorParser.prototype.registerNestingOperators = function(operator) {
|
|
for (var j = 0, len = arguments.length; j < len; j++) {
|
|
operator = arguments[j];
|
|
this.ruleNestingOperators[operator] = true;
|
|
}
|
|
return this;
|
|
};
|
|
|
|
CssSelectorParser.prototype.unregisterNestingOperators = function(operator) {
|
|
for (var j = 0, len = arguments.length; j < len; j++) {
|
|
operator = arguments[j];
|
|
delete this.ruleNestingOperators[operator];
|
|
}
|
|
return this;
|
|
};
|
|
|
|
CssSelectorParser.prototype.registerAttrEqualityMods = function(mod) {
|
|
for (var j = 0, len = arguments.length; j < len; j++) {
|
|
mod = arguments[j];
|
|
this.attrEqualityMods[mod] = true;
|
|
}
|
|
return this;
|
|
};
|
|
|
|
CssSelectorParser.prototype.unregisterAttrEqualityMods = function(mod) {
|
|
for (var j = 0, len = arguments.length; j < len; j++) {
|
|
mod = arguments[j];
|
|
delete this.attrEqualityMods[mod];
|
|
}
|
|
return this;
|
|
};
|
|
|
|
CssSelectorParser.prototype.enableSubstitutes = function() {
|
|
this.substitutesEnabled = true;
|
|
return this;
|
|
};
|
|
|
|
CssSelectorParser.prototype.disableSubstitutes = function() {
|
|
this.substitutesEnabled = false;
|
|
return this;
|
|
};
|
|
|
|
function isIdentStart(c) {
|
|
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c === '-') || (c === '_');
|
|
}
|
|
|
|
function isIdent(c) {
|
|
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c === '-' || c === '_';
|
|
}
|
|
|
|
function isHex(c) {
|
|
return (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') || (c >= '0' && c <= '9');
|
|
}
|
|
|
|
function isDecimal(c) {
|
|
return c >= '0' && c <= '9';
|
|
}
|
|
|
|
function isAttrMatchOperator(chr) {
|
|
return chr === '=' || chr === '^' || chr === '$' || chr === '*' || chr === '~';
|
|
}
|
|
|
|
var identSpecialChars = {
|
|
'!': true,
|
|
'"': true,
|
|
'#': true,
|
|
'$': true,
|
|
'%': true,
|
|
'&': true,
|
|
'\'': true,
|
|
'(': true,
|
|
')': true,
|
|
'*': true,
|
|
'+': true,
|
|
',': true,
|
|
'.': true,
|
|
'/': true,
|
|
';': true,
|
|
'<': true,
|
|
'=': true,
|
|
'>': true,
|
|
'?': true,
|
|
'@': true,
|
|
'[': true,
|
|
'\\': true,
|
|
']': true,
|
|
'^': true,
|
|
'`': true,
|
|
'{': true,
|
|
'|': true,
|
|
'}': true,
|
|
'~': true
|
|
};
|
|
|
|
var strReplacementsRev = {
|
|
'\n': '\\n',
|
|
'\r': '\\r',
|
|
'\t': '\\t',
|
|
'\f': '\\f',
|
|
'\v': '\\v'
|
|
};
|
|
|
|
var singleQuoteEscapeChars = {
|
|
n: '\n',
|
|
r: '\r',
|
|
t: '\t',
|
|
f: '\f',
|
|
'\\': '\\',
|
|
'\'': '\''
|
|
};
|
|
|
|
var doubleQuotesEscapeChars = {
|
|
n: '\n',
|
|
r: '\r',
|
|
t: '\t',
|
|
f: '\f',
|
|
'\\': '\\',
|
|
'"': '"'
|
|
};
|
|
|
|
function ParseContext(str, pos, pseudos, attrEqualityMods, ruleNestingOperators, substitutesEnabled) {
|
|
var chr, getIdent, getStr, l, skipWhitespace;
|
|
l = str.length;
|
|
chr = null;
|
|
getStr = function(quote, escapeTable) {
|
|
var esc, hex, result;
|
|
result = '';
|
|
pos++;
|
|
chr = str.charAt(pos);
|
|
while (pos < l) {
|
|
if (chr === quote) {
|
|
pos++;
|
|
return result;
|
|
} else if (chr === '\\') {
|
|
pos++;
|
|
chr = str.charAt(pos);
|
|
if (chr === quote) {
|
|
result += quote;
|
|
} else if (esc = escapeTable[chr]) {
|
|
result += esc;
|
|
} else if (isHex(chr)) {
|
|
hex = chr;
|
|
pos++;
|
|
chr = str.charAt(pos);
|
|
while (isHex(chr)) {
|
|
hex += chr;
|
|
pos++;
|
|
chr = str.charAt(pos);
|
|
}
|
|
if (chr === ' ') {
|
|
pos++;
|
|
chr = str.charAt(pos);
|
|
}
|
|
result += String.fromCharCode(parseInt(hex, 16));
|
|
continue;
|
|
} else {
|
|
result += chr;
|
|
}
|
|
} else {
|
|
result += chr;
|
|
}
|
|
pos++;
|
|
chr = str.charAt(pos);
|
|
}
|
|
return result;
|
|
};
|
|
getIdent = function() {
|
|
var result = '';
|
|
chr = str.charAt(pos);
|
|
while (pos < l) {
|
|
if (isIdent(chr)) {
|
|
result += chr;
|
|
} else if (chr === '\\') {
|
|
pos++;
|
|
if (pos >= l) {
|
|
throw Error('Expected symbol but end of file reached.');
|
|
}
|
|
chr = str.charAt(pos);
|
|
if (identSpecialChars[chr]) {
|
|
result += chr;
|
|
} else if (isHex(chr)) {
|
|
var hex = chr;
|
|
pos++;
|
|
chr = str.charAt(pos);
|
|
while (isHex(chr)) {
|
|
hex += chr;
|
|
pos++;
|
|
chr = str.charAt(pos);
|
|
}
|
|
if (chr === ' ') {
|
|
pos++;
|
|
chr = str.charAt(pos);
|
|
}
|
|
result += String.fromCharCode(parseInt(hex, 16));
|
|
continue;
|
|
} else {
|
|
result += chr;
|
|
}
|
|
} else {
|
|
return result;
|
|
}
|
|
pos++;
|
|
chr = str.charAt(pos);
|
|
}
|
|
return result;
|
|
};
|
|
skipWhitespace = function() {
|
|
chr = str.charAt(pos);
|
|
var result = false;
|
|
while (chr === ' ' || chr === "\t" || chr === "\n" || chr === "\r" || chr === "\f") {
|
|
result = true;
|
|
pos++;
|
|
chr = str.charAt(pos);
|
|
}
|
|
return result;
|
|
};
|
|
this.parse = function() {
|
|
var res = this.parseSelector();
|
|
if (pos < l) {
|
|
throw Error('Rule expected but "' + str.charAt(pos) + '" found.');
|
|
}
|
|
return res;
|
|
};
|
|
this.parseSelector = function() {
|
|
var res;
|
|
var selector = res = this.parseSingleSelector();
|
|
chr = str.charAt(pos);
|
|
while (chr === ',') {
|
|
pos++;
|
|
skipWhitespace();
|
|
if (res.type !== 'selectors') {
|
|
res = {
|
|
type: 'selectors',
|
|
selectors: [selector]
|
|
};
|
|
}
|
|
selector = this.parseSingleSelector();
|
|
if (!selector) {
|
|
throw Error('Rule expected after ",".');
|
|
}
|
|
res.selectors.push(selector);
|
|
}
|
|
return res;
|
|
};
|
|
|
|
this.parseSingleSelector = function() {
|
|
skipWhitespace();
|
|
var selector = {
|
|
type: 'ruleSet'
|
|
};
|
|
var rule = this.parseRule();
|
|
if (!rule) {
|
|
return null;
|
|
}
|
|
var currentRule = selector;
|
|
while (rule) {
|
|
rule.type = 'rule';
|
|
currentRule.rule = rule;
|
|
currentRule = rule;
|
|
skipWhitespace();
|
|
chr = str.charAt(pos);
|
|
if (pos >= l || chr === ',' || chr === ')') {
|
|
break;
|
|
}
|
|
if (ruleNestingOperators[chr]) {
|
|
var op = chr;
|
|
pos++;
|
|
skipWhitespace();
|
|
rule = this.parseRule();
|
|
if (!rule) {
|
|
throw Error('Rule expected after "' + op + '".');
|
|
}
|
|
rule.nestingOperator = op;
|
|
} else {
|
|
rule = this.parseRule();
|
|
if (rule) {
|
|
rule.nestingOperator = null;
|
|
}
|
|
}
|
|
}
|
|
return selector;
|
|
};
|
|
|
|
this.parseRule = function() {
|
|
var rule = null;
|
|
while (pos < l) {
|
|
chr = str.charAt(pos);
|
|
if (chr === '*') {
|
|
pos++;
|
|
(rule = rule || {}).tagName = '*';
|
|
} else if (isIdentStart(chr) || chr === '\\') {
|
|
(rule = rule || {}).tagName = getIdent();
|
|
} else if (chr === '.') {
|
|
pos++;
|
|
rule = rule || {};
|
|
(rule.classNames = rule.classNames || []).push(getIdent());
|
|
} else if (chr === '#') {
|
|
pos++;
|
|
(rule = rule || {}).id = getIdent();
|
|
} else if (chr === '[') {
|
|
pos++;
|
|
skipWhitespace();
|
|
var attr = {
|
|
name: getIdent()
|
|
};
|
|
skipWhitespace();
|
|
if (chr === ']') {
|
|
pos++;
|
|
} else {
|
|
var operator = '';
|
|
if (attrEqualityMods[chr]) {
|
|
operator = chr;
|
|
pos++;
|
|
chr = str.charAt(pos);
|
|
}
|
|
if (pos >= l) {
|
|
throw Error('Expected "=" but end of file reached.');
|
|
}
|
|
if (chr !== '=') {
|
|
throw Error('Expected "=" but "' + chr + '" found.');
|
|
}
|
|
attr.operator = operator + '=';
|
|
pos++;
|
|
skipWhitespace();
|
|
var attrValue = '';
|
|
attr.valueType = 'string';
|
|
if (chr === '"') {
|
|
attrValue = getStr('"', doubleQuotesEscapeChars);
|
|
} else if (chr === '\'') {
|
|
attrValue = getStr('\'', singleQuoteEscapeChars);
|
|
} else if (substitutesEnabled && chr === '$') {
|
|
pos++;
|
|
attrValue = getIdent();
|
|
attr.valueType = 'substitute';
|
|
} else {
|
|
while (pos < l) {
|
|
if (chr === ']') {
|
|
break;
|
|
}
|
|
attrValue += chr;
|
|
pos++;
|
|
chr = str.charAt(pos);
|
|
}
|
|
attrValue = attrValue.trim();
|
|
}
|
|
skipWhitespace();
|
|
if (pos >= l) {
|
|
throw Error('Expected "]" but end of file reached.');
|
|
}
|
|
if (chr !== ']') {
|
|
throw Error('Expected "]" but "' + chr + '" found.');
|
|
}
|
|
pos++;
|
|
attr.value = attrValue;
|
|
}
|
|
rule = rule || {};
|
|
(rule.attrs = rule.attrs || []).push(attr);
|
|
} else if (chr === ':') {
|
|
pos++;
|
|
var pseudoName = getIdent();
|
|
var pseudo = {
|
|
name: pseudoName
|
|
};
|
|
if (chr === '(') {
|
|
pos++;
|
|
var value = '';
|
|
skipWhitespace();
|
|
if (pseudos[pseudoName] === 'selector') {
|
|
pseudo.valueType = 'selector';
|
|
value = this.parseSelector();
|
|
} else {
|
|
pseudo.valueType = pseudos[pseudoName] || 'string';
|
|
if (chr === '"') {
|
|
value = getStr('"', doubleQuotesEscapeChars);
|
|
} else if (chr === '\'') {
|
|
value = getStr('\'', singleQuoteEscapeChars);
|
|
} else if (substitutesEnabled && chr === '$') {
|
|
pos++;
|
|
value = getIdent();
|
|
pseudo.valueType = 'substitute';
|
|
} else {
|
|
while (pos < l) {
|
|
if (chr === ')') {
|
|
break;
|
|
}
|
|
value += chr;
|
|
pos++;
|
|
chr = str.charAt(pos);
|
|
}
|
|
value = value.trim();
|
|
}
|
|
skipWhitespace();
|
|
}
|
|
if (pos >= l) {
|
|
throw Error('Expected ")" but end of file reached.');
|
|
}
|
|
if (chr !== ')') {
|
|
throw Error('Expected ")" but "' + chr + '" found.');
|
|
}
|
|
pos++;
|
|
pseudo.value = value;
|
|
}
|
|
rule = rule || {};
|
|
(rule.pseudos = rule.pseudos || []).push(pseudo);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
return rule;
|
|
};
|
|
return this;
|
|
}
|
|
|
|
CssSelectorParser.prototype.parse = function(str) {
|
|
var context = new ParseContext(
|
|
str,
|
|
0,
|
|
this.pseudos,
|
|
this.attrEqualityMods,
|
|
this.ruleNestingOperators,
|
|
this.substitutesEnabled
|
|
);
|
|
return context.parse();
|
|
};
|
|
|
|
CssSelectorParser.prototype.escapeIdentifier = function(s) {
|
|
var result = '';
|
|
var i = 0;
|
|
var len = s.length;
|
|
while (i < len) {
|
|
var chr = s.charAt(i);
|
|
if (identSpecialChars[chr]) {
|
|
result += '\\' + chr;
|
|
} else {
|
|
if (
|
|
!(
|
|
chr === '_' || chr === '-' ||
|
|
(chr >= 'A' && chr <= 'Z') ||
|
|
(chr >= 'a' && chr <= 'z') ||
|
|
(i !== 0 && chr >= '0' && chr <= '9')
|
|
)
|
|
) {
|
|
var charCode = chr.charCodeAt(0);
|
|
if ((charCode & 0xF800) === 0xD800) {
|
|
var extraCharCode = s.charCodeAt(i++);
|
|
if ((charCode & 0xFC00) !== 0xD800 || (extraCharCode & 0xFC00) !== 0xDC00) {
|
|
throw Error('UCS-2(decode): illegal sequence');
|
|
}
|
|
charCode = ((charCode & 0x3FF) << 10) + (extraCharCode & 0x3FF) + 0x10000;
|
|
}
|
|
result += '\\' + charCode.toString(16) + ' ';
|
|
} else {
|
|
result += chr;
|
|
}
|
|
}
|
|
i++;
|
|
}
|
|
return result;
|
|
};
|
|
|
|
CssSelectorParser.prototype.escapeStr = function(s) {
|
|
var result = '';
|
|
var i = 0;
|
|
var len = s.length;
|
|
var chr, replacement;
|
|
while (i < len) {
|
|
chr = s.charAt(i);
|
|
if (chr === '"') {
|
|
chr = '\\"';
|
|
} else if (chr === '\\') {
|
|
chr = '\\\\';
|
|
} else if (replacement = strReplacementsRev[chr]) {
|
|
chr = replacement;
|
|
}
|
|
result += chr;
|
|
i++;
|
|
}
|
|
return "\"" + result + "\"";
|
|
};
|
|
|
|
CssSelectorParser.prototype.render = function(path) {
|
|
return this._renderEntity(path).trim();
|
|
};
|
|
|
|
CssSelectorParser.prototype._renderEntity = function(entity) {
|
|
var currentEntity, parts, res;
|
|
res = '';
|
|
switch (entity.type) {
|
|
case 'ruleSet':
|
|
currentEntity = entity.rule;
|
|
parts = [];
|
|
while (currentEntity) {
|
|
if (currentEntity.nestingOperator) {
|
|
parts.push(currentEntity.nestingOperator);
|
|
}
|
|
parts.push(this._renderEntity(currentEntity));
|
|
currentEntity = currentEntity.rule;
|
|
}
|
|
res = parts.join(' ');
|
|
break;
|
|
case 'selectors':
|
|
res = entity.selectors.map(this._renderEntity, this).join(', ');
|
|
break;
|
|
case 'rule':
|
|
if (entity.tagName) {
|
|
if (entity.tagName === '*') {
|
|
res = '*';
|
|
} else {
|
|
res = this.escapeIdentifier(entity.tagName);
|
|
}
|
|
}
|
|
if (entity.id) {
|
|
res += "#" + this.escapeIdentifier(entity.id);
|
|
}
|
|
if (entity.classNames) {
|
|
res += entity.classNames.map(function(cn) {
|
|
return "." + (this.escapeIdentifier(cn));
|
|
}, this).join('');
|
|
}
|
|
if (entity.attrs) {
|
|
res += entity.attrs.map(function(attr) {
|
|
if (attr.operator) {
|
|
if (attr.valueType === 'substitute') {
|
|
return "[" + this.escapeIdentifier(attr.name) + attr.operator + "$" + attr.value + "]";
|
|
} else {
|
|
return "[" + this.escapeIdentifier(attr.name) + attr.operator + this.escapeStr(attr.value) + "]";
|
|
}
|
|
} else {
|
|
return "[" + this.escapeIdentifier(attr.name) + "]";
|
|
}
|
|
}, this).join('');
|
|
}
|
|
if (entity.pseudos) {
|
|
res += entity.pseudos.map(function(pseudo) {
|
|
if (pseudo.valueType) {
|
|
if (pseudo.valueType === 'selector') {
|
|
return ":" + this.escapeIdentifier(pseudo.name) + "(" + this._renderEntity(pseudo.value) + ")";
|
|
} else if (pseudo.valueType === 'substitute') {
|
|
return ":" + this.escapeIdentifier(pseudo.name) + "($" + pseudo.value + ")";
|
|
} else if (pseudo.valueType === 'numeric') {
|
|
return ":" + this.escapeIdentifier(pseudo.name) + "(" + pseudo.value + ")";
|
|
} else {
|
|
return ":" + this.escapeIdentifier(pseudo.name) + "(" + this.escapeIdentifier(pseudo.value) + ")";
|
|
}
|
|
} else {
|
|
return ":" + this.escapeIdentifier(pseudo.name);
|
|
}
|
|
}, this).join('');
|
|
}
|
|
break;
|
|
default:
|
|
throw Error('Unknown entity type: "' + entity.type(+'".'));
|
|
}
|
|
return res;
|
|
};
|
|
|
|
exports.CssSelectorParser = CssSelectorParser;
|