@acemir/cssom 0.9.0 → 0.9.2

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
@@ -1,6 +1,29 @@
1
1
  var CSSOM = {};
2
2
 
3
3
 
4
+ // NOTE: Check viability to add a validation for css values or use a dependency like csstree-validator
5
+ /**
6
+ * Regular expression to detect invalid characters in the value portion of a CSS style declaration.
7
+ *
8
+ * This regex matches a colon (:) that is not inside parentheses and not inside single or double quotes.
9
+ * It is used to ensure that the value part of a CSS property does not contain unexpected colons,
10
+ * which would indicate a malformed declaration (e.g., "color: foo:bar;" is invalid).
11
+ *
12
+ * The negative lookahead `(?![^(]*\))` ensures that the colon is not followed by a closing
13
+ * parenthesis without encountering an opening parenthesis, effectively ignoring colons inside
14
+ * function-like values (e.g., `url(data:image/png;base64,...)`).
15
+ *
16
+ * The lookahead `(?=(?:[^'"]|'[^']*'|"[^"]*")*$)` ensures that the colon is not inside single or double quotes,
17
+ * allowing colons within quoted strings (e.g., `content: ":";` or `background: url("foo:bar.png");`).
18
+ *
19
+ * Example:
20
+ * "color: red;" // valid, does not match
21
+ * "background: url(data:image/png;base64,...);" // valid, does not match
22
+ * "content: ':';" // valid, does not match
23
+ * "color: foo:bar;" // invalid, matches
24
+ */
25
+ var basicStylePropertyValueValidationRegExp = /:(?![^(]*\))(?=(?:[^'"]|'[^']*'|"[^"]*")*$)/;
26
+
4
27
  /**
5
28
  * @constructor
6
29
  * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration
@@ -36,21 +59,36 @@ CSSOM.CSSStyleDeclaration.prototype = {
36
59
  * @param {string} [priority=null] "important" or null
37
60
  * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration-setProperty
38
61
  */
39
- setProperty: function(name, value, priority) {
40
- if (this[name]) {
62
+ setProperty: function(name, value, priority, parseErrorHandler)
63
+ {
64
+ // NOTE: Check viability to add a validation for css values or use a dependency like csstree-validator
65
+ if (basicStylePropertyValueValidationRegExp.test(value)) {
66
+ parseErrorHandler && parseErrorHandler('Invalid CSSStyleDeclaration property value');
67
+ } else if (this[name]) {
41
68
  // Property already exist. Overwrite it.
42
69
  var index = Array.prototype.indexOf.call(this, name);
43
70
  if (index < 0) {
44
71
  this[this.length] = name;
45
72
  this.length++;
46
73
  }
74
+
75
+ // If the priority value of the incoming property is "important",
76
+ // or the value of the existing property is not "important",
77
+ // then remove the existing property and rewrite it.
78
+ if (priority || !this._importants[name]) {
79
+ this.removeProperty(name);
80
+ this[this.length] = name;
81
+ this.length++;
82
+ this[name] = value + '';
83
+ this._importants[name] = priority;
84
+ }
47
85
  } else {
48
86
  // New property.
49
87
  this[this.length] = name;
50
88
  this.length++;
89
+ this[name] = value + '';
90
+ this._importants[name] = priority;
51
91
  }
52
- this[name] = value + "";
53
- this._importants[name] = priority;
54
92
  },
55
93
 
56
94
  /**
@@ -632,6 +670,8 @@ CSSOM.CSSImportRule = function CSSImportRule() {
632
670
  CSSOM.CSSRule.call(this);
633
671
  this.href = "";
634
672
  this.media = new CSSOM.MediaList();
673
+ this.layerName = null;
674
+ this.supportsText = null;
635
675
  this.styleSheet = new CSSOM.CSSStyleSheet();
636
676
  };
637
677
 
@@ -642,7 +682,7 @@ CSSOM.CSSImportRule.prototype.type = 3;
642
682
  Object.defineProperty(CSSOM.CSSImportRule.prototype, "cssText", {
643
683
  get: function() {
644
684
  var mediaText = this.media.mediaText;
645
- return "@import url(" + this.href + ")" + (mediaText ? " " + mediaText : "") + ";";
685
+ return "@import url(" + this.href + ")" + (this.layerName !== null ? " layer" + (this.layerName && "(" + this.layerName + ")") : "" ) + (this.supportsText ? " supports(" + this.supportsText + ")" : "" ) + (mediaText ? " " + mediaText : "") + ";";
646
686
  },
647
687
  set: function(cssText) {
648
688
  var i = 0;
@@ -658,6 +698,12 @@ Object.defineProperty(CSSOM.CSSImportRule.prototype, "cssText", {
658
698
 
659
699
  var buffer = '';
660
700
  var index;
701
+
702
+ var layerRegExp = /layer\(([^)]*)\)/;
703
+ var layerRuleNameRegExp = /^(-?[_a-zA-Z]+[_a-zA-Z0-9-]*)$/;
704
+ var supportsRegExp = /supports\(([^)]+)\)/;
705
+ var doubleOrMoreSpacesRegExp = /\s{2,}/g;
706
+
661
707
  for (var character; (character = cssText.charAt(i)); i++) {
662
708
 
663
709
  switch (character) {
@@ -682,6 +728,9 @@ Object.defineProperty(CSSOM.CSSImportRule.prototype, "cssText", {
682
728
  break;
683
729
 
684
730
  case 'u':
731
+ if (state === 'media') {
732
+ buffer += character;
733
+ }
685
734
  if (state === 'url' && cssText.indexOf('url(', i) === i) {
686
735
  index = cssText.indexOf(')', i + 1);
687
736
  if (index === -1) {
@@ -701,7 +750,7 @@ Object.defineProperty(CSSOM.CSSImportRule.prototype, "cssText", {
701
750
  break;
702
751
 
703
752
  case '"':
704
- if (state === 'url') {
753
+ if (state === 'after-import' || state === 'url') {
705
754
  index = cssText.indexOf('"', i + 1);
706
755
  if (!index) {
707
756
  throw i + ": '\"' not found";
@@ -713,7 +762,7 @@ Object.defineProperty(CSSOM.CSSImportRule.prototype, "cssText", {
713
762
  break;
714
763
 
715
764
  case "'":
716
- if (state === 'url') {
765
+ if (state === 'after-import' || state === 'url') {
717
766
  index = cssText.indexOf("'", i + 1);
718
767
  if (!index) {
719
768
  throw i + ': "\'" not found';
@@ -727,7 +776,47 @@ Object.defineProperty(CSSOM.CSSImportRule.prototype, "cssText", {
727
776
  case ';':
728
777
  if (state === 'media') {
729
778
  if (buffer) {
730
- this.media.mediaText = buffer.trim();
779
+ var bufferTrimmed = buffer.trim();
780
+
781
+ if (bufferTrimmed.indexOf('layer') === 0) {
782
+ var layerMatch = bufferTrimmed.match(layerRegExp);
783
+
784
+ if (layerMatch) {
785
+ var layerName = layerMatch[1].trim();
786
+ bufferTrimmed = bufferTrimmed.replace(layerRegExp, '')
787
+ .replace(doubleOrMoreSpacesRegExp, ' ') // Replace double or more spaces with single space
788
+ .trim();
789
+
790
+ if (layerName.match(layerRuleNameRegExp) !== null) {
791
+ this.layerName = layerMatch[1].trim();
792
+ } else {
793
+ // REVIEW: In the browser, an empty layer() is not processed as a unamed layer
794
+ // and treats the rest of the string as mediaText, ignoring the parse of supports()
795
+ if (bufferTrimmed) {
796
+ this.media.mediaText = bufferTrimmed;
797
+ return;
798
+ }
799
+ }
800
+ } else {
801
+ this.layerName = "";
802
+ bufferTrimmed = bufferTrimmed.substring('layer'.length).trim()
803
+ }
804
+ }
805
+
806
+ var supportsMatch = bufferTrimmed.match(supportsRegExp);
807
+
808
+ if (supportsMatch && supportsMatch.index === 0) {
809
+ // REVIEW: In the browser, an empty supports() invalidates and ignores the entire @import rule
810
+ this.supportsText = supportsMatch[1].trim();
811
+ bufferTrimmed = bufferTrimmed.replace(supportsRegExp, '')
812
+ .replace(doubleOrMoreSpacesRegExp, ' ') // Replace double or more spaces with single space
813
+ .trim();
814
+ }
815
+
816
+ // REVIEW: In the browser, any invalid media is replaced with 'not all'
817
+ if (bufferTrimmed) {
818
+ this.media.mediaText = bufferTrimmed;
819
+ }
731
820
  }
732
821
  }
733
822
  break;
@@ -862,11 +951,14 @@ CSSOM.CSSStyleSheet.prototype.constructor = CSSOM.CSSStyleSheet;
862
951
  * -> "img{border:none;}body{margin:0;}"
863
952
  *
864
953
  * @param {string} rule
865
- * @param {number} index
954
+ * @param {number} [index=0]
866
955
  * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet-insertRule
867
956
  * @return {number} The index within the style sheet's rule collection of the newly inserted rule.
868
957
  */
869
958
  CSSOM.CSSStyleSheet.prototype.insertRule = function(rule, index) {
959
+ if (index === void 0) {
960
+ index = 0;
961
+ }
870
962
  if (index < 0 || index > this.cssRules.length) {
871
963
  throw new RangeError("INDEX_SIZE_ERR");
872
964
  }
@@ -1431,7 +1523,7 @@ CSSOM.CSSValueExpression.prototype._findMatchedIdx = function(token, idx, sep) {
1431
1523
  */
1432
1524
  CSSOM.CSSLayerBlockRule = function CSSLayerBlockRule() {
1433
1525
  CSSOM.CSSGroupingRule.call(this);
1434
- this.layerName = "";
1526
+ this.name = "";
1435
1527
  this.cssRules = [];
1436
1528
  };
1437
1529
 
@@ -1440,23 +1532,37 @@ CSSOM.CSSLayerBlockRule.prototype.constructor = CSSOM.CSSLayerBlockRule;
1440
1532
  CSSOM.CSSLayerBlockRule.prototype.type = 18;
1441
1533
 
1442
1534
  Object.defineProperties(CSSOM.CSSLayerBlockRule.prototype, {
1443
- layerNameText: {
1535
+ cssText: {
1444
1536
  get: function () {
1445
- return this.layerName;
1446
- },
1447
- set: function (value) {
1448
- this.layerName = value;
1537
+ var cssTexts = [];
1538
+ for (var i = 0, length = this.cssRules.length; i < length; i++) {
1539
+ cssTexts.push(this.cssRules[i].cssText);
1540
+ }
1541
+ return "@layer " + this.name + (this.name && " ") + "{" + cssTexts.join("") + "}";
1449
1542
  },
1450
1543
  configurable: true,
1451
1544
  enumerable: true,
1452
1545
  },
1546
+ });
1547
+
1548
+
1549
+ /**
1550
+ * @constructor
1551
+ * @see https://drafts.csswg.org/css-cascade-5/#csslayerstatementrule
1552
+ */
1553
+ CSSOM.CSSLayerStatementRule = function CSSLayerStatementRule() {
1554
+ CSSOM.CSSRule.call(this);
1555
+ this.nameList = [];
1556
+ };
1557
+
1558
+ CSSOM.CSSLayerStatementRule.prototype = new CSSOM.CSSRule();
1559
+ CSSOM.CSSLayerStatementRule.prototype.constructor = CSSOM.CSSLayerStatementRule;
1560
+ CSSOM.CSSLayerStatementRule.prototype.type = 0;
1561
+
1562
+ Object.defineProperties(CSSOM.CSSLayerStatementRule.prototype, {
1453
1563
  cssText: {
1454
1564
  get: function () {
1455
- var cssTexts = [];
1456
- for (var i = 0, length = this.cssRules.length; i < length; i++) {
1457
- cssTexts.push(this.cssRules[i].cssText);
1458
- }
1459
- return "@layer " + this.layerNameText + " {" + cssTexts.join("") + "}";
1565
+ return "@layer " + this.nameList.join(", ") + ";";
1460
1566
  },
1461
1567
  configurable: true,
1462
1568
  enumerable: true,
@@ -1467,7 +1573,8 @@ Object.defineProperties(CSSOM.CSSLayerBlockRule.prototype, {
1467
1573
  /**
1468
1574
  * @param {string} token
1469
1575
  */
1470
- CSSOM.parse = function parse(token) {
1576
+ CSSOM.parse = function parse(token, errorHandler) {
1577
+ errorHandler = errorHandler === undefined && (console && console.error);
1471
1578
 
1472
1579
  var i = 0;
1473
1580
 
@@ -1489,6 +1596,8 @@ CSSOM.parse = function parse(token) {
1489
1596
  var valueParenthesisDepth = 0;
1490
1597
 
1491
1598
  var SIGNIFICANT_WHITESPACE = {
1599
+ "name": true,
1600
+ "before-name": true,
1492
1601
  "selector": true,
1493
1602
  "value": true,
1494
1603
  "value-parenthesis": true,
@@ -1511,17 +1620,57 @@ CSSOM.parse = function parse(token) {
1511
1620
  var parentRule;
1512
1621
 
1513
1622
  var ancestorRules = [];
1514
- var hasAncestors = false;
1515
1623
  var prevScope;
1516
1624
 
1517
- var name, priority="", styleRule, mediaRule, containerRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule, startingStyleRule, layerBlockRule, nestedSelectorRule;
1625
+ var name, priority="", styleRule, mediaRule, containerRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule, startingStyleRule, layerBlockRule, layerStatementRule, nestedSelectorRule;
1518
1626
 
1519
1627
  var atKeyframesRegExp = /@(-(?:\w+-)+)?keyframes/g; // Match @keyframes and vendor-prefixed @keyframes
1520
- var atRulesStatemenRegExp = /(?<!{.*)[;}]\s*/; // Match a statement by verifying it finds a semicolon or closing brace not followed by another semicolon or closing brace
1628
+ // Regex above is not ES5 compliant
1629
+ // var atRulesStatemenRegExp = /(?<!{.*)[;}]\s*/; // Match a statement by verifying it finds a semicolon or closing brace not followed by another semicolon or closing brace
1521
1630
  var beforeRulePortionRegExp = /{(?!.*{)|}(?!.*})|;(?!.*;)|\*\/(?!.*\*\/)/g; // Match the closest allowed character (a opening or closing brace, a semicolon or a comment ending) before the rule
1522
1631
  var beforeRuleValidationRegExp = /^[\s{};]*(\*\/\s*)?$/; // Match that the portion before the rule is empty or contains only whitespace, semicolons, opening/closing braces, and optionally a comment ending (*/) followed by whitespace
1523
1632
  var forwardRuleValidationRegExp = /(?:\(|\s|\/\*)/; // Match that the rule is followed by any whitespace, a opening comment or a condition opening parenthesis
1633
+ var forwardImportRuleValidationRegExp = /(?:\s|\/\*|'|")/; // Match that the rule is followed by any whitespace, an opening comment, a single quote or double quote
1524
1634
  var forwardRuleClosingBraceRegExp = /{[^{}]*}|}/; // Finds the next closing brace of a rule block
1635
+ var forwardRuleSemicolonAndOpeningBraceRegExp = /^.*?({|;)/; // Finds the next semicolon or opening brace after the at-rule
1636
+ var layerRuleNameRegExp = /^(-?[_a-zA-Z]+[_a-zA-Z0-9-]*)$/; // Validates a single @layer name
1637
+
1638
+ /**
1639
+ * Searches for the first occurrence of a CSS at-rule statement terminator (`;` or `}`)
1640
+ * that is not inside a brace block within the given string. Mimics the behavior of a
1641
+ * regular expression match for such terminators, including any trailing whitespace.
1642
+ * @param {string} str - The string to search for at-rule statement terminators.
1643
+ * @returns {object | null} {0: string, index: number} or null if no match is found.
1644
+ */
1645
+ function atRulesStatemenRegExpES5Alternative(ruleSlice) {
1646
+ for (var i = 0; i < ruleSlice.length; i++) {
1647
+ var char = ruleSlice[i];
1648
+
1649
+ if (char === ';' || char === '}') {
1650
+ // Simulate negative lookbehind: check if there is a { before this position
1651
+ var sliceBefore = ruleSlice.substring(0, i);
1652
+ var openBraceIndex = sliceBefore.indexOf('{');
1653
+
1654
+ if (openBraceIndex === -1) {
1655
+ // No { found before, so we treat it as a valid match
1656
+ var match = char;
1657
+ var j = i + 1;
1658
+
1659
+ while (j < ruleSlice.length && /\s/.test(ruleSlice[j])) {
1660
+ match += ruleSlice[j];
1661
+ j++;
1662
+ }
1663
+
1664
+ var matchObj = [match];
1665
+ matchObj.index = i;
1666
+ matchObj.input = ruleSlice;
1667
+ return matchObj;
1668
+ }
1669
+ }
1670
+ }
1671
+
1672
+ return null;
1673
+ }
1525
1674
 
1526
1675
  /**
1527
1676
  * Finds the first balanced block (including nested braces) in the string, starting from fromIndex.
@@ -1530,17 +1679,18 @@ CSSOM.parse = function parse(token) {
1530
1679
  * @param {number} [fromIndex=0] - The index to start searching from.
1531
1680
  * @returns {object|null} - { 0: matchedString, index: startIndex, input: str } or null if not found.
1532
1681
  */
1533
- function matchBalancedBlock(str, fromIndex = 0) {
1534
- const openIndex = str.indexOf('{', fromIndex);
1682
+ function matchBalancedBlock(str, fromIndex) {
1683
+ fromIndex = fromIndex || 0;
1684
+ var openIndex = str.indexOf('{', fromIndex);
1535
1685
  if (openIndex === -1) return null;
1536
- let depth = 0;
1537
- for (let i = openIndex; i < str.length; i++) {
1686
+ var depth = 0;
1687
+ for (var i = openIndex; i < str.length; i++) {
1538
1688
  if (str[i] === '{') {
1539
1689
  depth++;
1540
1690
  } else if (str[i] === '}') {
1541
1691
  depth--;
1542
1692
  if (depth === 0) {
1543
- const matchedString = str.slice(openIndex, i + 1);
1693
+ var matchedString = str.slice(openIndex, i + 1);
1544
1694
  return {
1545
1695
  0: matchedString,
1546
1696
  index: openIndex,
@@ -1552,6 +1702,29 @@ CSSOM.parse = function parse(token) {
1552
1702
  return null;
1553
1703
  }
1554
1704
 
1705
+ /**
1706
+ * Advances the index `i` to skip over a balanced block of curly braces in the given string.
1707
+ * This is typically used to ignore the contents of a CSS rule block.
1708
+ *
1709
+ * @param {number} i - The current index in the string to start searching from.
1710
+ * @param {string} str - The string containing the CSS code.
1711
+ * @param {number} fromIndex - The index in the string where the balanced block search should begin.
1712
+ * @returns {number} The updated index after skipping the balanced block.
1713
+ */
1714
+ function ignoreBalancedBlock(i, str, fromIndex) {
1715
+ var ruleClosingMatch = matchBalancedBlock(str, fromIndex);
1716
+ if (ruleClosingMatch) {
1717
+ var ignoreRange = ruleClosingMatch.index + ruleClosingMatch[0].length;
1718
+ i+= ignoreRange;
1719
+ if (token.charAt(i) === '}') {
1720
+ i -= 1;
1721
+ }
1722
+ } else {
1723
+ i += str.length;
1724
+ }
1725
+ return i;
1726
+ }
1727
+
1555
1728
  var parseError = function(message) {
1556
1729
  var lines = token.substring(0, i).split('\n');
1557
1730
  var lineCount = lines.length;
@@ -1561,12 +1734,18 @@ CSSOM.parse = function parse(token) {
1561
1734
  /* jshint sub : true */
1562
1735
  error['char'] = charCount;
1563
1736
  error.styleSheet = styleSheet;
1564
- throw error;
1737
+ // Print the error but continue parsing the sheet
1738
+ try {
1739
+ throw error;
1740
+ } catch(e) {
1741
+ errorHandler && errorHandler(e);
1742
+ }
1565
1743
  };
1566
1744
 
1567
1745
  var validateAtRule = function(atRuleKey, validCallback, cannotBeNested) {
1568
1746
  var isValid = false;
1569
- var ruleRegExp = new RegExp(atRuleKey + forwardRuleValidationRegExp.source, forwardRuleValidationRegExp.flags);
1747
+ var sourceRuleRegExp = atRuleKey === "@import" ? forwardImportRuleValidationRegExp : forwardRuleValidationRegExp;
1748
+ var ruleRegExp = new RegExp(atRuleKey + sourceRuleRegExp.source, sourceRuleRegExp.flags);
1570
1749
  var ruleSlice = token.slice(i);
1571
1750
  // Not all rules can be nested, if the rule cannot be nested and is in the root scope, do not perform the check
1572
1751
  var shouldPerformCheck = cannotBeNested && currentScope !== styleSheet ? false : true;
@@ -1588,7 +1767,9 @@ CSSOM.parse = function parse(token) {
1588
1767
  // If it's invalid the browser will simply ignore the entire invalid block
1589
1768
  // Use regex to find the closing brace of the invalid rule
1590
1769
 
1591
- var ruleStatementMatch = ruleSlice.match(atRulesStatemenRegExp);
1770
+ // Regex used above is not ES5 compliant. Using alternative.
1771
+ // var ruleStatementMatch = ruleSlice.match(atRulesStatemenRegExp); //
1772
+ var ruleStatementMatch = atRulesStatemenRegExpES5Alternative(ruleSlice);
1592
1773
 
1593
1774
  // If it's a statement inside a nested rule, ignore only the statement
1594
1775
  if (ruleStatementMatch && currentScope !== styleSheet) {
@@ -1597,25 +1778,179 @@ CSSOM.parse = function parse(token) {
1597
1778
  return;
1598
1779
  }
1599
1780
 
1600
- // Ignore the entire rule block (if it's a statement it should ignore the statement plus the next block)
1601
- var ruleClosingMatch = matchBalancedBlock(ruleSlice);
1602
- if (ruleClosingMatch) {
1603
- const ignoreRange = ruleClosingMatch.index + ruleClosingMatch[0].length;
1604
- i+= ignoreRange;
1605
- if (token.charAt(i) === '}') {
1606
- i -= 1;
1781
+ // Check if there's a semicolon before the invalid at-rule and the first opening brace
1782
+ if (atRuleKey === "@layer") {
1783
+ var ruleSemicolonAndOpeningBraceMatch = ruleSlice.match(forwardRuleSemicolonAndOpeningBraceRegExp);
1784
+ if (ruleSemicolonAndOpeningBraceMatch && ruleSemicolonAndOpeningBraceMatch[1] === ";" ) {
1785
+ // Ignore the rule block until the semicolon
1786
+ i += ruleSemicolonAndOpeningBraceMatch.index + ruleSemicolonAndOpeningBraceMatch[0].length;
1787
+ state = "before-selector";
1788
+ return;
1607
1789
  }
1608
- } else {
1609
- i += ruleSlice.length;
1610
1790
  }
1791
+
1792
+ // Ignore the entire rule block (if it's a statement it should ignore the statement plus the next block)
1793
+ i = ignoreBalancedBlock(i, ruleSlice);
1611
1794
  state = "before-selector";
1612
1795
  } else {
1613
1796
  validCallback.call(this);
1614
1797
  }
1615
1798
  }
1616
1799
 
1617
- for (var character; (character = token.charAt(i)); i++) {
1800
+ /**
1801
+ * Regular expression to match a basic CSS selector.
1802
+ *
1803
+ * This regex matches the following selector components:
1804
+ * - Type selectors (e.g., `div`, `span`)
1805
+ * - Universal selector (`*`)
1806
+ * - ID selectors (e.g., `#header`)
1807
+ * - Class selectors (e.g., `.container`)
1808
+ * - Attribute selectors (e.g., `[type="text"]`)
1809
+ * - Pseudo-classes and pseudo-elements (e.g., `:hover`, `::before`, `:nth-child(2)`)
1810
+ * - The parent selector (`&`)
1811
+ * - Combinators (`>`, `+`, `~`) with optional whitespace
1812
+ * - Whitespace (descendant combinator)
1813
+ *
1814
+ * The pattern ensures that a string consists only of valid basic selector components,
1815
+ * possibly repeated and combined, but does not match full CSS selector groups separated by commas.
1816
+ *
1817
+ * @type {RegExp}
1818
+ */
1819
+ var basicSelectorRegExp = /^([a-zA-Z][a-zA-Z0-9_-]*|\*|#[a-zA-Z0-9_-]+|\.[a-zA-Z0-9_-]+|\[[^\[\]]*\]|::?[a-zA-Z0-9_-]+(?:\([^\(\)]*\))?|&|\s*[>+~]\s*|\s+)+$/;
1820
+
1821
+ /**
1822
+ * Regular expression to match CSS pseudo-classes with arguments.
1823
+ *
1824
+ * Matches patterns like `:pseudo-class(argument)`, capturing the pseudo-class name and its argument.
1825
+ *
1826
+ * Capture groups:
1827
+ * 1. The pseudo-class name (letters and hyphens).
1828
+ * 2. The argument inside the parentheses (any characters except a closing parenthesis).
1829
+ *
1830
+ * Global flag (`g`) is used to find all matches in the input string.
1831
+ *
1832
+ * Example match: `:nth-child(2n+1)`
1833
+ * - Group 1: "nth-child"
1834
+ * - Group 2: "2n+1"
1835
+ * @type {RegExp}
1836
+ */
1837
+ var globalPseudoClassRegExp = /:([a-zA-Z-]+)\(([^)]*)\)/g;
1838
+
1839
+ /**
1840
+ * Parses a CSS selector string and splits it into parts, handling nested parentheses.
1841
+ *
1842
+ * This function is useful for splitting selectors that may contain nested function-like
1843
+ * syntax (e.g., :not(.foo, .bar)), ensuring that commas inside parentheses do not split
1844
+ * the selector.
1845
+ *
1846
+ * @param {string} selector - The CSS selector string to parse.
1847
+ * @returns {string[]} An array of selector parts, split by top-level commas, with whitespace trimmed.
1848
+ */
1849
+ function parseNestedSelectors(selector) {
1850
+ var depth = 0;
1851
+ var buffer = "";
1852
+ var parts = [];
1853
+ var i, char;
1854
+
1855
+ for (i = 0; i < selector.length; i++) {
1856
+ char = selector.charAt(i);
1618
1857
 
1858
+ if (char === '(') {
1859
+ depth++;
1860
+ buffer += char;
1861
+ } else if (char === ')') {
1862
+ depth--;
1863
+ buffer += char;
1864
+ if (depth === 0) {
1865
+ parts.push(buffer.replace(/^\s+|\s+$/g, ""));
1866
+ buffer = "";
1867
+ }
1868
+ } else if (char === ',' && depth === 0) {
1869
+ if (buffer.replace(/^\s+|\s+$/g, "")) {
1870
+ parts.push(buffer.replace(/^\s+|\s+$/g, ""));
1871
+ }
1872
+ buffer = "";
1873
+ } else {
1874
+ buffer += char;
1875
+ }
1876
+ }
1877
+
1878
+ if (buffer.replace(/^\s+|\s+$/g, "")) {
1879
+ parts.push(buffer.replace(/^\s+|\s+$/g, ""));
1880
+ }
1881
+
1882
+ return parts;
1883
+ }
1884
+
1885
+ /**
1886
+ * Validates a CSS selector string, including handling of nested selectors within certain pseudo-classes.
1887
+ *
1888
+ * This function checks if the provided selector is valid according to the rules defined by
1889
+ * `basicSelectorRegExp`. For pseudo-classes that accept selector lists (such as :not, :is, :has, :where),
1890
+ * it recursively validates each nested selector using the same validation logic.
1891
+ *
1892
+ * @param {string} selector - The CSS selector string to validate.
1893
+ * @returns {boolean} Returns `true` if the selector is valid, otherwise `false`.
1894
+ */
1895
+ function validateSelector(selector) {
1896
+ var match, nestedSelectors, i;
1897
+
1898
+ // Only pseudo-classes that accept selector lists should recurse
1899
+ var selectorListPseudoClasses = {
1900
+ 'not': true,
1901
+ 'is': true,
1902
+ 'has': true,
1903
+ 'where': true
1904
+ };
1905
+
1906
+ // Reset regex lastIndex for global regex in ES5 loop
1907
+ var pseudoClassRegExp = new RegExp(globalPseudoClassRegExp.source, globalPseudoClassRegExp.flags);
1908
+ while ((match = pseudoClassRegExp.exec(selector)) !== null) {
1909
+ var pseudoClass = match[1];
1910
+ if (selectorListPseudoClasses.hasOwnProperty(pseudoClass)) {
1911
+ nestedSelectors = parseNestedSelectors(match[2]);
1912
+ // Validate each nested selector
1913
+ for (i = 0; i < nestedSelectors.length; i++) {
1914
+ if (!validateSelector(nestedSelectors[i])) {
1915
+ return false;
1916
+ }
1917
+ }
1918
+ }
1919
+ }
1920
+
1921
+ // Allow "&" anywhere in the selector for nested selectors
1922
+ return basicSelectorRegExp.test(selector);
1923
+ }
1924
+
1925
+ /**
1926
+ * Checks if a given CSS selector text is valid by splitting it by commas
1927
+ * and validating each individual selector using the `validateSelector` function.
1928
+ *
1929
+ * @param {string} selectorText - The CSS selector text to validate. Can contain multiple selectors separated by commas.
1930
+ * @returns {boolean} Returns true if all selectors are valid, otherwise false.
1931
+ */
1932
+ function isValidSelectorText(selectorText) {
1933
+ // Split selectorText by commas and validate each part
1934
+ var selectors = selectorText.split(',');
1935
+ for (var i = 0; i < selectors.length; i++) {
1936
+ if (!validateSelector(selectors[i].replace(/^\s+|\s+$/g, ""))) {
1937
+ return false;
1938
+ }
1939
+ }
1940
+ return true;
1941
+ }
1942
+
1943
+ var endingIndex = token.length - 1;
1944
+
1945
+ for (var character; (character = token.charAt(i)); i++) {
1946
+ if (i === endingIndex) {
1947
+ switch (state) {
1948
+ case "importRule":
1949
+ case "layerBlock":
1950
+ token += ";"
1951
+ }
1952
+ }
1953
+
1619
1954
  switch (character) {
1620
1955
 
1621
1956
  case " ":
@@ -1637,6 +1972,9 @@ CSSOM.parse = function parse(token) {
1637
1972
  parseError('Unmatched "');
1638
1973
  }
1639
1974
  } while (token[index - 2] === '\\');
1975
+ if (index === 0) {
1976
+ break;
1977
+ }
1640
1978
  buffer += token.slice(i, index);
1641
1979
  i = index - 1;
1642
1980
  switch (state) {
@@ -1645,6 +1983,9 @@ CSSOM.parse = function parse(token) {
1645
1983
  break;
1646
1984
  case 'importRule-begin':
1647
1985
  state = 'importRule';
1986
+ if (i === endingIndex) {
1987
+ token += ';'
1988
+ }
1648
1989
  break;
1649
1990
  }
1650
1991
  break;
@@ -1657,6 +1998,9 @@ CSSOM.parse = function parse(token) {
1657
1998
  parseError("Unmatched '");
1658
1999
  }
1659
2000
  } while (token[index - 2] === '\\');
2001
+ if (index === 0) {
2002
+ break;
2003
+ }
1660
2004
  buffer += token.slice(i, index);
1661
2005
  i = index - 1;
1662
2006
  switch (state) {
@@ -1792,8 +2136,13 @@ CSSOM.parse = function parse(token) {
1792
2136
  if (currentScope === styleSheet) {
1793
2137
  nestedSelectorRule = null;
1794
2138
  }
2139
+ if (state === 'before-selector') {
2140
+ parseError("Unexpected {");
2141
+ i = ignoreBalancedBlock(i, token.slice(i));
2142
+ break;
2143
+ }
1795
2144
  if (state === "selector" || state === "atRule") {
1796
- if (!nestedSelectorRule && buffer.includes(";")) {
2145
+ if (!nestedSelectorRule && buffer.indexOf(";") !== -1) {
1797
2146
  var ruleClosingMatch = token.slice(i).match(forwardRuleClosingBraceRegExp);
1798
2147
  if (ruleClosingMatch) {
1799
2148
  styleRule = null;
@@ -1810,6 +2159,7 @@ CSSOM.parse = function parse(token) {
1810
2159
  }
1811
2160
 
1812
2161
  currentScope = parentRule = styleRule;
2162
+ console.log('sel out', buffer);
1813
2163
  styleRule.selectorText = buffer.trim();
1814
2164
  styleRule.style.__starts = i;
1815
2165
  styleRule.parentStyleSheet = styleSheet;
@@ -1851,15 +2201,19 @@ CSSOM.parse = function parse(token) {
1851
2201
  buffer = "";
1852
2202
  state = "before-selector";
1853
2203
  } else if (state === "layerBlock") {
1854
- layerBlockRule.layerNameText = buffer.trim();
2204
+ layerBlockRule.name = buffer.trim();
1855
2205
 
1856
- if (parentRule) {
1857
- layerBlockRule.parentRule = parentRule;
1858
- ancestorRules.push(parentRule);
1859
- }
2206
+ var isValidName = layerBlockRule.name.length === 0 || layerBlockRule.name.match(layerRuleNameRegExp) !== null;
1860
2207
 
1861
- currentScope = parentRule = layerBlockRule;
1862
- layerBlockRule.parentStyleSheet = styleSheet;
2208
+ if (isValidName) {
2209
+ if (parentRule) {
2210
+ layerBlockRule.parentRule = parentRule;
2211
+ ancestorRules.push(parentRule);
2212
+ }
2213
+
2214
+ currentScope = parentRule = layerBlockRule;
2215
+ layerBlockRule.parentStyleSheet = styleSheet;
2216
+ }
1863
2217
  buffer = "";
1864
2218
  state = "before-selector";
1865
2219
  } else if (state === "hostRule-begin") {
@@ -1917,7 +2271,7 @@ CSSOM.parse = function parse(token) {
1917
2271
  documentRule.parentStyleSheet = styleSheet;
1918
2272
  buffer = "";
1919
2273
  state = "before-selector";
1920
- } else if (state === "name") {
2274
+ } else if (state === "before-name" || state === "name") {
1921
2275
  if (styleRule.constructor.name === "CSSNestedDeclarations") {
1922
2276
  if (styleRule.style.length) {
1923
2277
  parentRule.cssRules.push(styleRule);
@@ -1934,9 +2288,12 @@ CSSOM.parse = function parse(token) {
1934
2288
  styleRule.parentStyleSheet = styleSheet;
1935
2289
  }
1936
2290
 
1937
-
1938
2291
  styleRule = new CSSOM.CSSStyleRule();
1939
- styleRule.selectorText = buffer.trim();
2292
+ console.log('sel in', buffer);
2293
+ // In a nested selector, ensure each selector contains '&' at the beginning, except for selectors that already have '&' somewhere
2294
+ styleRule.selectorText = parseNestedSelectors(buffer.trim()).map(function(sel) {
2295
+ return sel.indexOf('&') === -1 ? '& ' + sel : sel;
2296
+ }).join(', ');
1940
2297
  styleRule.style.__starts = i - buffer.length;
1941
2298
  styleRule.parentRule = parentRule;
1942
2299
  nestedSelectorRule = styleRule;
@@ -2011,8 +2368,14 @@ CSSOM.parse = function parse(token) {
2011
2368
 
2012
2369
  case ";":
2013
2370
  switch (state) {
2371
+ case "before-value":
2372
+ case "before-name":
2373
+ parseError("Unexpected ;");
2374
+ buffer = "";
2375
+ state = "before-name";
2376
+ break;
2014
2377
  case "value":
2015
- styleRule.style.setProperty(name, buffer.trim(), priority);
2378
+ styleRule.style.setProperty(name, buffer.trim(), priority, parseError);
2016
2379
  priority = "";
2017
2380
  buffer = "";
2018
2381
  state = "before-name";
@@ -2022,10 +2385,34 @@ CSSOM.parse = function parse(token) {
2022
2385
  state = "before-selector";
2023
2386
  break;
2024
2387
  case "importRule":
2025
- importRule = new CSSOM.CSSImportRule();
2026
- importRule.parentStyleSheet = importRule.styleSheet.parentStyleSheet = styleSheet;
2027
- importRule.cssText = buffer + character;
2028
- styleSheet.cssRules.push(importRule);
2388
+ var isValid = styleSheet.cssRules.length === 0 || styleSheet.cssRules.some(function (rule) {
2389
+ return ['CSSImportRule', 'CSSLayerStatementRule'].indexOf(rule.constructor.name) !== -1
2390
+ });
2391
+ if (isValid) {
2392
+ importRule = new CSSOM.CSSImportRule();
2393
+ importRule.parentStyleSheet = importRule.styleSheet.parentStyleSheet = styleSheet;
2394
+ importRule.cssText = buffer + character;
2395
+ styleSheet.cssRules.push(importRule);
2396
+ }
2397
+ buffer = "";
2398
+ state = "before-selector";
2399
+ break;
2400
+ case "layerBlock":
2401
+ var nameListStr = buffer.trim().split(",").map(function (name) {
2402
+ return name.trim();
2403
+ });
2404
+ var isInvalid = parentRule !== undefined || nameListStr.some(function (name) {
2405
+ return name.trim().match(layerRuleNameRegExp) === null;
2406
+ });
2407
+
2408
+ if (!isInvalid) {
2409
+ layerStatementRule = new CSSOM.CSSLayerStatementRule();
2410
+ layerStatementRule.parentStyleSheet = styleSheet;
2411
+ layerStatementRule.__starts = layerBlockRule.__starts;
2412
+ layerStatementRule.__ends = i;
2413
+ layerStatementRule.nameList = nameListStr;
2414
+ styleSheet.cssRules.push(layerStatementRule);
2415
+ }
2029
2416
  buffer = "";
2030
2417
  state = "before-selector";
2031
2418
  break;
@@ -2038,9 +2425,10 @@ CSSOM.parse = function parse(token) {
2038
2425
  case "}":
2039
2426
  switch (state) {
2040
2427
  case "value":
2041
- styleRule.style.setProperty(name, buffer.trim(), priority);
2428
+ styleRule.style.setProperty(name, buffer.trim(), priority, parseError);
2042
2429
  priority = "";
2043
2430
  /* falls through */
2431
+ case "before-value":
2044
2432
  case "before-name":
2045
2433
  case "name":
2046
2434
  styleRule.__ends = i + 1;
@@ -2058,7 +2446,14 @@ CSSOM.parse = function parse(token) {
2058
2446
  currentScope = parentRule || styleSheet;
2059
2447
  }
2060
2448
 
2061
- currentScope.cssRules.push(styleRule);
2449
+ if (styleRule.constructor.name === "CSSStyleRule" && !isValidSelectorText(styleRule.selectorText)) {
2450
+ if (styleRule === nestedSelectorRule) {
2451
+ nestedSelectorRule = null;
2452
+ }
2453
+ parseError('Invalid CSSStyleRule.selectorText');
2454
+ } else {
2455
+ currentScope.cssRules.push(styleRule);
2456
+ }
2062
2457
  buffer = "";
2063
2458
  if (currentScope.constructor === CSSOM.CSSKeyframesRule) {
2064
2459
  state = "keyframeRule-begin";
@@ -2080,12 +2475,16 @@ CSSOM.parse = function parse(token) {
2080
2475
  case "selector":
2081
2476
  // End of media/supports/document rule.
2082
2477
  if (!parentRule) {
2478
+ parseError("Unexpected }");
2479
+
2480
+ var hasPreviousStyleRule = currentScope.cssRules.length && currentScope.cssRules[currentScope.cssRules.length - 1].constructor.name === "CSSStyleRule";
2481
+ if (hasPreviousStyleRule) {
2482
+ i = ignoreBalancedBlock(i, token.slice(i), 1);
2483
+ }
2484
+
2083
2485
  break;
2084
- //parseError("Unexpected }");
2085
2486
  }
2086
2487
 
2087
- // Handle rules nested in @media or @supports
2088
- hasAncestors = ancestorRules.length > 0;
2089
2488
 
2090
2489
  while (ancestorRules.length > 0) {
2091
2490
  parentRule = ancestorRules.pop();
@@ -2112,14 +2511,10 @@ CSSOM.parse = function parse(token) {
2112
2511
  } else {
2113
2512
  prevScope = currentScope;
2114
2513
  currentScope = parentRule;
2115
- currentScope.cssRules.push(prevScope);
2514
+ currentScope !== prevScope && currentScope.cssRules.push(prevScope);
2116
2515
  break;
2117
2516
  }
2118
2517
  }
2119
-
2120
- if (ancestorRules.length === 0) {
2121
- hasAncestors = false;
2122
- }
2123
2518
  }
2124
2519
 
2125
2520
  if (currentScope.parentRule == null) {
@@ -2133,8 +2528,12 @@ CSSOM.parse = function parse(token) {
2133
2528
  if (nestedSelectorRule === parentRule) {
2134
2529
  // Check if this selector is really starting inside another selector
2135
2530
  var nestedSelectorTokenToCurrentSelectorToken = token.slice(nestedSelectorRule.__starts, i + 1);
2136
-
2137
- if (nestedSelectorTokenToCurrentSelectorToken.match(/{/g)?.length === nestedSelectorTokenToCurrentSelectorToken.match(/}/g)?.length) {
2531
+ var openingBraceMatch = nestedSelectorTokenToCurrentSelectorToken.match(/{/g);
2532
+ var closingBraceMatch = nestedSelectorTokenToCurrentSelectorToken.match(/}/g);
2533
+ var openingBraceLen = openingBraceMatch && openingBraceMatch.length;
2534
+ var closingBraceLen = closingBraceMatch && closingBraceMatch.length;
2535
+
2536
+ if (openingBraceLen === closingBraceLen) {
2138
2537
  // If the number of opening and closing braces are equal, we can assume that the new selector is starting outside the nestedSelectorRule
2139
2538
  nestedSelectorRule.__ends = i + 1;
2140
2539
  nestedSelectorRule = null;
@@ -2263,6 +2662,10 @@ CSSOM.clone = function clone(stylesheet) {
2263
2662
  ruleClone.mediaText = rule.mediaText;
2264
2663
  }
2265
2664
 
2665
+ if (rule.hasOwnProperty('supportsText')) {
2666
+ ruleClone.supports = rule.supports;
2667
+ }
2668
+
2266
2669
  if (rule.hasOwnProperty('conditionText')) {
2267
2670
  ruleClone.conditionText = rule.conditionText;
2268
2671
  }
@@ -2271,6 +2674,18 @@ CSSOM.clone = function clone(stylesheet) {
2271
2674
  ruleClone.layerName = rule.layerName;
2272
2675
  }
2273
2676
 
2677
+ if (rule.hasOwnProperty('href')) {
2678
+ ruleClone.href = rule.href;
2679
+ }
2680
+
2681
+ if (rule.hasOwnProperty('name')) {
2682
+ ruleClone.name = rule.name;
2683
+ }
2684
+
2685
+ if (rule.hasOwnProperty('nameList')) {
2686
+ ruleClone.nameList = rule.nameList;
2687
+ }
2688
+
2274
2689
  if (rule.hasOwnProperty('cssRules')) {
2275
2690
  ruleClone.cssRules = clone(rule).cssRules;
2276
2691
  }