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;