@acemir/cssom 0.9.22 → 0.9.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/CSSOM.js CHANGED
@@ -34,6 +34,19 @@ function getErrorConstructor(context, errorType) {
34
34
  eval(errorType);
35
35
  }
36
36
 
37
+ /**
38
+ * Creates an appropriate error with context-aware constructor.
39
+ *
40
+ * @param {Object} context - The CSSOM object (rule, stylesheet, etc.)
41
+ * @param {string} errorType - The error type ('TypeError', 'RangeError', 'DOMException', etc.)
42
+ * @param {string} message - The error message
43
+ * @param {string} [name] - Optional name for DOMException
44
+ */
45
+ function createError(context, errorType, message, name) {
46
+ var ErrorConstructor = getErrorConstructor(context, errorType);
47
+ return new ErrorConstructor(message, name);
48
+ }
49
+
37
50
  /**
38
51
  * Creates and throws an appropriate error with context-aware constructor.
39
52
  *
@@ -43,9 +56,7 @@ function getErrorConstructor(context, errorType) {
43
56
  * @param {string} [name] - Optional name for DOMException
44
57
  */
45
58
  function throwError(context, errorType, message, name) {
46
- var ErrorConstructor = getErrorConstructor(context, errorType);
47
- var error = new ErrorConstructor(message, name);
48
- throw error;
59
+ throw createError(context, errorType, message, name);
49
60
  }
50
61
 
51
62
  /**
@@ -98,6 +109,7 @@ function throwIndexError(context, methodName, objectName, index, maxIndex, name)
98
109
  }
99
110
 
100
111
  var errorUtils = {
112
+ createError: createError,
101
113
  getErrorConstructor: getErrorConstructor,
102
114
  throwError: throwError,
103
115
  throwMissingArguments: throwMissingArguments,
@@ -628,12 +640,17 @@ Object.defineProperty(CSSOM.CSSStyleRule.prototype, "cssText", {
628
640
  get: function() {
629
641
  var text;
630
642
  if (this.selectorText) {
631
- var values = ""
643
+ var values = "";
632
644
  if (this.cssRules.length) {
633
645
  var valuesArr = [" {"];
634
646
  this.style.cssText && valuesArr.push(this.style.cssText);
635
- valuesArr.push(this.cssRules.map(function(rule){ return rule.cssText }).join("\n "));
636
- values = valuesArr.join("\n ") + "\n}"
647
+ valuesArr.push(this.cssRules.reduce(function(acc, rule){
648
+ if (rule.cssText !== "") {
649
+ acc.push(rule.cssText);
650
+ }
651
+ return acc;
652
+ }, []).join("\n "));
653
+ values = valuesArr.join("\n ") + "\n}";
637
654
  } else {
638
655
  values = " {" + (this.style.cssText ? " " + this.style.cssText : "") + " }";
639
656
  }
@@ -825,7 +842,9 @@ CSSOM.MediaList.prototype = {
825
842
  * @param {string} value
826
843
  */
