Files
ANDJJJJJJ/server/node_modules/@acemir/cssom/build/CSSOM.js

6612 lines
199 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 <name> { ... }"
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 <name> { ... }"
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<length; i++) {
this[i] = values[i].trim();
}
} else if (value === null) {
var length = this.length;
for (var i = 0; i < length; i++) {
delete this[i];
}
this.length = 0;
}
},
/**
* @param {string} medium
*/
appendMedium: function(medium) {
if (Array.prototype.indexOf.call(this, medium) === -1) {
this[this.length] = medium;
this.length++;
}
},
/**
* @param {string} medium
*/
deleteMedium: function(medium) {
var index = Array.prototype.indexOf.call(this, medium);
if (index !== -1) {
Array.prototype.splice.call(this, index, 1);
}
},
item: function(index) {
return this[index] || null;
},
toString: function() {
return this.mediaText;
}
};
/**
* @constructor
* @see http://dev.w3.org/csswg/cssom/#cssmediarule
* @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSMediaRule
*/
CSSOM.CSSMediaRule = function CSSMediaRule() {
CSSOM.CSSConditionRule.call(this);
this.__media = new CSSOM.MediaList();
};
CSSOM.CSSMediaRule.prototype = Object.create(CSSOM.CSSConditionRule.prototype);
CSSOM.CSSMediaRule.prototype.constructor = CSSOM.CSSMediaRule;
Object.setPrototypeOf(CSSOM.CSSMediaRule, CSSOM.CSSConditionRule);
Object.defineProperty(CSSOM.CSSMediaRule.prototype, "type", {
value: 4,
writable: false
});
// https://opensource.apple.com/source/WebCore/WebCore-7611.1.21.161.3/css/CSSMediaRule.cpp
Object.defineProperties(CSSOM.CSSMediaRule.prototype, {
"media": {
get: function() {
return this.__media;
},
set: function(value) {
if (typeof value === "string") {
this.__media.mediaText = value;
} else {
this.__media = value;
}
},
configurable: true,
enumerable: true
},
"conditionText": {
get: function() {
return this.media.mediaText;
}
},
"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 "@media " + this.media.mediaText + values;
}
}
});
/**
* @constructor
* @see https://drafts.csswg.org/css-contain-3/
* @see https://www.w3.org/TR/css-contain-3/
*/
CSSOM.CSSContainerRule = function CSSContainerRule() {
CSSOM.CSSConditionRule.call(this);
};
CSSOM.CSSContainerRule.prototype = Object.create(CSSOM.CSSConditionRule.prototype);
CSSOM.CSSContainerRule.prototype.constructor = CSSOM.CSSContainerRule;
Object.setPrototypeOf(CSSOM.CSSContainerRule, CSSOM.CSSConditionRule);
Object.defineProperty(CSSOM.CSSContainerRule.prototype, "type", {
value: 17,
writable: false
});
Object.defineProperties(CSSOM.CSSContainerRule.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 "@container " + this.conditionText + values;
}
},
"containerName": {
get: function() {
var parts = this.conditionText.trim().split(/\s+/);
if (parts.length > 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 sheets 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<rules.length; i++) {
result += rules[i].cssText + "\n";
}
return result;
};
/**
* @constructor
* @see http://www.w3.org/TR/css3-animations/#DOM-CSSKeyframesRule
*/
CSSOM.CSSKeyframesRule = function CSSKeyframesRule() {
CSSOM.CSSRule.call(this);
this.name = '';
this.cssRules = new CSSOM.CSSRuleList();
// Set up initial indexed access
this._setupIndexedAccess();
// Override cssRules methods after initial setup, store references as non-enumerable properties
var self = this;
var originalPush = this.cssRules.push;
var originalSplice = this.cssRules.splice;
// Create non-enumerable method overrides
Object.defineProperty(this.cssRules, 'push', {
value: function() {
var result = originalPush.apply(this, arguments);
self._setupIndexedAccess();
return result;
},
writable: true,
enumerable: false,
configurable: true
});
Object.defineProperty(this.cssRules, 'splice', {
value: function() {
var result = originalSplice.apply(this, arguments);
self._setupIndexedAccess();
return result;
},
writable: true,
enumerable: false,
configurable: true
});
};
CSSOM.CSSKeyframesRule.prototype = Object.create(CSSOM.CSSRule.prototype);
CSSOM.CSSKeyframesRule.prototype.constructor = CSSOM.CSSKeyframesRule;
Object.setPrototypeOf(CSSOM.CSSKeyframesRule, CSSOM.CSSRule);
Object.defineProperty(CSSOM.CSSKeyframesRule.prototype, "type", {
value: 7,
writable: false
});
// http://www.opensource.apple.com/source/WebCore/WebCore-955.66.1/css/WebKitCSSKeyframesRule.cpp
Object.defineProperty(CSSOM.CSSKeyframesRule.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}";
var cssWideKeywords = ['initial', 'inherit', 'revert', 'revert-layer', 'unset', 'none'];
var processedName = cssWideKeywords.includes(this.name) ? '"' + this.name + '"' : this.name;
return "@" + (this._vendorPrefix || '') + "keyframes " + processedName + values;
}
});
/**
* Appends a new keyframe rule to the list of keyframes.
*
* @param {string} rule - The keyframe rule string to append (e.g., "50% { opacity: 0.5; }")
* @see https://www.w3.org/TR/css-animations-1/#dom-csskeyframesrule-appendrule
*/
CSSOM.CSSKeyframesRule.prototype.appendRule = function appendRule(rule) {
if (arguments.length === 0) {
errorUtils.throwMissingArguments(this, 'appendRule', 'CSSKeyframesRule');
}
var parsedRule;
try {
// Parse the rule string as a keyframe rule
var tempStyleSheet = CSSOM.parse("@keyframes temp { " + rule + " }");
if (tempStyleSheet.cssRules.length > 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<length; i++) {
this[i] = values[i].trim();
}
},
/**
* @param {string} matcher
*/
appendMatcher: function(matcher) {
if (Array.prototype.indexOf.call(this, matcher) === -1) {
this[this.length] = matcher;
this.length++;
}
},
/**
* @param {string} matcher
*/
deleteMatcher: function(matcher) {
var index = Array.prototype.indexOf.call(this, matcher);
if (index !== -1) {
Array.prototype.splice.call(this, index, 1);
}
}
};
/**
* @constructor
* @see https://developer.mozilla.org/en/CSS/@-moz-document
* @deprecated This rule is a non-standard Mozilla-specific extension and is not part of any official CSS specification.
*/
CSSOM.CSSDocumentRule = function CSSDocumentRule() {
CSSOM.CSSRule.call(this);
this.matcher = new CSSOM.MatcherList();
this.cssRules = new CSSOM.CSSRuleList();
};
CSSOM.CSSDocumentRule.prototype = Object.create(CSSOM.CSSRule.prototype);
CSSOM.CSSDocumentRule.prototype.constructor = CSSOM.CSSDocumentRule;
Object.setPrototypeOf(CSSOM.CSSDocumentRule, CSSOM.CSSRule);
Object.defineProperty(CSSOM.CSSDocumentRule.prototype, "type", {
value: 10,
writable: false
});
//FIXME
//CSSOM.CSSDocumentRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule;
//CSSOM.CSSDocumentRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule;
Object.defineProperty(CSSOM.CSSDocumentRule.prototype, "cssText", {
get: function() {
var cssTexts = [];
for (var i=0, length=this.cssRules.length; i < length; i++) {
cssTexts.push(this.cssRules[i].cssText);
}
return "@-moz-document " + this.matcher.matcherText + " {" + (cssTexts.length ? "\n " + cssTexts.join("\n ") : "") + "\n}";
}
});
/**
* @constructor
* @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSValue
*
* TODO: add if needed
*/
CSSOM.CSSValue = function CSSValue() {
};
CSSOM.CSSValue.prototype = {
constructor: CSSOM.CSSValue,
// @see: http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSValue
set cssText(text) {
var name = this._getConstructorName();
throw new Error('DOMException: property "cssText" of "' + name + '" is readonly and can not be replaced with "' + text + '"!');
},
get cssText() {
var name = this._getConstructorName();
throw new Error('getter "cssText" of "' + name + '" is not implemented!');
},
_getConstructorName: function() {
var s = this.constructor.toString(),
c = s.match(/function\s([^\(]+)/),
name = c[1];
return name;
}
};
/**
* @constructor
* @see http://msdn.microsoft.com/en-us/library/ms537634(v=vs.85).aspx
*
*/
CSSOM.CSSValueExpression = function CSSValueExpression(token, idx) {
this._token = token;
this._idx = idx;
};
CSSOM.CSSValueExpression.prototype = Object.create(CSSOM.CSSValue.prototype);
CSSOM.CSSValueExpression.prototype.constructor = CSSOM.CSSValueExpression;
Object.setPrototypeOf(CSSOM.CSSValueExpression, CSSOM.CSSValue);
/**
* parse css expression() value
*
* @return {Object}
* - error:
* or
* - idx:
* - expression:
*
* Example:
*
* .selector {
* zoom: expression(documentElement.clientWidth > 1000 ? '1000px' : 'auto');
* }
*/
CSSOM.CSSValueExpression.prototype.parse = function() {
var token = this._token,
idx = this._idx;
var character = '',
expression = '',
error = '',
info,
paren = [];
for (; ; ++idx) {
character = token.charAt(idx);
// end of token
if (character === '') {
error = 'css expression error: unfinished expression!';
break;
}
switch(character) {
case '(':
paren.push(character);
expression += character;
break;
case ')':
paren.pop(character);
expression += character;
break;
case '/':
if ((info = this._parseJSComment(token, idx))) { // comment?
if (info.error) {
error = 'css expression error: unfinished comment in expression!';
} else {
idx = info.idx;
// ignore the comment
}
} else if ((info = this._parseJSRexExp(token, idx))) { // regexp
idx = info.idx;
expression += info.text;
} else { // other
expression += character;
}
break;
case "'":
case '"':
info = this._parseJSString(token, idx, character);
if (info) { // string
idx = info.idx;
expression += info.text;
} else {
expression += character;
}
break;
default:
expression += character;
break;
}
if (error) {
break;
}
// end of expression
if (paren.length === 0) {
break;
}
}
var ret;
if (error) {
ret = {
error: error
};
} else {
ret = {
idx: idx,
expression: expression
};
}
return ret;
};
/**
*
* @return {Object|false}
* - idx:
* - text:
* or
* - error:
* or
* false
*
*/
CSSOM.CSSValueExpression.prototype._parseJSComment = function(token, idx) {
var nextChar = token.charAt(idx + 1),
text;
if (nextChar === '/' || nextChar === '*') {
var startIdx = idx,
endIdx,
commentEndChar;
if (nextChar === '/') { // line comment
commentEndChar = '\n';
} else if (nextChar === '*') { // block comment
commentEndChar = '*/';
}
endIdx = token.indexOf(commentEndChar, startIdx + 1 + 1);
if (endIdx !== -1) {
endIdx = endIdx + commentEndChar.length - 1;
text = token.substring(idx, endIdx + 1);
return {
idx: endIdx,
text: text
};
} else {
var error = 'css expression error: unfinished comment in expression!';
return {
error: error
};
}
} else {
return false;
}
};
/**
*
* @return {Object|false}
* - idx:
* - text:
* or
* false
*
*/
CSSOM.CSSValueExpression.prototype._parseJSString = function(token, idx, sep) {
var endIdx = this._findMatchedIdx(token, idx, sep),
text;
if (endIdx === -1) {
return false;
} else {
text = token.substring(idx, endIdx + sep.length);
return {
idx: endIdx,
text: text
};
}
};
/**
* parse regexp in css expression
*
* @return {Object|false}
* - idx:
* - regExp:
* or
* false
*/
/*
all legal RegExp
/a/
(/a/)
[/a/]
[12, /a/]
!/a/
+/a/
-/a/
* /a/
/ /a/
%/a/
===/a/
!==/a/
==/a/
!=/a/
>/a/
>=/a/
</a/
<=/a/
&/a/
|/a/
^/a/
~/a/
<</a/
>>/a/
>>>/a/
&&/a/
||/a/
?/a/
=/a/
,/a/
delete /a/
in /a/
instanceof /a/
new /a/
typeof /a/
void /a/
*/
CSSOM.CSSValueExpression.prototype._parseJSRexExp = function(token, idx) {
var before = token.substring(0, idx).replace(/\s+$/, ""),
legalRegx = [
/^$/,
/\($/,
/\[$/,
/\!$/,
/\+$/,
/\-$/,
/\*$/,
/\/\s+/,
/\%$/,
/\=$/,
/\>$/,
/<$/,
/\&$/,
/\|$/,
/\^$/,
/\~$/,
/\?$/,
/\,$/,
/delete$/,
/in$/,
/instanceof$/,
/new$/,
/typeof$/,
/void$/
];
var isLegal = legalRegx.some(function(reg) {
return reg.test(before);
});
if (!isLegal) {
return false;
} else {
var sep = '/';
// same logic as string
return this._parseJSString(token, idx, sep);
}
};
/**
*
* find next sep(same line) index in `token`
*
* @return {Number}
*
*/
CSSOM.CSSValueExpression.prototype._findMatchedIdx = function(token, idx, sep) {
var startIdx = idx,
endIdx;
var NOT_FOUND = -1;
while(true) {
endIdx = token.indexOf(sep, startIdx + 1);
if (endIdx === -1) { // not found
endIdx = NOT_FOUND;
break;
} else {
var text = token.substring(idx + 1, endIdx),
matched = text.match(/\\+$/);
if (!matched || matched[0] % 2 === 0) { // not escaped
break;
} else {
startIdx = endIdx;
}
}
}
// boundary must be in the same line(js sting or regexp)
var nextNewLineIdx = token.indexOf('\n', idx + 1);
if (nextNewLineIdx < endIdx) {
endIdx = NOT_FOUND;
}
return endIdx;
};
/**
* @constructor
* @see https://drafts.csswg.org/css-cascade-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 (<end>)
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;
};