var CSSOM = { /** * Creates and configures a new CSSOM instance with the specified options. * * @param {Object} opts - Configuration options for the CSSOM instance * @param {Object} [opts.globalObject] - Optional global object to be assigned to CSSOM objects prototype * @returns {Object} A new CSSOM instance with the applied configuration * @description * This method creates a new instance of CSSOM and optionally * configures CSSStyleSheet with a global object reference. When a globalObject is provided * and CSSStyleSheet exists on the instance, it creates a new CSSStyleSheet constructor * using a factory function and assigns the globalObject to its prototype's __globalObject property. */ setup: function (opts) { var instance = Object.create(this); if (opts.globalObject) { if (instance.CSSStyleSheet) { var factoryCSSStyleSheet = createFunctionFactory(instance.CSSStyleSheet); var CSSStyleSheet = factoryCSSStyleSheet(); CSSStyleSheet.prototype.__globalObject = opts.globalObject; instance.CSSStyleSheet = CSSStyleSheet; } } return instance; } }; function createFunctionFactory(fn) { return function() { // Create a new function that delegates to the original var newFn = function() { return fn.apply(this, arguments); }; // Copy prototype chain Object.setPrototypeOf(newFn, Object.getPrototypeOf(fn)); // Copy own properties for (var key in fn) { if (Object.prototype.hasOwnProperty.call(fn, key)) { newFn[key] = fn[key]; } } // Clone the .prototype object for constructor-like behavior if (fn.prototype) { newFn.prototype = Object.create(fn.prototype); } return newFn; }; } // Utility functions for CSSOM error handling /** * Gets the appropriate error constructor from the global object context. * Tries to find the error constructor from parentStyleSheet.__globalObject, * then from __globalObject, then falls back to the native constructor. * * @param {Object} context - The CSSOM object (rule, stylesheet, etc.) * @param {string} errorType - The error type ('TypeError', 'RangeError', 'DOMException', etc.) * @return {Function} The error constructor */ function getErrorConstructor(context, errorType) { // Try parentStyleSheet.__globalObject first if (context.parentStyleSheet && context.parentStyleSheet.__globalObject && context.parentStyleSheet.__globalObject[errorType]) { return context.parentStyleSheet.__globalObject[errorType]; } // Try __parentStyleSheet (alternative naming) if (context.__parentStyleSheet && context.__parentStyleSheet.__globalObject && context.__parentStyleSheet.__globalObject[errorType]) { return context.__parentStyleSheet.__globalObject[errorType]; } // Try __globalObject on the context itself if (context.__globalObject && context.__globalObject[errorType]) { return context.__globalObject[errorType]; } // Fall back to native constructor return (typeof global !== 'undefined' && global[errorType]) || (typeof window !== 'undefined' && window[errorType]) || eval(errorType); } /** * Creates an appropriate error with context-aware constructor. * * @param {Object} context - The CSSOM object (rule, stylesheet, etc.) * @param {string} errorType - The error type ('TypeError', 'RangeError', 'DOMException', etc.) * @param {string} message - The error message * @param {string} [name] - Optional name for DOMException */ function createError(context, errorType, message, name) { var ErrorConstructor = getErrorConstructor(context, errorType); return new ErrorConstructor(message, name); } /** * Creates and throws an appropriate error with context-aware constructor. * * @param {Object} context - The CSSOM object (rule, stylesheet, etc.) * @param {string} errorType - The error type ('TypeError', 'RangeError', 'DOMException', etc.) * @param {string} message - The error message * @param {string} [name] - Optional name for DOMException */ function throwError(context, errorType, message, name) { throw createError(context, errorType, message, name); } /** * Throws a TypeError for missing required arguments. * * @param {Object} context - The CSSOM object * @param {string} methodName - The method name (e.g., 'appendRule') * @param {string} objectName - The object name (e.g., 'CSSKeyframesRule') * @param {number} [required=1] - Number of required arguments * @param {number} [provided=0] - Number of provided arguments */ function throwMissingArguments(context, methodName, objectName, required, provided) { required = required || 1; provided = provided || 0; var message = "Failed to execute '" + methodName + "' on '" + objectName + "': " + required + " argument" + (required > 1 ? "s" : "") + " required, but only " + provided + " present."; throwError(context, 'TypeError', message); } /** * Throws a DOMException for parse errors. * * @param {Object} context - The CSSOM object * @param {string} methodName - The method name * @param {string} objectName - The object name * @param {string} rule - The rule that failed to parse * @param {string} [name='SyntaxError'] - The DOMException name */ function throwParseError(context, methodName, objectName, rule, name) { var message = "Failed to execute '" + methodName + "' on '" + objectName + "': " + "Failed to parse the rule '" + rule + "'."; throwError(context, 'DOMException', message, name || 'SyntaxError'); } /** * Throws a DOMException for index errors. * * @param {Object} context - The CSSOM object * @param {string} methodName - The method name * @param {string} objectName - The object name * @param {number} index - The invalid index * @param {number} maxIndex - The maximum valid index * @param {string} [name='IndexSizeError'] - The DOMException name */ function throwIndexError(context, methodName, objectName, index, maxIndex, name) { var message = "Failed to execute '" + methodName + "' on '" + objectName + "': " + "The index provided (" + index + ") is larger than the maximum index (" + maxIndex + ")."; throwError(context, 'DOMException', message, name || 'IndexSizeError'); } var errorUtils = { createError: createError, getErrorConstructor: getErrorConstructor, throwError: throwError, throwMissingArguments: throwMissingArguments, throwParseError: throwParseError, throwIndexError: throwIndexError }; // Shared regex patterns for CSS parsing and validation // These patterns are compiled once and reused across multiple files for better performance // Regex patterns for CSS parsing var atKeyframesRegExp = /@(-(?:\w+-)+)?keyframes/g; // Match @keyframes and vendor-prefixed @keyframes 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, a condition opening parenthesis or a opening brace 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 // Regex patterns for CSS selector validation and parsing var cssCustomIdentifierRegExp = /^(-?[_a-zA-Z]+(\.[_a-zA-Z]+)*[_a-zA-Z0-9-]*)$/; // Validates a css custom identifier var startsWithCombinatorRegExp = /^\s*[>+~]/; // Checks if a selector starts with a CSS combinator (>, +, ~) /** * Parse `@page` selectorText for page name and pseudo-pages * Valid formats: * - (empty - no name, no pseudo-page) * - `:left`, `:right`, `:first`, `:blank` (pseudo-page only) * - `named` (named page only) * - `named:first` (named page with single pseudo-page) * - `named:first:left` (named page with multiple pseudo-pages) */ var atPageRuleSelectorRegExp = /^([^\s:]+)?((?::\w+)*)$/; // Validates @page rule selectors // Regex patterns for CSSImportRule parsing var layerRegExp = /layer\(([^)]*)\)/; // Matches layer() function in @import var layerRuleNameRegExp = /^(-?[_a-zA-Z]+(\.[_a-zA-Z]+)*[_a-zA-Z0-9-]*)$/; // Validates layer name (same as custom identifier) var doubleOrMoreSpacesRegExp = /\s{2,}/g; // Matches two or more consecutive whitespace characters // Regex patterns for CSS escape sequences and identifiers var startsWithHexEscapeRegExp = /^\\[0-9a-fA-F]/; // Checks if escape sequence starts with hex escape var identStartCharRegExp = /[a-zA-Z_\u00A0-\uFFFF]/; // Valid identifier start character var identCharRegExp = /^[a-zA-Z0-9_\-\u00A0-\uFFFF\\]/; // Valid identifier character var specialCharsNeedEscapeRegExp = /[!"#$%&'()*+,./:;<=>?@\[\\\]^`{|}~\s]/; // Characters that need escaping var combinatorOrSeparatorRegExp = /[\s>+~,()]/; // Selector boundaries and combinators var afterHexEscapeSeparatorRegExp = /[\s>+~,(){}\[\]]/; // Characters that separate after hex escape var trailingSpaceSeparatorRegExp = /[\s>+~,(){}]/; // Characters that allow trailing space var endsWithHexEscapeRegExp = /\\[0-9a-fA-F]{1,6}\s+$/; // Matches selector ending with hex escape + space(s) /** * 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 = /:(?![^(]*\))(?=(?:[^'"]|'[^']*'|"[^"]*")*$)/; // Attribute selector pattern: matches attribute-name operator value // Operators: =, ~=, |=, ^=, $=, *= // Rewritten to avoid ReDoS by using greedy match and trimming in JavaScript var attributeSelectorContentRegExp = /^([^\s=~|^$*]+)\s*(~=|\|=|\^=|\$=|\*=|=)\s*(.+)$/; // Selector validation patterns var pseudoElementRegExp = /::[a-zA-Z][\w-]*|:(before|after|first-line|first-letter)(?![a-zA-Z0-9_-])/; // Matches pseudo-elements var invalidCombinatorLtGtRegExp = /<>/; // Invalid <> combinator var invalidCombinatorDoubleGtRegExp = />>/; // Invalid >> combinator var consecutiveCombinatorsRegExp = /[>+~]\s*[>+~]/; // Invalid consecutive combinators var invalidSlottedRegExp = /(?:^|[\s>+~,\[])slotted\s*\(/i; // Invalid slotted() without :: var invalidPartRegExp = /(?:^|[\s>+~,\[])part\s*\(/i; // Invalid part() without :: var invalidCueRegExp = /(?:^|[\s>+~,\[])cue\s*\(/i; // Invalid cue() without :: var invalidCueRegionRegExp = /(?:^|[\s>+~,\[])cue-region\s*\(/i; // Invalid cue-region() without :: var invalidNestingPattern = /&(?![.\#\[:>\+~\s])[a-zA-Z]/; // Invalid & followed by type selector var emptyPseudoClassRegExp = /:(?:is|not|where|has)\(\s*\)/; // Empty pseudo-class like :is() var whitespaceNormalizationRegExp = /(['"])(?:\\.|[^\\])*?\1|(\r\n|\r|\n)/g; // Normalize newlines outside quotes var newlineRemovalRegExp = /\n/g; // Remove all newlines var whitespaceAndDotRegExp = /[\s.]/; // Matches whitespace or dot var declarationOrOpenBraceRegExp = /[{;}]/; // Matches declaration separator or open brace var ampersandRegExp = /&/; // Matches nesting selector var hexEscapeSequenceRegExp = /^([0-9a-fA-F]{1,6})[ \t\r\n\f]?/; // Matches hex escape sequence (1-6 hex digits optionally followed by whitespace) var attributeCaseFlagRegExp = /^(.+?)\s+([is])$/i; // Matches case-sensitivity flag at end of attribute value var prependedAmpersandRegExp = /^&\s+[:\\.]/; // Matches prepended ampersand pattern (& followed by space and : or .) var openBraceGlobalRegExp = /{/g; // Matches opening braces (global) var closeBraceGlobalRegExp = /}/g; // Matches closing braces (global) var scopePreludeSplitRegExp = /\s*\)\s*to\s+\(/; // Splits scope prelude by ") to (" var leadingWhitespaceRegExp = /^\s+/; // Matches leading whitespace (used to implement a ES5-compliant alternative to trimStart()) var doubleQuoteRegExp = /"/g; // Match all double quotes (for escaping in attribute values) var backslashRegExp = /\\/g; // Match all backslashes (for escaping in attribute values) var regexPatterns = { // Parsing patterns atKeyframesRegExp: atKeyframesRegExp, beforeRulePortionRegExp: beforeRulePortionRegExp, beforeRuleValidationRegExp: beforeRuleValidationRegExp, forwardRuleValidationRegExp: forwardRuleValidationRegExp, forwardImportRuleValidationRegExp: forwardImportRuleValidationRegExp, forwardRuleClosingBraceRegExp: forwardRuleClosingBraceRegExp, forwardRuleSemicolonAndOpeningBraceRegExp: forwardRuleSemicolonAndOpeningBraceRegExp, // Selector validation patterns cssCustomIdentifierRegExp: cssCustomIdentifierRegExp, startsWithCombinatorRegExp: startsWithCombinatorRegExp, atPageRuleSelectorRegExp: atPageRuleSelectorRegExp, // Parsing patterns used in CSSImportRule layerRegExp: layerRegExp, layerRuleNameRegExp: layerRuleNameRegExp, doubleOrMoreSpacesRegExp: doubleOrMoreSpacesRegExp, // Escape sequence and identifier patterns startsWithHexEscapeRegExp: startsWithHexEscapeRegExp, identStartCharRegExp: identStartCharRegExp, identCharRegExp: identCharRegExp, specialCharsNeedEscapeRegExp: specialCharsNeedEscapeRegExp, combinatorOrSeparatorRegExp: combinatorOrSeparatorRegExp, afterHexEscapeSeparatorRegExp: afterHexEscapeSeparatorRegExp, trailingSpaceSeparatorRegExp: trailingSpaceSeparatorRegExp, endsWithHexEscapeRegExp: endsWithHexEscapeRegExp, // Basic style property value validation basicStylePropertyValueValidationRegExp: basicStylePropertyValueValidationRegExp, // Attribute selector patterns attributeSelectorContentRegExp: attributeSelectorContentRegExp, // Selector validation patterns pseudoElementRegExp: pseudoElementRegExp, invalidCombinatorLtGtRegExp: invalidCombinatorLtGtRegExp, invalidCombinatorDoubleGtRegExp: invalidCombinatorDoubleGtRegExp, consecutiveCombinatorsRegExp: consecutiveCombinatorsRegExp, invalidSlottedRegExp: invalidSlottedRegExp, invalidPartRegExp: invalidPartRegExp, invalidCueRegExp: invalidCueRegExp, invalidCueRegionRegExp: invalidCueRegionRegExp, invalidNestingPattern: invalidNestingPattern, emptyPseudoClassRegExp: emptyPseudoClassRegExp, whitespaceNormalizationRegExp: whitespaceNormalizationRegExp, newlineRemovalRegExp: newlineRemovalRegExp, whitespaceAndDotRegExp: whitespaceAndDotRegExp, declarationOrOpenBraceRegExp: declarationOrOpenBraceRegExp, ampersandRegExp: ampersandRegExp, hexEscapeSequenceRegExp: hexEscapeSequenceRegExp, attributeCaseFlagRegExp: attributeCaseFlagRegExp, prependedAmpersandRegExp: prependedAmpersandRegExp, openBraceGlobalRegExp: openBraceGlobalRegExp, closeBraceGlobalRegExp: closeBraceGlobalRegExp, scopePreludeSplitRegExp: scopePreludeSplitRegExp, leadingWhitespaceRegExp: leadingWhitespaceRegExp, doubleQuoteRegExp: doubleQuoteRegExp, backslashRegExp: backslashRegExp }; /** * @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 var basicStylePropertyValueValidationRegExp = regexPatterns.basicStylePropertyValueValidationRegExp 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)); } } }; try { CSSOM.CSSStyleDeclaration = require("cssstyle").CSSStyleDeclaration; } catch (e) { // ignore } /** * @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; Object.defineProperties(CSSOM.CSSRule.prototype, { constructor: { value: CSSOM.CSSRule }, cssRule: { value: "", configurable: true, enumerable: true }, cssText: { get: function() { // Default getter: subclasses should override this return ""; }, set: function(cssText) { return cssText; } }, parentRule: { get: function() { return this.__parentRule } }, parentStyleSheet: { get: function() { return this.__parentStyleSheet } }, UNKNOWN_RULE: { value: 0, enumerable: true }, // obsolet STYLE_RULE: { value: 1, enumerable: true }, CHARSET_RULE: { value: 2, enumerable: true }, // obsolet IMPORT_RULE: { value: 3, enumerable: true }, MEDIA_RULE: { value: 4, enumerable: true }, FONT_FACE_RULE: { value: 5, enumerable: true }, PAGE_RULE: { value: 6, enumerable: true }, KEYFRAMES_RULE: { value: 7, enumerable: true }, KEYFRAME_RULE: { value: 8, enumerable: true }, MARGIN_RULE: { value: 9, enumerable: true }, NAMESPACE_RULE: { value: 10, enumerable: true }, COUNTER_STYLE_RULE: { value: 11, enumerable: true }, SUPPORTS_RULE: { value: 12, enumerable: true }, DOCUMENT_RULE: { value: 13, enumerable: true }, FONT_FEATURE_VALUES_RULE: { value: 14, enumerable: true }, VIEWPORT_RULE: { value: 15, enumerable: true }, REGION_STYLE_RULE: { value: 16, enumerable: true }, CONTAINER_RULE: { value: 17, enumerable: true }, LAYER_BLOCK_RULE: { value: 18, enumerable: true }, STARTING_STYLE_RULE: { value: 1002, enumerable: true }, }); /** * @constructor * @see https://drafts.csswg.org/cssom/#the-cssrulelist-interface */ CSSOM.CSSRuleList = function CSSRuleList(){ var arr = new Array(); Object.setPrototypeOf(arr, CSSOM.CSSRuleList.prototype); return arr; }; CSSOM.CSSRuleList.prototype = Object.create(Array.prototype); CSSOM.CSSRuleList.prototype.constructor = CSSOM.CSSRuleList; CSSOM.CSSRuleList.prototype.item = function(index) { return this[index] || null; }; /** * @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 = Object.create(CSSOM.CSSRule.prototype); CSSOM.CSSNestedDeclarations.prototype.constructor = CSSOM.CSSNestedDeclarations; Object.setPrototypeOf(CSSOM.CSSNestedDeclarations, CSSOM.CSSRule); Object.defineProperty(CSSOM.CSSNestedDeclarations.prototype, "type", { value: 0, writable: false }); Object.defineProperty(CSSOM.CSSNestedDeclarations.prototype, "style", { get: function() { return this.__style; }, set: function(value) { if (typeof value === "string") { this.__style.cssText = value; } else { this.__style = value; } } }); Object.defineProperty(CSSOM.CSSNestedDeclarations.prototype, "cssText", { get: function () { return this.style.cssText; } }); /** * @constructor * @see https://drafts.csswg.org/cssom/#the-cssgroupingrule-interface */ CSSOM.CSSGroupingRule = function CSSGroupingRule() { CSSOM.CSSRule.call(this); this.__cssRules = new CSSOM.CSSRuleList(); }; CSSOM.CSSGroupingRule.prototype = Object.create(CSSOM.CSSRule.prototype); CSSOM.CSSGroupingRule.prototype.constructor = CSSOM.CSSGroupingRule; Object.setPrototypeOf(CSSOM.CSSGroupingRule, CSSOM.CSSRule); Object.defineProperty(CSSOM.CSSGroupingRule.prototype, "cssRules", { get: function() { return this.__cssRules; } }); /** * 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 (rule === undefined && index === undefined) { errorUtils.throwMissingArguments(this, 'insertRule', this.constructor.name); } if (index === void 0) { index = 0; } index = Number(index); if (index < 0) { index = 4294967296 + index; } if (index > this.cssRules.length) { errorUtils.throwIndexError(this, 'insertRule', this.constructor.name, index, this.cssRules.length); } var ruleToParse = processedRuleToParse = String(rule); ruleToParse = ruleToParse.trim().replace(/^\/\*[\s\S]*?\*\/\s*/, ""); var isNestedSelector = this.constructor.name === "CSSStyleRule"; if (isNestedSelector === false) { var currentRule = this; while (currentRule.parentRule) { currentRule = currentRule.parentRule; if (currentRule.constructor.name === "CSSStyleRule") { isNestedSelector = true; break; } } } if (isNestedSelector) { processedRuleToParse = 's { n { } ' + ruleToParse + '}'; } var isScopeRule = this.constructor.name === "CSSScopeRule"; if (isScopeRule) { if (isNestedSelector) { processedRuleToParse = 's { ' + '@scope {' + ruleToParse + '}}'; } else { processedRuleToParse = '@scope {' + ruleToParse + '}'; } } var parsedRules = new CSSOM.CSSRuleList(); CSSOM.parse(processedRuleToParse, { styleSheet: this.parentStyleSheet, cssRules: parsedRules }); if (isScopeRule) { if (isNestedSelector) { parsedRules = parsedRules[0].cssRules[0].cssRules; } else { parsedRules = parsedRules[0].cssRules } } if (isNestedSelector) { parsedRules = parsedRules[0].cssRules.slice(1); } if (parsedRules.length !== 1) { if (isNestedSelector && parsedRules.length === 0 && ruleToParse.indexOf('@font-face') === 0) { errorUtils.throwError(this, 'DOMException', "Failed to execute 'insertRule' on '" + this.constructor.name + "': " + "Only conditional nested group rules, style rules, @scope rules, @apply rules, and nested declaration rules may be nested.", 'HierarchyRequestError'); } else { errorUtils.throwParseError(this, 'insertRule', this.constructor.name, ruleToParse, 'SyntaxError'); } } var cssRule = parsedRules[0]; if (cssRule.constructor.name === 'CSSNestedDeclarations' && cssRule.style.length === 0) { errorUtils.throwParseError(this, 'insertRule', this.constructor.name, ruleToParse, 'SyntaxError'); } // Check for rules that cannot be inserted inside a CSSGroupingRule if (cssRule.constructor.name === 'CSSImportRule' || cssRule.constructor.name === 'CSSNamespaceRule') { var ruleKeyword = cssRule.constructor.name === 'CSSImportRule' ? '@import' : '@namespace'; errorUtils.throwError(this, 'DOMException', "Failed to execute 'insertRule' on '" + this.constructor.name + "': " + "'" + ruleKeyword + "' rules cannot be inserted inside a group rule.", 'HierarchyRequestError'); } // Check for CSSLayerStatementRule (@layer statement rules) if (cssRule.constructor.name === 'CSSLayerStatementRule') { errorUtils.throwParseError(this, 'insertRule', this.constructor.name, ruleToParse, 'SyntaxError'); } 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 === undefined) { errorUtils.throwMissingArguments(this, 'deleteRule', this.constructor.name); } index = Number(index); if (index < 0) { index = 4294967296 + index; } if (index >= this.cssRules.length) { errorUtils.throwIndexError(this, 'deleteRule', this.constructor.name, index, this.cssRules.length); } this.cssRules[index].__parentRule = null; this.cssRules[index].__parentStyleSheet = null; this.cssRules.splice(index, 1); }; /** * @constructor * @see https://drafts.csswg.org/css-counter-styles/#the-csscounterstylerule-interface */ CSSOM.CSSCounterStyleRule = function CSSCounterStyleRule() { CSSOM.CSSRule.call(this); this.name = ""; this.__props = ""; }; CSSOM.CSSCounterStyleRule.prototype = Object.create(CSSOM.CSSRule.prototype); CSSOM.CSSCounterStyleRule.prototype.constructor = CSSOM.CSSCounterStyleRule; Object.setPrototypeOf(CSSOM.CSSCounterStyleRule, CSSOM.CSSRule); Object.defineProperty(CSSOM.CSSCounterStyleRule.prototype, "type", { value: 11, writable: false }); Object.defineProperty(CSSOM.CSSCounterStyleRule.prototype, "cssText", { get: function() { // FIXME : Implement real cssText generation based on properties return "@counter-style " + this.name + " { " + this.__props + " }"; } }); /** * NON-STANDARD * Rule text parser. * @param {string} cssText */ Object.defineProperty(CSSOM.CSSCounterStyleRule.prototype, "parse", { value: function(cssText) { // Extract the name from "@counter-style { ... }" var match = cssText.match(/@counter-style\s+([^\s{]+)\s*\{([^]*)\}/); if (match) { this.name = match[1]; // Get the text inside the brackets and clean it up var propsText = match[2]; this.__props = propsText.trim().replace(/\n/g, " ").replace(/(['"])(?:\\.|[^\\])*?\1|(\s{2,})/g, function (match, quote) { return quote ? match : ' '; }); } } }); /** * @constructor * @see https://drafts.css-houdini.org/css-properties-values-api/#the-css-property-rule-interface */ CSSOM.CSSPropertyRule = function CSSPropertyRule() { CSSOM.CSSRule.call(this); this.__name = ""; this.__syntax = ""; this.__inherits = false; this.__initialValue = null; }; CSSOM.CSSPropertyRule.prototype = Object.create(CSSOM.CSSRule.prototype); CSSOM.CSSPropertyRule.prototype.constructor = CSSOM.CSSPropertyRule; Object.setPrototypeOf(CSSOM.CSSPropertyRule, CSSOM.CSSRule); Object.defineProperty(CSSOM.CSSPropertyRule.prototype, "type", { value: 0, writable: false }); Object.defineProperty(CSSOM.CSSPropertyRule.prototype, "cssText", { get: function() { var text = "@property " + this.name + " {"; if (this.syntax !== "") { text += " syntax: \"" + this.syntax.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + "\";"; } text += " inherits: " + (this.inherits ? "true" : "false") + ";"; if (this.initialValue !== null) { text += " initial-value: " + this.initialValue + ";"; } text += " }"; return text; } }); Object.defineProperty(CSSOM.CSSPropertyRule.prototype, "name", { get: function() { return this.__name; } }); Object.defineProperty(CSSOM.CSSPropertyRule.prototype, "syntax", { get: function() { return this.__syntax; } }); Object.defineProperty(CSSOM.CSSPropertyRule.prototype, "inherits", { get: function() { return this.__inherits; } }); Object.defineProperty(CSSOM.CSSPropertyRule.prototype, "initialValue", { get: function() { return this.__initialValue; } }); /** * NON-STANDARD * Rule text parser. * @param {string} cssText * @returns {boolean} True if the rule is valid and was parsed successfully */ Object.defineProperty(CSSOM.CSSPropertyRule.prototype, "parse", { value: function(cssText) { // Extract the name from "@property { ... }" var match = cssText.match(/@property\s+(--[^\s{]+)\s*\{([^]*)\}/); if (!match) { return false; } this.__name = match[1]; var bodyText = match[2]; // Parse syntax descriptor (REQUIRED) var syntaxMatch = bodyText.match(/syntax\s*:\s*(['"])([^]*?)\1\s*;/); if (!syntaxMatch) { return false; // syntax is required } this.__syntax = syntaxMatch[2]; // Syntax cannot be empty if (this.__syntax === "") { return false; } // Parse inherits descriptor (REQUIRED) var inheritsMatch = bodyText.match(/inherits\s*:\s*(true|false)\s*;/); if (!inheritsMatch) { return false; // inherits is required } this.__inherits = inheritsMatch[1] === "true"; // Parse initial-value descriptor (OPTIONAL, but required if syntax is not "*") var initialValueMatch = bodyText.match(/initial-value\s*:\s*([^;]+);/); if (initialValueMatch) { this.__initialValue = initialValueMatch[1].trim(); } else { // If syntax is not "*", initial-value is required if (this.__syntax !== "*") { return false; } } return true; // Successfully parsed } }); /** * @constructor * @see https://www.w3.org/TR/css-conditional-3/#the-cssconditionrule-interface */ CSSOM.CSSConditionRule = function CSSConditionRule() { CSSOM.CSSGroupingRule.call(this); this.__conditionText = ''; }; CSSOM.CSSConditionRule.prototype = Object.create(CSSOM.CSSGroupingRule.prototype); CSSOM.CSSConditionRule.prototype.constructor = CSSOM.CSSConditionRule; Object.setPrototypeOf(CSSOM.CSSConditionRule, CSSOM.CSSGroupingRule); Object.defineProperty(CSSOM.CSSConditionRule.prototype, "conditionText", { get: function () { return this.__conditionText; } }); /** * @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 = Object.create(CSSOM.CSSGroupingRule.prototype); CSSOM.CSSStyleRule.prototype.constructor = CSSOM.CSSStyleRule; Object.setPrototypeOf(CSSOM.CSSStyleRule, CSSOM.CSSGroupingRule); Object.defineProperty(CSSOM.CSSStyleRule.prototype, "type", { value: 1, writable: false }); Object.defineProperty(CSSOM.CSSStyleRule.prototype, "selectorText", { get: function() { return this.__selectorText; }, set: function(value) { if (typeof value === "string") { // Don't trim if the value ends with a hex escape sequence followed by space // (e.g., ".\31 " where the space is part of the escape terminator) var endsWithHexEscapeRegExp = regexPatterns.endsWithHexEscapeRegExp; var endsWithEscape = endsWithHexEscapeRegExp.test(value); var trimmedValue = endsWithEscape ? value.replace(/\s+$/, ' ').trimStart() : value.trim(); if (trimmedValue === '') { return; } // TODO: Setting invalid selectorText should be ignored // There are some validations already on lib/parse.js // but the same validations should be applied here. // Check if we can move these validation logic to a shared function. this.__selectorText = trimmedValue; } }, configurable: true }); Object.defineProperty(CSSOM.CSSStyleRule.prototype, "style", { get: function() { return this.__style; }, set: function(value) { if (typeof value === "string") { this.__style.cssText = value; } else { this.__style = value; } }, configurable: true }); 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.reduce(function(acc, rule){ if (rule.cssText !== "") { acc.push(rule.cssText); } return acc; }, []).join("\n ")); values = valuesArr.join("\n ") + "\n}"; } else { values = " {" + (this.style.cssText ? " " + this.style.cssText : "") + " }"; } text = this.selectorText + values; } else { text = ""; } return text; } }); /** * @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) { if (typeof value === "string") { var values = value.split(",").filter(function(text){ return !!text; }); var length = this.length = values.length; for (var i=0; i 1 && parts[0] !== '(' && !parts[0].startsWith('(')) { return parts[0]; } return ""; } }, "containerQuery": { get: function() { var parts = this.conditionText.trim().split(/\s+/); if (parts.length > 1 && parts[0] !== '(' && !parts[0].startsWith('(')) { return parts.slice(1).join(' '); } return this.conditionText; } }, }); /** * @constructor * @see https://drafts.csswg.org/css-conditional-3/#the-csssupportsrule-interface */ CSSOM.CSSSupportsRule = function CSSSupportsRule() { CSSOM.CSSConditionRule.call(this); }; CSSOM.CSSSupportsRule.prototype = Object.create(CSSOM.CSSConditionRule.prototype); CSSOM.CSSSupportsRule.prototype.constructor = CSSOM.CSSSupportsRule; Object.setPrototypeOf(CSSOM.CSSSupportsRule, CSSOM.CSSConditionRule); Object.defineProperty(CSSOM.CSSSupportsRule.prototype, "type", { value: 12, writable: false }); Object.defineProperty(CSSOM.CSSSupportsRule.prototype, "cssText", { get: function() { var values = ""; var valuesArr = [" {"]; if (this.cssRules.length) { valuesArr.push(this.cssRules.reduce(function(acc, rule){ if (rule.cssText !== "") { acc.push(rule.cssText); } return acc; }, []).join("\n ")); } values = valuesArr.join("\n ") + "\n}"; return "@supports " + this.conditionText + values; } }); /** * @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 = Object.create(CSSOM.CSSRule.prototype); CSSOM.CSSImportRule.prototype.constructor = CSSOM.CSSImportRule; Object.setPrototypeOf(CSSOM.CSSImportRule, CSSOM.CSSRule); Object.defineProperty(CSSOM.CSSImportRule.prototype, "type", { value: 3, writable: false }); Object.defineProperty(CSSOM.CSSImportRule.prototype, "cssText", { get: function() { var mediaText = this.media.mediaText; return "@import url(\"" + this.href.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + "\")" + (this.layerName !== null ? " layer" + (this.layerName && "(" + this.layerName + ")") : "" ) + (this.supportsText ? " supports(" + this.supportsText + ")" : "" ) + (mediaText ? " " + mediaText : "") + ";"; } }); Object.defineProperty(CSSOM.CSSImportRule.prototype, "href", { get: function() { return this.__href; } }); Object.defineProperty(CSSOM.CSSImportRule.prototype, "media", { get: function() { return this.__media; }, set: function(value) { if (typeof value === "string") { this.__media.mediaText = value; } else { this.__media = value; } } }); Object.defineProperty(CSSOM.CSSImportRule.prototype, "layerName", { get: function() { return this.__layerName; } }); Object.defineProperty(CSSOM.CSSImportRule.prototype, "supportsText", { get: function() { return this.__supportsText; } }); Object.defineProperty(CSSOM.CSSImportRule.prototype, "styleSheet", { get: function() { return this.__styleSheet; } }); /** * NON-STANDARD * Rule text parser. * @param {string} cssText */ Object.defineProperty(CSSOM.CSSImportRule.prototype, "parse", { value: function(cssText) { var i = 0; /** * @import url(partial.css) screen, handheld; * || | * after-import media * | * url */ var state = ''; var buffer = ''; var index; var layerRegExp = regexPatterns.layerRegExp; var layerRuleNameRegExp = regexPatterns.layerRuleNameRegExp; var doubleOrMoreSpacesRegExp = regexPatterns.doubleOrMoreSpacesRegExp; /** * Extracts the content inside supports() handling nested parentheses. * @param {string} text - The text to parse * @returns {object|null} - {content: string, endIndex: number} or null if not found */ function extractSupportsContent(text) { var supportsIndex = text.indexOf('supports('); if (supportsIndex !== 0) { return null; } var depth = 0; var start = supportsIndex + 'supports('.length; var i = start; for (; i < text.length; i++) { if (text[i] === '(') { depth++; } else if (text[i] === ')') { if (depth === 0) { // Found the closing parenthesis for supports() return { content: text.slice(start, i), endIndex: i }; } depth--; } } return null; // Unbalanced parentheses } 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(); if (layerName.match(layerRuleNameRegExp) !== null) { this.__layerName = layerMatch[1].trim(); bufferTrimmed = bufferTrimmed.replace(layerRegExp, '') .replace(doubleOrMoreSpacesRegExp, ' ') // Replace double or more spaces with single space .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 supportsResult = extractSupportsContent(bufferTrimmed); if (supportsResult) { // REVIEW: In the browser, an empty supports() invalidates and ignores the entire @import rule this.__supportsText = supportsResult.content.trim(); // Remove the entire supports(...) from the buffer bufferTrimmed = bufferTrimmed.slice(0, 0) + bufferTrimmed.slice(supportsResult.endIndex + 1); bufferTrimmed = bufferTrimmed.replace(doubleOrMoreSpacesRegExp, ' ').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 https://drafts.csswg.org/cssom/#the-cssnamespacerule-interface */ CSSOM.CSSNamespaceRule = function CSSNamespaceRule() { CSSOM.CSSRule.call(this); this.__prefix = ""; this.__namespaceURI = ""; }; CSSOM.CSSNamespaceRule.prototype = Object.create(CSSOM.CSSRule.prototype); CSSOM.CSSNamespaceRule.prototype.constructor = CSSOM.CSSNamespaceRule; Object.setPrototypeOf(CSSOM.CSSNamespaceRule, CSSOM.CSSRule); Object.defineProperty(CSSOM.CSSNamespaceRule.prototype, "type", { value: 10, writable: false }); Object.defineProperty(CSSOM.CSSNamespaceRule.prototype, "cssText", { get: function() { return "@namespace" + (this.prefix && " " + this.prefix) + " url(\"" + this.namespaceURI + "\");"; } }); Object.defineProperty(CSSOM.CSSNamespaceRule.prototype, "prefix", { get: function() { return this.__prefix; } }); Object.defineProperty(CSSOM.CSSNamespaceRule.prototype, "namespaceURI", { get: function() { return this.__namespaceURI; } }); /** * NON-STANDARD * Rule text parser. * @param {string} cssText */ Object.defineProperty(CSSOM.CSSNamespaceRule.prototype, "parse", { value: function(cssText) { var newPrefix = ""; var newNamespaceURI = ""; // Remove @namespace and trim var text = cssText.trim(); if (text.indexOf('@namespace') === 0) { text = text.slice('@namespace'.length).trim(); } // Remove trailing semicolon if present if (text.charAt(text.length - 1) === ';') { text = text.slice(0, -1).trim(); } // Regex to match valid namespace syntax: // 1. [optional prefix] url("...") or [optional prefix] url('...') or [optional prefix] url() or [optional prefix] url(unquoted) // 2. [optional prefix] "..." or [optional prefix] '...' // The prefix must be a valid CSS identifier (letters, digits, hyphens, underscores, starting with letter or underscore) var re = /^(?:([a-zA-Z_][a-zA-Z0-9_-]*)\s+)?(?:url\(\s*(?:(['"])(.*?)\2\s*|([^)]*?))\s*\)|(['"])(.*?)\5)$/; var match = text.match(re); if (match) { // If prefix is present if (match[1]) { newPrefix = match[1]; } // If url(...) form with quotes if (typeof match[3] !== "undefined") { newNamespaceURI = match[3]; } // If url(...) form without quotes else if (typeof match[4] !== "undefined") { newNamespaceURI = match[4].trim(); } // If quoted string form else if (typeof match[6] !== "undefined") { newNamespaceURI = match[6]; } this.__prefix = newPrefix; this.__namespaceURI = newNamespaceURI; } else { throw new DOMException("Invalid @namespace rule", "InvalidStateError"); } } }); /** * @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 = Object.create(CSSOM.CSSRule.prototype); CSSOM.CSSFontFaceRule.prototype.constructor = CSSOM.CSSFontFaceRule; Object.setPrototypeOf(CSSOM.CSSFontFaceRule, CSSOM.CSSRule); Object.defineProperty(CSSOM.CSSFontFaceRule.prototype, "type", { value: 5, writable: false }); //FIXME //CSSOM.CSSFontFaceRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule; //CSSOM.CSSFontFaceRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule; Object.defineProperty(CSSOM.CSSFontFaceRule.prototype, "style", { get: function() { return this.__style; }, set: function(value) { if (typeof value === "string") { this.__style.cssText = value; } else { this.__style = value; } } }); // 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 ? " " + this.style.cssText : "") + " }"; } }); /** * @constructor * @see http://www.w3.org/TR/shadow-dom/#host-at-rule * @see http://html5index.org/Shadow%20DOM%20-%20CSSHostRule.html * @deprecated This rule was part of early Shadow DOM drafts but was removed in favor of the more flexible :host and :host-context() pseudo-classes in modern CSS for Web Components. */ CSSOM.CSSHostRule = function CSSHostRule() { CSSOM.CSSRule.call(this); this.cssRules = new CSSOM.CSSRuleList(); }; CSSOM.CSSHostRule.prototype = Object.create(CSSOM.CSSRule.prototype); CSSOM.CSSHostRule.prototype.constructor = CSSOM.CSSHostRule; Object.setPrototypeOf(CSSOM.CSSHostRule, CSSOM.CSSRule); Object.defineProperty(CSSOM.CSSHostRule.prototype, "type", { value: 1001, writable: false }); //FIXME //CSSOM.CSSHostRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule; //CSSOM.CSSHostRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule; Object.defineProperty(CSSOM.CSSHostRule.prototype, "cssText", { get: function() { var values = ""; var valuesArr = [" {"]; if (this.cssRules.length) { valuesArr.push(this.cssRules.reduce(function(acc, rule){ if (rule.cssText !== "") { acc.push(rule.cssText); } return acc; }, []).join("\n ")); } values = valuesArr.join("\n ") + "\n}"; return "@host" + values; } }); /** * @constructor * @see http://www.w3.org/TR/shadow-dom/#host-at-rule */ CSSOM.CSSStartingStyleRule = function CSSStartingStyleRule() { CSSOM.CSSGroupingRule.call(this); }; CSSOM.CSSStartingStyleRule.prototype = Object.create(CSSOM.CSSGroupingRule.prototype); CSSOM.CSSStartingStyleRule.prototype.constructor = CSSOM.CSSStartingStyleRule; Object.setPrototypeOf(CSSOM.CSSStartingStyleRule, CSSOM.CSSGroupingRule); Object.defineProperty(CSSOM.CSSStartingStyleRule.prototype, "type", { value: 1002, writable: false }); //FIXME //CSSOM.CSSStartingStyleRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule; //CSSOM.CSSStartingStyleRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule; Object.defineProperty(CSSOM.CSSStartingStyleRule.prototype, "cssText", { get: function() { var values = ""; var valuesArr = [" {"]; if (this.cssRules.length) { valuesArr.push(this.cssRules.reduce(function(acc, rule){ if (rule.cssText !== "") { acc.push(rule.cssText); } return acc; }, []).join("\n ")); } values = valuesArr.join("\n ") + "\n}"; return "@starting-style" + values; } }); /** * @see http://dev.w3.org/csswg/cssom/#the-stylesheet-interface */ CSSOM.StyleSheet = function StyleSheet() { this.__href = null; this.__ownerNode = null; this.__title = null; this.__media = new CSSOM.MediaList(); this.__parentStyleSheet = null; this.disabled = false; }; Object.defineProperties(CSSOM.StyleSheet.prototype, { type: { get: function() { return "text/css"; } }, href: { get: function() { return this.__href; } }, ownerNode: { get: function() { return this.__ownerNode; } }, title: { get: function() { return this.__title; } }, media: { get: function() { return this.__media; }, set: function(value) { if (typeof value === "string") { this.__media.mediaText = value; } else { this.__media = value; } } }, parentStyleSheet: { get: function() { return this.__parentStyleSheet; } } }); /** * @constructor * @param {CSSStyleSheetInit} [opts] - CSSStyleSheetInit options. * @param {string} [opts.baseURL] - The base URL of the stylesheet. * @param {boolean} [opts.disabled] - The disabled attribute of the stylesheet. * @param {MediaList | string} [opts.media] - The media attribute of the stylesheet. * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet */ CSSOM.CSSStyleSheet = function CSSStyleSheet(opts) { CSSOM.StyleSheet.call(this); this.__constructed = true; this.__cssRules = new CSSOM.CSSRuleList(); this.__ownerRule = null; if (opts && typeof opts === "object") { if (opts.baseURL && typeof opts.baseURL === "string") { this.__baseURL = opts.baseURL; } if (opts.media && typeof opts.media === "string") { this.media.mediaText = opts.media; } if (typeof opts.disabled === "boolean") { this.disabled = opts.disabled; } } }; CSSOM.CSSStyleSheet.prototype = Object.create(CSSOM.StyleSheet.prototype); CSSOM.CSSStyleSheet.prototype.constructor = CSSOM.CSSStyleSheet; Object.setPrototypeOf(CSSOM.CSSStyleSheet, CSSOM.StyleSheet); Object.defineProperty(CSSOM.CSSStyleSheet.prototype, "cssRules", { get: function() { return this.__cssRules; } }); Object.defineProperty(CSSOM.CSSStyleSheet.prototype, "rules", { get: function() { return this.__cssRules; } }); Object.defineProperty(CSSOM.CSSStyleSheet.prototype, "ownerRule", { get: function() { return this.__ownerRule; } }); /** * 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 (rule === undefined && index === undefined) { errorUtils.throwMissingArguments(this, 'insertRule', this.constructor.name); } if (index === void 0) { index = 0; } index = Number(index); if (index < 0) { index = 4294967296 + index; } if (index > this.cssRules.length) { errorUtils.throwIndexError(this, 'insertRule', this.constructor.name, index, this.cssRules.length); } var ruleToParse = String(rule); var parseErrors = []; var parsedSheet = CSSOM.parse(ruleToParse, undefined, function(err) { parseErrors.push(err); } ); if (parsedSheet.cssRules.length !== 1) { errorUtils.throwParseError(this, 'insertRule', this.constructor.name, ruleToParse, 'SyntaxError'); } var cssRule = parsedSheet.cssRules[0]; // Helper function to find the last index of a specific rule constructor function findLastIndexOfConstructor(rules, constructorName) { for (var i = rules.length - 1; i >= 0; i--) { if (rules[i].constructor.name === constructorName) { return i; } } return -1; } // Helper function to find the first index of a rule that's NOT of specified constructors function findFirstNonConstructorIndex(rules, constructorNames) { for (var i = 0; i < rules.length; i++) { if (constructorNames.indexOf(rules[i].constructor.name) === -1) { return i; } } return rules.length; } // Validate rule ordering based on CSS specification if (cssRule.constructor.name === 'CSSImportRule') { if (this.__constructed === true) { errorUtils.throwError(this, 'DOMException', "Failed to execute 'insertRule' on '" + this.constructor.name + "': Can't insert @import rules into a constructed stylesheet.", 'SyntaxError'); } // @import rules cannot be inserted after @layer rules that already exist // They can only be inserted at the beginning or after other @import rules var firstLayerIndex = findFirstNonConstructorIndex(this.cssRules, ['CSSImportRule']); if (firstLayerIndex < this.cssRules.length && this.cssRules[firstLayerIndex].constructor.name === 'CSSLayerStatementRule' && index > firstLayerIndex) { errorUtils.throwError(this, 'DOMException', "Failed to execute 'insertRule' on '" + this.constructor.name + "': Failed to insert the rule.", 'HierarchyRequestError'); } // Also cannot insert after @namespace or other rules var firstNonImportIndex = findFirstNonConstructorIndex(this.cssRules, ['CSSImportRule']); if (index > firstNonImportIndex && firstNonImportIndex < this.cssRules.length && this.cssRules[firstNonImportIndex].constructor.name !== 'CSSLayerStatementRule') { errorUtils.throwError(this, 'DOMException', "Failed to execute 'insertRule' on '" + this.constructor.name + "': Failed to insert the rule.", 'HierarchyRequestError'); } } else if (cssRule.constructor.name === 'CSSNamespaceRule') { // @namespace rules can come after @layer and @import, but before any other rules // They cannot come before @import rules var firstImportIndex = -1; for (var i = 0; i < this.cssRules.length; i++) { if (this.cssRules[i].constructor.name === 'CSSImportRule') { firstImportIndex = i; break; } } var firstNonImportNamespaceIndex = findFirstNonConstructorIndex(this.cssRules, [ 'CSSLayerStatementRule', 'CSSImportRule', 'CSSNamespaceRule' ]); // Cannot insert before @import rules if (firstImportIndex !== -1 && index <= firstImportIndex) { errorUtils.throwError(this, 'DOMException', "Failed to execute 'insertRule' on '" + this.constructor.name + "': Failed to insert the rule.", 'HierarchyRequestError'); } // Cannot insert if there are already non-special rules if (firstNonImportNamespaceIndex < this.cssRules.length) { errorUtils.throwError(this, 'DOMException', "Failed to execute 'insertRule' on '" + this.constructor.name + "': Failed to insert the rule.", 'InvalidStateError'); } // Cannot insert after other types of rules if (index > firstNonImportNamespaceIndex) { errorUtils.throwError(this, 'DOMException', "Failed to execute 'insertRule' on '" + this.constructor.name + "': Failed to insert the rule.", 'HierarchyRequestError'); } } else if (cssRule.constructor.name === 'CSSLayerStatementRule') { // @layer statement rules can be inserted anywhere before @import and @namespace // No additional restrictions beyond what's already handled } else { // Any other rule cannot be inserted before @import and @namespace var firstNonSpecialRuleIndex = findFirstNonConstructorIndex(this.cssRules, [ 'CSSLayerStatementRule', 'CSSImportRule', 'CSSNamespaceRule' ]); if (index < firstNonSpecialRuleIndex) { errorUtils.throwError(this, 'DOMException', "Failed to execute 'insertRule' on '" + this.constructor.name + "': Failed to insert the rule.", 'HierarchyRequestError'); } if (parseErrors.filter(function(error) { return !error.isNested; }).length !== 0) { errorUtils.throwParseError(this, 'insertRule', this.constructor.name, ruleToParse, 'SyntaxError'); } } cssRule.__parentStyleSheet = this; this.cssRules.splice(index, 0, cssRule); return index; }; CSSOM.CSSStyleSheet.prototype.addRule = function(selector, styleBlock, index) { if (index === void 0) { index = this.cssRules.length; } this.insertRule(selector + "{" + styleBlock + "}", index); return -1; }; /** * 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 === undefined) { errorUtils.throwMissingArguments(this, 'deleteRule', this.constructor.name); } index = Number(index); if (index < 0) { index = 4294967296 + index; } if (index >= this.cssRules.length) { errorUtils.throwIndexError(this, 'deleteRule', this.constructor.name, index, this.cssRules.length); } if (this.cssRules[index]) { if (this.cssRules[index].constructor.name == "CSSNamespaceRule") { var shouldContinue = this.cssRules.every(function (rule) { return ['CSSImportRule','CSSLayerStatementRule','CSSNamespaceRule'].indexOf(rule.constructor.name) !== -1 }); if (!shouldContinue) { errorUtils.throwError(this, 'DOMException', "Failed to execute 'deleteRule' on '" + this.constructor.name + "': Failed to delete rule.", "InvalidStateError"); } } if (this.cssRules[index].constructor.name == "CSSImportRule") { this.cssRules[index].styleSheet.__parentStyleSheet = null; } this.cssRules[index].__parentStyleSheet = null; } this.cssRules.splice(index, 1); }; CSSOM.CSSStyleSheet.prototype.removeRule = function(index) { if (index === void 0) { index = 0; } this.deleteRule(index); }; /** * Replaces the rules of a {@link CSSStyleSheet} * * @returns a promise * @see https://www.w3.org/TR/cssom-1/#dom-cssstylesheet-replace */ CSSOM.CSSStyleSheet.prototype.replace = function(text) { var _Promise; if (this.__globalObject && this.__globalObject['Promise']) { _Promise = this.__globalObject['Promise']; } else { _Promise = Promise; } var _setTimeout; if (this.__globalObject && this.__globalObject['setTimeout']) { _setTimeout = this.__globalObject['setTimeout']; } else { _setTimeout = setTimeout; } var sheet = this; return new _Promise(function (resolve, reject) { // If the constructed flag is not set, or the disallow modification flag is set, throw a NotAllowedError DOMException. if (!sheet.__constructed || sheet.__disallowModification) { reject(errorUtils.createError(sheet, 'DOMException', "Failed to execute 'replaceSync' on '" + sheet.constructor.name + "': Not allowed.", 'NotAllowedError')); } // Set the disallow modification flag. sheet.__disallowModification = true; // In parallel, do these steps: _setTimeout(function() { // Let rules be the result of running parse a stylesheet's contents from text. var rules = new CSSOM.CSSRuleList(); CSSOM.parse(text, { styleSheet: sheet, cssRules: rules }); // If rules contains one or more @import rules, remove those rules from rules. var i = 0; while (i < rules.length) { if (rules[i].constructor.name === 'CSSImportRule') { rules.splice(i, 1); } else { i++; } } // Set sheet's CSS rules to rules. sheet.__cssRules.splice.apply(sheet.__cssRules, [0, sheet.__cssRules.length].concat(rules)); // Unset sheet’s disallow modification flag. delete sheet.__disallowModification; // Resolve promise with sheet. resolve(sheet); }) }); } /** * Synchronously replaces the rules of a {@link CSSStyleSheet} * * @see https://www.w3.org/TR/cssom-1/#dom-cssstylesheet-replacesync */ CSSOM.CSSStyleSheet.prototype.replaceSync = function(text) { var sheet = this; // If the constructed flag is not set, or the disallow modification flag is set, throw a NotAllowedError DOMException. if (!sheet.__constructed || sheet.__disallowModification) { errorUtils.throwError(sheet, 'DOMException', "Failed to execute 'replaceSync' on '" + sheet.constructor.name + "': Not allowed.", 'NotAllowedError'); } // Let rules be the result of running parse a stylesheet's contents from text. var rules = new CSSOM.CSSRuleList(); CSSOM.parse(text, { styleSheet: sheet, cssRules: rules }); // If rules contains one or more @import rules, remove those rules from rules. var i = 0; while (i < rules.length) { if (rules[i].constructor.name === 'CSSImportRule') { rules.splice(i, 1); } else { i++; } } // Set sheet's CSS rules to rules. sheet.__cssRules.splice.apply(sheet.__cssRules, [0, sheet.__cssRules.length].concat(rules)); } /** * NON-STANDARD * @return {string} serialize stylesheet */ CSSOM.CSSStyleSheet.prototype.toString = function() { var result = ""; var rules = this.cssRules; for (var i=0; i 0 && tempStyleSheet.cssRules[0].cssRules.length > 0) { parsedRule = tempStyleSheet.cssRules[0].cssRules[0]; } else { throw new Error("Failed to parse keyframe rule"); } } catch (e) { errorUtils.throwParseError(this, 'appendRule', 'CSSKeyframesRule', rule); } parsedRule.__parentRule = this; this.cssRules.push(parsedRule); }; /** * Deletes a keyframe rule that matches the specified key. * * @param {string} select - The keyframe selector to delete (e.g., "50%", "from", "to") * @see https://www.w3.org/TR/css-animations-1/#dom-csskeyframesrule-deleterule */ CSSOM.CSSKeyframesRule.prototype.deleteRule = function deleteRule(select) { if (arguments.length === 0) { errorUtils.throwMissingArguments(this, 'deleteRule', 'CSSKeyframesRule'); } var normalizedSelect = this._normalizeKeyText(select); for (var i = 0; i < this.cssRules.length; i++) { var rule = this.cssRules[i]; if (this._normalizeKeyText(rule.keyText) === normalizedSelect) { rule.__parentRule = null; this.cssRules.splice(i, 1); return; } } }; /** * Finds and returns the keyframe rule that matches the specified key. * When multiple rules have the same key, returns the last one. * * @param {string} select - The keyframe selector to find (e.g., "50%", "from", "to") * @return {CSSKeyframeRule|null} The matching keyframe rule, or null if not found * @see https://www.w3.org/TR/css-animations-1/#dom-csskeyframesrule-findrule */ CSSOM.CSSKeyframesRule.prototype.findRule = function findRule(select) { if (arguments.length === 0) { errorUtils.throwMissingArguments(this, 'findRule', 'CSSKeyframesRule'); } var normalizedSelect = this._normalizeKeyText(select); // Iterate backwards to find the last matching rule for (var i = this.cssRules.length - 1; i >= 0; i--) { var rule = this.cssRules[i]; if (this._normalizeKeyText(rule.keyText) === normalizedSelect) { return rule; } } return null; }; /** * Normalizes keyframe selector text for comparison. * Handles "from" -> "0%" and "to" -> "100%" conversions and trims whitespace. * * @private * @param {string} keyText - The keyframe selector text to normalize * @return {string} The normalized keyframe selector text */ CSSOM.CSSKeyframesRule.prototype._normalizeKeyText = function _normalizeKeyText(keyText) { if (!keyText) return ''; var normalized = keyText.toString().trim().toLowerCase(); // Convert keywords to percentages for comparison if (normalized === 'from') { return '0%'; } else if (normalized === 'to') { return '100%'; } return normalized; }; /** * Makes CSSKeyframesRule iterable over its cssRules. * Allows for...of loops and other iterable methods. */ if (typeof Symbol !== 'undefined' && Symbol.iterator) { CSSOM.CSSKeyframesRule.prototype[Symbol.iterator] = function() { var index = 0; var cssRules = this.cssRules; return { next: function() { if (index < cssRules.length) { return { value: cssRules[index++], done: false }; } else { return { done: true }; } } }; }; } /** * Adds indexed getters for direct access to cssRules by index. * This enables rule[0], rule[1], etc. access patterns. * Works in environments where Proxy is not available (like jsdom). */ CSSOM.CSSKeyframesRule.prototype._setupIndexedAccess = function() { // Remove any existing indexed properties for (var i = 0; i < 1000; i++) { // reasonable upper limit if (this.hasOwnProperty(i)) { delete this[i]; } else { break; } } // Add indexed getters for current cssRules for (var i = 0; i < this.cssRules.length; i++) { (function(index) { Object.defineProperty(this, index, { get: function() { return this.cssRules[index]; }, enumerable: false, configurable: true }); }.call(this, i)); } // Update length property Object.defineProperty(this, 'length', { get: function() { return this.cssRules.length; }, enumerable: false, configurable: true }); }; /** * @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 = Object.create(CSSOM.CSSRule.prototype); CSSOM.CSSKeyframeRule.prototype.constructor = CSSOM.CSSKeyframeRule; Object.setPrototypeOf(CSSOM.CSSKeyframeRule, CSSOM.CSSRule); Object.defineProperty(CSSOM.CSSKeyframeRule.prototype, "type", { value: 8, writable: false }); //FIXME //CSSOM.CSSKeyframeRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule; //CSSOM.CSSKeyframeRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule; Object.defineProperty(CSSOM.CSSKeyframeRule.prototype, "style", { get: function() { return this.__style; }, set: function(value) { if (typeof value === "string") { this.__style.cssText = value; } else { this.__style = value; } } }); // 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 ? " " + 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 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/ 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-6/#cssscoperule */ CSSOM.CSSScopeRule = function CSSScopeRule() { CSSOM.CSSGroupingRule.call(this); this.__start = null; this.__end = null; }; CSSOM.CSSScopeRule.prototype = Object.create(CSSOM.CSSGroupingRule.prototype); CSSOM.CSSScopeRule.prototype.constructor = CSSOM.CSSScopeRule; Object.setPrototypeOf(CSSOM.CSSScopeRule, CSSOM.CSSGroupingRule); Object.defineProperties(CSSOM.CSSScopeRule.prototype, { type: { value: 0, writable: false, }, cssText: { get: function () { var values = ""; var valuesArr = [" {"]; if (this.cssRules.length) { valuesArr.push(this.cssRules.reduce(function(acc, rule){ if (rule.cssText !== "") { acc.push(rule.cssText); } return acc; }, []).join("\n ")); } values = valuesArr.join("\n ") + "\n}"; return "@scope" + (this.start ? " (" + this.start + ")" : "") + (this.end ? " to (" + this.end + ")" : "") + values; }, configurable: true, enumerable: true, }, start: { get: function () { return this.__start; } }, end: { get: function () { return this.__end; } } }); /** * @constructor * @see https://drafts.csswg.org/css-cascade-5/#csslayerblockrule */ CSSOM.CSSLayerBlockRule = function CSSLayerBlockRule() { CSSOM.CSSGroupingRule.call(this); this.name = ""; }; CSSOM.CSSLayerBlockRule.prototype = Object.create(CSSOM.CSSGroupingRule.prototype); CSSOM.CSSLayerBlockRule.prototype.constructor = CSSOM.CSSLayerBlockRule; Object.setPrototypeOf(CSSOM.CSSLayerBlockRule, CSSOM.CSSRule); Object.defineProperty(CSSOM.CSSLayerBlockRule.prototype, "type", { value: 18, writable: false }); Object.defineProperties(CSSOM.CSSLayerBlockRule.prototype, { cssText: { get: function () { var values = ""; var valuesArr = [" {"]; if (this.cssRules.length) { valuesArr.push(this.cssRules.reduce(function(acc, rule){ if (rule.cssText !== "") { acc.push(rule.cssText); } return acc; }, []).join("\n ")); } values = valuesArr.join("\n ") + "\n}"; return "@layer" + (this.name ? " " + this.name : "") + values; } }, }); /** * @constructor * @see https://drafts.csswg.org/css-cascade-5/#csslayerstatementrule */ CSSOM.CSSLayerStatementRule = function CSSLayerStatementRule() { CSSOM.CSSRule.call(this); this.nameList = []; }; CSSOM.CSSLayerStatementRule.prototype = Object.create(CSSOM.CSSRule.prototype); CSSOM.CSSLayerStatementRule.prototype.constructor = CSSOM.CSSLayerStatementRule; Object.setPrototypeOf(CSSOM.CSSLayerStatementRule, CSSOM.CSSRule); Object.defineProperty(CSSOM.CSSLayerStatementRule.prototype, "type", { value: 0, writable: false }); Object.defineProperties(CSSOM.CSSLayerStatementRule.prototype, { cssText: { get: function () { return "@layer " + this.nameList.join(", ") + ";"; } }, }); /** * @constructor * @see https://drafts.csswg.org/cssom/#the-csspagerule-interface */ CSSOM.CSSPageRule = function CSSPageRule() { CSSOM.CSSGroupingRule.call(this); this.__style = new CSSOM.CSSStyleDeclaration(); this.__style.parentRule = this; }; CSSOM.CSSPageRule.prototype = Object.create(CSSOM.CSSGroupingRule.prototype); CSSOM.CSSPageRule.prototype.constructor = CSSOM.CSSPageRule; Object.setPrototypeOf(CSSOM.CSSPageRule, CSSOM.CSSGroupingRule); Object.defineProperty(CSSOM.CSSPageRule.prototype, "type", { value: 6, writable: false }); Object.defineProperty(CSSOM.CSSPageRule.prototype, "selectorText", { get: function() { return this.__selectorText; }, set: function(value) { if (typeof value === "string") { var trimmedValue = value.trim(); // Empty selector is valid for @page if (trimmedValue === '') { this.__selectorText = ''; return; } var atPageRuleSelectorRegExp = regexPatterns.atPageRuleSelectorRegExp; var cssCustomIdentifierRegExp = regexPatterns.cssCustomIdentifierRegExp; var match = trimmedValue.match(atPageRuleSelectorRegExp); if (match) { var pageName = match[1] || ''; var pseudoPages = match[2] || ''; // Validate page name if present if (pageName) { // Page name can be an identifier or a string if (!cssCustomIdentifierRegExp.test(pageName)) { return; } } // Validate pseudo-pages if present if (pseudoPages) { var pseudos = pseudoPages.split(':').filter(function(p) { return p; }); var validPseudos = ['left', 'right', 'first', 'blank']; var allValid = true; for (var j = 0; j < pseudos.length; j++) { if (validPseudos.indexOf(pseudos[j].toLowerCase()) === -1) { allValid = false; break; } } if (!allValid) { return; // Invalid pseudo-page, do nothing } } this.__selectorText = pageName + pseudoPages.toLowerCase(); } } } }); Object.defineProperty(CSSOM.CSSPageRule.prototype, "style", { get: function() { return this.__style; }, set: function(value) { if (typeof value === "string") { this.__style.cssText = value; } else { this.__style = value; } } }); Object.defineProperty(CSSOM.CSSPageRule.prototype, "cssText", { get: function() { var values = ""; if (this.cssRules.length) { var valuesArr = [" {"]; this.style.cssText && valuesArr.push(this.style.cssText); valuesArr.push(this.cssRules.reduce(function(acc, rule){ if (rule.cssText !== "") { acc.push(rule.cssText); } return acc; }, []).join("\n ")); values = valuesArr.join("\n ") + "\n}"; } else { values = " {" + (this.style.cssText ? " " + this.style.cssText : "") + " }"; } return "@page" + (this.selectorText ? " " + this.selectorText : "") + values; } }); /** * Parses a CSS string and returns a `CSSStyleSheet` object representing the parsed stylesheet. * * @param {string} token - The CSS string to parse. * @param {object} [opts] - Optional parsing options. * @param {object} [opts.globalObject] - An optional global object to prioritize over the window object. Useful on jsdom webplatform tests. * @param {Element | ProcessingInstruction} [opts.ownerNode] - The owner node of the stylesheet. * @param {CSSRule} [opts.ownerRule] - The owner rule of the stylesheet. * @param {CSSOM.CSSStyleSheet} [opts.styleSheet] - Reuse a style sheet instead of creating a new one (e.g. as `parentStyleSheet`) * @param {CSSOM.CSSRuleList} [opts.cssRules] - Prepare all rules in this list instead of mutating the style sheet continually * @param {function|boolean} [errorHandler] - Optional error handler function or `true` to use `console.error`. * @returns {CSSOM.CSSStyleSheet} The parsed `CSSStyleSheet` object. */ CSSOM.parse = function parse(token, opts, errorHandler) { errorHandler = errorHandler === true ? (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 hasUnmatchedQuoteInSelector = false; // Track if current selector has unmatched quote var SIGNIFICANT_WHITESPACE = { "name": true, "before-name": true, "selector": true, "value": true, "value-parenthesis": true, "atRule": true, "importRule-begin": true, "importRule": true, "namespaceRule-begin": true, "namespaceRule": true, "atBlock": true, "containerBlock": true, "conditionBlock": true, "counterStyleBlock": true, "propertyBlock": true, 'documentRule-begin': true, "scopeBlock": true, "layerBlock": true, "pageBlock": true }; var styleSheet; if (opts && opts.styleSheet) { styleSheet = opts.styleSheet; } else { if (opts && opts.globalObject && opts.globalObject.CSSStyleSheet) { styleSheet = new opts.globalObject.CSSStyleSheet(); } else { styleSheet = new CSSOM.CSSStyleSheet(); } styleSheet.__constructed = false; } var topScope; if (opts && opts.cssRules) { topScope = { cssRules: opts.cssRules }; } else { topScope = styleSheet; } if (opts && opts.ownerNode) { styleSheet.__ownerNode = opts.ownerNode; var ownerNodeMedia = opts.ownerNode.media || (opts.ownerNode.getAttribute && opts.ownerNode.getAttribute("media")); if (ownerNodeMedia) { styleSheet.media.mediaText = ownerNodeMedia; } var ownerNodeTitle = opts.ownerNode.title || (opts.ownerNode.getAttribute && opts.ownerNode.getAttribute("title")); if (ownerNodeTitle) { styleSheet.__title = ownerNodeTitle; } } if (opts && opts.ownerRule) { styleSheet.__ownerRule = opts.ownerRule; } // @type CSSStyleSheet|CSSMediaRule|CSSContainerRule|CSSSupportsRule|CSSFontFaceRule|CSSKeyframesRule|CSSDocumentRule var currentScope = topScope; // @type CSSMediaRule|CSSContainerRule|CSSSupportsRule|CSSKeyframesRule|CSSDocumentRule var parentRule; var ancestorRules = []; var prevScope; var name, priority = "", styleRule, mediaRule, containerRule, counterStyleRule, propertyRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule, startingStyleRule, scopeRule, pageRule, layerBlockRule, layerStatementRule, nestedSelectorRule, namespaceRule; // Track defined namespace prefixes for validation var definedNamespacePrefixes = {}; // Track which rules have been added var ruleIdCounter = 0; var addedToParent = {}; var addedToTopScope = {}; var addedToCurrentScope = {}; // Helper to get unique ID for tracking rules function getRuleId(rule) { if (!rule.__parseId) { rule.__parseId = ++ruleIdCounter; } return rule.__parseId; } // Cache last validation boundary position // to avoid rescanning the entire token string for each at-rule var lastValidationBoundary = 0; // Pre-compile validation regexes for common at-rules var validationRegexCache = {}; function getValidationRegex(atRuleKey) { if (!validationRegexCache[atRuleKey]) { var sourceRuleRegExp = atRuleKey === "@import" ? forwardImportRuleValidationRegExp : forwardRuleValidationRegExp; validationRegexCache[atRuleKey] = new RegExp(atRuleKey + sourceRuleRegExp.source, sourceRuleRegExp.flags); } return validationRegexCache[atRuleKey]; } // Import regex patterns from shared module var atKeyframesRegExp = regexPatterns.atKeyframesRegExp; var beforeRulePortionRegExp = regexPatterns.beforeRulePortionRegExp; var beforeRuleValidationRegExp = regexPatterns.beforeRuleValidationRegExp; var forwardRuleValidationRegExp = regexPatterns.forwardRuleValidationRegExp; var forwardImportRuleValidationRegExp = regexPatterns.forwardImportRuleValidationRegExp; // Pre-compile regexBefore to avoid creating it on every validateAtRule call var regexBefore = new RegExp(beforeRulePortionRegExp.source, beforeRulePortionRegExp.flags); var forwardRuleClosingBraceRegExp = regexPatterns.forwardRuleClosingBraceRegExp; var forwardRuleSemicolonAndOpeningBraceRegExp = regexPatterns.forwardRuleSemicolonAndOpeningBraceRegExp; var cssCustomIdentifierRegExp = regexPatterns.cssCustomIdentifierRegExp; var startsWithCombinatorRegExp = regexPatterns.startsWithCombinatorRegExp; var atPageRuleSelectorRegExp = regexPatterns.atPageRuleSelectorRegExp; var startsWithHexEscapeRegExp = regexPatterns.startsWithHexEscapeRegExp; var identStartCharRegExp = regexPatterns.identStartCharRegExp; var identCharRegExp = regexPatterns.identCharRegExp; var specialCharsNeedEscapeRegExp = regexPatterns.specialCharsNeedEscapeRegExp; var combinatorOrSeparatorRegExp = regexPatterns.combinatorOrSeparatorRegExp; var afterHexEscapeSeparatorRegExp = regexPatterns.afterHexEscapeSeparatorRegExp; var trailingSpaceSeparatorRegExp = regexPatterns.trailingSpaceSeparatorRegExp; var endsWithHexEscapeRegExp = regexPatterns.endsWithHexEscapeRegExp; var attributeSelectorContentRegExp = regexPatterns.attributeSelectorContentRegExp; var pseudoElementRegExp = regexPatterns.pseudoElementRegExp; var invalidCombinatorLtGtRegExp = regexPatterns.invalidCombinatorLtGtRegExp; var invalidCombinatorDoubleGtRegExp = regexPatterns.invalidCombinatorDoubleGtRegExp; var consecutiveCombinatorsRegExp = regexPatterns.consecutiveCombinatorsRegExp; var invalidSlottedRegExp = regexPatterns.invalidSlottedRegExp; var invalidPartRegExp = regexPatterns.invalidPartRegExp; var invalidCueRegExp = regexPatterns.invalidCueRegExp; var invalidCueRegionRegExp = regexPatterns.invalidCueRegionRegExp; var invalidNestingPattern = regexPatterns.invalidNestingPattern; var emptyPseudoClassRegExp = regexPatterns.emptyPseudoClassRegExp; var whitespaceNormalizationRegExp = regexPatterns.whitespaceNormalizationRegExp; var newlineRemovalRegExp = regexPatterns.newlineRemovalRegExp; var whitespaceAndDotRegExp = regexPatterns.whitespaceAndDotRegExp; var declarationOrOpenBraceRegExp = regexPatterns.declarationOrOpenBraceRegExp; var ampersandRegExp = regexPatterns.ampersandRegExp; var hexEscapeSequenceRegExp = regexPatterns.hexEscapeSequenceRegExp; var attributeCaseFlagRegExp = regexPatterns.attributeCaseFlagRegExp; var prependedAmpersandRegExp = regexPatterns.prependedAmpersandRegExp; var openBraceGlobalRegExp = regexPatterns.openBraceGlobalRegExp; var closeBraceGlobalRegExp = regexPatterns.closeBraceGlobalRegExp; var scopePreludeSplitRegExp = regexPatterns.scopePreludeSplitRegExp; var leadingWhitespaceRegExp = regexPatterns.leadingWhitespaceRegExp; var doubleQuoteRegExp = regexPatterns.doubleQuoteRegExp; var backslashRegExp = regexPatterns.backslashRegExp; /** * 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; } /** * Parses the scope prelude and extracts start and end selectors. * @param {string} preludeContent - The scope prelude content (without @scope keyword) * @returns {object} Object with startSelector and endSelector properties */ function parseScopePrelude(preludeContent) { var parts = preludeContent.split(scopePreludeSplitRegExp); // Restore the parentheses that were consumed by the split if (parts.length === 2) { parts[0] = parts[0] + ')'; parts[1] = '(' + parts[1]; } var hasStart = parts[0] && parts[0].charAt(0) === '(' && parts[0].charAt(parts[0].length - 1) === ')'; var hasEnd = parts[1] && parts[1].charAt(0) === '(' && parts[1].charAt(parts[1].length - 1) === ')'; // Handle case: @scope to () var hasOnlyEnd = !hasStart && !hasEnd && parts[0].indexOf('to (') === 0 && parts[0].charAt(parts[0].length - 1) === ')'; var startSelector = ''; var endSelector = ''; if (hasStart) { startSelector = parts[0].slice(1, -1).trim(); } if (hasEnd) { endSelector = parts[1].slice(1, -1).trim(); } if (hasOnlyEnd) { endSelector = parts[0].slice(4, -1).trim(); } return { startSelector: startSelector, endSelector: endSelector, hasStart: hasStart, hasEnd: hasEnd, hasOnlyEnd: hasOnlyEnd }; }; /** * Checks if a selector contains pseudo-elements. * @param {string} selector - The CSS selector to check * @returns {boolean} True if the selector contains pseudo-elements */ function hasPseudoElement(selector) { // Match only double-colon (::) pseudo-elements // Also match legacy single-colon pseudo-elements: :before, :after, :first-line, :first-letter // These must NOT be followed by alphanumeric characters (to avoid matching :before-x or similar) return pseudoElementRegExp.test(selector); }; /** * Validates balanced parentheses, brackets, and quotes in a selector. * * @param {string} selector - The CSS selector to validate * @param {boolean} trackAttributes - Whether to track attribute selector context * @param {boolean} useStack - Whether to use a stack for parentheses (needed for nested validation) * @returns {boolean} True if the syntax is valid (all brackets, parentheses, and quotes are balanced) */ function validateBalancedSyntax(selector, trackAttributes, useStack) { var parenDepth = 0; var bracketDepth = 0; var inSingleQuote = false; var inDoubleQuote = false; var inAttr = false; var stack = useStack ? [] : null; for (var i = 0; i < selector.length; i++) { var char = selector[i]; // Handle escape sequences - skip hex escapes or simple escapes if (char === '\\') { var escapeLen = getEscapeSequenceLength(selector, i); if (escapeLen > 0) { i += escapeLen - 1; // -1 because loop will increment continue; } } if (inSingleQuote) { if (char === "'") { inSingleQuote = false; } } else if (inDoubleQuote) { if (char === '"') { inDoubleQuote = false; } } else if (trackAttributes && inAttr) { if (char === "]") { inAttr = false; } else if (char === "'") { inSingleQuote = true; } else if (char === '"') { inDoubleQuote = true; } } else { if (trackAttributes && char === "[") { inAttr = true; } else if (char === "'") { inSingleQuote = true; } else if (char === '"') { inDoubleQuote = true; } else if (char === '(') { if (useStack) { stack.push("("); } else { parenDepth++; } } else if (char === ')') { if (useStack) { if (!stack.length || stack.pop() !== "(") { return false; } } else { parenDepth--; if (parenDepth < 0) { return false; } } } else if (char === '[') { bracketDepth++; } else if (char === ']') { bracketDepth--; if (bracketDepth < 0) { return false; } } } } // Check if everything is balanced if (useStack) { return stack.length === 0 && bracketDepth === 0 && !inSingleQuote && !inDoubleQuote && !inAttr; } else { return parenDepth === 0 && bracketDepth === 0 && !inSingleQuote && !inDoubleQuote; } }; /** * Checks for basic syntax errors in selectors (mismatched parentheses, brackets, quotes). * @param {string} selector - The CSS selector to check * @returns {boolean} True if there are syntax errors */ function hasBasicSyntaxError(selector) { return !validateBalancedSyntax(selector, false, false); }; /** * Checks for invalid combinator patterns in selectors. * @param {string} selector - The CSS selector to check * @returns {boolean} True if the selector contains invalid combinators */ function hasInvalidCombinators(selector) { // Check for invalid combinator patterns: // - <> (not a valid combinator) // - >> (deep descendant combinator, deprecated and invalid) // - Multiple consecutive combinators like >>, >~, etc. if (invalidCombinatorLtGtRegExp.test(selector)) return true; if (invalidCombinatorDoubleGtRegExp.test(selector)) return true; // Check for other invalid consecutive combinator patterns if (consecutiveCombinatorsRegExp.test(selector)) return true; return false; }; /** * Checks for invalid pseudo-like syntax (function calls without proper pseudo prefix). * @param {string} selector - The CSS selector to check * @returns {boolean} True if the selector contains invalid pseudo-like syntax */ function hasInvalidPseudoSyntax(selector) { // Check for specific known pseudo-elements used without : or :: prefix // Examples: slotted(div), part(name), cue(selector) // These are ONLY valid as ::slotted(), ::part(), ::cue() var invalidPatterns = [ invalidSlottedRegExp, invalidPartRegExp, invalidCueRegExp, invalidCueRegionRegExp ]; for (var i = 0; i < invalidPatterns.length; i++) { if (invalidPatterns[i].test(selector)) { return true; } } return false; }; /** * Checks for invalid nesting selector (&) usage. * The & selector cannot be directly followed by a type selector without a delimiter. * Valid: &.class, &#id, &[attr], &:hover, &::before, & div, &>div * Invalid: &div, &span * @param {string} selector - The CSS selector to check * @returns {boolean} True if the selector contains invalid & usage */ function hasInvalidNestingSelector(selector) { // Check for & followed directly by a letter (type selector) without any delimiter // This regex matches & followed by a letter (start of type selector) that's not preceded by an escape // We need to exclude valid cases like &.class, &#id, &[attr], &:pseudo, &::pseudo, & (with space), &> return invalidNestingPattern.test(selector); }; /** * Checks if an at-rule can be nested based on parent chain validation. * Used for at-rules like `@counter-style`, `@property` and `@font-face` rules that can only be nested inside * `CSSScopeRule` or `CSSConditionRule` without `CSSStyleRule` in parent chain. * @returns {boolean} `true` if nesting is allowed, `false` otherwise */ function canAtRuleBeNested() { if (currentScope === topScope) { return true; // Top-level is always allowed } var hasStyleRuleInChain = false; var hasValidParent = false; // Check currentScope if (currentScope.constructor.name === 'CSSStyleRule') { hasStyleRuleInChain = true; } else if (currentScope instanceof CSSOM.CSSScopeRule || currentScope instanceof CSSOM.CSSConditionRule) { hasValidParent = true; } // Check ancestorRules for CSSStyleRule if (!hasStyleRuleInChain) { for (var j = 0; j < ancestorRules.length; j++) { if (ancestorRules[j].constructor.name === 'CSSStyleRule') { hasStyleRuleInChain = true; break; } if (ancestorRules[j] instanceof CSSOM.CSSScopeRule || ancestorRules[j] instanceof CSSOM.CSSConditionRule) { hasValidParent = true; } } } // Allow nesting if we have a valid parent and no style rule in the chain return hasValidParent && !hasStyleRuleInChain; } function validateAtRule(atRuleKey, validCallback, cannotBeNested) { var isValid = false; // Use cached regex instead of creating new one each time var ruleRegExp = getValidationRegex(atRuleKey); // Only slice what we need for validation (max 100 chars) // since we only check match at position 0 var lookAheadLength = Math.min(100, token.length - i); var ruleSlice = token.slice(i, i + lookAheadLength); // 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 !== topScope ? false : true; // First, check if there is no invalid characters just after the at-rule if (shouldPerformCheck && ruleSlice.search(ruleRegExp) === 0) { // Only scan from the last known validation boundary var searchStart = Math.max(0, lastValidationBoundary); var beforeSlice = token.slice(searchStart, i); // Use pre-compiled regex instead of creating new one each time var matches = beforeSlice.match(regexBefore); var lastI = matches ? searchStart + beforeSlice.lastIndexOf(matches[matches.length - 1]) : searchStart; 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; // Update the validation boundary cache to this position lastValidationBoundary = lastI; } } // Additional validation for @scope rule if (isValid && atRuleKey === "@scope") { var openBraceIndex = ruleSlice.indexOf('{'); if (openBraceIndex !== -1) { // Extract the rule prelude (everything between the at-rule and {) var rulePrelude = ruleSlice.slice(0, openBraceIndex).trim(); // Skip past at-rule keyword and whitespace var preludeContent = rulePrelude.slice("@scope".length).trim(); if (preludeContent.length > 0) { // Parse the scope prelude var parsedScopePrelude = parseScopePrelude(preludeContent); var startSelector = parsedScopePrelude.startSelector; var endSelector = parsedScopePrelude.endSelector; var hasStart = parsedScopePrelude.hasStart; var hasEnd = parsedScopePrelude.hasEnd; var hasOnlyEnd = parsedScopePrelude.hasOnlyEnd; // Validation rules for @scope: // 1. Empty selectors in parentheses are invalid: @scope () {} or @scope (.a) to () {} if ((hasStart && startSelector === '') || (hasEnd && endSelector === '') || (hasOnlyEnd && endSelector === '')) { isValid = false; } // 2. Pseudo-elements are invalid in scope selectors else if ((startSelector && hasPseudoElement(startSelector)) || (endSelector && hasPseudoElement(endSelector))) { isValid = false; } // 3. Basic syntax errors (mismatched parens, brackets, quotes) else if ((startSelector && hasBasicSyntaxError(startSelector)) || (endSelector && hasBasicSyntaxError(endSelector))) { isValid = false; } // 4. Invalid combinator patterns else if ((startSelector && hasInvalidCombinators(startSelector)) || (endSelector && hasInvalidCombinators(endSelector))) { isValid = false; } // 5. Invalid pseudo-like syntax (function without : or :: prefix) else if ((startSelector && hasInvalidPseudoSyntax(startSelector)) || (endSelector && hasInvalidPseudoSyntax(endSelector))) { isValid = false; } // 6. Invalid structure (no proper parentheses found when prelude is not empty) else if (!hasStart && !hasOnlyEnd) { isValid = false; } } // Empty prelude (@scope {}) is valid } } if (isValid && atRuleKey === "@page") { var openBraceIndex = ruleSlice.indexOf('{'); if (openBraceIndex !== -1) { // Extract the rule prelude (everything between the at-rule and {) var rulePrelude = ruleSlice.slice(0, openBraceIndex).trim(); // Skip past at-rule keyword and whitespace var preludeContent = rulePrelude.slice("@page".length).trim(); if (preludeContent.length > 0) { var trimmedValue = preludeContent.trim(); // Empty selector is valid for @page if (trimmedValue !== '') { // Parse @page selectorText for page name and pseudo-pages // Valid formats: // - (empty - no name, no pseudo-page) // - :left, :right, :first, :blank (pseudo-page only) // - named (named page only) // - named:first (named page with single pseudo-page) // - named:first:left (named page with multiple pseudo-pages) var match = trimmedValue.match(atPageRuleSelectorRegExp); if (match) { var pageName = match[1] || ''; var pseudoPages = match[2] || ''; // Validate page name if present if (pageName) { if (!cssCustomIdentifierRegExp.test(pageName)) { isValid = false; } } // Validate pseudo-pages if present if (pseudoPages) { var pseudos = pseudoPages.split(':').filter(function (p) { return p; }); var validPseudos = ['left', 'right', 'first', 'blank']; var allValid = true; for (var j = 0; j < pseudos.length; j++) { if (validPseudos.indexOf(pseudos[j].toLowerCase()) === -1) { allValid = false; break; } } if (!allValid) { isValid = false; } } } else { isValid = false; } } } } } 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 !== topScope) { 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); } } // Helper functions for looseSelectorValidator // Defined outside to avoid recreation on every validation call /** * Check if character is a valid identifier start * @param {string} c - Character to check * @returns {boolean} */ function isIdentStart(c) { return /[a-zA-Z_\u00A0-\uFFFF]/.test(c); } /** * Check if character is a valid identifier character * @param {string} c - Character to check * @returns {boolean} */ function isIdentChar(c) { return /[a-zA-Z0-9_\u00A0-\uFFFF\-]/.test(c); } /** * Helper function to validate CSS selector syntax without regex backtracking. * Iteratively parses the selector string to identify valid components. * * Supports: * - Escaped characters (e.g., .class\!, #id\@name) * - Namespace selectors (ns|element, *|element, |element) * - All standard CSS selectors (class, ID, type, attribute, pseudo, etc.) * - Combinators (>, +, ~, whitespace) * - Nesting selector (&) * * This approach eliminates exponential backtracking by using explicit character-by-character * parsing instead of nested quantifiers in regex. * * @param {string} selector - The selector to validate * @returns {boolean} - True if valid selector syntax */ function looseSelectorValidator(selector) { if (!selector || selector.length === 0) { return false; } var i = 0; var len = selector.length; var hasMatchedComponent = false; // Helper: Skip escaped character (backslash + hex escape or any char) function skipEscape() { if (i < len && selector[i] === '\\') { var escapeLen = getEscapeSequenceLength(selector, i); if (escapeLen > 0) { i += escapeLen; // Skip entire escape sequence return true; } } return false; } // Helper: Parse identifier (with possible escapes) function parseIdentifier() { var start = i; while (i < len) { if (skipEscape()) { continue; } else if (isIdentChar(selector[i])) { i++; } else { break; } } return i > start; } // Helper: Parse namespace prefix (optional) function parseNamespace() { var start = i; // Match: *| or identifier| or | if (i < len && selector[i] === '*') { i++; } else if (i < len && (isIdentStart(selector[i]) || selector[i] === '\\')) { parseIdentifier(); } if (i < len && selector[i] === '|') { i++; return true; } // Rollback if no pipe found i = start; return false; } // Helper: Parse pseudo-class/element arguments (with balanced parens) function parsePseudoArgs() { if (i >= len || selector[i] !== '(') { return false; } i++; // Skip opening paren var depth = 1; var inString = false; var stringChar = ''; while (i < len && depth > 0) { var c = selector[i]; if (c === '\\' && i + 1 < len) { i += 2; // Skip escaped character } else if (!inString && (c === '"' || c === '\'')) { inString = true; stringChar = c; i++; } else if (inString && c === stringChar) { inString = false; i++; } else if (!inString && c === '(') { depth++; i++; } else if (!inString && c === ')') { depth--; i++; } else { i++; } } return depth === 0; } // Main parsing loop while (i < len) { var matched = false; var start = i; // Skip whitespace while (i < len && /\s/.test(selector[i])) { i++; } if (i > start) { hasMatchedComponent = true; continue; } // Match combinators: >, +, ~ if (i < len && /[>+~]/.test(selector[i])) { i++; hasMatchedComponent = true; // Skip trailing whitespace while (i < len && /\s/.test(selector[i])) { i++; } continue; } // Match nesting selector: & if (i < len && selector[i] === '&') { i++; hasMatchedComponent = true; matched = true; } // Match class selector: .identifier else if (i < len && selector[i] === '.') { i++; if (parseIdentifier()) { hasMatchedComponent = true; matched = true; } } // Match ID selector: #identifier else if (i < len && selector[i] === '#') { i++; if (parseIdentifier()) { hasMatchedComponent = true; matched = true; } } // Match pseudo-class/element: :identifier or ::identifier else if (i < len && selector[i] === ':') { i++; if (i < len && selector[i] === ':') { i++; // Pseudo-element } if (parseIdentifier()) { parsePseudoArgs(); // Optional arguments hasMatchedComponent = true; matched = true; } } // Match attribute selector: [...] else if (i < len && selector[i] === '[') { i++; var depth = 1; while (i < len && depth > 0) { if (selector[i] === '\\') { i += 2; } else if (selector[i] === '\'') { i++; while (i < len && selector[i] !== '\'') { if (selector[i] === '\\') i += 2; else i++; } if (i < len) i++; // Skip closing quote } else if (selector[i] === '"') { i++; while (i < len && selector[i] !== '"') { if (selector[i] === '\\') i += 2; else i++; } if (i < len) i++; // Skip closing quote } else if (selector[i] === '[') { depth++; i++; } else if (selector[i] === ']') { depth--; i++; } else { i++; } } if (depth === 0) { hasMatchedComponent = true; matched = true; } } // Match type selector with optional namespace: [namespace|]identifier else if (i < len && (isIdentStart(selector[i]) || selector[i] === '\\' || selector[i] === '*' || selector[i] === '|')) { parseNamespace(); // Optional namespace prefix if (i < len && selector[i] === '*') { i++; // Universal selector hasMatchedComponent = true; matched = true; } else if (i < len && (isIdentStart(selector[i]) || selector[i] === '\\')) { if (parseIdentifier()) { hasMatchedComponent = true; matched = true; } } } // If no match found, invalid selector if (!matched && i === start) { return false; } } return hasMatchedComponent; } /** * 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 (`*`) * - Namespace selectors (e.g., `*|div`, `custom|div`, `|div`) * - 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) { // Guard against extremely long selectors to prevent potential regex performance issues // Reasonable selectors are typically under 1000 characters if (selector.length > 10000) { return false; } // Validate balanced syntax with attribute tracking and stack-based parentheses matching if (!validateBalancedSyntax(selector, true, true)) { return false; } // Check for invalid combinator patterns if (hasInvalidCombinators(selector)) { return false; } // Check for invalid pseudo-like syntax if (hasInvalidPseudoSyntax(selector)) { return false; } // Check for invalid nesting selector (&) usage if (hasInvalidNestingSelector(selector)) { return false; } // Check for invalid pseudo-class usage with quoted strings // Pseudo-classes like :lang(), :dir(), :nth-*() should not accept quoted strings // Using iterative parsing instead of regex to avoid exponential backtracking var noQuotesPseudos = ['lang', 'dir', 'nth-child', 'nth-last-child', 'nth-of-type', 'nth-last-of-type']; for (var idx = 0; idx < selector.length; idx++) { // Look for pseudo-class/element start if (selector[idx] === ':') { var pseudoStart = idx; idx++; // Skip second colon for pseudo-elements if (idx < selector.length && selector[idx] === ':') { idx++; } // Extract pseudo name var nameStart = idx; while (idx < selector.length && /[a-zA-Z0-9\-]/.test(selector[idx])) { idx++; } if (idx === nameStart) { continue; // No name found } var pseudoName = selector.substring(nameStart, idx).toLowerCase(); // Check if this pseudo has arguments if (idx < selector.length && selector[idx] === '(') { idx++; var contentStart = idx; var depth = 1; // Find matching closing paren (handle nesting) while (idx < selector.length && depth > 0) { if (selector[idx] === '\\') { idx += 2; // Skip escaped character } else if (selector[idx] === '(') { depth++; idx++; } else if (selector[idx] === ')') { depth--; idx++; } else { idx++; } } if (depth === 0) { var pseudoContent = selector.substring(contentStart, idx - 1); // Check if this pseudo should not have quoted strings for (var j = 0; j < noQuotesPseudos.length; j++) { if (pseudoName === noQuotesPseudos[j] && /['"]/.test(pseudoContent)) { return false; } } } } } } // Use the iterative validator to avoid regex backtracking issues return looseSelectorValidator(selector); } /** * Regular expression to match CSS pseudo-classes with arguments. * * Matches patterns like `:pseudo-class(argument)`, capturing the pseudo-class name and its argument. * * Capture groups: * 1. The pseudo-class name (letters and hyphens). * 2. The argument inside the parentheses (can contain nested parentheses, quoted strings, and other characters.). * * Global flag (`g`) is used to find all matches in the input string. * * Example matches: * - :nth-child(2n+1) * - :has(.sel:nth-child(3n)) * - :not(".foo, .bar") * * REPLACED WITH FUNCTION to avoid exponential backtracking. */ /** * Extract pseudo-classes with arguments from a selector using iterative parsing. * Replaces the previous globalPseudoClassRegExp to avoid exponential backtracking. * * Handles: * - Regular content without parentheses or quotes * - Single-quoted strings * - Double-quoted strings * - Nested parentheses (arbitrary depth) * * @param {string} selector - The CSS selector to parse * @returns {Array} Array of matches, each with: [fullMatch, pseudoName, pseudoArgs, startIndex] */ function extractPseudoClasses(selector) { var matches = []; for (var i = 0; i < selector.length; i++) { // Look for pseudo-class start (single or double colon) if (selector[i] === ':') { var pseudoStart = i; i++; // Skip second colon for pseudo-elements (::) if (i < selector.length && selector[i] === ':') { i++; } // Extract pseudo name var nameStart = i; while (i < selector.length && /[a-zA-Z\-]/.test(selector[i])) { i++; } if (i === nameStart) { continue; // No name found } var pseudoName = selector.substring(nameStart, i); // Check if this pseudo has arguments if (i < selector.length && selector[i] === '(') { i++; var argsStart = i; var depth = 1; var inSingleQuote = false; var inDoubleQuote = false; // Find matching closing paren (handle nesting and strings) while (i < selector.length && depth > 0) { var ch = selector[i]; if (ch === '\\') { i += 2; // Skip escaped character } else if (ch === "'" && !inDoubleQuote) { inSingleQuote = !inSingleQuote; i++; } else if (ch === '"' && !inSingleQuote) { inDoubleQuote = !inDoubleQuote; i++; } else if (ch === '(' && !inSingleQuote && !inDoubleQuote) { depth++; i++; } else if (ch === ')' && !inSingleQuote && !inDoubleQuote) { depth--; i++; } else { i++; } } if (depth === 0) { var pseudoArgs = selector.substring(argsStart, i - 1); var fullMatch = selector.substring(pseudoStart, i); // Store match in same format as regex: [fullMatch, pseudoName, pseudoArgs, startIndex] matches.push([fullMatch, pseudoName, pseudoArgs, pseudoStart]); } // Move back one since loop will increment i--; } } } return matches; } /** * Parses a CSS selector string and splits it into parts, handling nested parentheses. * * This function is useful for splitting selectors that may contain nested function-like * syntax (e.g., :not(.foo, .bar)), ensuring that commas inside parentheses do not split * the selector. * * @param {string} selector - The CSS selector string to parse. * @returns {string[]} An array of selector parts, split by top-level commas, with whitespace trimmed. */ function parseAndSplitNestedSelectors(selector) { var depth = 0; // Track parenthesis nesting depth var buffer = ""; // Accumulate characters for current selector part var parts = []; // Array of split selector parts var inSingleQuote = false; // Track if we're inside single quotes var inDoubleQuote = false; // Track if we're inside double quotes var i, char; for (i = 0; i < selector.length; i++) { char = selector.charAt(i); // Handle escape sequences - skip them entirely if (char === '\\' && i + 1 < selector.length) { buffer += char; i++; buffer += selector.charAt(i); continue; } // Handle single quote strings if (char === "'" && !inDoubleQuote) { inSingleQuote = !inSingleQuote; buffer += char; } // Handle double quote strings else if (char === '"' && !inSingleQuote) { inDoubleQuote = !inDoubleQuote; buffer += char; } // Process characters outside of quoted strings else if (!inSingleQuote && !inDoubleQuote) { if (char === '(') { // Entering a nested level (e.g., :is(...)) depth++; buffer += char; } else if (char === ')') { // Exiting a nested level depth--; buffer += char; } else if (char === ',' && depth === 0) { // Found a top-level comma separator - split here // Note: escaped commas (\,) are already handled above if (buffer.trim()) { parts.push(buffer.trim()); } buffer = ""; } else { // Regular character - add to buffer buffer += char; } } // Characters inside quoted strings - add to buffer else { buffer += char; } } // Add any remaining content in buffer as the last part var trimmed = buffer.trim(); if (trimmed) { // Preserve trailing space if selector ends with hex escape var endsWithHexEscape = endsWithHexEscapeRegExp.test(buffer); parts.push(endsWithHexEscape ? buffer.replace(leadingWhitespaceRegExp, '') : trimmed); } return parts; } /** * Validates a CSS selector string, including handling of nested selectors within certain pseudo-classes. * * This function checks if the provided selector is valid according to the rules defined by * `basicSelectorValidator`. For pseudo-classes that accept selector lists (such as :not, :is, :has, :where), * it recursively validates each nested selector using the same validation logic. * * @param {string} selector - The CSS selector string to validate. * @returns {boolean} Returns `true` if the selector is valid, otherwise `false`. */ // Cache to store validated selectors (previously a ES6 Map, now an ES5-compliant object) var validatedSelectorsCache = {}; // Only pseudo-classes that accept selector lists should recurse var selectorListPseudoClasses = { 'not': true, 'is': true, 'has': true, 'where': true }; function validateSelector(selector) { if (validatedSelectorsCache.hasOwnProperty(selector)) { return validatedSelectorsCache[selector]; } // Use function-based parsing to extract pseudo-classes (avoids backtracking) var pseudoClassMatches = extractPseudoClasses(selector); for (var j = 0; j < pseudoClassMatches.length; j++) { var pseudoClass = pseudoClassMatches[j][1]; if (selectorListPseudoClasses.hasOwnProperty(pseudoClass)) { var nestedSelectors = parseAndSplitNestedSelectors(pseudoClassMatches[j][2]); // Check if ANY selector in the list contains & (nesting selector) // If so, skip validation for the entire selector list since & will be replaced at runtime var hasAmpersand = false; for (var k = 0; k < nestedSelectors.length; k++) { if (ampersandRegExp.test(nestedSelectors[k])) { hasAmpersand = true; break; } } // If any selector has &, skip validation for this entire pseudo-class if (hasAmpersand) { continue; } // Otherwise, validate each selector normally for (var i = 0; i < nestedSelectors.length; i++) { var nestedSelector = nestedSelectors[i]; if (!validatedSelectorsCache.hasOwnProperty(nestedSelector)) { var nestedSelectorValidation = validateSelector(nestedSelector); validatedSelectorsCache[nestedSelector] = nestedSelectorValidation; if (!nestedSelectorValidation) { validatedSelectorsCache[selector] = false; return false; } } else if (!validatedSelectorsCache[nestedSelector]) { validatedSelectorsCache[selector] = false; return false; } } } } var basicSelectorValidation = basicSelectorValidator(selector); validatedSelectorsCache[selector] = basicSelectorValidation; return basicSelectorValidation; } /** * Validates namespace selectors by checking if the namespace prefix is defined. * * @param {string} selector - The CSS selector to validate * @returns {boolean} Returns true if the namespace is valid, false otherwise */ function validateNamespaceSelector(selector) { // Check if selector contains a namespace prefix // We need to ignore pipes inside attribute selectors var pipeIndex = -1; var inAttr = false; var inSingleQuote = false; var inDoubleQuote = false; for (var i = 0; i < selector.length; i++) { var char = selector[i]; // Handle escape sequences - skip hex escapes or simple escapes if (char === '\\') { var escapeLen = getEscapeSequenceLength(selector, i); if (escapeLen > 0) { i += escapeLen - 1; // -1 because loop will increment continue; } } if (inSingleQuote) { if (char === "'") { inSingleQuote = false; } } else if (inDoubleQuote) { if (char === '"') { 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 === "|" && !inAttr) { // This is a namespace separator, not an attribute operator pipeIndex = i; break; } } } if (pipeIndex === -1) { return true; // No namespace, always valid } var namespacePrefix = selector.substring(0, pipeIndex); // Universal namespace (*|) and default namespace (|) are always valid if (namespacePrefix === '*' || namespacePrefix === '') { return true; } // Check if the custom namespace prefix is defined return definedNamespacePrefixes.hasOwnProperty(namespacePrefix); } /** * Normalizes escape sequences in a selector to match browser behavior. * Decodes escape sequences and re-encodes them in canonical form. * * @param {string} selector - The selector to normalize * @returns {string} Normalized selector */ function normalizeSelectorEscapes(selector) { var result = ''; var i = 0; var nextChar = ''; // Track context for identifier boundaries var inIdentifier = false; var inAttribute = false; var attributeDepth = 0; var needsEscapeForIdent = false; var lastWasHexEscape = false; while (i < selector.length) { var char = selector[i]; // Track attribute selector context if (char === '[' && !inAttribute) { inAttribute = true; attributeDepth = 1; result += char; i++; needsEscapeForIdent = false; inIdentifier = false; lastWasHexEscape = false; continue; } if (inAttribute) { if (char === '[') attributeDepth++; if (char === ']') { attributeDepth--; if (attributeDepth === 0) inAttribute = false; } // Don't normalize escapes inside attribute selectors if (char === '\\' && i + 1 < selector.length) { var escapeLen = getEscapeSequenceLength(selector, i); result += selector.substr(i, escapeLen); i += escapeLen; } else { result += char; i++; } lastWasHexEscape = false; continue; } // Handle escape sequences if (char === '\\') { var escapeLen = getEscapeSequenceLength(selector, i); if (escapeLen > 0) { var escapeSeq = selector.substr(i, escapeLen); var decoded = decodeEscapeSequence(escapeSeq); var wasHexEscape = startsWithHexEscapeRegExp.test(escapeSeq); var hadTerminatingSpace = wasHexEscape && escapeSeq[escapeLen - 1] === ' '; nextChar = selector[i + escapeLen] || ''; // Check if this character needs escaping var needsEscape = false; var useHexEscape = false; if (needsEscapeForIdent) { // At start of identifier (after . # or -) // Digits must be escaped, letters/underscore/_/- don't need escaping if (isDigit(decoded)) { needsEscape = true; useHexEscape = true; } else if (decoded === '-') { // Dash at identifier start: keep escaped if it's the only character, // otherwise it can be decoded var remainingSelector = selector.substring(i + escapeLen); var hasMoreIdentChars = remainingSelector && identCharRegExp.test(remainingSelector[0]); needsEscape = !hasMoreIdentChars; } else if (!identStartCharRegExp.test(decoded)) { needsEscape = true; } } else { if (specialCharsNeedEscapeRegExp.test(decoded)) { needsEscape = true; } } if (needsEscape) { if (useHexEscape) { // Use normalized hex escape var codePoint = decoded.charCodeAt(0); var hex = codePoint.toString(16); result += '\\' + hex; // Add space if next char could continue the hex sequence, // or if at end of selector (to disambiguate the escape) if (isHexDigit(nextChar) || !nextChar || afterHexEscapeSeparatorRegExp.test(nextChar)) { result += ' '; lastWasHexEscape = false; } else { lastWasHexEscape = true; } } else { // Use simple character escape result += '\\' + decoded; lastWasHexEscape = false; } } else { // No escape needed, use the character directly // But if previous was hex escape (without terminating space) and this is alphanumeric, add space if (lastWasHexEscape && !hadTerminatingSpace && isAlphanumeric(decoded)) { result += ' '; } result += decoded; // Preserve terminating space at end of selector (when followed by non-ident char) if (hadTerminatingSpace && (!nextChar || afterHexEscapeSeparatorRegExp.test(nextChar))) { result += ' '; } lastWasHexEscape = false; } i += escapeLen; // After processing escape, check if we're still needing ident validation // Only stay in needsEscapeForIdent state if decoded was '-' needsEscapeForIdent = needsEscapeForIdent && decoded === '-'; inIdentifier = true; continue; } } // Handle regular characters if (char === '.' || char === '#') { result += char; needsEscapeForIdent = true; inIdentifier = false; lastWasHexEscape = false; i++; } else if (char === '-' && needsEscapeForIdent) { // Dash after . or # - next char must be valid ident start or digit (which needs escaping) result += char; needsEscapeForIdent = true; lastWasHexEscape = false; i++; } else if (isDigit(char) && needsEscapeForIdent) { // Digit at identifier start must be hex escaped var codePoint = char.charCodeAt(0); var hex = codePoint.toString(16); result += '\\' + hex; nextChar = selector[i + 1] || ''; // Add space if next char could continue the hex sequence, // or if at end of selector (to disambiguate the escape) if (isHexDigit(nextChar) || !nextChar || afterHexEscapeSeparatorRegExp.test(nextChar)) { result += ' '; lastWasHexEscape = false; } else { lastWasHexEscape = true; } needsEscapeForIdent = false; inIdentifier = true; i++; } else if (char === ':' || combinatorOrSeparatorRegExp.test(char)) { // Combinators, separators, and pseudo-class markers reset identifier state // Preserve trailing space from hex escape if (!(char === ' ' && lastWasHexEscape && result[result.length - 1] === ' ')) { result += char; } needsEscapeForIdent = false; inIdentifier = false; lastWasHexEscape = false; i++; } else if (isLetter(char) && lastWasHexEscape) { // Letter after hex escape needs a space separator result += ' ' + char; needsEscapeForIdent = false; inIdentifier = true; lastWasHexEscape = false; i++; } else if (char === ' ' && lastWasHexEscape) { // Trailing space - keep it if at end or before non-ident char nextChar = selector[i + 1] || ''; if (!nextChar || trailingSpaceSeparatorRegExp.test(nextChar)) { result += char; } needsEscapeForIdent = false; inIdentifier = false; lastWasHexEscape = false; i++; } else { result += char; needsEscapeForIdent = false; inIdentifier = true; lastWasHexEscape = false; i++; } } return result; } /** * Helper function to decode all escape sequences in a string. * * @param {string} str - The string to decode * @returns {string} The decoded string */ function decodeEscapeSequencesInString(str) { var result = ''; for (var i = 0; i < str.length; i++) { if (str[i] === '\\' && i + 1 < str.length) { // Get the escape sequence length var escapeLen = getEscapeSequenceLength(str, i); if (escapeLen > 0) { var escapeSeq = str.substr(i, escapeLen); var decoded = decodeEscapeSequence(escapeSeq); result += decoded; i += escapeLen - 1; // -1 because loop will increment continue; } } result += str[i]; } return result; } /** * Decodes a CSS escape sequence to its character value. * * @param {string} escapeSeq - The escape sequence (including backslash) * @returns {string} The decoded character */ function decodeEscapeSequence(escapeSeq) { if (escapeSeq.length < 2 || escapeSeq[0] !== '\\') { return escapeSeq; } var content = escapeSeq.substring(1); // Check if it's a hex escape var hexMatch = content.match(hexEscapeSequenceRegExp); if (hexMatch) { var codePoint = parseInt(hexMatch[1], 16); // Handle surrogate pairs for code points > 0xFFFF if (codePoint > 0xFFFF) { // Convert to surrogate pair codePoint -= 0x10000; var high = 0xD800 + (codePoint >> 10); var low = 0xDC00 + (codePoint & 0x3FF); return String.fromCharCode(high, low); } return String.fromCharCode(codePoint); } // Simple escape - return the character after backslash return content[0] || ''; } /** * Normalizes attribute selectors by ensuring values are properly quoted with double quotes. * Examples: * [attr=value] -> [attr="value"] * [attr="value"] -> [attr="value"] (unchanged) * [attr='value'] -> [attr="value"] (converted to double quotes) * * @param {string} selector - The selector to normalize * @returns {string|null} Normalized selector, or null if invalid */ function normalizeAttributeSelectors(selector) { var result = ''; var i = 0; while (i < selector.length) { // Look for attribute selector start if (selector[i] === '[') { result += '['; i++; var attrContent = ''; var depth = 1; // Find the closing bracket, handling nested brackets and escapes while (i < selector.length && depth > 0) { if (selector[i] === '\\' && i + 1 < selector.length) { attrContent += selector.substring(i, i + 2); i += 2; continue; } if (selector[i] === '[') depth++; if (selector[i] === ']') { depth--; if (depth === 0) break; } attrContent += selector[i]; i++; } // Normalize the attribute content var normalized = normalizeAttributeContent(attrContent); if (normalized === null) { // Invalid attribute selector (e.g., unclosed quote) return null; } result += normalized; if (i < selector.length && selector[i] === ']') { result += ']'; i++; } } else { result += selector[i]; i++; } } return result; } /** * Processes a quoted attribute value by checking for proper closure and decoding escape sequences. * @param {string} trimmedValue - The quoted value (with quotes) * @param {string} quoteChar - The quote character ('"' or "'") * @param {string} attrName - The attribute name * @param {string} operator - The attribute operator * @param {string} flag - Optional case-sensitivity flag * @returns {string|null} Normalized attribute content, or null if invalid */ function processQuotedAttributeValue(trimmedValue, quoteChar, attrName, operator, flag) { // Check if the closing quote is properly closed (not escaped) if (trimmedValue.length < 2) { return null; // Too short } // Find the actual closing quote (not escaped) var i = 1; var foundClose = false; while (i < trimmedValue.length) { if (trimmedValue[i] === '\\' && i + 1 < trimmedValue.length) { // Skip escape sequence var escapeLen = getEscapeSequenceLength(trimmedValue, i); i += escapeLen; continue; } if (trimmedValue[i] === quoteChar) { // Found closing quote foundClose = (i === trimmedValue.length - 1); break; } i++; } if (!foundClose) { return null; // Unclosed quote - invalid } // Extract inner value and decode escape sequences var innerValue = trimmedValue.slice(1, -1); var decodedValue = decodeEscapeSequencesInString(innerValue); // If decoded value contains quotes, we need to escape them var escapedValue = decodedValue.replace(doubleQuoteRegExp, '\\"'); return attrName + operator + '"' + escapedValue + '"' + (flag ? ' ' + flag : ''); } /** * Normalizes the content inside an attribute selector. * @param {string} content - The content between [ and ] * @returns {string} Normalized content, or null if invalid */ function normalizeAttributeContent(content) { // Match: attribute-name [operator] [value] [flag] var match = content.match(attributeSelectorContentRegExp); if (!match) { // No operator (e.g., [disabled]) or malformed - return as is return content; } var attrName = match[1]; var operator = match[2]; var valueAndFlag = match[3].trim(); // Trim here instead of in regex // Check if there's a case-sensitivity flag (i or s) at the end var flagMatch = valueAndFlag.match(attributeCaseFlagRegExp); var value = flagMatch ? flagMatch[1] : valueAndFlag; var flag = flagMatch ? flagMatch[2] : ''; // Check for unclosed quotes - this makes the selector invalid var trimmedValue = value.trim(); var firstChar = trimmedValue[0]; if (firstChar === '"') { return processQuotedAttributeValue(trimmedValue, '"', attrName, operator, flag); } if (firstChar === "'") { return processQuotedAttributeValue(trimmedValue, "'", attrName, operator, flag); } // Check for unescaped special characters in unquoted values // Escaped special characters are valid (e.g., \` is valid, but ` is not) var hasUnescapedSpecialChar = false; for (var i = 0; i < trimmedValue.length; i++) { var char = trimmedValue[i]; if (char === '\\' && i + 1 < trimmedValue.length) { // Skip the entire escape sequence var escapeLen = getEscapeSequenceLength(trimmedValue, i); if (escapeLen > 0) { i += escapeLen - 1; // -1 because loop will increment continue; } } // Check if this is an unescaped special character if (specialCharsNeedEscapeRegExp.test(char)) { hasUnescapedSpecialChar = true; break; } } if (hasUnescapedSpecialChar) { return null; // Unescaped special characters not allowed in unquoted attribute values } // Decode escape sequences in the value before quoting // Inside quotes, special characters don't need escaping var decodedValue = decodeEscapeSequencesInString(trimmedValue); // If the decoded value contains double quotes, escape them for the output // (since we're using double quotes as delimiters) var escapedValue = decodedValue.replace(backslashRegExp, '\\\\').replace(doubleQuoteRegExp, '\\"'); // Unquoted value - add double quotes with decoded and re-escaped content return attrName + operator + '"' + escapedValue + '"' + (flag ? ' ' + flag : ''); } /** * Processes a CSS selector text * * @param {string} selectorText - The CSS selector text to process * @returns {string} The processed selector text with normalized whitespace and invalid selectors removed */ function processSelectorText(selectorText) { // Normalize whitespace first var normalized = selectorText.replace(whitespaceNormalizationRegExp, function (match, _, newline) { if (newline) return " "; return match; }); // Normalize escape sequences to match browser behavior normalized = normalizeSelectorEscapes(normalized); // Normalize attribute selectors (add quotes to unquoted values) // Returns null if invalid (e.g., unclosed quotes) normalized = normalizeAttributeSelectors(normalized); if (normalized === null) { return ''; // Invalid selector - return empty to trigger validation failure } // Recursively process pseudo-classes to handle nesting return processNestedPseudoClasses(normalized); } /** * Recursively processes pseudo-classes to filter invalid selectors * * @param {string} selectorText - The CSS selector text to process * @param {number} depth - Current recursion depth (to prevent infinite loops) * @returns {string} The processed selector text with invalid selectors removed */ function processNestedPseudoClasses(selectorText, depth) { // Prevent infinite recursion if (typeof depth === 'undefined') { depth = 0; } if (depth > 10) { return selectorText; } var pseudoClassMatches = extractPseudoClasses(selectorText); // If no pseudo-classes found, return as-is if (pseudoClassMatches.length === 0) { return selectorText; } // Build result by processing matches from right to left (to preserve positions) var result = selectorText; for (var j = pseudoClassMatches.length - 1; j >= 0; j--) { var pseudoClass = pseudoClassMatches[j][1]; if (selectorListPseudoClasses.hasOwnProperty(pseudoClass)) { var fullMatch = pseudoClassMatches[j][0]; var pseudoArgs = pseudoClassMatches[j][2]; var matchStart = pseudoClassMatches[j][3]; // Check if ANY selector contains & BEFORE processing var nestedSelectorsRaw = parseAndSplitNestedSelectors(pseudoArgs); var hasAmpersand = false; for (var k = 0; k < nestedSelectorsRaw.length; k++) { if (ampersandRegExp.test(nestedSelectorsRaw[k])) { hasAmpersand = true; break; } } // If & is present, skip all processing (keep everything unchanged) if (hasAmpersand) { continue; } // Recursively process the arguments var processedArgs = processNestedPseudoClasses(pseudoArgs, depth + 1); var nestedSelectors = parseAndSplitNestedSelectors(processedArgs); // Filter out invalid selectors var validSelectors = []; for (var i = 0; i < nestedSelectors.length; i++) { var nestedSelector = nestedSelectors[i]; if (basicSelectorValidator(nestedSelector)) { validSelectors.push(nestedSelector); } } // Reconstruct the pseudo-class with only valid selectors var newArgs = validSelectors.join(', '); var newPseudoClass = ':' + pseudoClass + '(' + newArgs + ')'; // Replace in the result string using position (processing right to left preserves positions) result = result.substring(0, matchStart) + newPseudoClass + result.substring(matchStart + fullMatch.length); } } return result; return normalized; } /** * Checks if a selector contains newlines inside quoted strings. * Uses iterative parsing to avoid regex backtracking issues. * @param {string} selectorText - The selector to check * @returns {boolean} True if newlines found inside quotes */ function hasNewlineInQuotedString(selectorText) { for (var i = 0; i < selectorText.length; i++) { var char = selectorText[i]; // Start of single-quoted string if (char === "'") { i++; while (i < selectorText.length) { if (selectorText[i] === '\\' && i + 1 < selectorText.length) { // Skip escape sequence i += 2; continue; } if (selectorText[i] === "'") { // End of string break; } if (selectorText[i] === '\r' || selectorText[i] === '\n') { return true; } i++; } } // Start of double-quoted string else if (char === '"') { i++; while (i < selectorText.length) { if (selectorText[i] === '\\' && i + 1 < selectorText.length) { // Skip escape sequence i += 2; continue; } if (selectorText[i] === '"') { // End of string break; } if (selectorText[i] === '\r' || selectorText[i] === '\n') { return true; } i++; } } } return false; } /** * Checks if a given CSS selector text is valid by splitting it by commas * and validating each individual selector using the `validateSelector` function. * * @param {string} selectorText - The CSS selector text to validate. Can contain multiple selectors separated by commas. * @returns {boolean} Returns true if all selectors are valid, otherwise false. */ function isValidSelectorText(selectorText) { // TODO: The same validations here needs to be reused in CSSStyleRule.selectorText setter // TODO: Move these validation logic to a shared function to be reused in CSSStyleRule.selectorText setter // Check for empty or whitespace-only selector if (!selectorText || selectorText.trim() === '') { return false; } // Check for empty selector lists in pseudo-classes (e.g., :is(), :not(), :where(), :has()) // These are invalid after filtering out invalid selectors if (emptyPseudoClassRegExp.test(selectorText)) { return false; } // Check for newlines inside single or double quotes // Uses helper function to avoid regex security issues if (hasNewlineInQuotedString(selectorText)) { return false; } // Split selectorText by commas and validate each part var selectors = parseAndSplitNestedSelectors(selectorText); for (var i = 0; i < selectors.length; i++) { var selector = selectors[i].trim(); if (!validateSelector(selector) || !validateNamespaceSelector(selector)) { return false; } } return true; } function pushToAncestorRules(rule) { ancestorRules.push(rule); } function parseError(message, isNested) { 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; error.isNested = !!isNested; // Print the error but continue parsing the sheet try { throw error; } catch (e) { errorHandler && errorHandler(e); } }; /** * Handles invalid selectors with unmatched quotes by skipping the entire rule block. * @param {string} nextState - The parser state to transition to after skipping */ function handleUnmatchedQuoteInSelector(nextState) { // parseError('Invalid selector with unmatched quote: ' + buffer.trim()); // Skip this entire invalid rule including its block var ruleClosingMatch = token.slice(i).match(forwardRuleClosingBraceRegExp); if (ruleClosingMatch) { i += ruleClosingMatch.index + ruleClosingMatch[0].length - 1; } styleRule = null; buffer = ""; hasUnmatchedQuoteInSelector = false; // Reset flag state = nextState; } // Helper functions to check character types function isSelectorStartChar(char) { return '.:#&*['.indexOf(char) !== -1; } function isWhitespaceChar(char) { return ' \t\n\r'.indexOf(char) !== -1; } // Helper functions for character type checking (faster than regex for single chars) function isDigit(char) { var code = char.charCodeAt(0); return code >= 0x0030 && code <= 0x0039; // 0-9 } function isHexDigit(char) { if (!char) return false; var code = char.charCodeAt(0); return (code >= 0x0030 && code <= 0x0039) || // 0-9 (code >= 0x0041 && code <= 0x0046) || // A-F (code >= 0x0061 && code <= 0x0066); // a-f } function isLetter(char) { if (!char) return false; var code = char.charCodeAt(0); return (code >= 0x0041 && code <= 0x005A) || // A-Z (code >= 0x0061 && code <= 0x007A); // a-z } function isAlphanumeric(char) { var code = char.charCodeAt(0); return (code >= 0x0030 && code <= 0x0039) || // 0-9 (code >= 0x0041 && code <= 0x005A) || // A-Z (code >= 0x0061 && code <= 0x007A); // a-z } /** * Get the length of an escape sequence starting at the given position. * CSS escape sequences are: * - Backslash followed by 1-6 hex digits, optionally followed by a whitespace (consumed) * - Backslash followed by any non-hex character * @param {string} str - The string to check * @param {number} pos - Position of the backslash * @returns {number} Number of characters in the escape sequence (including backslash) */ function getEscapeSequenceLength(str, pos) { if (str[pos] !== '\\' || pos + 1 >= str.length) { return 0; } var nextChar = str[pos + 1]; // Check if it's a hex escape if (isHexDigit(nextChar)) { var hexLength = 1; // Count up to 6 hex digits while (hexLength < 6 && pos + 1 + hexLength < str.length && isHexDigit(str[pos + 1 + hexLength])) { hexLength++; } // Check if followed by optional whitespace (which gets consumed) if (pos + 1 + hexLength < str.length && isWhitespaceChar(str[pos + 1 + hexLength])) { return 1 + hexLength + 1; // backslash + hex digits + whitespace } return 1 + hexLength; // backslash + hex digits } // Simple escape: backslash + any character return 2; } /** * Check if a string contains an unescaped occurrence of a specific character * @param {string} str - The string to search * @param {string} char - The character to look for * @returns {boolean} True if the character appears unescaped */ function containsUnescaped(str, char) { for (var i = 0; i < str.length; i++) { if (str[i] === '\\') { var escapeLen = getEscapeSequenceLength(str, i); if (escapeLen > 0) { i += escapeLen - 1; // -1 because loop will increment continue; } } if (str[i] === char) { return true; } } return false; } var endingIndex = token.length - 1; var initialEndingIndex = endingIndex; for (var character; (character = token.charAt(i)); i++) { if (i === endingIndex) { switch (state) { case "importRule": case "namespaceRule": case "layerBlock": if (character !== ";") { token += ";"; endingIndex += 1; } break; case "value": if (character !== "}") { if (character === ";") { token += "}" } else { token += ";"; } endingIndex += 1; break; } case "name": case "before-name": if (character === "}") { token += " " } else { token += "}" } endingIndex += 1 break; case "before-selector": if (character !== "}" && currentScope !== styleSheet) { token += "}" endingIndex += 1 break; } } } // Handle escape sequences before processing special characters // CSS escape sequences: \HHHHHH (1-6 hex digits) optionally followed by whitespace, or \ + any char if (character === '\\' && i + 1 < token.length) { var escapeLen = getEscapeSequenceLength(token, i); if (escapeLen > 0) { buffer += token.substr(i, escapeLen); i += escapeLen - 1; // -1 because loop will increment continue; } } switch (character) { case " ": case "\t": case "\r": case "\n": case "\f": if (SIGNIFICANT_WHITESPACE[state]) { buffer += character; } break; // String case '"': index = i + 1; do { index = token.indexOf('"', index) + 1; if (!index) { parseError('Unmatched "'); // If we're parsing a selector, flag it as invalid if (state === "selector" || state === "atRule") { hasUnmatchedQuoteInSelector = true; } } } while (token[index - 2] === '\\'); if (index === 0) { break; } buffer += token.slice(i, index); i = index - 1; switch (state) { case 'before-value': state = 'value'; break; case 'importRule-begin': state = 'importRule'; if (i === endingIndex) { token += ';' } break; case 'namespaceRule-begin': state = 'namespaceRule'; if (i === endingIndex) { token += ';' } break; } break; case "'": index = i + 1; do { index = token.indexOf("'", index) + 1; if (!index) { parseError("Unmatched '"); // If we're parsing a selector, flag it as invalid if (state === "selector" || state === "atRule") { hasUnmatchedQuoteInSelector = true; } } } while (token[index - 2] === '\\'); if (index === 0) { break; } buffer += token.slice(i, index); i = index - 1; switch (state) { case 'before-value': state = 'value'; break; case 'importRule-begin': state = 'importRule'; break; case 'namespaceRule-begin': state = 'namespaceRule'; break; } break; // Comment case "/": if (token.charAt(i + 1) === "*") { i += 2; index = token.indexOf("*/", i); if (index === -1) { i = token.length - 1; buffer = ""; } else { i = index + 1; } } else { buffer += character; } if (state === "importRule-begin") { buffer += " "; state = "importRule"; } if (state === "namespaceRule-begin") { buffer += " "; state = "namespaceRule"; } break; // At-rule case "@": if (nestedSelectorRule) { if (styleRule && styleRule.constructor.name === "CSSNestedDeclarations") { currentScope.cssRules.push(styleRule); } // Only reset styleRule to parent if styleRule is not the nestedSelectorRule itself // This preserves nested selectors when followed immediately by @-rules if (styleRule !== nestedSelectorRule && nestedSelectorRule.parentRule && nestedSelectorRule.parentRule.constructor.name === "CSSStyleRule") { styleRule = nestedSelectorRule.parentRule; } // Don't reset nestedSelectorRule here - preserve it through @-rules } if (token.indexOf("@-moz-document", i) === i) { validateAtRule("@-moz-document", function () { state = "documentRule-begin"; documentRule = new CSSOM.CSSDocumentRule(); documentRule.__starts = i; i += "-moz-document".length; }); buffer = ""; break; } else if (token.indexOf("@media", i) === i) { validateAtRule("@media", function () { state = "atBlock"; mediaRule = new CSSOM.CSSMediaRule(); mediaRule.__starts = i; i += "media".length; }); buffer = ""; break; } else if (token.indexOf("@container", i) === i) { validateAtRule("@container", function () { state = "containerBlock"; containerRule = new CSSOM.CSSContainerRule(); containerRule.__starts = i; i += "container".length; }); buffer = ""; break; } else if (token.indexOf("@counter-style", i) === i) { buffer = ""; // @counter-style can be nested only inside CSSScopeRule or CSSConditionRule // and only if there's no CSSStyleRule in the parent chain var cannotBeNested = !canAtRuleBeNested(); validateAtRule("@counter-style", function () { state = "counterStyleBlock" counterStyleRule = new CSSOM.CSSCounterStyleRule(); counterStyleRule.__starts = i; i += "counter-style".length; }, cannotBeNested); break; } else if (token.indexOf("@property", i) === i) { buffer = ""; // @property can be nested only inside CSSScopeRule or CSSConditionRule // and only if there's no CSSStyleRule in the parent chain var cannotBeNested = !canAtRuleBeNested(); validateAtRule("@property", function () { state = "propertyBlock" propertyRule = new CSSOM.CSSPropertyRule(); propertyRule.__starts = i; i += "property".length; }, cannotBeNested); break; } else if (token.indexOf("@scope", i) === i) { validateAtRule("@scope", function () { state = "scopeBlock"; scopeRule = new CSSOM.CSSScopeRule(); scopeRule.__starts = i; i += "scope".length; }); buffer = ""; break; } else if (token.indexOf("@layer", i) === i) { validateAtRule("@layer", function () { state = "layerBlock" layerBlockRule = new CSSOM.CSSLayerBlockRule(); layerBlockRule.__starts = i; i += "layer".length; }); buffer = ""; break; } else if (token.indexOf("@page", i) === i) { validateAtRule("@page", function () { state = "pageBlock" pageRule = new CSSOM.CSSPageRule(); pageRule.__starts = i; i += "page".length; }); buffer = ""; break; } else if (token.indexOf("@supports", i) === i) { validateAtRule("@supports", function () { state = "conditionBlock"; supportsRule = new CSSOM.CSSSupportsRule(); supportsRule.__starts = i; i += "supports".length; }); buffer = ""; break; } else if (token.indexOf("@host", i) === i) { validateAtRule("@host", function () { state = "hostRule-begin"; i += "host".length; hostRule = new CSSOM.CSSHostRule(); hostRule.__starts = i; }); buffer = ""; break; } else if (token.indexOf("@starting-style", i) === i) { validateAtRule("@starting-style", function () { state = "startingStyleRule-begin"; i += "starting-style".length; startingStyleRule = new CSSOM.CSSStartingStyleRule(); startingStyleRule.__starts = i; }); buffer = ""; break; } else if (token.indexOf("@import", i) === i) { buffer = ""; validateAtRule("@import", function () { state = "importRule-begin"; i += "import".length; buffer += "@import"; }, true); break; } else if (token.indexOf("@namespace", i) === i) { buffer = ""; validateAtRule("@namespace", function () { state = "namespaceRule-begin"; i += "namespace".length; buffer += "@namespace"; }, true); break; } else if (token.indexOf("@font-face", i) === i) { buffer = ""; // @font-face can be nested only inside CSSScopeRule or CSSConditionRule // and only if there's no CSSStyleRule in the parent chain var cannotBeNested = !canAtRuleBeNested(); validateAtRule("@font-face", function () { state = "fontFaceRule-begin"; i += "font-face".length; fontFaceRule = new CSSOM.CSSFontFaceRule(); fontFaceRule.__starts = i; }, cannotBeNested); break; } else { // Reset lastIndex before using global regex (shared instance) atKeyframesRegExp.lastIndex = i; var matchKeyframes = atKeyframesRegExp.exec(token); if (matchKeyframes && matchKeyframes.index === i) { state = "keyframesRule-begin"; keyframesRule = new CSSOM.CSSKeyframesRule(); keyframesRule.__starts = i; keyframesRule._vendorPrefix = matchKeyframes[1]; // Will come out as undefined if no prefix was found i += matchKeyframes[0].length - 1; buffer = ""; break; } else if (state === "selector") { state = "atRule"; } } buffer += character; break; case "{": if (currentScope === topScope) { nestedSelectorRule = null; } if (state === 'before-selector') { parseError("Unexpected {"); i = ignoreBalancedBlock(i, token.slice(i)); break; } if (state === "selector" || state === "atRule") { if (!nestedSelectorRule && containsUnescaped(buffer, ";")) { var ruleClosingMatch = token.slice(i).match(forwardRuleClosingBraceRegExp); if (ruleClosingMatch) { styleRule = null; buffer = ""; state = "before-selector"; i += ruleClosingMatch.index + ruleClosingMatch[0].length; break; } } // Ensure styleRule exists before trying to set properties on it if (!styleRule) { styleRule = new CSSOM.CSSStyleRule(); styleRule.__starts = i; } // Check if tokenizer detected an unmatched quote BEFORE setting up the rule if (hasUnmatchedQuoteInSelector) { handleUnmatchedQuoteInSelector("before-selector"); break; } var originalParentRule = parentRule; if (parentRule) { styleRule.__parentRule = parentRule; pushToAncestorRules(parentRule); } currentScope = parentRule = styleRule; var processedSelectorText = processSelectorText(buffer.trim()); // In a nested selector, ensure each selector contains '&' at the beginning, except for selectors that already have '&' somewhere if (originalParentRule && originalParentRule.constructor.name === "CSSStyleRule") { styleRule.selectorText = parseAndSplitNestedSelectors(processedSelectorText).map(function (sel) { // Add & at the beginning if there's no & in the selector, or if it starts with a combinator return (sel.indexOf('&') === -1 || startsWithCombinatorRegExp.test(sel)) ? '& ' + sel : sel; }).join(', '); } else { // Normalize comma spacing: split by commas and rejoin with ", " styleRule.selectorText = parseAndSplitNestedSelectors(processedSelectorText).join(', '); } styleRule.style.__starts = i; styleRule.__parentStyleSheet = styleSheet; buffer = ""; state = "before-name"; } else if (state === "atBlock") { mediaRule.media.mediaText = buffer.trim(); if (parentRule) { mediaRule.__parentRule = parentRule; pushToAncestorRules(parentRule); // If entering @media from within a CSSStyleRule, set nestedSelectorRule // so that & selectors and declarations work correctly inside if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) { nestedSelectorRule = parentRule; } } currentScope = parentRule = mediaRule; pushToAncestorRules(mediaRule); mediaRule.__parentStyleSheet = styleSheet; // Don't reset styleRule to null if it's a nested CSSStyleRule that will contain this @-rule if (!styleRule || styleRule.constructor.name !== "CSSStyleRule" || !styleRule.__parentRule) { styleRule = null; // Reset styleRule when entering @-rule } buffer = ""; state = "before-selector"; } else if (state === "containerBlock") { containerRule.__conditionText = buffer.trim(); if (parentRule) { containerRule.__parentRule = parentRule; pushToAncestorRules(parentRule); if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) { nestedSelectorRule = parentRule; } } currentScope = parentRule = containerRule; pushToAncestorRules(containerRule); containerRule.__parentStyleSheet = styleSheet; styleRule = null; // Reset styleRule when entering @-rule buffer = ""; state = "before-selector"; } else if (state === "counterStyleBlock") { var counterStyleName = buffer.trim().replace(newlineRemovalRegExp, ""); // Validate: name cannot be empty, contain whitespace, or contain dots var isValidCounterStyleName = counterStyleName.length > 0 && !whitespaceAndDotRegExp.test(counterStyleName); if (isValidCounterStyleName) { counterStyleRule.name = counterStyleName; if (parentRule) { counterStyleRule.__parentRule = parentRule; } counterStyleRule.__parentStyleSheet = styleSheet; styleRule = counterStyleRule; } buffer = ""; } else if (state === "propertyBlock") { var propertyName = buffer.trim().replace(newlineRemovalRegExp, ""); // Validate: name must start with -- (custom property) var isValidPropertyName = propertyName.indexOf("--") === 0; if (isValidPropertyName) { propertyRule.__name = propertyName; if (parentRule) { propertyRule.__parentRule = parentRule; } propertyRule.__parentStyleSheet = styleSheet; styleRule = propertyRule; } buffer = ""; } else if (state === "conditionBlock") { supportsRule.__conditionText = buffer.trim(); if (parentRule) { supportsRule.__parentRule = parentRule; pushToAncestorRules(parentRule); if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) { nestedSelectorRule = parentRule; } } currentScope = parentRule = supportsRule; pushToAncestorRules(supportsRule); supportsRule.__parentStyleSheet = styleSheet; styleRule = null; // Reset styleRule when entering @-rule buffer = ""; state = "before-selector"; } else if (state === "scopeBlock") { var parsedScopePrelude = parseScopePrelude(buffer.trim()); if (parsedScopePrelude.hasStart) { scopeRule.__start = parsedScopePrelude.startSelector; } if (parsedScopePrelude.hasEnd) { scopeRule.__end = parsedScopePrelude.endSelector; } if (parsedScopePrelude.hasOnlyEnd) { scopeRule.__end = parsedScopePrelude.endSelector; } if (parentRule) { scopeRule.__parentRule = parentRule; pushToAncestorRules(parentRule); if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) { nestedSelectorRule = parentRule; } } currentScope = parentRule = scopeRule; pushToAncestorRules(scopeRule); scopeRule.__parentStyleSheet = styleSheet; styleRule = null; // Reset styleRule when entering @-rule buffer = ""; state = "before-selector"; } else if (state === "layerBlock") { layerBlockRule.name = buffer.trim(); var isValidName = layerBlockRule.name.length === 0 || layerBlockRule.name.match(cssCustomIdentifierRegExp) !== null; if (isValidName) { if (parentRule) { layerBlockRule.__parentRule = parentRule; pushToAncestorRules(parentRule); if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) { nestedSelectorRule = parentRule; } } currentScope = parentRule = layerBlockRule; pushToAncestorRules(layerBlockRule); layerBlockRule.__parentStyleSheet = styleSheet; } styleRule = null; // Reset styleRule when entering @-rule buffer = ""; state = "before-selector"; } else if (state === "pageBlock") { pageRule.selectorText = buffer.trim(); if (parentRule) { pageRule.__parentRule = parentRule; pushToAncestorRules(parentRule); } currentScope = parentRule = pageRule; pageRule.__parentStyleSheet = styleSheet; styleRule = pageRule; buffer = ""; state = "before-name"; } else if (state === "hostRule-begin") { if (parentRule) { pushToAncestorRules(parentRule); } currentScope = parentRule = hostRule; pushToAncestorRules(hostRule); hostRule.__parentStyleSheet = styleSheet; buffer = ""; state = "before-selector"; } else if (state === "startingStyleRule-begin") { if (parentRule) { startingStyleRule.__parentRule = parentRule; pushToAncestorRules(parentRule); if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) { nestedSelectorRule = parentRule; } } currentScope = parentRule = startingStyleRule; pushToAncestorRules(startingStyleRule); startingStyleRule.__parentStyleSheet = styleSheet; styleRule = null; // Reset styleRule when entering @-rule buffer = ""; state = "before-selector"; } else if (state === "fontFaceRule-begin") { if (parentRule) { fontFaceRule.__parentRule = parentRule; } fontFaceRule.__parentStyleSheet = styleSheet; styleRule = fontFaceRule; buffer = ""; state = "before-name"; } else if (state === "keyframesRule-begin") { keyframesRule.name = buffer.trim(); if (parentRule) { pushToAncestorRules(parentRule); keyframesRule.__parentRule = parentRule; } keyframesRule.__parentStyleSheet = styleSheet; currentScope = parentRule = keyframesRule; buffer = ""; state = "keyframeRule-begin"; } else if (state === "keyframeRule-begin") { styleRule = new CSSOM.CSSKeyframeRule(); styleRule.keyText = buffer.trim(); styleRule.__starts = i; buffer = ""; state = "before-name"; } else if (state === "documentRule-begin") { // FIXME: what if this '{' is in the url text of the match function? documentRule.matcher.matcherText = buffer.trim(); if (parentRule) { pushToAncestorRules(parentRule); documentRule.__parentRule = parentRule; } currentScope = parentRule = documentRule; pushToAncestorRules(documentRule); documentRule.__parentStyleSheet = styleSheet; buffer = ""; state = "before-selector"; } else if (state === "before-name" || state === "name") { // @font-face and similar rules don't support nested selectors // If we encounter a nested selector block inside them, skip it if (styleRule.constructor.name === "CSSFontFaceRule" || styleRule.constructor.name === "CSSKeyframeRule" || (styleRule.constructor.name === "CSSPageRule" && parentRule === styleRule)) { // Skip the nested block var ruleClosingMatch = token.slice(i).match(forwardRuleClosingBraceRegExp); if (ruleClosingMatch) { i += ruleClosingMatch.index + ruleClosingMatch[0].length - 1; buffer = ""; state = "before-name"; break; } } if (styleRule.constructor.name === "CSSNestedDeclarations") { if (styleRule.style.length) { parentRule.cssRules.push(styleRule); styleRule.__parentRule = parentRule; styleRule.__parentStyleSheet = styleSheet; pushToAncestorRules(parentRule); } else { // If the styleRule is empty, we can assume that it's a nested selector pushToAncestorRules(parentRule); } } else { currentScope = parentRule = styleRule; pushToAncestorRules(parentRule); styleRule.__parentStyleSheet = styleSheet; } styleRule = new CSSOM.CSSStyleRule(); // Check if tokenizer detected an unmatched quote BEFORE setting up the rule if (hasUnmatchedQuoteInSelector) { handleUnmatchedQuoteInSelector("before-name"); break; } var processedSelectorText = processSelectorText(buffer.trim()); // In a nested selector, ensure each selector contains '&' at the beginning, except for selectors that already have '&' somewhere if (parentRule.constructor.name === "CSSScopeRule" || (parentRule.constructor.name !== "CSSStyleRule" && parentRule.parentRule === null)) { // Normalize comma spacing: split by commas and rejoin with ", " styleRule.selectorText = parseAndSplitNestedSelectors(processedSelectorText).join(', '); } else { styleRule.selectorText = parseAndSplitNestedSelectors(processedSelectorText).map(function (sel) { // Add & at the beginning if there's no & in the selector, or if it starts with a combinator return (sel.indexOf('&') === -1 || startsWithCombinatorRegExp.test(sel)) ? '& ' + sel : sel; }).join(', '); } styleRule.style.__starts = i - buffer.length; styleRule.__parentRule = parentRule; // Only set nestedSelectorRule if we're directly inside a CSSStyleRule or CSSScopeRule, // not inside other grouping rules like @media/@supports if (parentRule.constructor.name === "CSSStyleRule" || parentRule.constructor.name === "CSSScopeRule") { nestedSelectorRule = styleRule; } // Set __parentStyleSheet for the new nested styleRule styleRule.__parentStyleSheet = styleSheet; // Update currentScope and parentRule to the new nested styleRule // so that subsequent content (like @-rules) will be children of this rule currentScope = parentRule = styleRule; buffer = ""; state = "before-name"; } break; case ":": if (state === "name") { // It can be a nested selector, let's check var openBraceBeforeMatch = token.slice(i).match(declarationOrOpenBraceRegExp); var hasOpenBraceBefore = openBraceBeforeMatch && openBraceBeforeMatch[0] === '{'; if (hasOpenBraceBefore) { // Is a selector buffer += character; } else { // Is a declaration name = buffer.trim(); buffer = ""; state = "before-value"; } } else { buffer += character; } break; case "(": if (state === 'value') { // ie css expression mode if (buffer.trim() === 'expression') { var info = (new CSSOM.CSSValueExpression(token, i)).parse(); if (info.error) { parseError(info.error); } else { buffer += info.expression; i = info.idx; } } else { state = 'value-parenthesis'; //always ensure this is reset to 1 on transition //from value to value-parenthesis valueParenthesisDepth = 1; buffer += character; } } else if (state === 'value-parenthesis') { valueParenthesisDepth++; buffer += character; } else { buffer += character; } break; case ")": if (state === 'value-parenthesis') { valueParenthesisDepth--; if (valueParenthesisDepth === 0) state = 'value'; } buffer += character; break; case "!": if (state === "value" && token.indexOf("!important", i) === i) { priority = "important"; i += "important".length; } else { buffer += character; } break; case ";": switch (state) { case "before-value": case "before-name": parseError("Unexpected ;"); buffer = ""; state = "before-name"; break; case "value": styleRule.style.setProperty(name, buffer.trim(), priority, parseError); priority = ""; buffer = ""; state = "before-name"; break; case "atRule": buffer = ""; state = "before-selector"; break; case "importRule": var isValid = topScope.cssRules.length === 0 || topScope.cssRules.some(function (rule) { return ['CSSImportRule', 'CSSLayerStatementRule'].indexOf(rule.constructor.name) !== -1 }); if (isValid) { importRule = new CSSOM.CSSImportRule(); if (opts && opts.globalObject && opts.globalObject.CSSStyleSheet) { importRule.__styleSheet = new opts.globalObject.CSSStyleSheet(); } importRule.styleSheet.__constructed = false; importRule.__parentStyleSheet = importRule.styleSheet.__parentStyleSheet = styleSheet; importRule.parse(buffer + character); topScope.cssRules.push(importRule); } buffer = ""; state = "before-selector"; break; case "namespaceRule": var isValid = topScope.cssRules.length === 0 || topScope.cssRules.every(function (rule) { return ['CSSImportRule', 'CSSLayerStatementRule', 'CSSNamespaceRule'].indexOf(rule.constructor.name) !== -1 }); if (isValid) { try { // Validate namespace syntax before creating the rule var testNamespaceRule = new CSSOM.CSSNamespaceRule(); testNamespaceRule.parse(buffer + character); namespaceRule = testNamespaceRule; namespaceRule.__parentStyleSheet = styleSheet; topScope.cssRules.push(namespaceRule); // Track the namespace prefix for validation if (namespaceRule.prefix) { definedNamespacePrefixes[namespaceRule.prefix] = namespaceRule.namespaceURI; } } catch (e) { parseError(e.message); } } buffer = ""; state = "before-selector"; break; case "layerBlock": var nameListStr = buffer.trim().split(",").map(function (name) { return name.trim(); }); var isInvalid = nameListStr.some(function (name) { return name.trim().match(cssCustomIdentifierRegExp) === null; }); // Check if there's a CSSStyleRule in the parent chain var hasStyleRuleParent = false; if (parentRule) { var checkParent = parentRule; while (checkParent) { if (checkParent.constructor.name === "CSSStyleRule") { hasStyleRuleParent = true; break; } checkParent = checkParent.__parentRule; } } if (!isInvalid && !hasStyleRuleParent) { layerStatementRule = new CSSOM.CSSLayerStatementRule(); layerStatementRule.__parentStyleSheet = styleSheet; layerStatementRule.__starts = layerBlockRule.__starts; layerStatementRule.__ends = i; layerStatementRule.nameList = nameListStr; // Add to parent rule if nested, otherwise to top scope if (parentRule) { layerStatementRule.__parentRule = parentRule; parentRule.cssRules.push(layerStatementRule); } else { topScope.cssRules.push(layerStatementRule); } } buffer = ""; state = "before-selector"; break; default: buffer += character; break; } break; case "}": if (state === "counterStyleBlock") { // FIXME : Implement missing properties on CSSCounterStyleRule interface and update parse method // For now it's just assigning entire rule text if (counterStyleRule.name) { // Only process if name was set (valid) counterStyleRule.parse("@counter-style " + counterStyleRule.name + " { " + buffer + " }"); counterStyleRule.__ends = i + 1; // Add to parent's cssRules if (counterStyleRule.__parentRule) { counterStyleRule.__parentRule.cssRules.push(counterStyleRule); } else { topScope.cssRules.push(counterStyleRule); } } // Restore currentScope to parent after closing this rule if (counterStyleRule.__parentRule) { currentScope = counterStyleRule.__parentRule; } styleRule = null; buffer = ""; state = "before-selector"; break; } if (state === "propertyBlock") { // Only process if name was set (valid) if (propertyRule.__name) { var parseSuccess = propertyRule.parse("@property " + propertyRule.__name + " { " + buffer + " }"); // Only add the rule if parse was successful (syntax, inherits, and initial-value validation passed) if (parseSuccess) { propertyRule.__ends = i + 1; // Add to parent's cssRules if (propertyRule.__parentRule) { propertyRule.__parentRule.cssRules.push(propertyRule); } else { topScope.cssRules.push(propertyRule); } } } // Restore currentScope to parent after closing this rule if (propertyRule.__parentRule) { currentScope = propertyRule.__parentRule; } styleRule = null; buffer = ""; state = "before-selector"; break; } switch (state) { case "value": styleRule.style.setProperty(name, buffer.trim(), priority, parseError); priority = ""; /* falls through */ case "before-value": case "before-name": case "name": styleRule.__ends = i + 1; if (parentRule === styleRule) { parentRule = ancestorRules.pop() } if (parentRule) { styleRule.__parentRule = parentRule; } styleRule.__parentStyleSheet = styleSheet; if (currentScope === styleRule) { currentScope = parentRule || topScope; } if (styleRule.constructor.name === "CSSStyleRule" && !isValidSelectorText(styleRule.selectorText)) { if (styleRule === nestedSelectorRule) { nestedSelectorRule = null; } parseError('Invalid CSSStyleRule (selectorText = "' + styleRule.selectorText + '")', styleRule.parentRule !== null); } else { if (styleRule.parentRule) { styleRule.parentRule.cssRules.push(styleRule); } else { currentScope.cssRules.push(styleRule); } } buffer = ""; if (currentScope.constructor === CSSOM.CSSKeyframesRule) { state = "keyframeRule-begin"; } else { state = "before-selector"; } if (styleRule.constructor.name === "CSSNestedDeclarations") { if (currentScope !== topScope) { // Only set nestedSelectorRule if currentScope is CSSStyleRule or CSSScopeRule // Not for other grouping rules like @media/@supports if (currentScope.constructor.name === "CSSStyleRule" || currentScope.constructor.name === "CSSScopeRule") { nestedSelectorRule = currentScope; } } styleRule = null; } else { // Update nestedSelectorRule when closing a CSSStyleRule if (styleRule === nestedSelectorRule) { var selector = styleRule.selectorText && styleRule.selectorText.trim(); // Check if this is proper nesting (&.class, &:pseudo) vs prepended & (& :is, & .class with space) // Prepended & has pattern "& X" where X starts with : or . var isPrependedAmpersand = selector && selector.match(prependedAmpersandRegExp); // Check if parent is a grouping rule that can contain nested selectors var isGroupingRule = currentScope && currentScope instanceof CSSOM.CSSGroupingRule; if (!isPrependedAmpersand && isGroupingRule) { // Proper nesting - set nestedSelectorRule to parent for more nested selectors // But only if it's a CSSStyleRule or CSSScopeRule, not other grouping rules like @media if (currentScope.constructor.name === "CSSStyleRule" || currentScope.constructor.name === "CSSScopeRule") { nestedSelectorRule = currentScope; } // If currentScope is another type of grouping rule (like @media), keep nestedSelectorRule unchanged } else { // Prepended & or not nested in grouping rule - reset to prevent CSSNestedDeclarations nestedSelectorRule = null; } } else if (nestedSelectorRule && currentScope instanceof CSSOM.CSSGroupingRule) { // When closing a nested rule that's not the nestedSelectorRule itself, // maintain nestedSelectorRule if we're still inside a grouping rule // This ensures declarations after nested selectors inside @media/@supports etc. work correctly } styleRule = null; break; } case "keyframeRule-begin": case "before-selector": case "selector": // End of media/supports/document rule. if (!parentRule) { parseError("Unexpected }"); var hasPreviousStyleRule = currentScope.cssRules.length && currentScope.cssRules[currentScope.cssRules.length - 1].constructor.name === "CSSStyleRule"; if (hasPreviousStyleRule) { i = ignoreBalancedBlock(i, token.slice(i), 1); } break; } // Find the actual parent rule by popping from ancestor stack while (ancestorRules.length > 0) { parentRule = ancestorRules.pop(); // Skip if we popped the current scope itself (happens because we push both rule and parent) if (parentRule === currentScope) { continue; } // Only process valid grouping rules if (!(parentRule instanceof CSSOM.CSSGroupingRule && (parentRule.constructor.name !== 'CSSStyleRule' || parentRule.__parentRule))) { continue; } // Determine if we're closing a special nested selector context var isClosingNestedSelectorContext = nestedSelectorRule && (currentScope === nestedSelectorRule || nestedSelectorRule.__parentRule === currentScope); if (isClosingNestedSelectorContext) { // Closing the nestedSelectorRule or its direct container if (nestedSelectorRule.parentRule) { // Add nestedSelectorRule to its parent and update scope prevScope = nestedSelectorRule; currentScope = nestedSelectorRule.parentRule; // Use object lookup instead of O(n) indexOf var scopeId = getRuleId(prevScope); if (!addedToCurrentScope[scopeId]) { currentScope.cssRules.push(prevScope); addedToCurrentScope[scopeId] = true; } nestedSelectorRule = currentScope; // Stop here to preserve context for sibling selectors break; } else { // Top-level CSSStyleRule with nested grouping rule prevScope = currentScope; var actualParent = ancestorRules.length > 0 ? ancestorRules[ancestorRules.length - 1] : nestedSelectorRule; if (actualParent !== prevScope) { actualParent.cssRules.push(prevScope); } currentScope = actualParent; parentRule = actualParent; break; } } else { // Regular case: add currentScope to parentRule prevScope = currentScope; if (parentRule !== prevScope) { parentRule.cssRules.push(prevScope); } break; } } // If currentScope has a __parentRule and wasn't added yet, add it if (ancestorRules.length === 0 && currentScope.__parentRule && currentScope.__parentRule.cssRules) { // Use object lookup instead of O(n) findIndex var parentId = getRuleId(currentScope); if (!addedToParent[parentId]) { currentScope.__parentRule.cssRules.push(currentScope); addedToParent[parentId] = true; } } // Only handle top-level rule closing if we processed all ancestors if (ancestorRules.length === 0 && currentScope.parentRule == null) { currentScope.__ends = i + 1; // Use object lookup instead of O(n) findIndex var topId = getRuleId(currentScope); if (currentScope !== topScope && !addedToTopScope[topId]) { topScope.cssRules.push(currentScope); addedToTopScope[topId] = true; } currentScope = topScope; if (nestedSelectorRule === parentRule) { // Check if this selector is really starting inside another selector var nestedSelectorTokenToCurrentSelectorToken = token.slice(nestedSelectorRule.__starts, i + 1); var openingBraceMatch = nestedSelectorTokenToCurrentSelectorToken.match(openBraceGlobalRegExp); var closingBraceMatch = nestedSelectorTokenToCurrentSelectorToken.match(closeBraceGlobalRegExp); var openingBraceLen = openingBraceMatch && openingBraceMatch.length; var closingBraceLen = closingBraceMatch && closingBraceMatch.length; if (openingBraceLen === closingBraceLen) { // If the number of opening and closing braces are equal, we can assume that the new selector is starting outside the nestedSelectorRule nestedSelectorRule.__ends = i + 1; nestedSelectorRule = null; parentRule = null; } } else { parentRule = null; } } else { currentScope = parentRule; } buffer = ""; state = "before-selector"; break; } break; default: switch (state) { case "before-selector": state = "selector"; if ((styleRule || scopeRule) && parentRule) { // Assuming it's a declaration inside Nested Selector OR a Nested Declaration // If Declaration inside Nested Selector let's keep the same styleRule if (!isSelectorStartChar(character) && !isWhitespaceChar(character) && parentRule instanceof CSSOM.CSSGroupingRule) { // parentRule.__parentRule = styleRule; state = "before-name"; if (styleRule !== parentRule) { styleRule = new CSSOM.CSSNestedDeclarations(); styleRule.__starts = i; } } } else if (nestedSelectorRule && parentRule && parentRule instanceof CSSOM.CSSGroupingRule) { if (isSelectorStartChar(character)) { // If starting with a selector character, create CSSStyleRule instead of CSSNestedDeclarations styleRule = new CSSOM.CSSStyleRule(); styleRule.__starts = i; } else if (!isWhitespaceChar(character)) { // Starting a declaration (not whitespace, not a selector) state = "before-name"; // Check if we should create CSSNestedDeclarations // This happens if: parent has cssRules OR nestedSelectorRule exists (indicating CSSStyleRule in hierarchy) if (parentRule.cssRules.length || nestedSelectorRule) { currentScope = parentRule; // Only set nestedSelectorRule if parentRule is CSSStyleRule or CSSScopeRule if (parentRule.constructor.name === "CSSStyleRule" || parentRule.constructor.name === "CSSScopeRule") { nestedSelectorRule = parentRule; } styleRule = new CSSOM.CSSNestedDeclarations(); styleRule.__starts = i; } else { if (parentRule.constructor.name === "CSSStyleRule") { styleRule = parentRule; } else { styleRule = new CSSOM.CSSStyleRule(); styleRule.__starts = i; } } } } break; case "before-name": state = "name"; break; case "before-value": state = "value"; break; case "importRule-begin": state = "importRule"; break; case "namespaceRule-begin": state = "namespaceRule"; break; } buffer += character; break; } // Auto-close all unclosed nested structures // Check AFTER processing the character, at the ORIGINAL ending index // Only add closing braces if CSS is incomplete (not at top scope) if (i === initialEndingIndex && (currentScope !== topScope || ancestorRules.length > 0)) { var needsClosing = ancestorRules.length; if (currentScope !== topScope && ancestorRules.indexOf(currentScope) === -1) { needsClosing += 1; } // Add closing braces for all unclosed structures for (var closeIdx = 0; closeIdx < needsClosing; closeIdx++) { token += "}"; endingIndex += 1; } } } if (buffer.trim() !== "") { parseError("Unexpected end of input"); } return styleSheet; }; /** * Produces a deep copy of stylesheet — the instance variables of stylesheet are copied recursively. * @param {CSSStyleSheet|CSSOM.CSSStyleSheet} stylesheet * @nosideeffects * @return {CSSOM.CSSStyleSheet} */ CSSOM.clone = function clone(stylesheet) { var cloned = new CSSOM.CSSStyleSheet(); var rules = stylesheet.cssRules; if (!rules) { return cloned; } for (var i = 0, rulesLength = rules.length; i < rulesLength; i++) { var rule = rules[i]; var ruleClone = cloned.cssRules[i] = new rule.constructor(); var style = rule.style; if (style) { var styleClone = ruleClone.style = new CSSOM.CSSStyleDeclaration(); for (var j = 0, styleLength = style.length; j < styleLength; j++) { var name = styleClone[j] = style[j]; styleClone[name] = style[name]; styleClone._importants[name] = style.getPropertyPriority(name); } styleClone.length = style.length; } if (rule.hasOwnProperty('keyText')) { ruleClone.keyText = rule.keyText; } if (rule.hasOwnProperty('selectorText')) { ruleClone.selectorText = rule.selectorText; } if (rule.hasOwnProperty('mediaText')) { ruleClone.mediaText = rule.mediaText; } if (rule.hasOwnProperty('supportsText')) { ruleClone.supports = rule.supports; } if (rule.hasOwnProperty('conditionText')) { ruleClone.conditionText = rule.conditionText; } if (rule.hasOwnProperty('layerName')) { ruleClone.layerName = rule.layerName; } if (rule.hasOwnProperty('href')) { ruleClone.href = rule.href; } if (rule.hasOwnProperty('name')) { ruleClone.name = rule.name; } if (rule.hasOwnProperty('nameList')) { ruleClone.nameList = rule.nameList; } if (rule.hasOwnProperty('cssRules')) { ruleClone.cssRules = clone(rule).cssRules; } } return cloned; };