@acemir/cssom 0.9.23 → 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;
@@ -3695,11 +4250,46 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3695
4250
  case "namespaceRule":
3696
4251
  case "layerBlock":
3697
4252
  if (character !== ";") {
3698
- token += ";"
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;
3699
4280
  }
3700
4281
  }
3701
4282
  }
3702
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
+
3703
4293
  switch (character) {
3704
4294
 
3705
4295
  case " ":
@@ -3797,6 +4387,15 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3797
4387
 
3798
4388
  // At-rule
3799
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
+ }
3800
4399
  if (token.indexOf("@-moz-document", i) === i) {
3801
4400
  validateAtRule("@-moz-document", function(){
3802
4401
  state = "documentRule-begin";
@@ -3931,7 +4530,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3931
4530
  break;
3932
4531
 
3933
4532
  case "{":
3934
- if (currentScope === styleSheet) {
4533
+ if (currentScope === topScope) {
3935
4534
  nestedSelectorRule = null;
3936
4535
  }
3937
4536
  if (state === 'before-selector') {
@@ -3953,7 +4552,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3953
4552
 
3954
4553
  if (parentRule) {
3955
4554
  styleRule.__parentRule = parentRule;
3956
- ancestorRules.push(parentRule);
4555
+ pushToAncestorRules(parentRule);
3957
4556
  }
3958
4557
 
3959
4558
  currentScope = parentRule = styleRule;
@@ -3967,7 +4566,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3967
4566
 
3968
4567
  if (parentRule) {
3969
4568
  mediaRule.__parentRule = parentRule;
3970
- ancestorRules.push(parentRule);
4569
+ pushToAncestorRules(parentRule);
3971
4570
  }
3972
4571
 
3973
4572
  currentScope = parentRule = mediaRule;
@@ -3979,7 +4578,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3979
4578
 
3980
4579
  if (parentRule) {
3981
4580
  containerRule.__parentRule = parentRule;
3982
- ancestorRules.push(parentRule);
4581
+ pushToAncestorRules(parentRule);
3983
4582
  }
3984
4583
  currentScope = parentRule = containerRule;
3985
4584
  containerRule.__parentStyleSheet = styleSheet;
@@ -3996,7 +4595,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3996
4595
 
3997
4596
  if (parentRule) {
3998
4597
  supportsRule.__parentRule = parentRule;
3999
- ancestorRules.push(parentRule);
4598
+ pushToAncestorRules(parentRule);
4000
4599
  }
4001
4600
 
4002
4601
  currentScope = parentRule = supportsRule;
@@ -4018,7 +4617,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4018
4617
 
4019
4618
  if (parentRule) {
4020
4619
  scopeRule.__parentRule = parentRule;
4021
- ancestorRules.push(parentRule);
4620
+ pushToAncestorRules(parentRule);
4022
4621
  }
4023
4622
  currentScope = parentRule = scopeRule;
4024
4623
  scopeRule.__parentStyleSheet = styleSheet;
@@ -4032,7 +4631,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4032
4631
  if (isValidName) {
4033
4632
  if (parentRule) {
4034
4633
  layerBlockRule.__parentRule = parentRule;
4035
- ancestorRules.push(parentRule);
4634
+ pushToAncestorRules(parentRule);
4036
4635
  }
4037
4636
 
4038
4637
  currentScope = parentRule = layerBlockRule;
@@ -4045,7 +4644,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4045
4644
 
4046
4645
  if (parentRule) {
4047
4646
  pageRule.__parentRule = parentRule;
4048
- ancestorRules.push(parentRule);
4647
+ pushToAncestorRules(parentRule);
4049
4648
  }
4050
4649
 
4051
4650
  currentScope = parentRule = pageRule;
@@ -4055,7 +4654,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4055
4654
  state = "before-name";
4056
4655
  } else if (state === "hostRule-begin") {
4057
4656
  if (parentRule) {
4058
- ancestorRules.push(parentRule);
4657
+ pushToAncestorRules(parentRule);
4059
4658
  }
4060
4659
 
4061
4660
  currentScope = parentRule = hostRule;
@@ -4065,7 +4664,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4065
4664
  } else if (state === "startingStyleRule-begin") {
4066
4665
  if (parentRule) {
4067
4666
  startingStyleRule.__parentRule = parentRule;
4068
- ancestorRules.push(parentRule);
4667
+ pushToAncestorRules(parentRule);
4069
4668
  }
4070
4669
 
4071
4670
  currentScope = parentRule = startingStyleRule;
@@ -4084,7 +4683,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4084
4683
  } else if (state === "keyframesRule-begin") {
4085
4684
  keyframesRule.name = buffer.trim();
4086
4685
  if (parentRule) {
4087
- ancestorRules.push(parentRule);
4686
+ pushToAncestorRules(parentRule);
4088
4687
  keyframesRule.__parentRule = parentRule;
4089
4688
  }
4090
4689
  keyframesRule.__parentStyleSheet = styleSheet;
@@ -4101,7 +4700,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4101
4700
  // FIXME: what if this '{' is in the url text of the match function?
4102
4701
  documentRule.matcher.matcherText = buffer.trim();
4103
4702
  if (parentRule) {
4104
- ancestorRules.push(parentRule);
4703
+ pushToAncestorRules(parentRule);
4105
4704
  documentRule.__parentRule = parentRule;
4106
4705
  }
4107
4706
  currentScope = parentRule = documentRule;
@@ -4114,21 +4713,21 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4114
4713
  parentRule.cssRules.push(styleRule);
4115
4714
  styleRule.__parentRule = parentRule;
4116
4715
  styleRule.__parentStyleSheet = styleSheet;
4117
- ancestorRules.push(parentRule);
4716
+ pushToAncestorRules(parentRule);
4118
4717
  } else {
4119
4718
  // If the styleRule is empty, we can assume that it's a nested selector
4120
- ancestorRules.push(parentRule);
4719
+ pushToAncestorRules(parentRule);
4121
4720
  }
4122
4721
  } else {
4123
4722
  currentScope = parentRule = styleRule;
4124
- ancestorRules.push(parentRule);
4723
+ pushToAncestorRules(parentRule);
4125
4724
  styleRule.__parentStyleSheet = styleSheet;
4126
4725
  }
4127
4726
 
4128
4727
  styleRule = new CSSOM.CSSStyleRule();
4129
4728
  var processedSelectorText = processSelectorText(buffer.trim());
4130
4729
  // In a nested selector, ensure each selector contains '&' at the beginning, except for selectors that already have '&' somewhere
4131
- if (parentRule.constructor.name !== "CSSStyleRule" && parentRule.parentRule === null) {
4730
+ if (parentRule.constructor.name === "CSSScopeRule" || (parentRule.constructor.name !== "CSSStyleRule" && parentRule.parentRule === null)) {
4132
4731
  styleRule.selectorText = processedSelectorText;
4133
4732
  } else {
4134
4733
  styleRule.selectorText = parseAndSplitNestedSelectors(processedSelectorText).map(function(sel) {
@@ -4227,20 +4826,20 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4227
4826
  state = "before-selector";
4228
4827
  break;
4229
4828
  case "importRule":
4230
- var isValid = styleSheet.cssRules.length === 0 || styleSheet.cssRules.some(function (rule) {
4829
+ var isValid = topScope.cssRules.length === 0 || topScope.cssRules.some(function (rule) {
4231
4830
  return ['CSSImportRule', 'CSSLayerStatementRule'].indexOf(rule.constructor.name) !== -1
4232
4831
  });
4233
4832
  if (isValid) {
4234
4833
  importRule = new CSSOM.CSSImportRule();
4235
4834
  importRule.__parentStyleSheet = importRule.styleSheet.__parentStyleSheet = styleSheet;
4236
4835
  importRule.cssText = buffer + character;
4237
- styleSheet.cssRules.push(importRule);
4836
+ topScope.cssRules.push(importRule);
4238
4837
  }
4239
4838
  buffer = "";
4240
4839
  state = "before-selector";
4241
4840
  break;
4242
4841
  case "namespaceRule":
4243
- var isValid = styleSheet.cssRules.length === 0 || styleSheet.cssRules.every(function (rule) {
4842
+ var isValid = topScope.cssRules.length === 0 || topScope.cssRules.every(function (rule) {
4244
4843
  return ['CSSImportRule','CSSLayerStatementRule','CSSNamespaceRule'].indexOf(rule.constructor.name) !== -1
4245
4844
  });
4246
4845
  if (isValid) {
@@ -4251,7 +4850,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4251
4850
 
4252
4851
  namespaceRule = testNamespaceRule;
4253
4852
  namespaceRule.__parentStyleSheet = styleSheet;
4254
- styleSheet.cssRules.push(namespaceRule);
4853
+ topScope.cssRules.push(namespaceRule);
4255
4854
 
4256
4855
  // Track the namespace prefix for validation
4257
4856
  if (namespaceRule.prefix) {
@@ -4278,7 +4877,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4278
4877
  layerStatementRule.__starts = layerBlockRule.__starts;
4279
4878
  layerStatementRule.__ends = i;
4280
4879
  layerStatementRule.nameList = nameListStr;
4281
- styleSheet.cssRules.push(layerStatementRule);
4880
+ topScope.cssRules.push(layerStatementRule);
4282
4881
  }
4283
4882
  buffer = "";
4284
4883
  state = "before-selector";
@@ -4319,7 +4918,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4319
4918
  styleRule.__parentStyleSheet = styleSheet;
4320
4919
 
4321
4920
  if (currentScope === styleRule) {
4322
- currentScope = parentRule || styleSheet;
4921
+ currentScope = parentRule || topScope;
4323
4922
  }
4324
4923
 
4325
4924
  if (styleRule.constructor.name === "CSSStyleRule" && !isValidSelectorText(styleRule.selectorText)) {
@@ -4328,7 +4927,11 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4328
4927
  }
4329
4928
  parseError('Invalid CSSStyleRule (selectorText = "' + styleRule.selectorText + '")', styleRule.parentRule !== null);
4330
4929
  } else {
4331
- currentScope.cssRules.push(styleRule);
4930
+ if (styleRule.parentRule) {
4931
+ styleRule.parentRule.cssRules.push(styleRule);
4932
+ } else {
4933
+ currentScope.cssRules.push(styleRule);
4934
+ }
4332
4935
  }
4333
4936
  buffer = "";
4334
4937
  if (currentScope.constructor === CSSOM.CSSKeyframesRule) {
@@ -4338,7 +4941,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4338
4941
  }
4339
4942
 
4340
4943
  if (styleRule.constructor.name === "CSSNestedDeclarations") {
4341
- if (currentScope !== styleSheet) {
4944
+ if (currentScope !== topScope) {
4342
4945
  nestedSelectorRule = currentScope;
4343
4946
  }
4344
4947
  styleRule = null;
@@ -4361,7 +4964,6 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4361
4964
  break;
4362
4965
  }
4363
4966
 
4364
-
4365
4967
  while (ancestorRules.length > 0) {
4366
4968
  parentRule = ancestorRules.pop();
4367
4969
 
@@ -4387,8 +4989,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4387
4989
  }
4388
4990
  } else {
4389
4991
  prevScope = currentScope;
4390
- currentScope = parentRule;
4391
- currentScope !== prevScope && currentScope.cssRules.push(prevScope);
4992
+ parentRule !== prevScope && parentRule.cssRules.push(prevScope);
4392
4993
  break;
4393
4994
  }
4394
4995
  }
@@ -4396,12 +4997,12 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4396
4997
 
4397
4998
  if (currentScope.parentRule == null) {
4398
4999
  currentScope.__ends = i + 1;
4399
- if (currentScope !== styleSheet && styleSheet.cssRules.findIndex(function (rule) {
5000
+ if (currentScope !== topScope && topScope.cssRules.findIndex(function (rule) {
4400
5001
  return rule === currentScope
4401
5002
  }) === -1) {
4402
- styleSheet.cssRules.push(currentScope);
5003
+ topScope.cssRules.push(currentScope);
4403
5004
  }
4404
- currentScope = styleSheet;
5005
+ currentScope = topScope;
4405
5006
  if (nestedSelectorRule === parentRule) {
4406
5007
  // Check if this selector is really starting inside another selector
4407
5008
  var nestedSelectorTokenToCurrentSelectorToken = token.slice(nestedSelectorRule.__starts, i + 1);
@@ -4420,6 +5021,8 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4420
5021
  parentRule = null;
4421
5022
 
4422
5023
  }
5024
+ } else {
5025
+ currentScope = parentRule;
4423
5026
  }
4424
5027
 
4425
5028
  buffer = "";
@@ -4432,7 +5035,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
4432
5035
  switch (state) {
4433
5036
  case "before-selector":
4434
5037
  state = "selector";
4435
- if (styleRule && parentRule) {
5038
+ if ((styleRule || scopeRule) && parentRule) {
4436
5039
  // Assuming it's a declaration inside Nested Selector OR a Nested Declaration
4437
5040
  // If Declaration inside Nested Selector let's keep the same styleRule
4438
5041
  if (