From 5b8b71bb495463b05d66a53a7ec41e200207e957 Mon Sep 17 00:00:00 2001 From: Edoardo Luppi Date: Wed, 14 Oct 2026 12:30:36 +0200 Subject: [PATCH 1/4] test: validate whether nested CSS declarations are parsed Should fail with the existing cssom package. Should pass with the updated acemir/cssom package. Refs: #318 --- test/html/style-element.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test/html/style-element.js b/test/html/style-element.js index 0822d388..f50259b5 100644 --- a/test/html/style-element.js +++ b/test/html/style-element.js @@ -27,4 +27,14 @@ assert(style.sheet.cssRules.length === 1, true, 'style.sheet.cssRules.length'); assert(style.sheet.cssRules[0].selectorText, '.test'); assert(style.sheet.cssRules[0].style.color, 'white'); -assert(style.sheet.cssRules[0].style['font-size'], '12px'); \ No newline at end of file +assert(style.sheet.cssRules[0].style['font-size'], '12px'); + +document = parseHTML('').document; + +({firstElementChild: style} = document); +assert(style.sheet.cssRules.length, 1); +assert(style.sheet.cssRules[0].selectorText, '.parent'); +assert(style.sheet.cssRules[0].style.color, 'blue'); +assert(style.sheet.cssRules[0].cssRules.length, 1); +assert(style.sheet.cssRules[0].cssRules[0].selectorText, '& .child'); +assert(style.sheet.cssRules[0].cssRules[0].style.color, 'red'); From 9b72168291c6eb8ae0414959ce9d780cd232af23 Mon Sep 17 00:00:00 2001 From: Edoardo Luppi Date: Wed, 14 Oct 2026 12:12:36 +0200 Subject: [PATCH 2/4] build: run npm audit fix --- package-lock.json | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 837cac8b..e04dca58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -894,9 +894,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -1953,10 +1953,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -2605,16 +2615,16 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/test-exclude/node_modules/glob": { From 5d1ae45cd7650120d2b828ae65f5e5ff49c19035 Mon Sep 17 00:00:00 2001 From: Edoardo Luppi Date: Wed, 14 Oct 2026 12:15:56 +0200 Subject: [PATCH 3/4] build: replace cssom with acemir/cssom --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index e04dca58..53324613 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "version": "0.18.13", "license": "ISC", "dependencies": { + "@acemir/cssom": "^0.9.32", "css-select": "^7.0.0", - "cssom": "^0.5.0", "html-escaper": "^3.0.3", "htmlparser2": "^10.1.0", "uhyphen": "^0.2.0" @@ -37,6 +37,12 @@ } } }, + "node_modules/@acemir/cssom": { + "version": "0.9.32", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.32.tgz", + "integrity": "sha512-OijrAUu7rrJhSIoHNg6LMPS5e344nwUHA+1EFYYjrh64D+GmytUsfBQjci45TjT+rd5Spsp8B5xsZDT+TgyXXQ==", + "license": "MIT" + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -1098,12 +1104,6 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/cssom": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", - "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", - "license": "MIT" - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", diff --git a/package.json b/package.json index 9d106714..d240c77b 100644 --- a/package.json +++ b/package.json @@ -70,8 +70,8 @@ } }, "dependencies": { + "@acemir/cssom": "^0.9.32", "css-select": "^7.0.0", - "cssom": "^0.5.0", "html-escaper": "^3.0.3", "htmlparser2": "^10.1.0", "uhyphen": "^0.2.0" From 646bdac11db853055111795ee0d80c8e68b2ed56 Mon Sep 17 00:00:00 2001 From: Edoardo Luppi Date: Wed, 14 Oct 2026 12:18:39 +0200 Subject: [PATCH 4/4] refactor: update cssom imports to acemir/cssom Fixes: #318 --- cjs/html/style-element.js | 2 +- esm/html/style-element.js | 2 +- worker.js | 8016 ++++++++++++++++++++++++++++++------- 3 files changed, 6583 insertions(+), 1437 deletions(-) diff --git a/cjs/html/style-element.js b/cjs/html/style-element.js index 131e7372..78cf8a0f 100644 --- a/cjs/html/style-element.js +++ b/cjs/html/style-element.js @@ -1,5 +1,5 @@ 'use strict'; -const {parse} = require('cssom'); +const {parse} = require('@acemir/cssom'); const {registerHTMLClass} = require('../shared/register-html-class.js'); const {SHEET} = require('../shared/symbols.js'); diff --git a/esm/html/style-element.js b/esm/html/style-element.js index ce4111e9..9f23dde0 100644 --- a/esm/html/style-element.js +++ b/esm/html/style-element.js @@ -1,4 +1,4 @@ -import {parse} from 'cssom'; +import {parse} from '@acemir/cssom'; import {registerHTMLClass} from '../shared/register-html-class.js'; import {SHEET} from '../shared/symbols.js'; diff --git a/worker.js b/worker.js index 2671c7f5..31bb78a1 100644 --- a/worker.js +++ b/worker.js @@ -6631,7 +6631,7 @@ const existsOne = (test, elements) => elements.some( ); const getAttributeValue = (element, name) => name === 'class' ? - element.classList.value : element.getAttribute(name); + (element.getAttributeNode('class')?.value ?? null) : element.getAttribute(name); const getChildren = ({childNodes}) => childNodes; @@ -8631,391 +8631,414 @@ class HTMLBodyElement extends HTMLElement { } } -var lib = {}; - -var CSSStyleDeclaration = {}; - -var parse$1 = {}; - -var CSSStyleSheet = {}; - -var StyleSheet = {}; - -var hasRequiredStyleSheet; +var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; -function requireStyleSheet () { - if (hasRequiredStyleSheet) return StyleSheet; - hasRequiredStyleSheet = 1; - //.CommonJS - var CSSOM = {}; - ///CommonJS +var lib = {}; +var CSSOM_1; +var hasRequiredCSSOM; - /** - * @constructor - * @see http://dev.w3.org/csswg/cssom/#the-stylesheet-interface - */ - CSSOM.StyleSheet = function StyleSheet() { - this.parentStyleSheet = null; +function requireCSSOM () { + if (hasRequiredCSSOM) return CSSOM_1; + hasRequiredCSSOM = 1; + 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); + }; - //.CommonJS - StyleSheet.StyleSheet = CSSOM.StyleSheet; - ///CommonJS - return StyleSheet; -} - -var CSSStyleRule = {}; - -var CSSRule = {}; - -var hasRequiredCSSRule; - -function requireCSSRule () { - if (hasRequiredCSSRule) return CSSRule; - hasRequiredCSSRule = 1; - //.CommonJS - var CSSOM = {}; - ///CommonJS - - - /** - * @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; + // 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]; + } + } - CSSOM.CSSRule.prototype = { - constructor: CSSOM.CSSRule - //FIXME - }; + // Clone the .prototype object for constructor-like behavior + if (fn.prototype) { + newFn.prototype = Object.create(fn.prototype); + } + return newFn; + }; + } //.CommonJS - CSSRule.CSSRule = CSSOM.CSSRule; + CSSOM_1 = CSSOM; ///CommonJS - return CSSRule; + return CSSOM_1; } -var hasRequiredCSSStyleRule; +var errorUtils = {}; -function requireCSSStyleRule () { - if (hasRequiredCSSStyleRule) return CSSStyleRule; - hasRequiredCSSStyleRule = 1; - //.CommonJS - var CSSOM = { - CSSStyleDeclaration: requireCSSStyleDeclaration().CSSStyleDeclaration, - CSSRule: requireCSSRule().CSSRule - }; - ///CommonJS +var hasRequiredErrorUtils; +function requireErrorUtils () { + if (hasRequiredErrorUtils) return errorUtils; + hasRequiredErrorUtils = 1; + // Utility functions for CSSOM error handling /** - * @constructor - * @see http://dev.w3.org/csswg/cssom/#cssstylerule - * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleRule + * 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 */ - CSSOM.CSSStyleRule = function CSSStyleRule() { - CSSOM.CSSRule.call(this); - this.selectorText = ""; - this.style = new CSSOM.CSSStyleDeclaration(); - this.style.parentRule = this; - }; - - CSSOM.CSSStyleRule.prototype = new CSSOM.CSSRule(); - CSSOM.CSSStyleRule.prototype.constructor = CSSOM.CSSStyleRule; - CSSOM.CSSStyleRule.prototype.type = 1; - - Object.defineProperty(CSSOM.CSSStyleRule.prototype, "cssText", { - get: function() { - var text; - if (this.selectorText) { - text = this.selectorText + " {" + this.style.cssText + "}"; - } else { - text = ""; - } - return text; - }, - set: function(cssText) { - var rule = CSSOM.CSSStyleRule.parse(cssText); - this.style = rule.style; - this.selectorText = rule.selectorText; + 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 commonjsGlobal !== 'undefined' && commonjsGlobal[errorType]) || + (typeof window !== 'undefined' && window[errorType]) || + eval(errorType); + } /** - * NON-STANDARD - * lightweight version of parse.js. - * @param {string} ruleText - * @return CSSStyleRule + * 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 */ - CSSOM.CSSStyleRule.parse = function(ruleText) { - var i = 0; - var state = "selector"; - var index; - var j = i; - var buffer = ""; - - var SIGNIFICANT_WHITESPACE = { - "selector": true, - "value": true - }; - - var styleRule = new CSSOM.CSSStyleRule(); - var name, priority=""; - - for (var character; (character = ruleText.charAt(i)); i++) { - - switch (character) { - - case " ": - case "\t": - case "\r": - case "\n": - case "\f": - if (SIGNIFICANT_WHITESPACE[state]) { - // Squash 2 or more white-spaces in the row into 1 - switch (ruleText.charAt(i - 1)) { - case " ": - case "\t": - case "\r": - case "\n": - case "\f": - break; - default: - buffer += " "; - break; - } - } - break; + function createError(context, errorType, message, name) { + var ErrorConstructor = getErrorConstructor(context, errorType); + return new ErrorConstructor(message, name); + } - // String - case '"': - j = i + 1; - index = ruleText.indexOf('"', j) + 1; - if (!index) { - throw '" is missing'; - } - buffer += ruleText.slice(i, index); - i = index - 1; - break; - - case "'": - j = i + 1; - index = ruleText.indexOf("'", j) + 1; - if (!index) { - throw "' is missing"; - } - buffer += ruleText.slice(i, index); - i = index - 1; - break; - - // Comment - case "/": - if (ruleText.charAt(i + 1) === "*") { - i += 2; - index = ruleText.indexOf("*/", i); - if (index === -1) { - throw new SyntaxError("Missing */"); - } else { - i = index + 1; - } - } else { - buffer += character; - } - break; - - case "{": - if (state === "selector") { - styleRule.selectorText = buffer.trim(); - buffer = ""; - state = "name"; - } - break; - - case ":": - if (state === "name") { - name = buffer.trim(); - buffer = ""; - state = "value"; - } else { - buffer += character; - } - break; - - case "!": - if (state === "value" && ruleText.indexOf("!important", i) === i) { - priority = "important"; - i += "important".length; - } else { - buffer += character; - } - break; - - case ";": - if (state === "value") { - styleRule.style.setProperty(name, buffer.trim(), priority); - priority = ""; - buffer = ""; - state = "name"; - } else { - buffer += character; - } - break; - - case "}": - if (state === "value") { - styleRule.style.setProperty(name, buffer.trim(), priority); - priority = ""; - buffer = ""; - } else if (state === "name") { - break; - } else { - buffer += character; - } - state = "selector"; - break; - - default: - buffer += character; - break; + /** + * 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); + } - return styleRule; + /** + * 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$1 = { + createError: createError, + getErrorConstructor: getErrorConstructor, + throwError: throwError, + throwMissingArguments: throwMissingArguments, + throwParseError: throwParseError, + throwIndexError: throwIndexError }; - //.CommonJS - CSSStyleRule.CSSStyleRule = CSSOM.CSSStyleRule; + errorUtils.errorUtils = errorUtils$1; ///CommonJS - return CSSStyleRule; + return errorUtils; } -var hasRequiredCSSStyleSheet; +var regexPatterns = {}; -function requireCSSStyleSheet () { - if (hasRequiredCSSStyleSheet) return CSSStyleSheet; - hasRequiredCSSStyleSheet = 1; - //.CommonJS - var CSSOM = { - StyleSheet: requireStyleSheet().StyleSheet, - CSSStyleRule: requireCSSStyleRule().CSSStyleRule - }; - ///CommonJS +var hasRequiredRegexPatterns; +function requireRegexPatterns () { + if (hasRequiredRegexPatterns) return regexPatterns; + hasRequiredRegexPatterns = 1; + // 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 (>, +, ~) /** - * @constructor - * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet + * 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) */ - CSSOM.CSSStyleSheet = function CSSStyleSheet() { - CSSOM.StyleSheet.call(this); - this.cssRules = []; - }; + 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 - CSSOM.CSSStyleSheet.prototype = new CSSOM.StyleSheet(); - CSSOM.CSSStyleSheet.prototype.constructor = CSSOM.CSSStyleSheet; + // 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) /** - * Used to insert a new rule into the style sheet. The new rule now becomes part of the cascade. + * Regular expression to detect invalid characters in the value portion of a CSS style declaration. * - * 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;}" + * 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). * - * @param {string} rule - * @param {number} index - * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet-insertRule - * @return {number} The index within the style sheet's rule collection of the newly inserted rule. - */ - CSSOM.CSSStyleSheet.prototype.insertRule = function(rule, index) { - if (index < 0 || index > this.cssRules.length) { - throw new RangeError("INDEX_SIZE_ERR"); - } - var cssRule = CSSOM.parse(rule).cssRules[0]; - cssRule.parentStyleSheet = this; - this.cssRules.splice(index, 0, cssRule); - return index; - }; - - - /** - * Used to delete a rule from the style sheet. + * 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,...)`). * - * 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;}" + * 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");`). * - * @param {number} index within the style sheet's rule list of the rule to remove. - * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet-deleteRule - */ - CSSOM.CSSStyleSheet.prototype.deleteRule = function(index) { - if (index < 0 || index >= this.cssRules.length) { - throw new RangeError("INDEX_SIZE_ERR"); - } - this.cssRules.splice(index, 1); - }; - - - /** - * NON-STANDARD - * @return {string} serialize stylesheet + * 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 */ - CSSOM.CSSStyleSheet.prototype.toString = function() { - var result = ""; - var rules = this.cssRules; - for (var i=0; i/; // 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) + + // Media query validation patterns + var invalidMediaOperatorWithoutSpaceRegExp = /\)(and|or)\(/i; // )and( or )or( - missing space after operator + var invalidMediaNotAfterOperatorRegExp = /\b(and|or)\s+not\b/i; // and not or or not - not after operator + var invalidMediaNotIdentifierRegExp = /\)\s+not\(/i; // ) not( - not() identifier after media feature + var invalidMediaNotAfterFeatureRegExp = /\)\s+not\s+/i; // ) not - not operator after media feature (only valid at start or after comma) + var invalidMediaOperatorBetweenParensRegExp = /\)\s+(?!and\b|or\b)[a-z]+\s+\(/i; // ) xx ( - invalid operator between ) and ( + var invalidMediaNotWithMediaTypeRegExp = /\bnot\s+[a-z][a-z0-9_-]*\s+\(/i; // not ( - missing 'and' between media type and feature + + // Media query normalization patterns + var mediaFeatureColonRegExp = /(\(\s*)([^():]+):(\s*)([^()]*\))/g; // Match colon in media features + var identifierBeforeParenRegExp = /[a-z0-9_-]/i; // Match identifier character before opening paren + var mediaAndOperatorRegExp = /\s*\band\b\s*/gi; // Match 'and' operator with word boundaries + var mediaOrOperatorRegExp = /\s*\bor\b\s*/gi; // Match 'or' operator with word boundaries + var mediaNotOperatorStartRegExp = /^\s*not\s+(?=\S)/i; // Match 'not' operator at start + var mediaNotOperatorRegExp = /\s+\bnot\s+(?=\S)/gi; // Match 'not' operator after space + + var regexPatterns$1 = { + // 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, + + // Media query validation patterns + invalidMediaOperatorWithoutSpaceRegExp: invalidMediaOperatorWithoutSpaceRegExp, + invalidMediaNotAfterOperatorRegExp: invalidMediaNotAfterOperatorRegExp, + invalidMediaNotIdentifierRegExp: invalidMediaNotIdentifierRegExp, + invalidMediaNotAfterFeatureRegExp: invalidMediaNotAfterFeatureRegExp, + invalidMediaOperatorBetweenParensRegExp: invalidMediaOperatorBetweenParensRegExp, + invalidMediaNotWithMediaTypeRegExp: invalidMediaNotWithMediaTypeRegExp, + + // Media query normalization patterns + mediaFeatureColonRegExp: mediaFeatureColonRegExp, + identifierBeforeParenRegExp: identifierBeforeParenRegExp, + mediaAndOperatorRegExp: mediaAndOperatorRegExp, + mediaOrOperatorRegExp: mediaOrOperatorRegExp, + mediaNotOperatorStartRegExp: mediaNotOperatorStartRegExp, + mediaNotOperatorRegExp: mediaNotOperatorRegExp }; - //.CommonJS - CSSStyleSheet.CSSStyleSheet = CSSOM.CSSStyleSheet; - CSSOM.parse = requireParse().parse; // Cannot be included sooner due to the mutual dependency between parse.js and CSSStyleSheet.js + regexPatterns.regexPatterns = regexPatterns$1; ///CommonJS - return CSSStyleSheet; + return regexPatterns; } -var CSSImportRule = {}; +var CSSStyleDeclaration = {}; + +var parse$1 = {}; + +var CSSStyleSheet = {}; var MediaList = {}; @@ -9052,10 +9075,20 @@ function requireMediaList () { * @param {string} value */ set mediaText(value) { - var values = value.split(","); - var length = this.length = values.length; - for (var i=0; i this.cssRules.length) { - throw new RangeError("INDEX_SIZE_ERR"); + 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'); } - var cssRule = CSSOM.parse(rule).cssRules[0]; - cssRule.parentRule = this; + + cssRule.__parentRule = this; this.cssRules.splice(index, 0, cssRule); return index; }; @@ -9294,10 +9494,19 @@ function requireCSSGroupingRule () { * @see https://www.w3.org/TR/cssom-1/#dom-cssgroupingrule-deleterule */ CSSOM.CSSGroupingRule.prototype.deleteRule = function deleteRule(index) { - if (index < 0 || index >= this.cssRules.length) { - throw new RangeError("INDEX_SIZE_ERR"); + if (index === undefined) { + errorUtils.throwMissingArguments(this, 'deleteRule', this.constructor.name); } - this.cssRules.splice(index, 1)[0].parentRule = null; + 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); }; //.CommonJS @@ -9306,1315 +9515,6159 @@ function requireCSSGroupingRule () { return CSSGroupingRule; } -var CSSMediaRule = {}; - -var CSSConditionRule = {}; - -var hasRequiredCSSConditionRule; +var hasRequiredCSSStyleRule; -function requireCSSConditionRule () { - if (hasRequiredCSSConditionRule) return CSSConditionRule; - hasRequiredCSSConditionRule = 1; +function requireCSSStyleRule () { + if (hasRequiredCSSStyleRule) return CSSStyleRule; + hasRequiredCSSStyleRule = 1; //.CommonJS var CSSOM = { - CSSRule: requireCSSRule().CSSRule, - CSSGroupingRule: requireCSSGroupingRule().CSSGroupingRule + CSSStyleDeclaration: requireCSSStyleDeclaration().CSSStyleDeclaration, + CSSRule: requireCSSRule().CSSRule, + CSSRuleList: requireCSSRuleList().CSSRuleList, + CSSGroupingRule: requireCSSGroupingRule().CSSGroupingRule, }; + var regexPatterns = requireRegexPatterns().regexPatterns; + // Use cssstyle if available + try { + CSSOM.CSSStyleDeclaration = require("cssstyle").CSSStyleDeclaration; + } catch (e) { + // ignore + } ///CommonJS /** * @constructor - * @see https://www.w3.org/TR/css-conditional-3/#the-cssconditionrule-interface + * @see http://dev.w3.org/csswg/cssom/#cssstylerule + * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleRule */ - CSSOM.CSSConditionRule = function CSSConditionRule() { - CSSOM.CSSGroupingRule.call(this); - this.cssRules = []; + CSSOM.CSSStyleRule = function CSSStyleRule() { + CSSOM.CSSGroupingRule.call(this); + this.__selectorText = ""; + this.__style = new CSSOM.CSSStyleDeclaration(); + this.__style.parentRule = this; }; - CSSOM.CSSConditionRule.prototype = new CSSOM.CSSGroupingRule(); - CSSOM.CSSConditionRule.prototype.constructor = CSSOM.CSSConditionRule; - CSSOM.CSSConditionRule.prototype.conditionText = ''; - CSSOM.CSSConditionRule.prototype.cssText = ''; + CSSOM.CSSStyleRule.prototype = Object.create(CSSOM.CSSGroupingRule.prototype); + CSSOM.CSSStyleRule.prototype.constructor = CSSOM.CSSStyleRule; - //.CommonJS - CSSConditionRule.CSSConditionRule = CSSOM.CSSConditionRule; - ///CommonJS - return CSSConditionRule; + 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; + } + }); + + //.CommonJS + CSSStyleRule.CSSStyleRule = CSSOM.CSSStyleRule; + ///CommonJS + return CSSStyleRule; } -var hasRequiredCSSMediaRule; +var hasRequiredCSSStyleSheet; -function requireCSSMediaRule () { - if (hasRequiredCSSMediaRule) return CSSMediaRule; - hasRequiredCSSMediaRule = 1; +function requireCSSStyleSheet () { + if (hasRequiredCSSStyleSheet) return CSSStyleSheet; + hasRequiredCSSStyleSheet = 1; //.CommonJS var CSSOM = { - CSSRule: requireCSSRule().CSSRule, - CSSGroupingRule: requireCSSGroupingRule().CSSGroupingRule, - CSSConditionRule: requireCSSConditionRule().CSSConditionRule, - MediaList: requireMediaList().MediaList + MediaList: requireMediaList().MediaList, + StyleSheet: requireStyleSheet().StyleSheet, + CSSRuleList: requireCSSRuleList().CSSRuleList, + CSSStyleRule: requireCSSStyleRule().CSSStyleRule, }; + var errorUtils = requireErrorUtils().errorUtils; ///CommonJS /** * @constructor - * @see http://dev.w3.org/csswg/cssom/#cssmediarule - * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSMediaRule + * @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.CSSMediaRule = function CSSMediaRule() { - CSSOM.CSSConditionRule.call(this); - this.media = new CSSOM.MediaList(); + 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.CSSMediaRule.prototype = new CSSOM.CSSConditionRule(); - CSSOM.CSSMediaRule.prototype.constructor = CSSOM.CSSMediaRule; - CSSOM.CSSMediaRule.prototype.type = 4; - // https://opensource.apple.com/source/WebCore/WebCore-7611.1.21.161.3/css/CSSMediaRule.cpp - Object.defineProperties(CSSOM.CSSMediaRule.prototype, { - "conditionText": { - get: function() { - return this.media.mediaText; - }, - set: function(value) { - this.media.mediaText = value; - }, - configurable: true, - enumerable: true - }, - "cssText": { - get: function() { - var cssTexts = []; - for (var i=0, length=this.cssRules.length; i < length; i++) { - cssTexts.push(this.cssRules[i].cssText); - } - return "@media " + this.media.mediaText + " {" + cssTexts.join("") + "}"; - }, - configurable: true, - enumerable: true - } + 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 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') ; else { + // Any other rule cannot be inserted before @import and @namespace + var firstNonSpecialRuleIndex = findFirstNonConstructorIndex(this.cssRules, [ + 'CSSLayerStatementRule', + 'CSSImportRule', + 'CSSNamespaceRule' + ]); + + if (index < firstNonSpecialRuleIndex) { + errorUtils.throwError(this, 'DOMException', + "Failed to execute 'insertRule' on '" + this.constructor.name + "': Failed to insert the rule.", + 'HierarchyRequestError'); + } + + if (parseErrors.filter(function(error) { return !error.isNested; }).length !== 0) { + errorUtils.throwParseError(this, 'insertRule', this.constructor.name, ruleToParse, 'SyntaxError'); + } + } + + cssRule.__parentStyleSheet = this; + this.cssRules.splice(index, 0, cssRule); + return index; + }; + + CSSOM.CSSStyleSheet.prototype.addRule = function(selector, styleBlock, index) { + if (index === void 0) { + index = this.cssRules.length; + } + this.insertRule(selector + "{" + styleBlock + "}", index); + return -1; + }; + + /** + * Used to delete a rule from the style sheet. + * + * sheet = new Sheet("img{border:none} body{margin:0}") + * sheet.toString() + * -> "img{border:none;}body{margin:0;}" + * sheet.deleteRule(0) + * sheet.toString() + * -> "body{margin:0;}" + * + * @param {number} index within the style sheet's rule list of the rule to remove. + * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet-deleteRule + */ + CSSOM.CSSStyleSheet.prototype.deleteRule = function(index) { + if (index === undefined) { + errorUtils.throwMissingArguments(this, 'deleteRule', this.constructor.name); + } + index = Number(index); + if (index < 0) { + index = 4294967296 + index; + } + if (index >= this.cssRules.length) { + errorUtils.throwIndexError(this, 'deleteRule', this.constructor.name, index, this.cssRules.length); + } + if (this.cssRules[index]) { + if (this.cssRules[index].constructor.name == "CSSNamespaceRule") { + var shouldContinue = this.cssRules.every(function (rule) { + return ['CSSImportRule','CSSLayerStatementRule','CSSNamespaceRule'].indexOf(rule.constructor.name) !== -1 + }); + if (!shouldContinue) { + errorUtils.throwError(this, 'DOMException', "Failed to execute 'deleteRule' on '" + this.constructor.name + "': Failed to delete rule.", "InvalidStateError"); + } + } + if (this.cssRules[index].constructor.name == "CSSImportRule") { + this.cssRules[index].styleSheet.__parentStyleSheet = null; + } + + this.cssRules[index].__parentStyleSheet = null; + } + this.cssRules.splice(index, 1); + }; + + CSSOM.CSSStyleSheet.prototype.removeRule = function(index) { + if (index === void 0) { + index = 0; + } + this.deleteRule(index); + }; + + + /** + * Replaces the rules of a {@link CSSStyleSheet} + * + * @returns a promise + * @see https://www.w3.org/TR/cssom-1/#dom-cssstylesheet-replace + */ + CSSOM.CSSStyleSheet.prototype.replace = function(text) { + var _Promise; + if (this.__globalObject && this.__globalObject['Promise']) { + _Promise = this.__globalObject['Promise']; + } else { + _Promise = Promise; + } + var _setTimeout; + if (this.__globalObject && this.__globalObject['setTimeout']) { + _setTimeout = this.__globalObject['setTimeout']; + } else { + _setTimeout = setTimeout; + } + var sheet = this; + return new _Promise(function (resolve, reject) { + // If the constructed flag is not set, or the disallow modification flag is set, throw a NotAllowedError DOMException. + if (!sheet.__constructed || sheet.__disallowModification) { + reject(errorUtils.createError(sheet, 'DOMException', + "Failed to execute 'replaceSync' on '" + sheet.constructor.name + "': Not allowed.", + 'NotAllowedError')); + } + // Set the disallow modification flag. + sheet.__disallowModification = true; + + // In parallel, do these steps: + _setTimeout(function() { + // Let rules be the result of running parse a stylesheet's contents from text. + var rules = new CSSOM.CSSRuleList(); + CSSOM.parse(text, { styleSheet: sheet, cssRules: rules }); + // If rules contains one or more @import rules, remove those rules from rules. + var i = 0; + while (i < rules.length) { + if (rules[i].constructor.name === 'CSSImportRule') { + rules.splice(i, 1); + } else { + i++; + } + } + // Set sheet's CSS rules to rules. + sheet.__cssRules.splice.apply(sheet.__cssRules, [0, sheet.__cssRules.length].concat(rules)); + // Unset sheet’s disallow modification flag. + delete sheet.__disallowModification; + // Resolve promise with sheet. + resolve(sheet); + }); + }); + }; + + /** + * Synchronously replaces the rules of a {@link CSSStyleSheet} + * + * @see https://www.w3.org/TR/cssom-1/#dom-cssstylesheet-replacesync + */ + CSSOM.CSSStyleSheet.prototype.replaceSync = function(text) { + var sheet = this; + // If the constructed flag is not set, or the disallow modification flag is set, throw a NotAllowedError DOMException. + if (!sheet.__constructed || sheet.__disallowModification) { + errorUtils.throwError(sheet, 'DOMException', + "Failed to execute 'replaceSync' on '" + sheet.constructor.name + "': Not allowed.", + 'NotAllowedError'); + } + // Let rules be the result of running parse a stylesheet's contents from text. + var rules = new CSSOM.CSSRuleList(); + CSSOM.parse(text, { styleSheet: sheet, cssRules: rules }); + // If rules contains one or more @import rules, remove those rules from rules. + var i = 0; + while (i < rules.length) { + if (rules[i].constructor.name === 'CSSImportRule') { + rules.splice(i, 1); + } else { + i++; + } + } + // Set sheet's CSS rules to rules. + sheet.__cssRules.splice.apply(sheet.__cssRules, [0, sheet.__cssRules.length].concat(rules)); + }; + + /** + * NON-STANDARD + * @return {string} serialize stylesheet + */ + CSSOM.CSSStyleSheet.prototype.toString = function() { + var result = ""; + var rules = this.cssRules; + for (var i=0; i 1000 ? '1000px' : 'auto'); - * } + * NON-STANDARD + * Rule text parser. + * @param {string} cssText */ - CSSOM.CSSValueExpression.prototype.parse = function() { - var token = this._token, - idx = this._idx; + Object.defineProperty(CSSOM.CSSCounterStyleRule.prototype, "parse", { + value: function(cssText) { + // Extract the name from "@counter-style { ... }" + var match = cssText.match(/@counter-style\s+([^\s{]+)\s*\{([^]*)\}/); + if (match) { + this.name = match[1]; + // Get the text inside the brackets and clean it up + var propsText = match[2]; + this.__props = propsText.trim().replace(/\n/g, " ").replace(/(['"])(?:\\.|[^\\])*?\1|(\s{2,})/g, function (match, quote) { + return quote ? match : ' '; + }); + } + } + }); - var character = '', - expression = '', - error = '', - info, - paren = []; + //.CommonJS + CSSCounterStyleRule.CSSCounterStyleRule = CSSOM.CSSCounterStyleRule; + ///CommonJS + return CSSCounterStyleRule; +} +var CSSPropertyRule = {}; - for (; ; ++idx) { - character = token.charAt(idx); +var hasRequiredCSSPropertyRule; - // end of token - if (character === '') { - error = 'css expression error: unfinished expression!'; - break; - } +function requireCSSPropertyRule () { + if (hasRequiredCSSPropertyRule) return CSSPropertyRule; + hasRequiredCSSPropertyRule = 1; + //.CommonJS + var CSSOM = { + CSSRule: requireCSSRule().CSSRule + }; + ///CommonJS - switch(character) { - case '(': - paren.push(character); - expression += character; - break; - case ')': - paren.pop(character); - expression += character; - break; + /** + * @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; + }; - 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; + CSSOM.CSSPropertyRule.prototype = Object.create(CSSOM.CSSRule.prototype); + CSSOM.CSSPropertyRule.prototype.constructor = CSSOM.CSSPropertyRule; - case "'": - case '"': - info = this._parseJSString(token, idx, character); - if (info) { // string - idx = info.idx; - expression += info.text; - } else { - expression += character; - } - break; + Object.setPrototypeOf(CSSOM.CSSPropertyRule, CSSOM.CSSRule); - default: - expression += character; - break; - } + Object.defineProperty(CSSOM.CSSPropertyRule.prototype, "type", { + value: 0, + writable: false + }); - if (error) { - break; + Object.defineProperty(CSSOM.CSSPropertyRule.prototype, "cssText", { + get: function() { + var text = "@property " + this.name + " {"; + if (this.syntax !== "") { + text += " syntax: \"" + this.syntax.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + "\";"; } - - // end of expression - if (paren.length === 0) { - break; + text += " inherits: " + (this.inherits ? "true" : "false") + ";"; + if (this.initialValue !== null) { + text += " initial-value: " + this.initialValue + ";"; } + text += " }"; + return text; } + }); - var ret; - if (error) { - ret = { - error: error - }; - } else { - ret = { - idx: idx, - expression: expression - }; + Object.defineProperty(CSSOM.CSSPropertyRule.prototype, "name", { + get: function() { + return this.__name; } + }); - return ret; - }; + 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; + } + }); /** - * - * @return {Object|false} - * - idx: - * - text: - * or - * - error: - * or - * false - * + * NON-STANDARD + * Rule text parser. + * @param {string} cssText + * @returns {boolean} True if the rule is valid and was parsed successfully */ - 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 = '*/'; + Object.defineProperty(CSSOM.CSSPropertyRule.prototype, "parse", { + value: function(cssText) { + // Extract the name from "@property { ... }" + var match = cssText.match(/@property\s+(--[^\s{]+)\s*\{([^]*)\}/); + if (!match) { + return false; } - - 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 - }; + + 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 { - var error = 'css expression error: unfinished comment in expression!'; - return { - error: error - }; + // If syntax is not "*", initial-value is required + if (this.__syntax !== "*") { + return false; + } } - } else { - return false; + + return true; // Successfully parsed } - }; + }); + //.CommonJS + CSSPropertyRule.CSSPropertyRule = CSSOM.CSSPropertyRule; + ///CommonJS + return CSSPropertyRule; +} - /** - * - * @return {Object|false} - * - idx: - * - text: - * or - * false - * - */ - CSSOM.CSSValueExpression.prototype._parseJSString = function(token, idx, sep) { - var endIdx = this._findMatchedIdx(token, idx, sep), - text; +var CSSContainerRule = {}; - if (endIdx === -1) { - return false; - } else { - text = token.substring(idx, endIdx + sep.length); +var hasRequiredCSSContainerRule; - return { - idx: endIdx, - text: text - }; - } +function requireCSSContainerRule () { + if (hasRequiredCSSContainerRule) return CSSContainerRule; + hasRequiredCSSContainerRule = 1; + //.CommonJS + var CSSOM = { + CSSRule: requireCSSRule().CSSRule, + CSSRuleList: requireCSSRuleList().CSSRuleList, + CSSGroupingRule: requireCSSGroupingRule().CSSGroupingRule, + CSSConditionRule: requireCSSConditionRule().CSSConditionRule, }; + ///CommonJS /** - * parse regexp in css expression - * - * @return {Object|false} - * - idx: - * - regExp: - * or - * false + * @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; - all legal RegExp - - /a/ - (/a/) - [/a/] - [12, /a/] + Object.setPrototypeOf(CSSOM.CSSContainerRule, CSSOM.CSSConditionRule); - !/a/ + Object.defineProperty(CSSOM.CSSContainerRule.prototype, "type", { + value: 17, + writable: false + }); - +/a/ - -/a/ - * /a/ - / /a/ - %/a/ + 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; + } + }, + }); - ===/a/ - !==/a/ - ==/a/ - !=/a/ - >/a/ - >=/a/ - >/a/ - >>>/a/ + //.CommonJS + CSSContainerRule.CSSContainerRule = CSSOM.CSSContainerRule; + ///CommonJS + return CSSContainerRule; +} - &&/a/ - ||/a/ - ?/a/ - =/a/ - ,/a/ +var CSSSupportsRule = {}; - delete /a/ - in /a/ - instanceof /a/ - new /a/ - typeof /a/ - void /a/ +var hasRequiredCSSSupportsRule; - */ - CSSOM.CSSValueExpression.prototype._parseJSRexExp = function(token, idx) { - var before = token.substring(0, idx).replace(/\s+$/, ""), - legalRegx = [ - /^$/, - /\($/, - /\[$/, - /\!$/, - /\+$/, - /\-$/, - /\*$/, - /\/\s+/, - /\%$/, - /\=$/, - /\>$/, - /<$/, - /\&$/, - /\|$/, - /\^$/, - /\~$/, - /\?$/, - /\,$/, - /delete$/, - /in$/, - /instanceof$/, - /new$/, - /typeof$/, - /void$/ - ]; +function requireCSSSupportsRule () { + if (hasRequiredCSSSupportsRule) return CSSSupportsRule; + hasRequiredCSSSupportsRule = 1; + //.CommonJS + var CSSOM = { + CSSRule: requireCSSRule().CSSRule, + CSSRuleList: requireCSSRuleList().CSSRuleList, + CSSGroupingRule: requireCSSGroupingRule().CSSGroupingRule, + CSSConditionRule: requireCSSConditionRule().CSSConditionRule + }; + ///CommonJS - var isLegal = legalRegx.some(function(reg) { - return reg.test(before); - }); - if (!isLegal) { - return false; - } else { - var sep = '/'; + /** + * @constructor + * @see https://drafts.csswg.org/css-conditional-3/#the-csssupportsrule-interface + */ + CSSOM.CSSSupportsRule = function CSSSupportsRule() { + CSSOM.CSSConditionRule.call(this); + }; - // same logic as string - return this._parseJSString(token, idx, sep); - } + 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; + } + }); + + //.CommonJS + CSSSupportsRule.CSSSupportsRule = CSSOM.CSSSupportsRule; + ///CommonJS + return CSSSupportsRule; +} + +var CSSFontFaceRule = {}; + +var hasRequiredCSSFontFaceRule; + +function requireCSSFontFaceRule () { + if (hasRequiredCSSFontFaceRule) return CSSFontFaceRule; + hasRequiredCSSFontFaceRule = 1; + //.CommonJS + var CSSOM = { + CSSStyleDeclaration: requireCSSStyleDeclaration().CSSStyleDeclaration, + CSSRule: requireCSSRule().CSSRule }; + // Use cssstyle if available + try { + CSSOM.CSSStyleDeclaration = require("cssstyle").CSSStyleDeclaration; + } catch (e) { + // ignore + } + ///CommonJS /** - * - * find next sep(same line) index in `token` - * - * @return {Number} - * + * @constructor + * @see http://dev.w3.org/csswg/cssom/#css-font-face-rule */ - CSSOM.CSSValueExpression.prototype._findMatchedIdx = function(token, idx, sep) { - var startIdx = idx, - endIdx; + CSSOM.CSSFontFaceRule = function CSSFontFaceRule() { + CSSOM.CSSRule.call(this); + this.__style = new CSSOM.CSSStyleDeclaration(); + this.__style.parentRule = this; + }; - var NOT_FOUND = -1; + CSSOM.CSSFontFaceRule.prototype = Object.create(CSSOM.CSSRule.prototype); + CSSOM.CSSFontFaceRule.prototype.constructor = CSSOM.CSSFontFaceRule; - while(true) { - endIdx = token.indexOf(sep, startIdx + 1); + Object.setPrototypeOf(CSSOM.CSSFontFaceRule, CSSOM.CSSRule); - if (endIdx === -1) { // not found - endIdx = NOT_FOUND; - break; + 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 { - var text = token.substring(idx + 1, endIdx), - matched = text.match(/\\+$/); - if (!matched || matched[0] % 2 === 0) { // not escaped - break; - } else { - startIdx = endIdx; - } + this.__style = value; } } + }); - // 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; - }; - - + // 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 : "") + " }"; + } + }); //.CommonJS - CSSValueExpression.CSSValueExpression = CSSOM.CSSValueExpression; + CSSFontFaceRule.CSSFontFaceRule = CSSOM.CSSFontFaceRule; ///CommonJS - return CSSValueExpression; + return CSSFontFaceRule; } -var CSSDocumentRule = {}; - -var MatcherList = {}; +var CSSHostRule = {}; -var hasRequiredMatcherList; +var hasRequiredCSSHostRule; -function requireMatcherList () { - if (hasRequiredMatcherList) return MatcherList; - hasRequiredMatcherList = 1; +function requireCSSHostRule () { + if (hasRequiredCSSHostRule) return CSSHostRule; + hasRequiredCSSHostRule = 1; //.CommonJS - var CSSOM = {}; + var CSSOM = { + CSSRule: requireCSSRule().CSSRule, + CSSRuleList: requireCSSRuleList().CSSRuleList + }; ///CommonJS /** * @constructor - * @see https://developer.mozilla.org/en/CSS/@-moz-document + * @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.MatcherList = function MatcherList(){ - this.length = 0; + CSSOM.CSSHostRule = function CSSHostRule() { + CSSOM.CSSRule.call(this); + this.cssRules = new CSSOM.CSSRuleList(); }; - CSSOM.MatcherList.prototype = { + CSSOM.CSSHostRule.prototype = Object.create(CSSOM.CSSRule.prototype); + CSSOM.CSSHostRule.prototype.constructor = CSSOM.CSSHostRule; - constructor: CSSOM.MatcherList, + Object.setPrototypeOf(CSSOM.CSSHostRule, CSSOM.CSSRule); - /** - * @return {string} - */ - get matcherText() { - return Array.prototype.join.call(this, ", "); - }, + Object.defineProperty(CSSOM.CSSHostRule.prototype, "type", { + value: 1001, + writable: false + }); - /** - * @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 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 + }); + }; - var styleSheet = new CSSOM.CSSStyleSheet(); - // @type CSSStyleSheet|CSSMediaRule|CSSSupportsRule|CSSFontFaceRule|CSSKeyframesRule|CSSDocumentRule - var currentScope = styleSheet; - // @type CSSMediaRule|CSSSupportsRule|CSSKeyframesRule|CSSDocumentRule - var parentRule; - var ancestorRules = []; - var hasAncestors = false; - var prevScope; - var name, priority="", styleRule, mediaRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule; + //.CommonJS + CSSKeyframesRule.CSSKeyframesRule = CSSOM.CSSKeyframesRule; + ///CommonJS + return CSSKeyframesRule; +} + +var CSSValueExpression = {}; + +var CSSValue = {}; + +var hasRequiredCSSValue; + +function requireCSSValue () { + if (hasRequiredCSSValue) return CSSValue; + hasRequiredCSSValue = 1; + //.CommonJS + var CSSOM = {}; + ///CommonJS + + + /** + * @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; + } + }; + + + //.CommonJS + CSSValue.CSSValue = CSSOM.CSSValue; + ///CommonJS + return CSSValue; +} + +var hasRequiredCSSValueExpression; + +function requireCSSValueExpression () { + if (hasRequiredCSSValueExpression) return CSSValueExpression; + hasRequiredCSSValueExpression = 1; + //.CommonJS + var CSSOM = { + CSSValue: requireCSSValue().CSSValue + }; + ///CommonJS + + + /** + * @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/ + + 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; + }; + + + + + //.CommonJS + CSSValueExpression.CSSValueExpression = CSSOM.CSSValueExpression; + ///CommonJS + return CSSValueExpression; +} + +var CSSDocumentRule = {}; + +var MatcherList = {}; + +var hasRequiredMatcherList; + +function requireMatcherList () { + if (hasRequiredMatcherList) return MatcherList; + hasRequiredMatcherList = 1; + //.CommonJS + var CSSOM = {}; + ///CommonJS + + + /** + * @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) + 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] === '|' || 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] === '\\' || 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] === ':') { + 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 = ''; + 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; + 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 === '-'; + continue; + } + } + + // Handle regular characters + if (char === '.' || char === '#') { + result += char; + needsEscapeForIdent = true; + 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; + 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; + lastWasHexEscape = false; + i++; + } else if (isLetter(char) && lastWasHexEscape) { + // Letter after hex escape needs a space separator + result += ' ' + char; + needsEscapeForIdent = false; + 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; + lastWasHexEscape = false; + i++; + } else { + result += char; + needsEscapeForIdent = false; + 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; + } + + /** + * 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; + } + + /** + * Validates media query syntax for invalid patterns. + * + * @param {string} text - The media/condition text to validate + * @returns {boolean} True if invalid, false if valid + */ + function isInvalidConditionText(text) { + return invalidMediaOperatorWithoutSpaceRegExp.test(text) || + invalidMediaNotAfterOperatorRegExp.test(text) || + invalidMediaNotIdentifierRegExp.test(text) || + invalidMediaNotAfterFeatureRegExp.test(text) || + invalidMediaOperatorBetweenParensRegExp.test(text) || + invalidMediaNotWithMediaTypeRegExp.test(text); + } + + /** + * Normalizes media query or condition text to match browser serialization. + * Ensures spaces after colons in media features and around logical operators. + * Returns "not all" for invalid syntax (e.g., missing spaces after operators). + * + * @param {string} text - The media/condition text to normalize + * @returns {string} Normalized text or "not all" if invalid + */ + function normalizeConditionText(text) { + if (isInvalidConditionText(text)) { + return 'not all'; + } + + var result = text; + + // Normalize colons only in media features, not in function calls + // Function syntax: identifier( - should be preserved as-is + // Media feature: (name: value) - should normalize colon spacing + // Strategy: only normalize colons that come after '(' without an identifier before it + result = result.replace(mediaFeatureColonRegExp, function(match, openParen, name, spaces, rest) { + // Check if there's an identifier immediately before the opening paren + var beforeMatch = result.lastIndexOf(match); + var beforeParen = beforeMatch > 0 ? result.charAt(beforeMatch - 1) : ''; + + // If character before '(' is alphanumeric, it's a function call - keep as-is + if (identifierBeforeParenRegExp.test(beforeParen)) { + return match; + } + + // Otherwise it's a media feature - normalize colon + return openParen + name + ': ' + rest; + }); + + // Normalize 'and' operator with word boundaries: ensure exactly one space before and after + result = result.replace(mediaAndOperatorRegExp, ' and '); + + // Normalize 'or' operator with word boundaries: ensure exactly one space before and after + result = result.replace(mediaOrOperatorRegExp, ' or '); + + // Normalize 'not' operator only when followed by space (not when followed by '(') + // 'not(' is a valid identifier/function, 'not ' is the operator + // At start: ensure exactly one space after + result = result.replace(mediaNotOperatorStartRegExp, 'not '); + + // After space: ensure exactly one space before and after (but not 'not(') + result = result.replace(mediaNotOperatorRegExp, ' not '); + + return result.trim(); + } + + 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 = normalizeConditionText(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 = normalizeConditionText(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 = normalizeConditionText(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); + } - var atKeyframesRegExp = /@(-(?:\w+-)+)?keyframes/g; + currentScope = parentRule = pageRule; + pageRule.__parentStyleSheet = styleSheet; + styleRule = pageRule; + buffer = ""; + state = "before-name"; + } else if (state === "hostRule-begin") { + if (parentRule) { + pushToAncestorRules(parentRule); + } - var parseError = function(message) { - var lines = token.substring(0, i).split('\n'); - var lineCount = lines.length; - var charCount = lines.pop().length + 1; - var error = new Error(message + ' (line ' + lineCount + ', char ' + charCount + ')'); - error.line = lineCount; - /* jshint sub : true */ - error['char'] = charCount; - error.styleSheet = styleSheet; - throw error; - }; + 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; + } + } - for (var character; (character = token.charAt(i)); i++) { + currentScope = parentRule = startingStyleRule; + pushToAncestorRules(startingStyleRule); + startingStyleRule.__parentStyleSheet = styleSheet; + styleRule = null; // Reset styleRule when entering @-rule + buffer = ""; + state = "before-selector"; - switch (character) { + } 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; + } + } - case " ": - case "\t": - case "\r": - case "\n": - case "\f": - if (SIGNIFICANT_WHITESPACE[state]) { - buffer += character; - } - 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; + } - // String - case '"': - index = i + 1; - do { - index = token.indexOf('"', index) + 1; - if (!index) { - parseError('Unmatched "'); - } - } while (token[index - 2] === '\\'); - buffer += token.slice(i, index); - i = index - 1; - switch (state) { - case 'before-value': - state = 'value'; - break; - case 'importRule-begin': - state = 'importRule'; - break; - } - break; + 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; - case "'": - index = i + 1; - do { - index = token.indexOf("'", index) + 1; - if (!index) { - parseError("Unmatched '"); + buffer = ""; + state = "before-name"; } - } while (token[index - 2] === '\\'); - buffer += token.slice(i, index); - i = index - 1; - switch (state) { - case 'before-value': - state = 'value'; - break; - case 'importRule-begin': - state = 'importRule'; - break; - } - break; + break; - // Comment - case "/": - if (token.charAt(i + 1) === "*") { - i += 2; - index = token.indexOf("*/", i); - if (index === -1) { - parseError("Missing */"); + 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 { - i = index + 1; + buffer += character; } - } else { - buffer += character; - } - if (state === "importRule-begin") { - buffer += " "; - state = "importRule"; - } - break; - - // At-rule - case "@": - if (token.indexOf("@-moz-document", i) === i) { - state = "documentRule-begin"; - documentRule = new CSSOM.CSSDocumentRule(); - documentRule.__starts = i; - i += "-moz-document".length; - buffer = ""; - break; - } else if (token.indexOf("@media", i) === i) { - state = "atBlock"; - mediaRule = new CSSOM.CSSMediaRule(); - mediaRule.__starts = i; - i += "media".length; - buffer = ""; - break; - } else if (token.indexOf("@supports", i) === i) { - state = "conditionBlock"; - supportsRule = new CSSOM.CSSSupportsRule(); - supportsRule.__starts = i; - i += "supports".length; - buffer = ""; break; - } else if (token.indexOf("@host", i) === i) { - state = "hostRule-begin"; - i += "host".length; - hostRule = new CSSOM.CSSHostRule(); - hostRule.__starts = i; - buffer = ""; - break; - } else if (token.indexOf("@import", i) === i) { - state = "importRule-begin"; - i += "import".length; - buffer += "@import"; - break; - } else if (token.indexOf("@font-face", i) === i) { - state = "fontFaceRule-begin"; - i += "font-face".length; - fontFaceRule = new CSSOM.CSSFontFaceRule(); - fontFaceRule.__starts = i; - buffer = ""; - break; - } else { - 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 (state === "selector" || state === "atRule") { - styleRule.selectorText = buffer.trim(); - styleRule.style.__starts = i; - buffer = ""; - state = "before-name"; - } else if (state === "atBlock") { - mediaRule.media.mediaText = buffer.trim(); - - if (parentRule) { - ancestorRules.push(parentRule); - } - - currentScope = parentRule = mediaRule; - mediaRule.parentStyleSheet = styleSheet; - buffer = ""; - state = "before-selector"; - } else if (state === "conditionBlock") { - supportsRule.conditionText = buffer.trim(); - if (parentRule) { - ancestorRules.push(parentRule); - } - - currentScope = parentRule = supportsRule; - supportsRule.parentStyleSheet = styleSheet; - buffer = ""; - state = "before-selector"; - } else if (state === "hostRule-begin") { - if (parentRule) { - ancestorRules.push(parentRule); + 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; - currentScope = parentRule = hostRule; - hostRule.parentStyleSheet = styleSheet; - 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) { - ancestorRules.push(parentRule); - keyframesRule.parentRule = parentRule; + case ")": + if (state === 'value-parenthesis') { + valueParenthesisDepth--; + if (valueParenthesisDepth === 0) state = 'value'; } - 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) { - ancestorRules.push(parentRule); - documentRule.parentRule = parentRule; - } - currentScope = parentRule = documentRule; - documentRule.parentStyleSheet = styleSheet; - buffer = ""; - state = "before-selector"; - } - break; - - case ":": - if (state === "name") { - 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(); + break; - if (info.error) { - parseError(info.error); - } else { - buffer += info.expression; - i = info.idx; - } + case "!": + if (state === "value" && token.indexOf("!important", i) === i) { + priority = "important"; + i += "important".length; } 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; + break; - case ")": - if (state === 'value-parenthesis') { - valueParenthesisDepth--; - if (valueParenthesisDepth === 0) state = 'value'; - } - 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; + } + } - case "!": - if (state === "value" && token.indexOf("!important", i) === i) { - priority = "important"; - i += "important".length; - } else { - buffer += character; - } - break; + 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 ";": - switch (state) { - case "value": - styleRule.style.setProperty(name, buffer.trim(), priority); - priority = ""; - buffer = ""; - state = "before-name"; - break; - case "atRule": + 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; - case "importRule": - importRule = new CSSOM.CSSImportRule(); - importRule.parentStyleSheet = importRule.styleSheet.parentStyleSheet = styleSheet; - importRule.cssText = buffer + character; - styleSheet.cssRules.push(importRule); + } + 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; - default: - buffer += character; - break; - } - break; - - case "}": - switch (state) { - case "value": - styleRule.style.setProperty(name, buffer.trim(), priority); - priority = ""; + } + switch (state) { + case "value": + styleRule.style.setProperty(name, buffer.trim(), priority, parseError); + priority = ""; /* falls through */ - case "before-name": - case "name": - styleRule.__ends = i + 1; - if (parentRule) { - styleRule.parentRule = parentRule; - } - styleRule.parentStyleSheet = styleSheet; - currentScope.cssRules.push(styleRule); - buffer = ""; - if (currentScope.constructor === CSSOM.CSSKeyframesRule) { - state = "keyframeRule-begin"; - } else { - state = "before-selector"; - } - break; - case "keyframeRule-begin": - case "before-selector": - case "selector": - // End of media/supports/document rule. - if (!parentRule) { - parseError("Unexpected }"); - } + case "before-value": + case "before-name": + case "name": + styleRule.__ends = i + 1; - // Handle rules nested in @media or @supports - hasAncestors = ancestorRules.length > 0; + if (parentRule === styleRule) { + parentRule = ancestorRules.pop(); + } - while (ancestorRules.length > 0) { - parentRule = ancestorRules.pop(); + if (parentRule) { + styleRule.__parentRule = parentRule; + } + styleRule.__parentStyleSheet = styleSheet; - if ( - parentRule.constructor.name === "CSSMediaRule" - || parentRule.constructor.name === "CSSSupportsRule" - ) { - prevScope = currentScope; - currentScope = parentRule; - currentScope.cssRules.push(prevScope); + 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; + } + } + 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); + } - if (ancestorRules.length === 0) { - hasAncestors = false; + break; } - } - - if (!hasAncestors) { - currentScope.__ends = i + 1; - styleSheet.cssRules.push(currentScope); - currentScope = styleSheet; - parentRule = null; - } - buffer = ""; - state = "before-selector"; - break; - } - 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; + } + } - default: - switch (state) { - case "before-selector": - state = "selector"; - 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; + // 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; } - buffer += character; - break; } } + if (buffer.trim() !== "") { + parseError("Unexpected end of input"); + } + return styleSheet; }; @@ -10624,18 +15677,30 @@ function requireParse () { // The following modules cannot be included sooner due to the mutual dependency with parse.js CSSOM.CSSStyleSheet = requireCSSStyleSheet().CSSStyleSheet; CSSOM.CSSStyleRule = requireCSSStyleRule().CSSStyleRule; + CSSOM.CSSNestedDeclarations = requireCSSNestedDeclarations().CSSNestedDeclarations; CSSOM.CSSImportRule = requireCSSImportRule().CSSImportRule; + CSSOM.CSSNamespaceRule = requireCSSNamespaceRule().CSSNamespaceRule; CSSOM.CSSGroupingRule = requireCSSGroupingRule().CSSGroupingRule; CSSOM.CSSMediaRule = requireCSSMediaRule().CSSMediaRule; + CSSOM.CSSCounterStyleRule = requireCSSCounterStyleRule().CSSCounterStyleRule; + CSSOM.CSSPropertyRule = requireCSSPropertyRule().CSSPropertyRule; + CSSOM.CSSContainerRule = requireCSSContainerRule().CSSContainerRule; CSSOM.CSSConditionRule = requireCSSConditionRule().CSSConditionRule; CSSOM.CSSSupportsRule = requireCSSSupportsRule().CSSSupportsRule; CSSOM.CSSFontFaceRule = requireCSSFontFaceRule().CSSFontFaceRule; CSSOM.CSSHostRule = requireCSSHostRule().CSSHostRule; + CSSOM.CSSStartingStyleRule = requireCSSStartingStyleRule().CSSStartingStyleRule; CSSOM.CSSStyleDeclaration = requireCSSStyleDeclaration().CSSStyleDeclaration; CSSOM.CSSKeyframeRule = requireCSSKeyframeRule().CSSKeyframeRule; CSSOM.CSSKeyframesRule = requireCSSKeyframesRule().CSSKeyframesRule; CSSOM.CSSValueExpression = requireCSSValueExpression().CSSValueExpression; CSSOM.CSSDocumentRule = requireCSSDocumentRule().CSSDocumentRule; + CSSOM.CSSScopeRule = requireCSSScopeRule().CSSScopeRule; + CSSOM.CSSLayerBlockRule = requireCSSLayerBlockRule().CSSLayerBlockRule; + CSSOM.CSSLayerStatementRule = requireCSSLayerStatementRule().CSSLayerStatementRule; + CSSOM.CSSPageRule = requireCSSPageRule().CSSPageRule; + // Use cssstyle if available + requireCssstyleTryCatchBlock(); ///CommonJS return parse$1; } @@ -10647,8 +15712,11 @@ function requireCSSStyleDeclaration () { hasRequiredCSSStyleDeclaration = 1; //.CommonJS var CSSOM = {}; + var regexPatterns = requireRegexPatterns().regexPatterns; ///CommonJS + // Internal-use regular expression for indexed property names like '0', '1', '2', etc. + var indexedPropertyNameRegExp = /^(0|[1-9][0-9]*)$/; /** * @constructor @@ -10685,21 +15753,49 @@ function requireCSSStyleDeclaration () { * @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) { - if (this[name]) { + setProperty: function(name, value, priority, parseErrorHandler) + { + // Because values are stored directly on this object, we must ignore CSS declaration + // names that would otherwise replace the object's length, parser fields, or methods. + // Numeric names would also overwrite entries like this[0], which hold a property name. + if (name === "length" || + name === "parentRule" || + name === "_importants" || + name === "__starts" || + name in CSSOM.CSSStyleDeclaration.prototype || + indexedPropertyNameRegExp.test(name) + ) { + return; + } + // 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; } - this[name] = value + ""; - this._importants[name] = priority; }, /** @@ -10807,15 +15903,26 @@ function requireClone () { var CSSOM = { CSSStyleSheet: requireCSSStyleSheet().CSSStyleSheet, CSSRule: requireCSSRule().CSSRule, + CSSNestedDeclarations: requireCSSNestedDeclarations().CSSNestedDeclarations, CSSStyleRule: requireCSSStyleRule().CSSStyleRule, CSSGroupingRule: requireCSSGroupingRule().CSSGroupingRule, CSSConditionRule: requireCSSConditionRule().CSSConditionRule, CSSMediaRule: requireCSSMediaRule().CSSMediaRule, + CSSContainerRule: requireCSSContainerRule().CSSContainerRule, CSSSupportsRule: requireCSSSupportsRule().CSSSupportsRule, CSSStyleDeclaration: requireCSSStyleDeclaration().CSSStyleDeclaration, CSSKeyframeRule: requireCSSKeyframeRule().CSSKeyframeRule, - CSSKeyframesRule: requireCSSKeyframesRule().CSSKeyframesRule + CSSKeyframesRule: requireCSSKeyframesRule().CSSKeyframesRule, + CSSScopeRule: requireCSSScopeRule().CSSScopeRule, + CSSLayerBlockRule: requireCSSLayerBlockRule().CSSLayerBlockRule, + CSSLayerStatementRule: requireCSSLayerStatementRule().CSSLayerStatementRule }; + // Use cssstyle if available + try { + CSSOM.CSSStyleDeclaration = require("cssstyle").CSSStyleDeclaration; + } catch (e) { + // ignore + } ///CommonJS @@ -10842,11 +15949,11 @@ function requireClone () { 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); + var name = style[j]; + // Consumers can change style[j] directly, so we must use setProperty here to ensure + // reserved names are ignored before they can overwrite fields on the clone object + styleClone.setProperty(name, style.getPropertyValue(name), style.getPropertyPriority(name)); } - styleClone.length = style.length; } if (rule.hasOwnProperty('keyText')) { @@ -10861,10 +15968,30 @@ function requireClone () { 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; } @@ -10886,17 +16013,32 @@ function requireLib () { if (hasRequiredLib) return lib; hasRequiredLib = 1; + lib.setup = requireCSSOM().setup; + + requireErrorUtils(); + requireRegexPatterns(); + lib.CSSStyleDeclaration = requireCSSStyleDeclaration().CSSStyleDeclaration; + + requireCssstyleTryCatchBlock(); + lib.CSSRule = requireCSSRule().CSSRule; + lib.CSSRuleList = requireCSSRuleList().CSSRuleList; + lib.CSSNestedDeclarations = requireCSSNestedDeclarations().CSSNestedDeclarations; lib.CSSGroupingRule = requireCSSGroupingRule().CSSGroupingRule; + lib.CSSCounterStyleRule = requireCSSCounterStyleRule().CSSCounterStyleRule; + lib.CSSPropertyRule = requireCSSPropertyRule().CSSPropertyRule; lib.CSSConditionRule = requireCSSConditionRule().CSSConditionRule; lib.CSSStyleRule = requireCSSStyleRule().CSSStyleRule; lib.MediaList = requireMediaList().MediaList; lib.CSSMediaRule = requireCSSMediaRule().CSSMediaRule; + lib.CSSContainerRule = requireCSSContainerRule().CSSContainerRule; lib.CSSSupportsRule = requireCSSSupportsRule().CSSSupportsRule; lib.CSSImportRule = requireCSSImportRule().CSSImportRule; + lib.CSSNamespaceRule = requireCSSNamespaceRule().CSSNamespaceRule; lib.CSSFontFaceRule = requireCSSFontFaceRule().CSSFontFaceRule; lib.CSSHostRule = requireCSSHostRule().CSSHostRule; + lib.CSSStartingStyleRule = requireCSSStartingStyleRule().CSSStartingStyleRule; lib.StyleSheet = requireStyleSheet().StyleSheet; lib.CSSStyleSheet = requireCSSStyleSheet().CSSStyleSheet; lib.CSSKeyframesRule = requireCSSKeyframesRule().CSSKeyframesRule; @@ -10905,6 +16047,10 @@ function requireLib () { lib.CSSDocumentRule = requireCSSDocumentRule().CSSDocumentRule; lib.CSSValue = requireCSSValue().CSSValue; lib.CSSValueExpression = requireCSSValueExpression().CSSValueExpression; + lib.CSSScopeRule = requireCSSScopeRule().CSSScopeRule; + lib.CSSLayerBlockRule = requireCSSLayerBlockRule().CSSLayerBlockRule; + lib.CSSLayerStatementRule = requireCSSLayerStatementRule().CSSLayerStatementRule; + lib.CSSPageRule = requireCSSPageRule().CSSPageRule; lib.parse = requireParse().parse; lib.clone = requireClone().clone; return lib; @@ -12144,7 +17290,7 @@ const globalExports = assign( } ); -const window = new WeakMap; +const window$1 = new WeakMap; /** * @implements globalThis.Document @@ -12167,8 +17313,8 @@ let Document$1 = class Document extends NonElementParentNode { * @type {globalThis.Document['defaultView']} */ get defaultView() { - if (!window.has(this)) - window.set(this, new Proxy(globalThis, { + if (!window$1.has(this)) + window$1.set(this, new Proxy(globalThis, { set: (target, name, value) => { switch (name) { case 'addEventListener': @@ -12203,7 +17349,7 @@ let Document$1 = class Document extends NonElementParentNode { }; /* c8 ignore stop */ case 'window': - return window.get(this); + return window$1.get(this); case 'customElements': if (!this[CUSTOM_ELEMENTS].registry) this[CUSTOM_ELEMENTS] = new CustomElementRegistry(this); @@ -12226,7 +17372,7 @@ let Document$1 = class Document extends NonElementParentNode { globalThis[name]; } })); - return window.get(this); + return window$1.get(this); } get doctype() {