827
844
  set mediaText(value) {
828
- var values = value.split(",");
845
+ var values = value.split(",").filter(function(text){
846
+ return !!text;
847
+ });
829
848
  var length = this.length = values.length;
830
849
  for (var i=0; i<length; i++) {
831
850
  this[i] = values[i].trim();
@@ -895,11 +914,18 @@ Object.defineProperties(CSSOM.CSSMediaRule.prototype, {
895
914
  },
896
915
  "cssText": {
897
916
  get: function() {
898
- var cssTexts = [];
899
- for (var i=0, length=this.cssRules.length; i < length; i++) {
900
- cssTexts.push(this.cssRules[i].cssText);
917
+ var values = "";
918
+ var valuesArr = [" {"];
919
+ if (this.cssRules.length) {
920
+ valuesArr.push(this.cssRules.reduce(function(acc, rule){
921
+ if (rule.cssText !== "") {
922
+ acc.push(rule.cssText);
923
+ }
924
+ return acc;
925
+ }, []).join("\n "));
901
926
  }
902
- return "@media " + this.media.mediaText + " {" + (cssTexts.length ? "\n " + cssTexts.join("\n ") : "") + "\n}";
927
+ values = valuesArr.join("\n ") + "\n}";
928
+ return "@media " + this.media.mediaText + values;
903
929
  },
904
930
  configurable: true,
905
931
  enumerable: true
@@ -927,11 +953,18 @@ CSSOM.CSSContainerRule.prototype.type = 17;
927
953
  Object.defineProperties(CSSOM.CSSContainerRule.prototype, {
928
954
  "cssText": {
929
955
  get: function() {
930
- var cssTexts = [];
931
- for (var i=0, length=this.cssRules.length; i < length; i++) {
932
- cssTexts.push(this.cssRules[i].cssText);
956
+ var values = "";
957
+ var valuesArr = [" {"];
958
+ if (this.cssRules.length) {
959
+ valuesArr.push(this.cssRules.reduce(function(acc, rule){
960
+ if (rule.cssText !== "") {
961
+ acc.push(rule.cssText);
962
+ }
963
+ return acc;
964
+ }, []).join("\n "));
933
965
  }
934
- return "@container " + this.conditionText + " {" + (cssTexts.length ? "\n " + cssTexts.join("\n ") : "") + "\n}";
966
+ values = valuesArr.join("\n ") + "\n}";
967
+ return "@container " + this.conditionText + values;
935
968
  },
936
969
  configurable: true,
937
970
  enumerable: true
@@ -975,13 +1008,18 @@ CSSOM.CSSSupportsRule.prototype.type = 12;
975
1008
 
976
1009
  Object.defineProperty(CSSOM.CSSSupportsRule.prototype, "cssText", {
977
1010
  get: function() {
978
- var cssTexts = [];
979
-
980
- for (var i = 0, length = this.cssRules.length; i < length; i++) {
981
- cssTexts.push(this.cssRules[i].cssText);
1011
+ var values = "";
1012
+ var valuesArr = [" {"];
1013
+ if (this.cssRules.length) {
1014
+ valuesArr.push(this.cssRules.reduce(function(acc, rule){
1015
+ if (rule.cssText !== "") {
1016
+ acc.push(rule.cssText);
1017
+ }
1018
+ return acc;
1019
+ }, []).join("\n "));
982
1020
  }
983
-
984
- return "@supports " + this.conditionText + " {" + (cssTexts.length ? "\n " + cssTexts.join("\n ") : "") + "\n}";
1021
+ values = valuesArr.join("\n ") + "\n}";
1022
+ return "@supports " + this.conditionText + values;
985
1023
  }
986
1024
  });
987
1025
 
@@ -1368,6 +1406,8 @@ Object.defineProperty(CSSOM.CSSFontFaceRule.prototype, "cssText", {
1368
1406
  /**
1369
1407
  * @constructor
1370
1408
  * @see http://www.w3.org/TR/shadow-dom/#host-at-rule
1409
+ * @see http://html5index.org/Shadow%20DOM%20-%20CSSHostRule.html
1410
+ * @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.
1371
1411
  */
1372
1412
  CSSOM.CSSHostRule = function CSSHostRule() {
1373
1413
  CSSOM.CSSRule.call(this);
@@ -1383,11 +1423,18 @@ CSSOM.CSSHostRule.prototype.type = 1001;
1383
1423
 
1384
1424
  Object.defineProperty(CSSOM.CSSHostRule.prototype, "cssText", {
1385
1425
  get: function() {
1386
- var cssTexts = [];
1387
- for (var i=0, length=this.cssRules.length; i < length; i++) {
1388
- cssTexts.push(this.cssRules[i].cssText);
1426
+ var values = "";
1427
+ var valuesArr = [" {"];
1428
+ if (this.cssRules.length) {
1429
+ valuesArr.push(this.cssRules.reduce(function(acc, rule){
1430
+ if (rule.cssText !== "") {
1431
+ acc.push(rule.cssText);
1432
+ }
1433
+ return acc;
1434
+ }, []).join("\n "));
1389
1435
  }
1390
- return "@host {" + (cssTexts.length ? "\n " + cssTexts.join("\n ") : "") + "\n}";
1436
+ values = valuesArr.join("\n ") + "\n}";
1437
+ return "@host" + values;
1391
1438
  }
1392
1439
  });
1393
1440
 
@@ -1413,11 +1460,18 @@ CSSOM.CSSStartingStyleRule.prototype.type = 1002;
1413
1460
 
1414
1461
  Object.defineProperty(CSSOM.CSSStartingStyleRule.prototype, "cssText", {
1415
1462
  get: function() {
1416
- var cssTexts = [];
1417
- for (var i=0, length=this.cssRules.length; i < length; i++) {
1418
- cssTexts.push(this.cssRules[i].cssText);
1463
+ var values = "";
1464
+ var valuesArr = [" {"];
1465
+ if (this.cssRules.length) {
1466
+ valuesArr.push(this.cssRules.reduce(function(acc, rule){
1467
+ if (rule.cssText !== "") {
1468
+ acc.push(rule.cssText);
1469
+ }
1470
+ return acc;
1471
+ }, []).join("\n "));
1419
1472
  }
1420
- return "@starting-style {" + (cssTexts.length ? "\n " + cssTexts.join("\n ") : "") + "\n}";
1473
+ values = valuesArr.join("\n ") + "\n}";
1474
+ return "@starting-style" + values;
1421
1475
  }
1422
1476
  });
1423
1477
 
@@ -1465,6 +1519,7 @@ Object.defineProperties(CSSOM.StyleSheet.prototype, {
1465
1519
  */
1466
1520
  CSSOM.CSSStyleSheet = function CSSStyleSheet() {
1467
1521
  CSSOM.StyleSheet.call(this);
1522
+ this.__constructed = true;
1468
1523
  this.cssRules = new CSSOM.CSSRuleList();
1469
1524
  };
1470
1525
 
@@ -1677,6 +1732,84 @@ CSSOM.CSSStyleSheet.prototype.removeRule = function(index) {
1677
1732
  this.deleteRule(index);
1678
1733
  };
1679
1734
 
1735
+
1736
+ /**
1737
+ * Replaces the rules of a {@link CSSStyleSheet}
1738
+ *
1739
+ * @returns a promise
1740
+ * @see https://www.w3.org/TR/cssom-1/#dom-cssstylesheet-replace
1741
+ */
1742
+ CSSOM.CSSStyleSheet.prototype.replace = function(text) {
1743
+ var _Promise;
1744
+ if (this.__globalObject) {
1745
+ _Promise = this.__globalObject['Promise'];
1746
+ } else {
1747
+ _Promise = Promise;
1748
+ }
1749
+ var sheet = this;
1750
+ return new _Promise(function (resolve, reject) {
1751
+ // If the constructed flag is not set, or the disallow modification flag is set, throw a NotAllowedError DOMException.
1752
+ if (!sheet.__constructed || sheet.__disallowModification) {
1753
+ reject(errorUtils.createError(sheet, 'DOMException',
1754
+ "Failed to execute 'replaceSync' on '" + sheet.constructor.name + "': Not allowed.",
1755
+ 'NotAllowedError'));
1756
+ }
1757
+ // Set the disallow modification flag.
1758
+ sheet.__disallowModification = true;
1759
+
1760
+ // In parallel, do these steps:
1761
+ setTimeout(function() {
1762
+ // Let rules be the result of running parse a stylesheet's contents from text.
1763
+ var rules = new CSSOM.CSSRuleList();
1764
+ CSSOM.parse(text, { styleSheet: sheet, cssRules: rules });
1765
+ // If rules contains one or more @import rules, remove those rules from rules.
1766
+ var i = 0;
1767
+ while (i < rules.length) {
1768
+ if (rules[i].constructor.name === 'CSSImportRule') {
1769
+ rules.splice(i, 1);
1770
+ } else {
1771
+ i++;
1772
+ }
1773
+ }
1774
+ // Set sheet's CSS rules to rules.
1775
+ sheet.cssRules = rules;
1776
+ // Unset sheet’s disallow modification flag.
1777
+ delete sheet.__disallowModification;
1778
+ // Resolve promise with sheet.
1779
+ resolve(sheet);
1780
+ })
1781
+ });
1782
+ }
1783
+
1784
+ /**
1785
+ * Synchronously replaces the rules of a {@link CSSStyleSheet}
1786
+ *
1787
+ * @see https://www.w3.org/TR/cssom-1/#dom-cssstylesheet-replacesync
1788
+ */
1789
+ CSSOM.CSSStyleSheet.prototype.replaceSync = function(text) {
1790
+ var sheet = this;
1791
+ // If the constructed flag is not set, or the disallow modification flag is set, throw a NotAllowedError DOMException.
1792
+ if (!sheet.__constructed || sheet.__disallowModification) {
1793
+ errorUtils.throwError(sheet, 'DOMException',
1794
+ "Failed to execute 'replaceSync' on '" + sheet.constructor.name + "': Not allowed.",
1795
+ 'NotAllowedError');
1796
+ }
1797
+ // Let rules be the result of running parse a stylesheet's contents from text.
1798
+ var rules = new CSSOM.CSSRuleList();
1799
+ CSSOM.parse(text, { styleSheet: sheet, cssRules: rules });
1800
+ // If rules contains one or more @import rules, remove those rules from rules.
1801
+ var i = 0;
1802
+ while (i < rules.length) {
1803
+ if (rules[i].constructor.name === 'CSSImportRule') {
1804
+ rules.splice(i, 1);
1805
+ } else {
1806
+ i++;
1807
+ }
1808
+ }
1809
+ // Set sheet's CSS rules to rules.
1810
+ sheet.cssRules = rules;
1811
+ }
1812
+
1680
1813
  /**
1681
1814
  * NON-STANDARD
1682
1815
  * @return {string} serialize stylesheet
@@ -1743,13 +1876,20 @@ CSSOM.CSSKeyframesRule.prototype.type = 7;
1743
1876
  // http://www.opensource.apple.com/source/WebCore/WebCore-955.66.1/css/WebKitCSSKeyframesRule.cpp
1744
1877
  Object.defineProperty(CSSOM.CSSKeyframesRule.prototype, "cssText", {
1745
1878
  get: function() {
1746
- var cssTexts = [];
1747
- for (var i=0, length=this.cssRules.length; i < length; i++) {
1748
- cssTexts.push(this.cssRules[i].cssText);
1749
- }
1879
+ var values = "";
1880
+ var valuesArr = [" {"];
1881
+ if (this.cssRules.length) {
1882
+ valuesArr.push(this.cssRules.reduce(function(acc, rule){
1883
+ if (rule.cssText !== "") {
1884
+ acc.push(rule.cssText);
1885
+ }
1886
+ return acc;
1887
+ }, []).join("\n "));
1888
+ }
1889
+ values = valuesArr.join("\n ") + "\n}";
1750
1890
  var cssWideKeywords = ['initial', 'inherit', 'revert', 'revert-layer', 'unset', 'none'];
1751
1891
  var processedName = cssWideKeywords.includes(this.name) ? '"' + this.name + '"' : this.name;
1752
- return "@" + (this._vendorPrefix || '') + "keyframes " + processedName + (processedName && " ") + "{" + (cssTexts.length ? "\n " + cssTexts.join("\n ") : "") + "\n}";
1892
+ return "@" + (this._vendorPrefix || '') + "keyframes " + processedName + values;
1753
1893
  }
1754
1894
  });
1755
1895
 
@@ -2024,6 +2164,7 @@ CSSOM.MatcherList.prototype = {
2024
2164
  /**
2025
2165
  * @constructor
2026
2166
  * @see https://developer.mozilla.org/en/CSS/@-moz-document
2167
+ * @deprecated This rule is a non-standard Mozilla-specific extension and is not part of any official CSS specification.
2027
2168
  */
2028
2169
  CSSOM.CSSDocumentRule = function CSSDocumentRule() {
2029
2170
  CSSOM.CSSRule.call(this);
@@ -2450,11 +2591,18 @@ Object.defineProperties(CSSOM.CSSScopeRule.prototype, {
2450
2591
  },
2451
2592
  cssText: {
2452
2593
  get: function () {
2453
- var cssTexts = [];
2454
- for (var i = 0, length = this.cssRules.length; i < length; i++) {
2455
- cssTexts.push(this.cssRules[i].cssText);
2594
+ var values = "";
2595
+ var valuesArr = [" {"];
2596
+ if (this.cssRules.length) {
2597
+ valuesArr.push(this.cssRules.reduce(function(acc, rule){
2598
+ if (rule.cssText !== "") {
2599
+ acc.push(rule.cssText);
2600
+ }
2601
+ return acc;
2602
+ }, []).join("\n "));
2456
2603
  }
2457
- return "@scope " + (this.start ? "(" + this.start + ") " : "") + (this.end ? "to (" + this.end + ") " : "") + "{" + (cssTexts.length ? "\n " + cssTexts.join("\n ") : "") + "\n}";
2604
+ values = valuesArr.join("\n ") + "\n}";
2605
+ return "@scope" + (this.start ? " (" + this.start + ")" : "") + (this.end ? " to (" + this.end + ")" : "") + values;
2458
2606
  },
2459
2607
  configurable: true,
2460
2608
  enumerable: true,
@@ -2490,11 +2638,18 @@ CSSOM.CSSLayerBlockRule.prototype.type = 18;
2490
2638
  Object.defineProperties(CSSOM.CSSLayerBlockRule.prototype, {
2491
2639
  cssText: {
2492
2640
  get: function () {
2493
- var cssTexts = [];
2494
- for (var i = 0, length = this.cssRules.length; i < length; i++) {
2495
- cssTexts.push(this.cssRules[i].cssText);
2641
+ var values = "";
2642
+ var valuesArr = [" {"];
2643
+ if (this.cssRules.length) {
2644
+ valuesArr.push(this.cssRules.reduce(function(acc, rule){
2645
+ if (rule.cssText !== "") {
2646
+ acc.push(rule.cssText);
2647
+ }
2648
+ return acc;
2649
+ }, []).join("\n "));
2496
2650
  }
2497
- return "@layer " + this.name + (this.name && " ") + "{" + (cssTexts.length ? "\n " + cssTexts.join("\n ") : "") + "\n}";
2651
+ values = valuesArr.join("\n ") + "\n}";
2652
+ return "@layer" + (this.name ? " " + this.name : "") + values;
2498
2653
  },
2499
2654
  configurable: true,
2500
2655
  enumerable: true,
@@ -2623,12 +2778,17 @@ Object.defineProperty(CSSOM.CSSPageRule.prototype, "style", {
2623
2778
 
2624
2779
  Object.defineProperty(CSSOM.CSSPageRule.prototype, "cssText", {
2625
2780
  get: function() {
2626
- var values = ""
2781
+ var values = "";
2627
2782
  if (this.cssRules.length) {
2628
2783
  var valuesArr = [" {"];
2629
2784
  this.style.cssText && valuesArr.push(this.style.cssText);
2630
- valuesArr.push(this.cssRules.map(function(rule){ return rule.cssText }).join("\n "));
2631
- values = valuesArr.join("\n ") + "\n}"
2785
+ valuesArr.push(this.cssRules.reduce(function(acc, rule){
2786
+ if (rule.cssText !== "") {
2787
+ acc.push(rule.cssText);
2788
+ }
2789
+ return acc;
2790
+ }, []).join("\n "));
2791
+ values = valuesArr.join("\n ") + "\n}";
2632
2792
  } else {
2633
2793
  values = " {" + (this.style.cssText ? " " + this.style.cssText : "") + " }";
2634
2794
  }
@@ -2797,6 +2957,8 @@ CSSOM.CSSPageRule.parse = function(ruleText) {
2797
2957
  * @param {string} token - The CSS string to parse.
2798
2958
  * @param {object} [opts] - Optional parsing options.
2799
2959
  * @param {object} [opts.globalObject] - An optional global object to attach to the stylesheet. Useful on jsdom webplatform tests.
2960
+ * @param {CSSOM.CSSStyleSheet} [opts.styleSheet] - Reuse a style sheet instead of creating a new one (e.g. as `parentStyleSheet`)
2961
+ * @param {CSSOM.CSSRuleList} [opts.cssRules] - Prepare all rules in this list instead of mutating the style sheet continually
2800
2962
  * @param {function|boolean} [errorHandler] - Optional error handler function or `true` to use `console.error`.
2801
2963
  * @returns {CSSOM.CSSStyleSheet} The parsed CSSStyleSheet object.
2802
2964
  */
@@ -2843,14 +3005,26 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2843
3005
  "pageBlock": true
2844
3006
  };
2845
3007
 
2846
- var styleSheet = new CSSOM.CSSStyleSheet();
3008
+ var styleSheet;
3009
+ if (opts && opts.styleSheet) {
3010
+ styleSheet = opts.styleSheet;
3011
+ } else {
3012
+ styleSheet = new CSSOM.CSSStyleSheet()
3013
+ }
3014
+
3015
+ var topScope;
3016
+ if (opts && opts.cssRules) {
3017
+ topScope = { cssRules: opts.cssRules };
3018
+ } else {
3019
+ topScope = styleSheet;
3020
+ }
2847
3021
 
2848
3022
  if (opts && opts.globalObject) {
2849
3023
  styleSheet.__globalObject = opts.globalObject;
2850
3024
  }
2851
3025
 
2852
3026
  // @type CSSStyleSheet|CSSMediaRule|CSSContainerRule|CSSSupportsRule|CSSFontFaceRule|CSSKeyframesRule|CSSDocumentRule
2853
- var currentScope = styleSheet;
3027
+ var currentScope = topScope;
2854
3028
 
2855
3029
  // @type CSSMediaRule|CSSContainerRule|CSSSupportsRule|CSSKeyframesRule|CSSDocumentRule
2856
3030
  var parentRule;
@@ -3179,7 +3353,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3179
3353
  var ruleRegExp = new RegExp(atRuleKey + sourceRuleRegExp.source, sourceRuleRegExp.flags);
3180
3354
  var ruleSlice = token.slice(i);
3181
3355
  // Not all rules can be nested, if the rule cannot be nested and is in the root scope, do not perform the check
3182
- var shouldPerformCheck = cannotBeNested && currentScope !== styleSheet ? false : true;
3356
+ var shouldPerformCheck = cannotBeNested && currentScope !== topScope ? false : true;
3183
3357
  // First, check if there is no invalid characters just after the at-rule
3184
3358
  if (shouldPerformCheck && ruleSlice.search(ruleRegExp) === 0) {
3185
3359
  // Find the closest allowed character before the at-rule (a opening or closing brace, a semicolon or a comment ending)
@@ -3311,7 +3485,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3311
3485
  var ruleStatementMatch = atRulesStatemenRegExpES5Alternative(ruleSlice);
3312
3486
 
3313
3487
  // If it's a statement inside a nested rule, ignore only the statement
3314
- if (ruleStatementMatch && currentScope !== styleSheet) {
3488
+ if (ruleStatementMatch && currentScope !== topScope) {
3315
3489
  var ignoreEnd = ruleStatementMatch[0].indexOf(";");
3316
3490
  i += ruleStatementMatch.index + ignoreEnd;
3317
3491
  return;
@@ -3336,6 +3510,255 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3336
3510
  }
3337
3511
  }
3338
3512
 
3513
+ // Helper functions for looseSelectorValidator
3514
+ // Defined outside to avoid recreation on every validation call
3515
+
3516
+ /**
3517
+ * Check if character is a valid identifier start
3518
+ * @param {string} c - Character to check
3519
+ * @returns {boolean}
3520
+ */
3521
+ function isIdentStart(c) {
3522
+ return /[a-zA-Z_\u00A0-\uFFFF]/.test(c);
3523
+ }
3524
+
3525
+ /**
3526
+ * Check if character is a valid identifier character
3527
+ * @param {string} c - Character to check
3528
+ * @returns {boolean}
3529
+ */
3530
+ function isIdentChar(c) {
3531
+ return /[a-zA-Z0-9_\u00A0-\uFFFF\-]/.test(c);
3532
+ }
3533
+
3534
+ /**
3535
+ * Helper function to validate CSS selector syntax without regex backtracking.
3536
+ * Iteratively parses the selector string to identify valid components.
3537
+ *
3538
+ * Supports:
3539
+ * - Escaped special characters (e.g., .class\!, #id\@name)
3540
+ * - Namespace selectors (ns|element, *|element, |element)
3541
+ * - All standard CSS selectors (class, ID, type, attribute, pseudo, etc.)
3542
+ * - Combinators (>, +, ~, whitespace)
3543
+ * - Nesting selector (&)
3544
+ *
3545
+ * This approach eliminates exponential backtracking by using explicit character-by-character
3546
+ * parsing instead of nested quantifiers in regex.
3547
+ *
3548
+ * @param {string} selector - The selector to validate
3549
+ * @returns {boolean} - True if valid selector syntax
3550
+ */
3551
+ function looseSelectorValidator(selector) {
3552
+ if (!selector || selector.length === 0) {
3553
+ return false;
3554
+ }
3555
+
3556
+ var i = 0;
3557
+ var len = selector.length;
3558
+ var hasMatchedComponent = false;
3559
+
3560
+ // Helper: Skip escaped character (backslash + any char)
3561
+ function skipEscape() {
3562
+ if (i < len && selector[i] === '\\') {
3563
+ i += 2; // Skip backslash and next character
3564
+ return true;
3565
+ }
3566
+ return false;
3567
+ }
3568
+
3569
+ // Helper: Parse identifier (with possible escapes)
3570
+ function parseIdentifier() {
3571
+ var start = i;
3572
+ while (i < len) {
3573
+ if (skipEscape()) {
3574
+ continue;
3575
+ } else if (isIdentChar(selector[i])) {
3576
+ i++;
3577
+ } else {
3578
+ break;
3579
+ }
3580
+ }
3581
+ return i > start;
3582
+ }
3583
+
3584
+ // Helper: Parse namespace prefix (optional)
3585
+ function parseNamespace() {
3586
+ var start = i;
3587
+
3588
+ // Match: *| or identifier| or |
3589
+ if (i < len && selector[i] === '*') {
3590
+ i++;
3591
+ } else if (i < len && (isIdentStart(selector[i]) || selector[i] === '\\')) {
3592
+ parseIdentifier();
3593
+ }
3594
+
3595
+ if (i < len && selector[i] === '|') {
3596
+ i++;
3597
+ return true;
3598
+ }
3599
+
3600
+ // Rollback if no pipe found
3601
+ i = start;
3602
+ return false;
3603
+ }
3604
+
3605
+ // Helper: Parse pseudo-class/element arguments (with balanced parens)
3606
+ function parsePseudoArgs() {
3607
+ if (i >= len || selector[i] !== '(') {
3608
+ return false;
3609
+ }
3610
+
3611
+ i++; // Skip opening paren
3612
+ var depth = 1;
3613
+ var inString = false;
3614
+ var stringChar = '';
3615
+
3616
+ while (i < len && depth > 0) {
3617
+ var c = selector[i];
3618
+
3619
+ if (c === '\\' && i + 1 < len) {
3620
+ i += 2; // Skip escaped character
3621
+ } else if (!inString && (c === '"' || c === '\'')) {
3622
+ inString = true;
3623
+ stringChar = c;
3624
+ i++;
3625
+ } else if (inString && c === stringChar) {
3626
+ inString = false;
3627
+ i++;
3628
+ } else if (!inString && c === '(') {
3629
+ depth++;
3630
+ i++;
3631
+ } else if (!inString && c === ')') {
3632
+ depth--;
3633
+ i++;
3634
+ } else {
3635
+ i++;
3636
+ }
3637
+ }
3638
+
3639
+ return depth === 0;
3640
+ }
3641
+
3642
+ // Main parsing loop
3643
+ while (i < len) {
3644
+ var matched = false;
3645
+ var start = i;
3646
+
3647
+ // Skip whitespace
3648
+ while (i < len && /\s/.test(selector[i])) {
3649
+ i++;
3650
+ }
3651
+ if (i > start) {
3652
+ hasMatchedComponent = true;
3653
+ continue;
3654
+ }
3655
+
3656
+ // Match combinators: >, +, ~
3657
+ if (i < len && /[>+~]/.test(selector[i])) {
3658
+ i++;
3659
+ hasMatchedComponent = true;
3660
+ // Skip trailing whitespace
3661
+ while (i < len && /\s/.test(selector[i])) {
3662
+ i++;
3663
+ }
3664
+ continue;
3665
+ }
3666
+
3667
+ // Match nesting selector: &
3668
+ if (i < len && selector[i] === '&') {
3669
+ i++;
3670
+ hasMatchedComponent = true;
3671
+ matched = true;
3672
+ }
3673
+ // Match class selector: .identifier
3674
+ else if (i < len && selector[i] === '.') {
3675
+ i++;
3676
+ if (parseIdentifier()) {
3677
+ hasMatchedComponent = true;
3678
+ matched = true;
3679
+ }
3680
+ }
3681
+ // Match ID selector: #identifier
3682
+ else if (i < len && selector[i] === '#') {
3683
+ i++;
3684
+ if (parseIdentifier()) {
3685
+ hasMatchedComponent = true;
3686
+ matched = true;
3687
+ }
3688
+ }
3689
+ // Match pseudo-class/element: :identifier or ::identifier
3690
+ else if (i < len && selector[i] === ':') {
3691
+ i++;
3692
+ if (i < len && selector[i] === ':') {
3693
+ i++; // Pseudo-element
3694
+ }
3695
+ if (parseIdentifier()) {
3696
+ parsePseudoArgs(); // Optional arguments
3697
+ hasMatchedComponent = true;
3698
+ matched = true;
3699
+ }
3700
+ }
3701
+ // Match attribute selector: [...]
3702
+ else if (i < len && selector[i] === '[') {
3703
+ i++;
3704
+ var depth = 1;
3705
+ while (i < len && depth > 0) {
3706
+ if (selector[i] === '\\') {
3707
+ i += 2;
3708
+ } else if (selector[i] === '\'') {
3709
+ i++;
3710
+ while (i < len && selector[i] !== '\'') {
3711
+ if (selector[i] === '\\') i += 2;
3712
+ else i++;
3713
+ }
3714
+ if (i < len) i++; // Skip closing quote
3715
+ } else if (selector[i] === '"') {
3716
+ i++;
3717
+ while (i < len && selector[i] !== '"') {
3718
+ if (selector[i] === '\\') i += 2;
3719
+ else i++;
3720
+ }
3721
+ if (i < len) i++; // Skip closing quote
3722
+ } else if (selector[i] === '[') {
3723
+ depth++;
3724
+ i++;
3725
+ } else if (selector[i] === ']') {
3726
+ depth--;
3727
+ i++;
3728
+ } else {
3729
+ i++;
3730
+ }
3731
+ }
3732
+ if (depth === 0) {
3733
+ hasMatchedComponent = true;
3734
+ matched = true;
3735
+ }
3736
+ }
3737
+ // Match type selector with optional namespace: [namespace|]identifier
3738
+ else if (i < len && (isIdentStart(selector[i]) || selector[i] === '\\' || selector[i] === '*' || selector[i] === '|')) {
3739
+ parseNamespace(); // Optional namespace prefix
3740
+
3741
+ if (i < len && selector[i] === '*') {
3742
+ i++; // Universal selector
3743
+ hasMatchedComponent = true;
3744
+ matched = true;
3745
+ } else if (i < len && (isIdentStart(selector[i]) || selector[i] === '\\')) {
3746
+ if (parseIdentifier()) {
3747
+ hasMatchedComponent = true;
3748
+ matched = true;
3749
+ }
3750
+ }
3751
+ }
3752
+
3753
+ // If no match found, invalid selector
3754
+ if (!matched && i === start) {
3755
+ return false;
3756
+ }
3757
+ }
3758
+
3759
+ return hasMatchedComponent;
3760
+ }
3761
+
3339
3762
  /**
3340
3763
  * Validates a basic CSS selector, allowing for deeply nested balanced parentheses in pseudo-classes.
3341
3764
  * This function replaces the previous basicSelectorRegExp.
@@ -3360,6 +3783,12 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3360
3783
  * @returns {boolean}
3361
3784
  */
3362
3785
  function basicSelectorValidator(selector) {
3786
+ // Guard against extremely long selectors to prevent potential regex performance issues
3787
+ // Reasonable selectors are typically under 1000 characters
3788
+ if (selector.length > 10000) {
3789
+ return false;
3790
+ }
3791
+
3363
3792
  // Validate balanced syntax with attribute tracking and stack-based parentheses matching
3364
3793
  if (!validateBalancedSyntax(selector, true, true)) {
3365
3794
  return false;
@@ -3382,31 +3811,69 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3382
3811
 
3383
3812
  // Check for invalid pseudo-class usage with quoted strings
3384
3813
  // Pseudo-classes like :lang(), :dir(), :nth-*() should not accept quoted strings
3385
- var pseudoPattern = /::?([a-zA-Z][\w-]*)\(([^)]+)\)/g;
3386
- var pseudoMatch;
3387
- while ((pseudoMatch = pseudoPattern.exec(selector)) !== null) {
3388
- var pseudoName = pseudoMatch[1];
3389
- var pseudoContent = pseudoMatch[2];
3390
-
3391
- // List of pseudo-classes that should not accept quoted strings
3392
- // :lang() - accepts language codes: en, fr-CA
3393
- // :dir() - accepts direction: ltr, rtl
3394
- // :nth-*() - accepts An+B notation: 2n+1, odd, even
3395
- var noQuotesPseudos = ['lang', 'dir', 'nth-child', 'nth-last-child', 'nth-of-type', 'nth-last-of-type'];
3396
-
3397
- for (var i = 0; i < noQuotesPseudos.length; i++) {
3398
- if (pseudoName === noQuotesPseudos[i] && /['"]/.test(pseudoContent)) {
3399
- return false;
3814
+ // Using iterative parsing instead of regex to avoid exponential backtracking
3815
+ var noQuotesPseudos = ['lang', 'dir', 'nth-child', 'nth-last-child', 'nth-of-type', 'nth-last-of-type'];
3816
+
3817
+ for (var idx = 0; idx < selector.length; idx++) {
3818
+ // Look for pseudo-class/element start
3819
+ if (selector[idx] === ':') {
3820
+ var pseudoStart = idx;
3821
+ idx++;
3822
+
3823
+ // Skip second colon for pseudo-elements
3824
+ if (idx < selector.length && selector[idx] === ':') {
3825
+ idx++;
3826
+ }
3827
+
3828
+ // Extract pseudo name
3829
+ var nameStart = idx;
3830
+ while (idx < selector.length && /[a-zA-Z0-9\-]/.test(selector[idx])) {
3831
+ idx++;
3832
+ }
3833
+
3834
+ if (idx === nameStart) {
3835
+ continue; // No name found
3836
+ }
3837
+
3838
+ var pseudoName = selector.substring(nameStart, idx).toLowerCase();
3839
+
3840
+ // Check if this pseudo has arguments
3841
+ if (idx < selector.length && selector[idx] === '(') {
3842
+ idx++;
3843
+ var contentStart = idx;
3844
+ var depth = 1;
3845
+
3846
+ // Find matching closing paren (handle nesting)
3847
+ while (idx < selector.length && depth > 0) {
3848
+ if (selector[idx] === '\\') {
3849
+ idx += 2; // Skip escaped character
3850
+ } else if (selector[idx] === '(') {
3851
+ depth++;
3852
+ idx++;
3853
+ } else if (selector[idx] === ')') {
3854
+ depth--;
3855
+ idx++;
3856
+ } else {
3857
+ idx++;
3858
+ }
3859
+ }
3860
+
3861
+ if (depth === 0) {
3862
+ var pseudoContent = selector.substring(contentStart, idx - 1);
3863
+
3864
+ // Check if this pseudo should not have quoted strings
3865
+ for (var j = 0; j < noQuotesPseudos.length; j++) {
3866
+ if (pseudoName === noQuotesPseudos[j] && /['"]/.test(pseudoContent)) {
3867
+ return false;
3868
+ }
3869
+ }
3870
+ }
3400
3871
  }
3401
3872
  }
3402
3873
  }
3403
3874
 
3404
- // Fallback to a loose regexp for the overall selector structure (without deep paren matching)
3405
- // This is similar to the original, but without nested paren limitations
3406
- // Modified to support namespace selectors: *|element, prefix|element, |element
3407
- // Fixed attribute selector regex to properly handle |=, ~=, ^=, $=, *= operators
3408
- var looseSelectorRegExp = /^((?:(?:\*|[a-zA-Z_\u00A0-\uFFFF\\][a-zA-Z0-9_\u00A0-\uFFFF\-\\]*|)\|)?[a-zA-Z_\u00A0-\uFFFF\\][a-zA-Z0-9_\u00A0-\uFFFF\-\\]*|(?:(?:\*|[a-zA-Z_\u00A0-\uFFFF\\][a-zA-Z0-9_\u00A0-\uFFFF\-\\]*|)\|)?\*|#[a-zA-Z0-9_\u00A0-\uFFFF\-\\]+|\.[a-zA-Z0-9_\u00A0-\uFFFF\-\\]+|\[(?:[^\[\]'"]|'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*")*(?:\s+[iI])?\]|::?[a-zA-Z0-9_\u00A0-\uFFFF\-\\]+(?:\((.*)\))?|&|\s*[>+~]\s*|\s+)+$/;
3409
- return looseSelectorRegExp.test(selector);
3875
+ // Use the iterative validator to avoid regex backtracking issues
3876
+ return looseSelectorValidator(selector);
3410
3877
  }
3411
3878
 
3412
3879
  /**
@@ -3424,9 +3891,96 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3424
3891
  * - :nth-child(2n+1)
3425
3892
  * - :has(.sel:nth-child(3n))
3426
3893
  * - :not(".foo, .bar")
3427
- * @type {RegExp}
3894
+ *
3895
+ * REPLACED WITH FUNCTION to avoid exponential backtracking.
3428
3896
  */
3429
- var globalPseudoClassRegExp = /:([a-zA-Z-]+)\(((?:[^()"]+|"[^"]*"|'[^']*'|\((?:[^()"]+|"[^"]*"|'[^']*')*\))*?)\)/g;
3897
+
3898
+ /**
3899
+ * Extract pseudo-classes with arguments from a selector using iterative parsing.
3900
+ * Replaces the previous globalPseudoClassRegExp to avoid exponential backtracking.
3901
+ *
3902
+ * Handles:
3903
+ * - Regular content without parentheses or quotes
3904
+ * - Single-quoted strings
3905
+ * - Double-quoted strings
3906
+ * - Nested parentheses (arbitrary depth)
3907
+ *
3908
+ * @param {string} selector - The CSS selector to parse
3909
+ * @returns {Array} Array of matches, each with: [fullMatch, pseudoName, pseudoArgs, startIndex]
3910
+ */
3911
+ function extractPseudoClasses(selector) {
3912
+ var matches = [];
3913
+
3914
+ for (var i = 0; i < selector.length; i++) {
3915
+ // Look for pseudo-class start (single or double colon)
3916
+ if (selector[i] === ':') {
3917
+ var pseudoStart = i;
3918
+ i++;
3919
+
3920
+ // Skip second colon for pseudo-elements (::)
3921
+ if (i < selector.length && selector[i] === ':') {
3922
+ i++;
3923
+ }
3924
+
3925
+ // Extract pseudo name
3926
+ var nameStart = i;
3927
+ while (i < selector.length && /[a-zA-Z\-]/.test(selector[i])) {
3928
+ i++;
3929
+ }
3930
+
3931
+ if (i === nameStart) {
3932
+ continue; // No name found
3933
+ }
3934
+
3935
+ var pseudoName = selector.substring(nameStart, i);
3936
+
3937
+ // Check if this pseudo has arguments
3938
+ if (i < selector.length && selector[i] === '(') {
3939
+ i++;
3940
+ var argsStart = i;
3941
+ var depth = 1;
3942
+ var inSingleQuote = false;
3943
+ var inDoubleQuote = false;
3944
+
3945
+ // Find matching closing paren (handle nesting and strings)
3946
+ while (i < selector.length && depth > 0) {
3947
+ var ch = selector[i];
3948
+
3949
+ if (ch === '\\') {
3950
+ i += 2; // Skip escaped character
3951
+ } else if (ch === "'" && !inDoubleQuote) {
3952
+ inSingleQuote = !inSingleQuote;
3953
+ i++;
3954
+ } else if (ch === '"' && !inSingleQuote) {
3955
+ inDoubleQuote = !inDoubleQuote;
3956
+ i++;
3957
+ } else if (ch === '(' && !inSingleQuote && !inDoubleQuote) {
3958
+ depth++;
3959
+ i++;
3960
+ } else if (ch === ')' && !inSingleQuote && !inDoubleQuote) {
3961
+ depth--;
3962
+ i++;
3963
+ } else {
3964
+ i++;
3965
+ }
3966
+ }
3967
+
3968
+ if (depth === 0) {
3969
+ var pseudoArgs = selector.substring(argsStart, i - 1);
3970
+ var fullMatch = selector.substring(pseudoStart, i);
3971
+
3972
+ // Store match in same format as regex: [fullMatch, pseudoName, pseudoArgs, startIndex]
3973
+ matches.push([fullMatch, pseudoName, pseudoArgs, pseudoStart]);
3974
+ }
3975
+
3976
+ // Move back one since loop will increment
3977
+ i--;
3978
+ }
3979
+ }
3980
+ }
3981
+
3982
+ return matches;
3983
+ }
3430
3984
 
3431
3985
  /**
3432
3986
  * Parses a CSS selector string and splits it into parts, handling nested parentheses.
@@ -3521,13 +4075,8 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3521
4075
  return validatedSelectorsCache[selector];
3522
4076
  }
3523
4077
 
3524
- // Use a non-global regex to find all pseudo-classes with arguments
3525
- var pseudoClassMatches = [];
3526
- var pseudoClassRegExp = new RegExp(globalPseudoClassRegExp.source, globalPseudoClassRegExp.flags);
3527
- var match;
3528
- while ((match = pseudoClassRegExp.exec(selector)) !== null) {
3529
- pseudoClassMatches.push(match);
3530
- }
4078
+ // Use function-based parsing to extract pseudo-classes (avoids backtracking)
4079
+ var pseudoClassMatches = extractPseudoClasses(selector);
3531
4080
 
3532
4081
  for (var j = 0; j < pseudoClassMatches.length; j++) {
3533
4082
  var pseudoClass = pseudoClassMatches[j][1];
@@ -3668,6 +4217,12 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3668
4217
  return true;
3669
4218
  }
3670
4219
 
4220
+ function pushToAncestorRules(rule) {
4221
+ if (ancestorRules.indexOf(rule) === -1) {
4222
+ ancestorRules.push(rule);
4223
+ }
4224
+ }
4225
+
3671
4226
  function parseError(message, isNested) {
3672
4227
  var lines = token.substring(0, i).split('\n');
3673
4228
  var lineCount = lines.length;
@@ -3694,10 +4249,47 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3694
4249
  case "importRule":
3695
4250
  case "namespaceRule":
3696
4251
  case "layerBlock":
3697
- token += ";"
4252
+ if (character !== ";") {
4253
+ token += ";";
4254
+ break;
4255
+ }
4256
+ case "value":
4257
+ if (character !== "}") {
4258
+ if (character === ";") {
4259
+ token += "}"
4260
+ } else {
4261
+ token += ";";
4262
+ }
4263
+ endingIndex += 1;
4264
+ break;
4265
+ }
4266
+ case "name":
4267
+ case "before-name":
4268
+ if (character === "}") {
4269
+ token += " "
4270
+ } else {
4271
+ token += "}"
4272
+ }
4273
+ endingIndex += 1
4274
+ break;
4275
+ case "before-selector":
4276
+ if (character !== "}" && currentScope !== styleSheet) {
4277
+ token += "}"
4278
+ endingIndex += 1
4279
+ break;
4280
+ }
3698
4281
  }
3699
4282
  }
3700
4283
 
4284
+ // Handle escape sequences before processing special characters
4285
+ // If we encounter a backslash, add both the backslash and the next character to buffer
4286
+ // and skip the next iteration to prevent the escaped character from being interpreted
4287
+ if (character === '\\' && i + 1 < token.length) {
4288
+ buffer += character + token.charAt(i + 1);
4289
+ i++; // Skip the next character
4290
+ continue;
4291
+ }
4292
+
3701
4293
  switch (character) {
3702
4294
 
3703
4295
  case " ":
@@ -3795,6 +4387,15 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3795
4387
 
3796
4388
  // At-rule
3797
4389
  case "@":
4390
+ if (nestedSelectorRule) {
4391
+ if (styleRule && styleRule.constructor.name === "CSSNestedDeclarations") {
4392
+ currentScope.cssRules.push(styleRule);
4393
+ }
4394
+ if (nestedSelectorRule.parentRule.constructor.name === "CSSStyleRule") {
4395
+ styleRule = nestedSelectorRule.parentRule;
4396
+ }
4397
+ nestedSelectorRule = null;
4398
+ }
3798
4399
  if (token.indexOf("@-moz-document", i) === i) {
3799
4400
  validateAtRule("@-moz-document", function(){
3800
4401
  state = "documentRule-begin";
@@ -3929,7 +4530,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3929
4530
  break;
3930
4531
 
3931
4532
  case "{":
3932
- if (currentScope === styleSheet) {
4533
+ if (currentScope === topScope) {
3933
4534
  nestedSelectorRule = null;
3934
4535
  }
3935
4536
  if (state === 'before-selector') {
@@ -3951,7 +4552,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3951
4552
 
3952
4553
  if (parentRule) {
3953
4554
  styleRule.__parentRule = parentRule;
3954
- ancestorRules.push(parentRule);
4555
+ pushToAncestorRules(parentRule);
3955
4556
  }
3956
4557
 
3957
4558
  currentScope = parentRule = styleRule;
@@ -3965,7 +4566,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3965
4566
 
3966
4567
  if (parentRule) {
3967
4568
  mediaRule.__parentRule = parentRule;
3968
- ancestorRules.push(parentRule);
4569
+ pushToAncestorRules(parentRule);
3969
4570
  }
3970
4571
 
3971
4572
  currentScope = parentRule = mediaRule;
@@ -3977,7 +4578,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3977
4578
 
3978
4579
  if (parentRule) {
3979
4580
  containerRule.__parentRule = parentRule;
3980
- ancestorRules.push(parentRule);
4581
+ pushToAncestorRules(parentRule);
3981
4582
  }
3982
4583
  currentScope = parentRule = containerRule;
3983
4584
  containerRule.__parentStyleSheet = styleSheet;
@@ -3994,7 +4595,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3994
4595
 
3995
4596
  if (parentRule) {
3996
4597
  supportsRule.__parentRule = parentRule;
3997
- ancestorRules.push(parentRule);
4598
+ pushToAncestorRules(parentRule);
3998
4599
  }
3999
4600
 
4000
4601
  currentScope = parentRule = supportsRule;
@@ -4016,7 +4617,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4016
4617
 
4017
4618
  if (parentRule) {
4018
4619
  scopeRule.__parentRule = parentRule;
4019
- ancestorRules.push(parentRule);
4620
+ pushToAncestorRules(parentRule);
4020
4621
  }
4021
4622
  currentScope = parentRule = scopeRule;
4022
4623
  scopeRule.__parentStyleSheet = styleSheet;
@@ -4030,7 +4631,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4030
4631
  if (isValidName) {
4031
4632
  if (parentRule) {
4032
4633
  layerBlockRule.__parentRule = parentRule;
4033
- ancestorRules.push(parentRule);
4634
+ pushToAncestorRules(parentRule);
4034
4635
  }
4035
4636
 
4036
4637
  currentScope = parentRule = layerBlockRule;
@@ -4043,7 +4644,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4043
4644
 
4044
4645
  if (parentRule) {
4045
4646
  pageRule.__parentRule = parentRule;
4046
- ancestorRules.push(parentRule);
4647
+ pushToAncestorRules(parentRule);
4047
4648
  }
4048
4649
 
4049
4650
  currentScope = parentRule = pageRule;
@@ -4053,7 +4654,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4053
4654
  state = "before-name";
4054
4655
  } else if (state === "hostRule-begin") {
4055
4656
  if (parentRule) {
4056
- ancestorRules.push(parentRule);
4657
+ pushToAncestorRules(parentRule);
4057
4658
  }
4058
4659
 
4059
4660
  currentScope = parentRule = hostRule;
@@ -4063,7 +4664,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4063
4664
  } else if (state === "startingStyleRule-begin") {
4064
4665
  if (parentRule) {
4065
4666
  startingStyleRule.__parentRule = parentRule;
4066
- ancestorRules.push(parentRule);
4667
+ pushToAncestorRules(parentRule);
4067
4668
  }
4068
4669
 
4069
4670
  currentScope = parentRule = startingStyleRule;
@@ -4082,7 +4683,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4082
4683
  } else if (state === "keyframesRule-begin") {
4083
4684
  keyframesRule.name = buffer.trim();
4084
4685
  if (parentRule) {
4085
- ancestorRules.push(parentRule);
4686
+ pushToAncestorRules(parentRule);
4086
4687
  keyframesRule.__parentRule = parentRule;
4087
4688
  }
4088
4689
  keyframesRule.__parentStyleSheet = styleSheet;
@@ -4099,7 +4700,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4099
4700
  // FIXME: what if this '{' is in the url text of the match function?
4100
4701
  documentRule.matcher.matcherText = buffer.trim();
4101
4702
  if (parentRule) {
4102
- ancestorRules.push(parentRule);
4703
+ pushToAncestorRules(parentRule);
4103
4704
  documentRule.__parentRule = parentRule;
4104
4705
  }
4105
4706
  currentScope = parentRule = documentRule;
@@ -4112,21 +4713,21 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4112
4713
  parentRule.cssRules.push(styleRule);
4113
4714
  styleRule.__parentRule = parentRule;
4114
4715
  styleRule.__parentStyleSheet = styleSheet;
4115
- ancestorRules.push(parentRule);
4716
+ pushToAncestorRules(parentRule);
4116
4717
  } else {
4117
4718
  // If the styleRule is empty, we can assume that it's a nested selector
4118
- ancestorRules.push(parentRule);
4719
+ pushToAncestorRules(parentRule);
4119
4720
  }
4120
4721
  } else {
4121
4722
  currentScope = parentRule = styleRule;
4122
- ancestorRules.push(parentRule);
4723
+ pushToAncestorRules(parentRule);
4123
4724
  styleRule.__parentStyleSheet = styleSheet;
4124
4725
  }
4125
4726
 
4126
4727
  styleRule = new CSSOM.CSSStyleRule();
4127
4728
  var processedSelectorText = processSelectorText(buffer.trim());
4128
4729
  // In a nested selector, ensure each selector contains '&' at the beginning, except for selectors that already have '&' somewhere
4129
- if (parentRule.constructor.name !== "CSSStyleRule" && parentRule.parentRule === null) {
4730
+ if (parentRule.constructor.name === "CSSScopeRule" || (parentRule.constructor.name !== "CSSStyleRule" && parentRule.parentRule === null)) {
4130
4731
  styleRule.selectorText = processedSelectorText;
4131
4732
  } else {
4132
4733
  styleRule.selectorText = parseAndSplitNestedSelectors(processedSelectorText).map(function(sel) {
@@ -4225,20 +4826,20 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4225
4826
  state = "before-selector";
4226
4827
  break;
4227
4828
  case "importRule":
4228
- var isValid = styleSheet.cssRules.length === 0 || styleSheet.cssRules.some(function (rule) {
4829
+ var isValid = topScope.cssRules.length === 0 || topScope.cssRules.some(function (rule) {
4229
4830
  return ['CSSImportRule', 'CSSLayerStatementRule'].indexOf(rule.constructor.name) !== -1
4230
4831
  });
4231
4832
  if (isValid) {
4232
4833
  importRule = new CSSOM.CSSImportRule();
4233
4834
  importRule.__parentStyleSheet = importRule.styleSheet.__parentStyleSheet = styleSheet;
4234
4835
  importRule.cssText = buffer + character;
4235
- styleSheet.cssRules.push(importRule);
4836
+ topScope.cssRules.push(importRule);
4236
4837
  }
4237
4838
  buffer = "";
4238
4839
  state = "before-selector";
4239
4840
  break;
4240
4841
  case "namespaceRule":
4241
- var isValid = styleSheet.cssRules.length === 0 || styleSheet.cssRules.every(function (rule) {
4842
+ var isValid = topScope.cssRules.length === 0 || topScope.cssRules.every(function (rule) {
4242
4843
  return ['CSSImportRule','CSSLayerStatementRule','CSSNamespaceRule'].indexOf(rule.constructor.name) !== -1
4243
4844
  });
4244
4845
  if (isValid) {
@@ -4249,7 +4850,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4249
4850
 
4250
4851
  namespaceRule = testNamespaceRule;
4251
4852
  namespaceRule.__parentStyleSheet = styleSheet;
4252
- styleSheet.cssRules.push(namespaceRule);
4853
+ topScope.cssRules.push(namespaceRule);
4253
4854
 
4254
4855
  // Track the namespace prefix for validation
4255
4856
  if (namespaceRule.prefix) {
@@ -4276,7 +4877,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4276
4877
  layerStatementRule.__starts = layerBlockRule.__starts;
4277
4878
  layerStatementRule.__ends = i;
4278
4879
  layerStatementRule.nameList = nameListStr;
4279
- styleSheet.cssRules.push(layerStatementRule);
4880
+ topScope.cssRules.push(layerStatementRule);
4280
4881
  }
4281
4882
  buffer = "";
4282
4883
  state = "before-selector";
@@ -4317,7 +4918,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4317
4918
  styleRule.__parentStyleSheet = styleSheet;
4318
4919
 
4319
4920
  if (currentScope === styleRule) {
4320
- currentScope = parentRule || styleSheet;
4921
+ currentScope = parentRule || topScope;
4321
4922
  }
4322
4923
 
4323
4924
  if (styleRule.constructor.name === "CSSStyleRule" && !isValidSelectorText(styleRule.selectorText)) {
@@ -4326,7 +4927,11 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4326
4927
  }
4327
4928
  parseError('Invalid CSSStyleRule (selectorText = "' + styleRule.selectorText + '")', styleRule.parentRule !== null);
4328
4929
  } else {
4329
- currentScope.cssRules.push(styleRule);
4930
+ if (styleRule.parentRule) {
4931
+ styleRule.parentRule.cssRules.push(styleRule);
4932
+ } else {
4933
+ currentScope.cssRules.push(styleRule);
4934
+ }
4330
4935
  }
4331
4936
  buffer = "";
4332
4937
  if (currentScope.constructor === CSSOM.CSSKeyframesRule) {
@@ -4336,7 +4941,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4336
4941
  }
4337
4942
 
4338
4943
  if (styleRule.constructor.name === "CSSNestedDeclarations") {
4339
- if (currentScope !== styleSheet) {
4944
+ if (currentScope !== topScope) {
4340
4945
  nestedSelectorRule = currentScope;
4341
4946
  }
4342
4947
  styleRule = null;
@@ -4359,7 +4964,6 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4359
4964
  break;
4360
4965
  }
4361
4966
 
4362
-
4363
4967
  while (ancestorRules.length > 0) {
4364
4968
  parentRule = ancestorRules.pop();
4365
4969
 
@@ -4385,8 +4989,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4385
4989
  }
4386
4990
  } else {
4387
4991
  prevScope = currentScope;
4388
- currentScope = parentRule;
4389
- currentScope !== prevScope && currentScope.cssRules.push(prevScope);
4992
+ parentRule !== prevScope && parentRule.cssRules.push(prevScope);
4390
4993
  break;
4391
4994
  }
4392
4995
  }
@@ -4394,12 +4997,12 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4394
4997
 
4395
4998
  if (currentScope.parentRule == null) {
4396
4999
  currentScope.__ends = i + 1;
4397
- if (currentScope !== styleSheet && styleSheet.cssRules.findIndex(function (rule) {
5000
+ if (currentScope !== topScope && topScope.cssRules.findIndex(function (rule) {
4398
5001
  return rule === currentScope
4399
5002
  }) === -1) {
4400
- styleSheet.cssRules.push(currentScope);
5003
+ topScope.cssRules.push(currentScope);
4401
5004
  }
4402
- currentScope = styleSheet;
5005
+ currentScope = topScope;
4403
5006
  if (nestedSelectorRule === parentRule) {
4404
5007
  // Check if this selector is really starting inside another selector
4405
5008
  var nestedSelectorTokenToCurrentSelectorToken = token.slice(nestedSelectorRule.__starts, i + 1);
@@ -4418,6 +5021,8 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4418
5021
  parentRule = null;
4419
5022
 
4420
5023
  }
5024
+ } else {
5025
+ currentScope = parentRule;
4421
5026
  }
4422
5027
 
4423
5028
  buffer = "";
@@ -4430,7 +5035,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4430
5035
  switch (state) {
4431
5036
  case "before-selector":
4432
5037
  state = "selector";
4433
- if (styleRule && parentRule) {
5038
+ if ((styleRule || scopeRule) && parentRule) {
4434
5039
  // Assuming it's a declaration inside Nested Selector OR a Nested Declaration
4435
5040
  // If Declaration inside Nested Selector let's keep the same styleRule
4436
5041
  if (