UNPKG

@acemir/cssom

Version:

CSS Object Model implementation and CSS parser

1,888 lines (1,620 loc) 78.8 kB
var CSSOM = {}; // NOTE: Check viability to add a validation for css values or use a dependency like csstree-validator /** * Regular expression to detect invalid characters in the value portion of a CSS style declaration. * * This regex matches a colon (:) that is not inside parentheses and not inside single or double quotes. * It is used to ensure that the value part of a CSS property does not contain unexpected colons, * which would indicate a malformed declaration (e.g., "color: foo:bar;" is invalid). * * The negative lookahead `(?![^(]*\))` ensures that the colon is not followed by a closing * parenthesis without encountering an opening parenthesis, effectively ignoring colons inside * function-like values (e.g., `url(data:image/png;base64,...)`). * * The lookahead `(?=(?:[^'"]|'[^']*'|"[^"]*")*$)` ensures that the colon is not inside single or double quotes, * allowing colons within quoted strings (e.g., `content: ":";` or `background: url("foo:bar.png");`). * * Example: * "color: red;" // valid, does not match * "background: url(data:image/png;base64,...);" // valid, does not match * "content: ':';" // valid, does not match * "color: foo:bar;" // invalid, matches */ var basicStylePropertyValueValidationRegExp = /:(?![^(]*\))(?=(?:[^'"]|'[^']*'|"[^"]*")*$)/; /** * @constructor * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration */ CSSOM.CSSStyleDeclaration = function CSSStyleDeclaration(){ this.length = 0; this.parentRule = null; // NON-STANDARD this._importants = {}; }; CSSOM.CSSStyleDeclaration.prototype = { constructor: CSSOM.CSSStyleDeclaration, /** * * @param {string} name * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration-getPropertyValue * @return {string} the value of the property if it has been explicitly set for this declaration block. * Returns the empty string if the property has not been set. */ getPropertyValue: function(name) { return this[name] || ""; }, /** * * @param {string} name * @param {string} value * @param {string} [priority=null] "important" or null * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration-setProperty */ setProperty: function(name, value, priority, parseErrorHandler) { // NOTE: Check viability to add a validation for css values or use a dependency like csstree-validator if (basicStylePropertyValueValidationRegExp.test(value)) { parseErrorHandler && parseErrorHandler('Invalid CSSStyleDeclaration property (name = "' + name + '", value = "' + value + '")'); } else if (this[name]) { // Property already exist. Overwrite it. var index = Array.prototype.indexOf.call(this, name); if (index < 0) { this[this.length] = name; this.length++; } // If the priority value of the incoming property is "important", // or the value of the existing property is not "important", // then remove the existing property and rewrite it. if (priority || !this._importants[name]) { this.removeProperty(name); this[this.length] = name; this.length++; this[name] = value + ''; this._importants[name] = priority; } } else { // New property. this[this.length] = name; this.length++; this[name] = value + ''; this._importants[name] = priority; } }, /** * * @param {string} name * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration-removeProperty * @return {string} the value of the property if it has been explicitly set for this declaration block. * Returns the empty string if the property has not been set or the property name does not correspond to a known CSS property. */ removeProperty: function(name) { if (!(name in this)) { return ""; } var index = Array.prototype.indexOf.call(this, name); if (index < 0) { return ""; } var prevValue = this[name]; this[name] = ""; // That's what WebKit and Opera do Array.prototype.splice.call(this, index, 1); // That's what Firefox does //this[index] = "" return prevValue; }, getPropertyCSSValue: function() { //FIXME }, /** * * @param {String} name */ getPropertyPriority: function(name) { return this._importants[name] || ""; }, /** * element.style.overflow = "auto" * element.style.getPropertyShorthand("overflow-x") * -> "overflow" */ getPropertyShorthand: function() { //FIXME }, isPropertyImplicit: function() { //FIXME }, // Doesn't work in IE < 9 get cssText(){ var properties = []; for (var i=0, length=this.length; i < length; ++i) { var name = this[i]; var value = this.getPropertyValue(name); var priority = this.getPropertyPriority(name); if (priority) { priority = " !" + priority; } properties[i] = name + ": " + value + priority + ";"; } return properties.join(" "); }, set cssText(text){ var i, name; for (i = this.length; i--;) { name = this[i]; this[name] = ""; } Array.prototype.splice.call(this, 0, this.length); this._importants = {}; var dummyRule = CSSOM.parse('#bogus{' + text + '}').cssRules[0].style; var length = dummyRule.length; for (i = 0; i < length; ++i) { name = dummyRule[i]; this.setProperty(dummyRule[i], dummyRule.getPropertyValue(name), dummyRule.getPropertyPriority(name)); } } }; /** * @constructor * @see http://dev.w3.org/csswg/cssom/#the-cssrule-interface * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSRule */ CSSOM.CSSRule = function CSSRule() { this.parentRule = null; this.parentStyleSheet = null; }; CSSOM.CSSRule.UNKNOWN_RULE = 0; // obsolete CSSOM.CSSRule.STYLE_RULE = 1; CSSOM.CSSRule.CHARSET_RULE = 2; // obsolete CSSOM.CSSRule.IMPORT_RULE = 3; CSSOM.CSSRule.MEDIA_RULE = 4; CSSOM.CSSRule.FONT_FACE_RULE = 5; CSSOM.CSSRule.PAGE_RULE = 6; CSSOM.CSSRule.KEYFRAMES_RULE = 7; CSSOM.CSSRule.KEYFRAME_RULE = 8; CSSOM.CSSRule.MARGIN_RULE = 9; CSSOM.CSSRule.NAMESPACE_RULE = 10; CSSOM.CSSRule.COUNTER_STYLE_RULE = 11; CSSOM.CSSRule.SUPPORTS_RULE = 12; CSSOM.CSSRule.DOCUMENT_RULE = 13; CSSOM.CSSRule.FONT_FEATURE_VALUES_RULE = 14; CSSOM.CSSRule.VIEWPORT_RULE = 15; CSSOM.CSSRule.REGION_STYLE_RULE = 16; CSSOM.CSSRule.CONTAINER_RULE = 17; CSSOM.CSSRule.LAYER_BLOCK_RULE = 18; CSSOM.CSSRule.STARTING_STYLE_RULE = 1002; CSSOM.CSSRule.prototype = { constructor: CSSOM.CSSRule, //FIXME }; exports.CSSRule = CSSOM.CSSRule; ///CommonJS /** * @constructor * @see https://drafts.csswg.org/css-nesting-1/ */ CSSOM.CSSNestedDeclarations = function CSSNestedDeclarations() { CSSOM.CSSRule.call(this); this.style = new CSSOM.CSSStyleDeclaration(); this.style.parentRule = this; }; CSSOM.CSSNestedDeclarations.prototype = new CSSOM.CSSRule(); CSSOM.CSSNestedDeclarations.prototype.constructor = CSSOM.CSSNestedDeclarations; CSSOM.CSSNestedDeclarations.prototype.type = 0; Object.defineProperty(CSSOM.CSSNestedDeclarations.prototype, "cssText", { get: function () { return this.style.cssText; }, configurable: true, enumerable: true, }); /** * @constructor * @see https://drafts.csswg.org/cssom/#the-cssgroupingrule-interface */ CSSOM.CSSGroupingRule = function CSSGroupingRule() { CSSOM.CSSRule.call(this); this.cssRules = []; }; CSSOM.CSSGroupingRule.prototype = new CSSOM.CSSRule(); CSSOM.CSSGroupingRule.prototype.constructor = CSSOM.CSSGroupingRule; /** * Used to insert a new CSS rule to a list of CSS rules. * * @example * cssGroupingRule.cssText * -> "body{margin:0;}" * cssGroupingRule.insertRule("img{border:none;}", 1) * -> 1 * cssGroupingRule.cssText * -> "body{margin:0;}img{border:none;}" * * @param {string} rule * @param {number} [index] * @see https://www.w3.org/TR/cssom-1/#dom-cssgroupingrule-insertrule * @return {number} The index within the grouping rule's collection of the newly inserted rule. */ CSSOM.CSSGroupingRule.prototype.insertRule = function insertRule(rule, index) { if (index < 0 || index > this.cssRules.length) { throw new RangeError("INDEX_SIZE_ERR"); } var cssRule = CSSOM.parse(rule).cssRules[0]; cssRule.parentRule = this; this.cssRules.splice(index, 0, cssRule); return index; }; /** * Used to delete a rule from the grouping rule. * * cssGroupingRule.cssText * -> "img{border:none;}body{margin:0;}" * cssGroupingRule.deleteRule(0) * cssGroupingRule.cssText * -> "body{margin:0;}" * * @param {number} index within the grouping rule's rule list of the rule to remove. * @see https://www.w3.org/TR/cssom-1/#dom-cssgroupingrule-deleterule */ CSSOM.CSSGroupingRule.prototype.deleteRule = function deleteRule(index) { if (index < 0 || index >= this.cssRules.length) { throw new RangeError("INDEX_SIZE_ERR"); } this.cssRules.splice(index, 1)[0].parentRule = null; }; /** * @constructor * @see https://drafts.csswg.org/css-counter-styles/#the-csscounterstylerule-interface */ CSSOM.CSSCounterStyleRule = function CSSCounterStyleRule() { CSSOM.CSSRule.call(this); this.name = ""; }; CSSOM.CSSCounterStyleRule.prototype = new CSSOM.CSSRule(); CSSOM.CSSCounterStyleRule.prototype.constructor = CSSOM.CSSCounterStyleRule; CSSOM.CSSCounterStyleRule.prototype.type = 11; /** * @constructor * @see https://www.w3.org/TR/css-conditional-3/#the-cssconditionrule-interface */ CSSOM.CSSConditionRule = function CSSConditionRule() { CSSOM.CSSGroupingRule.call(this); this.cssRules = []; }; CSSOM.CSSConditionRule.prototype = new CSSOM.CSSGroupingRule(); CSSOM.CSSConditionRule.prototype.constructor = CSSOM.CSSConditionRule; CSSOM.CSSConditionRule.prototype.conditionText = '' CSSOM.CSSConditionRule.prototype.cssText = '' /** * @constructor * @see http://dev.w3.org/csswg/cssom/#cssstylerule * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleRule */ CSSOM.CSSStyleRule = function CSSStyleRule() { CSSOM.CSSGroupingRule.call(this); this.selectorText = ""; this.style = new CSSOM.CSSStyleDeclaration(); this.style.parentRule = this; }; CSSOM.CSSStyleRule.prototype = new CSSOM.CSSGroupingRule(); CSSOM.CSSStyleRule.prototype.constructor = CSSOM.CSSStyleRule; CSSOM.CSSStyleRule.prototype.type = 1; Object.defineProperty(CSSOM.CSSStyleRule.prototype, "cssText", { get: function() { var text; if (this.selectorText) { var values = "" if (this.cssRules.length) { var valuesArr = [" {"]; this.style.cssText && valuesArr.push(this.style.cssText); valuesArr.push(this.cssRules.map(function(rule){ return rule.cssText }).join("\n ")); values = valuesArr.join("\n ") + "\n}" } else { values = " {" + this.style.cssText + "}"; } text = this.selectorText + values; } else { text = ""; } return text; }, set: function(cssText) { var rule = CSSOM.CSSStyleRule.parse(cssText); this.style = rule.style; this.selectorText = rule.selectorText; } }); /** * NON-STANDARD * lightweight version of parse.js. * @param {string} ruleText * @return CSSStyleRule */ CSSOM.CSSStyleRule.parse = function(ruleText) { var i = 0; var state = "selector"; var index; var j = i; var buffer = ""; var SIGNIFICANT_WHITESPACE = { "selector": true, "value": true }; var styleRule = new CSSOM.CSSStyleRule(); var name, priority=""; for (var character; (character = ruleText.charAt(i)); i++) { switch (character) { case " ": case "\t": case "\r": case "\n": case "\f": if (SIGNIFICANT_WHITESPACE[state]) { // Squash 2 or more white-spaces in the row into 1 switch (ruleText.charAt(i - 1)) { case " ": case "\t": case "\r": case "\n": case "\f": break; default: buffer += " "; break; } } break; // String case '"': j = i + 1; index = ruleText.indexOf('"', j) + 1; if (!index) { throw '" is missing'; } buffer += ruleText.slice(i, index); i = index - 1; break; case "'": j = i + 1; index = ruleText.indexOf("'", j) + 1; if (!index) { throw "' is missing"; } buffer += ruleText.slice(i, index); i = index - 1; break; // Comment case "/": if (ruleText.charAt(i + 1) === "*") { i += 2; index = ruleText.indexOf("*/", i); if (index === -1) { throw new SyntaxError("Missing */"); } else { i = index + 1; } } else { buffer += character; } break; case "{": if (state === "selector") { styleRule.selectorText = buffer.trim(); buffer = ""; state = "name"; } break; case ":": if (state === "name") { name = buffer.trim(); buffer = ""; state = "value"; } else { buffer += character; } break; case "!": if (state === "value" && ruleText.indexOf("!important", i) === i) { priority = "important"; i += "important".length; } else { buffer += character; } break; case ";": if (state === "value") { styleRule.style.setProperty(name, buffer.trim(), priority); priority = ""; buffer = ""; state = "name"; } else { buffer += character; } break; case "}": if (state === "value") { styleRule.style.setProperty(name, buffer.trim(), priority); priority = ""; buffer = ""; } else if (state === "name") { break; } else { buffer += character; } state = "selector"; break; default: buffer += character; break; } } return styleRule; }; /** * @constructor * @see http://dev.w3.org/csswg/cssom/#the-medialist-interface */ CSSOM.MediaList = function MediaList(){ this.length = 0; }; CSSOM.MediaList.prototype = { constructor: CSSOM.MediaList, /** * @return {string} */ get mediaText() { return Array.prototype.join.call(this, ", "); }, /** * @param {string} value */ set mediaText(value) { var values = value.split(","); var length = this.length = values.length; for (var i=0; i<length; i++) { this[i] = values[i].trim(); } }, /** * @param {string} medium */ appendMedium: function(medium) { if (Array.prototype.indexOf.call(this, medium) === -1) { this[this.length] = medium; this.length++; } }, /** * @param {string} medium */ deleteMedium: function(medium) { var index = Array.prototype.indexOf.call(this, medium); if (index !== -1) { Array.prototype.splice.call(this, index, 1); } } }; /** * @constructor * @see http://dev.w3.org/csswg/cssom/#cssmediarule * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSMediaRule */ CSSOM.CSSMediaRule = function CSSMediaRule() { CSSOM.CSSConditionRule.call(this); this.media = new CSSOM.MediaList(); }; CSSOM.CSSMediaRule.prototype = new CSSOM.CSSConditionRule(); CSSOM.CSSMediaRule.prototype.constructor = CSSOM.CSSMediaRule; CSSOM.CSSMediaRule.prototype.type = 4; // https://opensource.apple.com/source/WebCore/WebCore-7611.1.21.161.3/css/CSSMediaRule.cpp Object.defineProperties(CSSOM.CSSMediaRule.prototype, { "conditionText": { get: function() { return this.media.mediaText; }, set: function(value) { this.media.mediaText = value; }, configurable: true, enumerable: true }, "cssText": { get: function() { var cssTexts = []; for (var i=0, length=this.cssRules.length; i < length; i++) { cssTexts.push(this.cssRules[i].cssText); } return "@media " + this.media.mediaText + " {" + cssTexts.join("") + "}"; }, configurable: true, enumerable: true } }); /** * @constructor * @see https://drafts.csswg.org/css-contain-3/ * @see https://www.w3.org/TR/css-contain-3/ */ CSSOM.CSSContainerRule = function CSSContainerRule() { CSSOM.CSSConditionRule.call(this); }; CSSOM.CSSContainerRule.prototype = new CSSOM.CSSConditionRule(); CSSOM.CSSContainerRule.prototype.constructor = CSSOM.CSSContainerRule; CSSOM.CSSContainerRule.prototype.type = 17; Object.defineProperties(CSSOM.CSSContainerRule.prototype, { "conditionText": { get: function() { return this.containerText; }, set: function(value) { this.containerText = value; }, configurable: true, enumerable: true }, "cssText": { get: function() { var cssTexts = []; for (var i=0, length=this.cssRules.length; i < length; i++) { cssTexts.push(this.cssRules[i].cssText); } return "@container " + this.containerText + " {" + cssTexts.join("") + "}"; }, configurable: true, enumerable: true } }); /** * @constructor * @see https://drafts.csswg.org/css-conditional-3/#the-csssupportsrule-interface */ CSSOM.CSSSupportsRule = function CSSSupportsRule() { CSSOM.CSSConditionRule.call(this); }; CSSOM.CSSSupportsRule.prototype = new CSSOM.CSSConditionRule(); CSSOM.CSSSupportsRule.prototype.constructor = CSSOM.CSSSupportsRule; CSSOM.CSSSupportsRule.prototype.type = 12; Object.defineProperty(CSSOM.CSSSupportsRule.prototype, "cssText", { get: function() { var cssTexts = []; for (var i = 0, length = this.cssRules.length; i < length; i++) { cssTexts.push(this.cssRules[i].cssText); } return "@supports " + this.conditionText + " {" + cssTexts.join("") + "}"; } }); /** * @constructor * @see http://dev.w3.org/csswg/cssom/#cssimportrule * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSImportRule */ CSSOM.CSSImportRule = function CSSImportRule() { CSSOM.CSSRule.call(this); this.href = ""; this.media = new CSSOM.MediaList(); this.layerName = null; this.supportsText = null; this.styleSheet = new CSSOM.CSSStyleSheet(); }; CSSOM.CSSImportRule.prototype = new CSSOM.CSSRule(); CSSOM.CSSImportRule.prototype.constructor = CSSOM.CSSImportRule; CSSOM.CSSImportRule.prototype.type = 3; Object.defineProperty(CSSOM.CSSImportRule.prototype, "cssText", { get: function() { var mediaText = this.media.mediaText; return "@import url(" + this.href + ")" + (this.layerName !== null ? " layer" + (this.layerName && "(" + this.layerName + ")") : "" ) + (this.supportsText ? " supports(" + this.supportsText + ")" : "" ) + (mediaText ? " " + mediaText : "") + ";"; }, set: function(cssText) { var i = 0; /** * @import url(partial.css) screen, handheld; * || | * after-import media * | * url */ var state = ''; var buffer = ''; var index; var layerRegExp = /layer\(([^)]*)\)/; var layerRuleNameRegExp = /^(-?[_a-zA-Z]+[_a-zA-Z0-9-]*)$/; var supportsRegExp = /supports\(([^)]+)\)/; var doubleOrMoreSpacesRegExp = /\s{2,}/g; for (var character; (character = cssText.charAt(i)); i++) { switch (character) { case ' ': case '\t': case '\r': case '\n': case '\f': if (state === 'after-import') { state = 'url'; } else { buffer += character; } break; case '@': if (!state && cssText.indexOf('@import', i) === i) { state = 'after-import'; i += 'import'.length; buffer = ''; } break; case 'u': if (state === 'media') { buffer += character; } if (state === 'url' && cssText.indexOf('url(', i) === i) { index = cssText.indexOf(')', i + 1); if (index === -1) { throw i + ': ")" not found'; } i += 'url('.length; var url = cssText.slice(i, index); if (url[0] === url[url.length - 1]) { if (url[0] === '"' || url[0] === "'") { url = url.slice(1, -1); } } this.href = url; i = index; state = 'media'; } break; case '"': if (state === 'after-import' || state === 'url') { index = cssText.indexOf('"', i + 1); if (!index) { throw i + ": '\"' not found"; } this.href = cssText.slice(i + 1, index); i = index; state = 'media'; } break; case "'": if (state === 'after-import' || state === 'url') { index = cssText.indexOf("'", i + 1); if (!index) { throw i + ': "\'" not found'; } this.href = cssText.slice(i + 1, index); i = index; state = 'media'; } break; case ';': if (state === 'media') { if (buffer) { var bufferTrimmed = buffer.trim(); if (bufferTrimmed.indexOf('layer') === 0) { var layerMatch = bufferTrimmed.match(layerRegExp); if (layerMatch) { var layerName = layerMatch[1].trim(); bufferTrimmed = bufferTrimmed.replace(layerRegExp, '') .replace(doubleOrMoreSpacesRegExp, ' ') // Replace double or more spaces with single space .trim(); if (layerName.match(layerRuleNameRegExp) !== null) { this.layerName = layerMatch[1].trim(); } else { // REVIEW: In the browser, an empty layer() is not processed as a unamed layer // and treats the rest of the string as mediaText, ignoring the parse of supports() if (bufferTrimmed) { this.media.mediaText = bufferTrimmed; return; } } } else { this.layerName = ""; bufferTrimmed = bufferTrimmed.substring('layer'.length).trim() } } var supportsMatch = bufferTrimmed.match(supportsRegExp); if (supportsMatch && supportsMatch.index === 0) { // REVIEW: In the browser, an empty supports() invalidates and ignores the entire @import rule this.supportsText = supportsMatch[1].trim(); bufferTrimmed = bufferTrimmed.replace(supportsRegExp, '') .replace(doubleOrMoreSpacesRegExp, ' ') // Replace double or more spaces with single space .trim(); } // REVIEW: In the browser, any invalid media is replaced with 'not all' if (bufferTrimmed) { this.media.mediaText = bufferTrimmed; } } } break; default: if (state === 'media') { buffer += character; } break; } } } }); /** * @constructor * @see http://dev.w3.org/csswg/cssom/#css-font-face-rule */ CSSOM.CSSFontFaceRule = function CSSFontFaceRule() { CSSOM.CSSRule.call(this); this.style = new CSSOM.CSSStyleDeclaration(); this.style.parentRule = this; }; CSSOM.CSSFontFaceRule.prototype = new CSSOM.CSSRule(); CSSOM.CSSFontFaceRule.prototype.constructor = CSSOM.CSSFontFaceRule; CSSOM.CSSFontFaceRule.prototype.type = 5; //FIXME //CSSOM.CSSFontFaceRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule; //CSSOM.CSSFontFaceRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule; // http://www.opensource.apple.com/source/WebCore/WebCore-955.66.1/css/WebKitCSSFontFaceRule.cpp Object.defineProperty(CSSOM.CSSFontFaceRule.prototype, "cssText", { get: function() { return "@font-face {" + this.style.cssText + "}"; } }); /** * @constructor * @see http://www.w3.org/TR/shadow-dom/#host-at-rule */ CSSOM.CSSHostRule = function CSSHostRule() { CSSOM.CSSRule.call(this); this.cssRules = []; }; CSSOM.CSSHostRule.prototype = new CSSOM.CSSRule(); CSSOM.CSSHostRule.prototype.constructor = CSSOM.CSSHostRule; CSSOM.CSSHostRule.prototype.type = 1001; //FIXME //CSSOM.CSSHostRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule; //CSSOM.CSSHostRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule; Object.defineProperty(CSSOM.CSSHostRule.prototype, "cssText", { get: function() { var cssTexts = []; for (var i=0, length=this.cssRules.length; i < length; i++) { cssTexts.push(this.cssRules[i].cssText); } return "@host {" + cssTexts.join("") + "}"; } }); /** * @constructor * @see http://www.w3.org/TR/shadow-dom/#host-at-rule */ CSSOM.CSSStartingStyleRule = function CSSStartingStyleRule() { CSSOM.CSSRule.call(this); this.cssRules = []; }; CSSOM.CSSStartingStyleRule.prototype = new CSSOM.CSSRule(); CSSOM.CSSStartingStyleRule.prototype.constructor = CSSOM.CSSStartingStyleRule; CSSOM.CSSStartingStyleRule.prototype.type = 1002; //FIXME //CSSOM.CSSStartingStyleRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule; //CSSOM.CSSStartingStyleRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule; Object.defineProperty(CSSOM.CSSStartingStyleRule.prototype, "cssText", { get: function() { var cssTexts = []; for (var i=0, length=this.cssRules.length; i < length; i++) { cssTexts.push(this.cssRules[i].cssText); } return "@starting-style {" + cssTexts.join("") + "}"; } }); /** * @constructor * @see http://dev.w3.org/csswg/cssom/#the-stylesheet-interface */ CSSOM.StyleSheet = function StyleSheet() { this.parentStyleSheet = null; }; /** * @constructor * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet */ CSSOM.CSSStyleSheet = function CSSStyleSheet() { CSSOM.StyleSheet.call(this); this.cssRules = []; }; CSSOM.CSSStyleSheet.prototype = new CSSOM.StyleSheet(); CSSOM.CSSStyleSheet.prototype.constructor = CSSOM.CSSStyleSheet; /** * Used to insert a new rule into the style sheet. The new rule now becomes part of the cascade. * * sheet = new Sheet("body {margin: 0}") * sheet.toString() * -> "body{margin:0;}" * sheet.insertRule("img {border: none}", 0) * -> 0 * sheet.toString() * -> "img{border:none;}body{margin:0;}" * * @param {string} rule * @param {number} [index=0] * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet-insertRule * @return {number} The index within the style sheet's rule collection of the newly inserted rule. */ CSSOM.CSSStyleSheet.prototype.insertRule = function(rule, index) { if (index === void 0) { index = 0; } if (index < 0 || index > this.cssRules.length) { throw new RangeError("INDEX_SIZE_ERR"); } var cssRule = CSSOM.parse(rule).cssRules[0]; cssRule.parentStyleSheet = this; this.cssRules.splice(index, 0, cssRule); return index; }; /** * Used to delete a rule from the style sheet. * * sheet = new Sheet("img{border:none} body{margin:0}") * sheet.toString() * -> "img{border:none;}body{margin:0;}" * sheet.deleteRule(0) * sheet.toString() * -> "body{margin:0;}" * * @param {number} index within the style sheet's rule list of the rule to remove. * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet-deleteRule */ CSSOM.CSSStyleSheet.prototype.deleteRule = function(index) { if (index < 0 || index >= this.cssRules.length) { throw new RangeError("INDEX_SIZE_ERR"); } this.cssRules.splice(index, 1); }; /** * NON-STANDARD * @return {string} serialize stylesheet */ CSSOM.CSSStyleSheet.prototype.toString = function() { var result = ""; var rules = this.cssRules; for (var i=0; i<rules.length; i++) { result += rules[i].cssText + "\n"; } return result; }; /** * @constructor * @see http://www.w3.org/TR/css3-animations/#DOM-CSSKeyframesRule */ CSSOM.CSSKeyframesRule = function CSSKeyframesRule() { CSSOM.CSSRule.call(this); this.name = ''; this.cssRules = []; }; CSSOM.CSSKeyframesRule.prototype = new CSSOM.CSSRule(); CSSOM.CSSKeyframesRule.prototype.constructor = CSSOM.CSSKeyframesRule; CSSOM.CSSKeyframesRule.prototype.type = 7; //FIXME //CSSOM.CSSKeyframesRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule; //CSSOM.CSSKeyframesRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule; // http://www.opensource.apple.com/source/WebCore/WebCore-955.66.1/css/WebKitCSSKeyframesRule.cpp Object.defineProperty(CSSOM.CSSKeyframesRule.prototype, "cssText", { get: function() { var cssTexts = []; for (var i=0, length=this.cssRules.length; i < length; i++) { cssTexts.push(" " + this.cssRules[i].cssText); } return "@" + (this._vendorPrefix || '') + "keyframes " + this.name + " { \n" + cssTexts.join("\n") + "\n}"; } }); /** * @constructor * @see http://www.w3.org/TR/css3-animations/#DOM-CSSKeyframeRule */ CSSOM.CSSKeyframeRule = function CSSKeyframeRule() { CSSOM.CSSRule.call(this); this.keyText = ''; this.style = new CSSOM.CSSStyleDeclaration(); this.style.parentRule = this; }; CSSOM.CSSKeyframeRule.prototype = new CSSOM.CSSRule(); CSSOM.CSSKeyframeRule.prototype.constructor = CSSOM.CSSKeyframeRule; CSSOM.CSSKeyframeRule.prototype.type = 8; //FIXME //CSSOM.CSSKeyframeRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule; //CSSOM.CSSKeyframeRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule; // http://www.opensource.apple.com/source/WebCore/WebCore-955.66.1/css/WebKitCSSKeyframeRule.cpp Object.defineProperty(CSSOM.CSSKeyframeRule.prototype, "cssText", { get: function() { return this.keyText + " {" + this.style.cssText + "} "; } }); /** * @constructor * @see https://developer.mozilla.org/en/CSS/@-moz-document */ CSSOM.MatcherList = function MatcherList(){ this.length = 0; }; CSSOM.MatcherList.prototype = { constructor: CSSOM.MatcherList, /** * @return {string} */ get matcherText() { return Array.prototype.join.call(this, ", "); }, /** * @param {string} value */ set matcherText(value) { // just a temporary solution, actually it may be wrong by just split the value with ',', because a url can include ','. var values = value.split(","); var length = this.length = values.length; for (var i=0; i<length; i++) { this[i] = values[i].trim(); } }, /** * @param {string} matcher */ appendMatcher: function(matcher) { if (Array.prototype.indexOf.call(this, matcher) === -1) { this[this.length] = matcher; this.length++; } }, /** * @param {string} matcher */ deleteMatcher: function(matcher) { var index = Array.prototype.indexOf.call(this, matcher); if (index !== -1) { Array.prototype.splice.call(this, index, 1); } } }; /** * @constructor * @see https://developer.mozilla.org/en/CSS/@-moz-document */ CSSOM.CSSDocumentRule = function CSSDocumentRule() { CSSOM.CSSRule.call(this); this.matcher = new CSSOM.MatcherList(); this.cssRules = []; }; CSSOM.CSSDocumentRule.prototype = new CSSOM.CSSRule(); CSSOM.CSSDocumentRule.prototype.constructor = CSSOM.CSSDocumentRule; CSSOM.CSSDocumentRule.prototype.type = 10; //FIXME //CSSOM.CSSDocumentRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule; //CSSOM.CSSDocumentRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule; Object.defineProperty(CSSOM.CSSDocumentRule.prototype, "cssText", { get: function() { var cssTexts = []; for (var i=0, length=this.cssRules.length; i < length; i++) { cssTexts.push(this.cssRules[i].cssText); } return "@-moz-document " + this.matcher.matcherText + " {" + cssTexts.join("") + "}"; } }); /** * @constructor * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSValue * * TODO: add if needed */ CSSOM.CSSValue = function CSSValue() { }; CSSOM.CSSValue.prototype = { constructor: CSSOM.CSSValue, // @see: http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSValue set cssText(text) { var name = this._getConstructorName(); throw new Error('DOMException: property "cssText" of "' + name + '" is readonly and can not be replaced with "' + text + '"!'); }, get cssText() { var name = this._getConstructorName(); throw new Error('getter "cssText" of "' + name + '" is not implemented!'); }, _getConstructorName: function() { var s = this.constructor.toString(), c = s.match(/function\s([^\(]+)/), name = c[1]; return name; } }; /** * @constructor * @see http://msdn.microsoft.com/en-us/library/ms537634(v=vs.85).aspx * */ CSSOM.CSSValueExpression = function CSSValueExpression(token, idx) { this._token = token; this._idx = idx; }; CSSOM.CSSValueExpression.prototype = new CSSOM.CSSValue(); CSSOM.CSSValueExpression.prototype.constructor = CSSOM.CSSValueExpression; /** * parse css expression() value * * @return {Object} * - error: * or * - idx: * - expression: * * Example: * * .selector { * zoom: expression(documentElement.clientWidth > 1000 ? '1000px' : 'auto'); * } */ CSSOM.CSSValueExpression.prototype.parse = function() { var token = this._token, idx = this._idx; var character = '', expression = '', error = '', info, paren = []; for (; ; ++idx) { character = token.charAt(idx); // end of token if (character === '') { error = 'css expression error: unfinished expression!'; break; } switch(character) { case '(': paren.push(character); expression += character; break; case ')': paren.pop(character); expression += character; break; case '/': if ((info = this._parseJSComment(token, idx))) { // comment? if (info.error) { error = 'css expression error: unfinished comment in expression!'; } else { idx = info.idx; // ignore the comment } } else if ((info = this._parseJSRexExp(token, idx))) { // regexp idx = info.idx; expression += info.text; } else { // other expression += character; } break; case "'": case '"': info = this._parseJSString(token, idx, character); if (info) { // string idx = info.idx; expression += info.text; } else { expression += character; } break; default: expression += character; break; } if (error) { break; } // end of expression if (paren.length === 0) { break; } } var ret; if (error) { ret = { error: error }; } else { ret = { idx: idx, expression: expression }; } return ret; }; /** * * @return {Object|false} * - idx: * - text: * or * - error: * or * false * */ CSSOM.CSSValueExpression.prototype._parseJSComment = function(token, idx) { var nextChar = token.charAt(idx + 1), text; if (nextChar === '/' || nextChar === '*') { var startIdx = idx, endIdx, commentEndChar; if (nextChar === '/') { // line comment commentEndChar = '\n'; } else if (nextChar === '*') { // block comment commentEndChar = '*/'; } endIdx = token.indexOf(commentEndChar, startIdx + 1 + 1); if (endIdx !== -1) { endIdx = endIdx + commentEndChar.length - 1; text = token.substring(idx, endIdx + 1); return { idx: endIdx, text: text }; } else { var error = 'css expression error: unfinished comment in expression!'; return { error: error }; } } else { return false; } }; /** * * @return {Object|false} * - idx: * - text: * or * false * */ CSSOM.CSSValueExpression.prototype._parseJSString = function(token, idx, sep) { var endIdx = this._findMatchedIdx(token, idx, sep), text; if (endIdx === -1) { return false; } else { text = token.substring(idx, endIdx + sep.length); return { idx: endIdx, text: text }; } }; /** * parse regexp in css expression * * @return {Object|false} * - idx: * - regExp: * or * false */ /* all legal RegExp /a/ (/a/) [/a/] [12, /a/] !/a/ +/a/ -/a/ * /a/ / /a/ %/a/ ===/a/ !==/a/ ==/a/ !=/a/ >/a/ >=/a/ </a/ <=/a/ &/a/ |/a/ ^/a/ ~/a/ <</a/ >>/a/ >>>/a/ &&/a/ ||/a/ ?/a/ =/a/ ,/a/ delete /a/ in /a/ instanceof /a/ new /a/ typeof /a/ void /a/ */ CSSOM.CSSValueExpression.prototype._parseJSRexExp = function(token, idx) { var before = token.substring(0, idx).replace(/\s+$/, ""), legalRegx = [ /^$/, /\($/, /\[$/, /\!$/, /\+$/, /\-$/, /\*$/, /\/\s+/, /\%$/, /\=$/, /\>$/, /<$/, /\&$/, /\|$/, /\^$/, /\~$/, /\?$/, /\,$/, /delete$/, /in$/, /instanceof$/, /new$/, /typeof$/, /void$/ ]; var isLegal = legalRegx.some(function(reg) { return reg.test(before); }); if (!isLegal) { return false; } else { var sep = '/'; // same logic as string return this._parseJSString(token, idx, sep); } }; /** * * find next sep(same line) index in `token` * * @return {Number} * */ CSSOM.CSSValueExpression.prototype._findMatchedIdx = function(token, idx, sep) { var startIdx = idx, endIdx; var NOT_FOUND = -1; while(true) { endIdx = token.indexOf(sep, startIdx + 1); if (endIdx === -1) { // not found endIdx = NOT_FOUND; break; } else { var text = token.substring(idx + 1, endIdx), matched = text.match(/\\+$/); if (!matched || matched[0] % 2 === 0) { // not escaped break; } else { startIdx = endIdx; } } } // boundary must be in the same line(js sting or regexp) var nextNewLineIdx = token.indexOf('\n', idx + 1); if (nextNewLineIdx < endIdx) { endIdx = NOT_FOUND; } return endIdx; }; /** * @constructor * @see https://drafts.csswg.org/css-cascade-5/#csslayerblockrule */ CSSOM.CSSLayerBlockRule = function CSSLayerBlockRule() { CSSOM.CSSGroupingRule.call(this); this.name = ""; this.cssRules = []; }; CSSOM.CSSLayerBlockRule.prototype = new CSSOM.CSSGroupingRule(); CSSOM.CSSLayerBlockRule.prototype.constructor = CSSOM.CSSLayerBlockRule; CSSOM.CSSLayerBlockRule.prototype.type = 18; Object.defineProperties(CSSOM.CSSLayerBlockRule.prototype, { cssText: { get: function () { var cssTexts = []; for (var i = 0, length = this.cssRules.length; i < length; i++) { cssTexts.push(this.cssRules[i].cssText); } return "@layer " + this.name + (this.name && " ") + "{" + cssTexts.join("") + "}"; }, configurable: true, enumerable: true, }, }); /** * @constructor * @see https://drafts.csswg.org/css-cascade-5/#csslayerstatementrule */ CSSOM.CSSLayerStatementRule = function CSSLayerStatementRule() { CSSOM.CSSRule.call(this); this.nameList = []; }; CSSOM.CSSLayerStatementRule.prototype = new CSSOM.CSSRule(); CSSOM.CSSLayerStatementRule.prototype.constructor = CSSOM.CSSLayerStatementRule; CSSOM.CSSLayerStatementRule.prototype.type = 0; Object.defineProperties(CSSOM.CSSLayerStatementRule.prototype, { cssText: { get: function () { return "@layer " + this.nameList.join(", ") + ";"; }, configurable: true, enumerable: true, }, }); /** * @param {string} token */ CSSOM.parse = function parse(token, errorHandler) { errorHandler = errorHandler === undefined ? (console && console.error) : errorHandler; var i = 0; /** "before-selector" or "selector" or "atRule" or "atBlock" or "conditionBlock" or "before-name" or "name" or "before-value" or "value" */ var state = "before-selector"; var index; var buffer = ""; var valueParenthesisDepth = 0; var SIGNIFICANT_WHITESPACE = { "name": true, "before-name": true, "selector": true, "value": true, "value-parenthesis": true, "atRule": true, "importRule-begin": true, "importRule": true, "atBlock": true, "containerBlock": true, "conditionBlock": true, "counterStyleBlock": true, 'documentRule-begin': true, "layerBlock": true }; var styleSheet = new CSSOM.CSSStyleSheet(); // @type CSSStyleSheet|CSSMediaRule|CSSContainerRule|CSSSupportsRule|CSSFontFaceRule|CSSKeyframesRule|CSSDocumentRule var currentScope = styleSheet; // @type CSSMediaRule|CSSContainerRule|CSSSupportsRule|CSSKeyframesRule|CSSDocumentRule var parentRule; var ancestorRules = []; var prevScope; var name, priority="", styleRule, mediaRule, containerRule, counterStyleRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule, startingStyleRule, layerBlockRule, layerStatementRule, nestedSelectorRule; var atKeyframesRegExp = /@(-(?:\w+-)+)?keyframes/g; // Match @keyframes and vendor-prefixed @keyframes // Regex above is not ES5 compliant // var atRulesStatemenRegExp = /(?<!{.*)[;}]\s*/; // Match a statement by verifying it finds a semicolon or closing brace not followed by another semicolon or closing brace var beforeRulePortionRegExp = /{(?!.*{)|}(?!.*})|;(?!.*;)|\*\/(?!.*\*\/)/g; // Match the closest allowed character (a opening or closing brace, a semicolon or a comment ending) before the rule var beforeRuleValidationRegExp = /^[\s{};]*(\*\/\s*)?$/; // Match that the portion before the rule is empty or contains only whitespace, semicolons, opening/closing braces, and optionally a comment ending (*/) followed by whitespace var forwardRuleValidationRegExp = /(?:\(|\s|\/\*)/; // Match that the rule is followed by any whitespace, a opening comment or a condition opening parenthesis var forwardImportRuleValidationRegExp = /(?:\s|\/\*|'|")/; // Match that the rule is followed by any whitespace, an opening comment, a single quote or double quote var forwardRuleClosingBraceRegExp = /{[^{}]*}|}/; // Finds the next closing brace of a rule block var forwardRuleSemicolonAndOpeningBraceRegExp = /^.*?({|;)/; // Finds the next semicolon or opening brace after the at-rule var layerRuleNameRegExp = /^(-?[_a-zA-Z]+[_a-zA-Z0-9-]*)$/; // Validates a single @layer name /** * Searches for the first occurrence of a CSS at-rule statement terminator (`;` or `}`) * that is not inside a brace block within the given string. Mimics the behavior of a * regular expression match for such terminators, including any trailing whitespace. * @param {string} str - The string to search for at-rule statement terminators. * @returns {object | null} {0: string, index: number} or null if no match is found. */ function atRulesStatemenRegExpES5Alternative(ruleSlice) { for (var i = 0; i < ruleSlice.length; i++) { var char = ruleSlice[i]; if (char === ';' || char === '}') { // Simulate negative lookbehind: check if there is a { before this position var sliceBefore = ruleSlice.substring(0, i); var openBraceIndex = sliceBefore.indexOf('{'); if (openBraceIndex === -1) { // No { found before, so we treat it as a valid match var match = char; var j = i + 1; while (j < ruleSlice.length && /\s/.test(ruleSlice[j])) { match += ruleSlice[j]; j++; } var matchObj = [match]; matchObj.index = i; matchObj.input = ruleSlice; return matchObj; } } } return null; } /** * Finds the first balanced block (including nested braces) in the string, starting from fromIndex. * Returns an object similar to RegExp.prototype.match output. * @param {string} str - The string to search. * @param {number} [fromIndex=0] - The index to start searching from. * @returns {object|null} - { 0: matchedString, index: startIndex, input: str } or null if not found. */ function matchBalancedBlock(str, fromIndex) { fromIndex = fromIndex || 0; var openIndex = str.indexOf('{', fromIndex); if (openIndex === -1) return null; var depth = 0; for (var i = openIndex; i < str.length; i++) { if (str[i] === '{') { depth++; } else if (str[i] === '}') { depth--; if (depth === 0) { var matchedString = str.slice(openIndex, i + 1); return { 0: matchedString, index: openIndex, input: str }; } } } return null; } /** * Advances the index `i` to skip over a balanced block of curly braces in the given string. * This is typically used to ignore the contents of a CSS rule block. * * @param {number} i - The current index in the string to start searching from. * @param {string} str - The string containing the CSS code. * @param {number} fromIndex - The index in the string where the balanced block search should begin. * @returns {number} The updated index after skipping the balanced block. */ function ignoreBalancedBlock(i, str, fromIndex) { var ruleClosingMatch = matchBalancedBlock(str, fromIndex); if (ruleClosingMatch) { var ignoreRange = ruleClosingMatch.index + ruleClosingMatch[0].length; i+= ignoreRange; if (token.charAt(i) === '}') { i -= 1; } } else { i += str.length; } return i; } var parseError = function(message) { var lines = token.substring(0, i).split('\n'); var lineCount = lines.length; var charCount = lines.pop().length + 1; var error = new Error(message + ' (line ' + lineCount + ', char ' + charCount + ')'); error.line = lineCount; /* jshint sub : true */ error['char'] = charCount; error.styleSheet = styleSheet; // Print the error but continue parsing the sheet try { throw error; } catch(e) { errorHandler && errorHandler(e); } }; var validateAtRule = function(atRuleKey, validCallback, cannotBeNested) { var isValid = false; var sourceRuleRegExp = atRuleKey === "@import" ? forwardImportRuleValidationRegExp : forwardRuleValidationRegExp; var ruleRegExp = new RegExp(atRuleKey + sourceRuleRegExp.source, sourceRuleRegExp.flags); var ruleSlice = token.slice(i); // Not all rules can be nested, if the rule cannot be nested and is in the root scope, do not perform the check var shouldPerformCheck = cannotBeNested && currentScope !== styleSheet ? false : true; // First, check if there is no invalid characters just after the at-rule if (shouldPerformCheck && ruleSlice.search(ruleRegExp) === 0) { // Find the closest allowed character before the at-rule (a opening or closing brace, a semicolon or a comment ending) var beforeSlice = token.slice(0, i); var regexBefore = new RegExp(beforeRulePortionRegExp.source, beforeRulePortionRegExp.flags); var matches = beforeSlice.match(regexBefore); var lastI = matches ? beforeSlice.lastIndexOf(matches[matches.length - 1]) : 0; var toCheckSlice = token.slice(lastI, i); // Check if we don't have any invalid in the portion before the `at-rule` and the closest allowed character var checkedSlice = toCheckSlice.search(beforeRuleValidationRegExp); if (checkedSlice === 0) { isValid = true; } } if (!isValid) { // If it's invalid the browser will simply ignore the entire invalid block // Use regex to find the closing brace of the invalid rule // Regex used above is not ES5 compliant. Using alternative. // var ruleStatementMatch = ruleSlice.match(atRulesStatemenRegExp); // var ruleStatementMatch = atRulesStatemenRegExpES5Alternative(ruleSlice); // If it's a statement inside a nested rule, ignore only the statement if (ruleStatementMatch && currentScope !== styleSheet) { var ignoreEnd = ruleStatementMatch[0].indexOf(";"); i += ruleStatementMatch.index + ignoreEnd; return; } // Check if there's a semicolon before the invalid at-rule and the first opening brace if (atRuleKey === "@layer") { var ruleSemicolonAndOpeningBraceMatch = ruleSlice.match(forwardRuleSemicolonAndOpeningBraceRegExp); if (ruleSemicolonAndOpeningBraceMatch && ruleSemicolonAndOpeningBraceMatch[1] === ";" ) { // Ignore the rule block until the semicolon i += ruleSemicolonAndOpeningBraceMatch.index + ruleSemicolonAndOpeningBraceMatch[0].length; state = "before-selector"; return; } } // Ignore the entire rule block (if it's a statement it should ignore the statement plus the next block) i = ignoreBalancedBlock(i, ruleSlice); state = "before-selector"; } else { validCallback.call(this); } } /** * Validates a basic CSS selector, allowing for deeply nested balanced parentheses in pseudo-classes. * This function replaces the previous basicSelectorRegExp. * * This function matches: * - Type selectors (e.g., `div`, `span`) * - Universal selector (`*`) * - ID selectors (e.g., `#header`, `#a\ b`, `#åèiöú`) * - Class selectors (e.g., `.container`, `.a\ b`, `.åèiöú`) * - Attribute selectors (e.g., `[type="text"]`) * - Pseudo-classes and pseudo-elements (e.g., `:hover`, `::before`, `:nth-child(2)`) * - Pseudo-classes with nested parentheses, including cases where parentheses are nested inside arguments, * such as `:has(.sel:nth-child(3n))` * - The parent selector (`&`) * - Combinators (`>`, `+`, `~`) with optional whitespace * - Whitespace (descendant combinator) * * Unicode and escape sequences are allowed in identifiers. * * @param {string} selector * @returns {boolean} */ function basicSelectorValidator(selector) { var length = selector.length; var i = 0; var stack = []; var inAttr = false; var inSingleQuote = false; var inDoubleQuote = false; while (i < length) { var char = selector[i]; if (inSingleQuote) { if (char === "'" && selector[i - 1] !== "\\") { inSingleQuote = false; } } else if (inDoubleQuote) { if (char === '"' && selector[i - 1] !== "\\") { inDoubleQuote = false; } } else if (inAttr) { if (char === "]") { inAttr = false; } else if (char === "'") { inSingleQuote = true; } else if (char === '"') { inDoubleQuote = true; } } else { if (char === "[") { inAttr = true; } else if (char === "'") { inSingleQuote = true; } else if (char === '"') { inDoubleQuote = true; } else if (char === "(") { stack.push("("); } else if (char === ")") { if (!stack.length || stack.pop() !== "(") { return false; } } } i++; } // If any stack or quote/attr context remains, it's invalid if (stack.length || inAttr || inSingleQuote || inDoubleQuote) { return false; } // Fallback to a loose regexp for the overall selector structure (without deep paren matching) //