@acemir/cssom 0.9.1 → 0.9.3

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,8 +59,12 @@ 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) {
@@ -1546,7 +1573,8 @@ Object.defineProperties(CSSOM.CSSLayerStatementRule.prototype, {
1546
1573
  /**
1547
1574
  * @param {string} token
1548
1575
  */
1549
- CSSOM.parse = function parse(token) {
1576
+ CSSOM.parse = function parse(token, errorHandler) {
1577
+ errorHandler = errorHandler === undefined && (console && console.error);
1550
1578
 
1551
1579
  var i = 0;
1552
1580
 
@@ -1568,6 +1596,8 @@ CSSOM.parse = function parse(token) {
1568
1596
  var valueParenthesisDepth = 0;
1569
1597
 
1570
1598
  var SIGNIFICANT_WHITESPACE = {
1599
+ "name": true,
1600
+ "before-name": true,
1571
1601
  "selector": true,
1572
1602
  "value": true,
1573
1603
  "value-parenthesis": true,
@@ -1672,6 +1702,29 @@ CSSOM.parse = function parse(token) {
1672
1702
  return null;
1673
1703
  }
1674
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
+
1675
1728
  var parseError = function(message) {
1676
1729
  var lines = token.substring(0, i).split('\n');
1677
1730
  var lineCount = lines.length;
@@ -1681,7 +1734,12 @@ CSSOM.parse = function parse(token) {
1681
1734
  /* jshint sub : true */
1682
1735
  error['char'] = charCount;
1683
1736
  error.styleSheet = styleSheet;
1684
- throw error;
1737
+ // Print the error but continue parsing the sheet
1738
+ try {
1739
+ throw error;
1740
+ } catch(e) {
1741
+ errorHandler && errorHandler(e);
1742
+ }
1685
1743
  };
1686
1744
 
1687
1745
  var validateAtRule = function(atRuleKey, validCallback, cannotBeNested) {
@@ -1732,22 +1790,169 @@ CSSOM.parse = function parse(token) {
1732
1790
  }
1733
1791
 
1734
1792
  // Ignore the entire rule block (if it's a statement it should ignore the statement plus the next block)
1735
- var ruleClosingMatch = matchBalancedBlock(ruleSlice);
1736
- if (ruleClosingMatch) {
1737
- var ignoreRange = ruleClosingMatch.index + ruleClosingMatch[0].length;
1738
- i+= ignoreRange;
1739
- if (token.charAt(i) === '}') {
1740
- i -= 1;
1741
- }
1742
- } else {
1743
- i += ruleSlice.length;
1744
- }
1793
+ i = ignoreBalancedBlock(i, ruleSlice);
1745
1794
  state = "before-selector";
1746
1795
  } else {
1747
1796
  validCallback.call(this);
1748
1797
  }
1749
1798
  }
1750
1799
 
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_-]+|\[[^\[\]]*(?:\s+[iI])?\]|::?[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-]+)\(([^)]*?(?:\s+[iI])?)\)/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 parseAndSplitNestedSelectors(selector) {
1850
+ var depth = 0;
1851
+ var buffer = "";
1852
+ var parts = [];
1853
+ var inSingleQuote = false;
1854
+ var inDoubleQuote = false;
1855
+ var i, char;
1856
+
1857
+ for (i = 0; i < selector.length; i++) {
1858
+ char = selector.charAt(i);
1859
+
1860
+ if (char === "'" && !inDoubleQuote) {
1861
+ inSingleQuote = !inSingleQuote;
1862
+ buffer += char;
1863
+ } else if (char === '"' && !inSingleQuote) {
1864
+ inDoubleQuote = !inDoubleQuote;
1865
+ buffer += char;
1866
+ } else if (!inSingleQuote && !inDoubleQuote) {
1867
+ if (char === '(') {
1868
+ depth++;
1869
+ buffer += char;
1870
+ } else if (char === ')') {
1871
+ depth--;
1872
+ buffer += char;
1873
+ if (depth === 0) {
1874
+ parts.push(buffer.replace(/^\s+|\s+$/g, ""));
1875
+ buffer = "";
1876
+ }
1877
+ } else if (char === ',' && depth === 0) {
1878
+ if (buffer.replace(/^\s+|\s+$/g, "")) {
1879
+ parts.push(buffer.replace(/^\s+|\s+$/g, ""));
1880
+ }
1881
+ buffer = "";
1882
+ } else {
1883
+ buffer += char;
1884
+ }
1885
+ } else {
1886
+ buffer += char;
1887
+ }
1888
+ }
1889
+
1890
+ if (buffer.replace(/^\s+|\s+$/g, "")) {
1891
+ parts.push(buffer.replace(/^\s+|\s+$/g, ""));
1892
+ }
1893
+
1894
+ return parts;
1895
+ }
1896
+
1897
+ /**
1898
+ * Validates a CSS selector string, including handling of nested selectors within certain pseudo-classes.
1899
+ *
1900
+ * This function checks if the provided selector is valid according to the rules defined by
1901
+ * `basicSelectorRegExp`. For pseudo-classes that accept selector lists (such as :not, :is, :has, :where),
1902
+ * it recursively validates each nested selector using the same validation logic.
1903
+ *
1904
+ * @param {string} selector - The CSS selector string to validate.
1905
+ * @returns {boolean} Returns `true` if the selector is valid, otherwise `false`.
1906
+ */
1907
+ function validateSelector(selector) {
1908
+ var match, nestedSelectors, i;
1909
+
1910
+ // Only pseudo-classes that accept selector lists should recurse
1911
+ var selectorListPseudoClasses = {
1912
+ 'not': true,
1913
+ 'is': true,
1914
+ 'has': true,
1915
+ 'where': true
1916
+ };
1917
+
1918
+ // Reset regex lastIndex for global regex in ES5 loop
1919
+ var pseudoClassRegExp = new RegExp(globalPseudoClassRegExp.source, globalPseudoClassRegExp.flags);
1920
+ while ((match = pseudoClassRegExp.exec(selector)) !== null) {
1921
+ var pseudoClass = match[1];
1922
+ if (selectorListPseudoClasses.hasOwnProperty(pseudoClass)) {
1923
+ nestedSelectors = parseAndSplitNestedSelectors(match[2]);
1924
+ // Validate each nested selector
1925
+ for (i = 0; i < nestedSelectors.length; i++) {
1926
+ if (!validateSelector(nestedSelectors[i])) {
1927
+ return false;
1928
+ }
1929
+ }
1930
+ }
1931
+ }
1932
+
1933
+ // Allow "&" anywhere in the selector for nested selectors
1934
+ return basicSelectorRegExp.test(selector);
1935
+ }
1936
+
1937
+ /**
1938
+ * Checks if a given CSS selector text is valid by splitting it by commas
1939
+ * and validating each individual selector using the `validateSelector` function.
1940
+ *
1941
+ * @param {string} selectorText - The CSS selector text to validate. Can contain multiple selectors separated by commas.
1942
+ * @returns {boolean} Returns true if all selectors are valid, otherwise false.
1943
+ */
1944
+ function isValidSelectorText(selectorText) {
1945
+ // Split selectorText by commas and validate each part
1946
+ var selectors = parseAndSplitNestedSelectors(selectorText);
1947
+ for (var i = 0; i < selectors.length; i++) {
1948
+ var processedSelectors = selectors[i].replace(/^\s+|\s+$/g, "");
1949
+ if (!validateSelector(processedSelectors)) {
1950
+ return false;
1951
+ }
1952
+ }
1953
+ return true;
1954
+ }
1955
+
1751
1956
  var endingIndex = token.length - 1;
1752
1957
 
1753
1958
  for (var character; (character = token.charAt(i)); i++) {
@@ -1780,6 +1985,9 @@ CSSOM.parse = function parse(token) {
1780
1985
  parseError('Unmatched "');
1781
1986
  }
1782
1987
  } while (token[index - 2] === '\\');
1988
+ if (index === 0) {
1989
+ break;
1990
+ }
1783
1991
  buffer += token.slice(i, index);
1784
1992
  i = index - 1;
1785
1993
  switch (state) {
@@ -1803,6 +2011,9 @@ CSSOM.parse = function parse(token) {
1803
2011
  parseError("Unmatched '");
1804
2012
  }
1805
2013
  } while (token[index - 2] === '\\');
2014
+ if (index === 0) {
2015
+ break;
2016
+ }
1806
2017
  buffer += token.slice(i, index);
1807
2018
  i = index - 1;
1808
2019
  switch (state) {
@@ -1938,6 +2149,11 @@ CSSOM.parse = function parse(token) {
1938
2149
  if (currentScope === styleSheet) {
1939
2150
  nestedSelectorRule = null;
1940
2151
  }
2152
+ if (state === 'before-selector') {
2153
+ parseError("Unexpected {");
2154
+ i = ignoreBalancedBlock(i, token.slice(i));
2155
+ break;
2156
+ }
1941
2157
  if (state === "selector" || state === "atRule") {
1942
2158
  if (!nestedSelectorRule && buffer.indexOf(";") !== -1) {
1943
2159
  var ruleClosingMatch = token.slice(i).match(forwardRuleClosingBraceRegExp);
@@ -2067,7 +2283,7 @@ CSSOM.parse = function parse(token) {
2067
2283
  documentRule.parentStyleSheet = styleSheet;
2068
2284
  buffer = "";
2069
2285
  state = "before-selector";
2070
- } else if (state === "name") {
2286
+ } else if (state === "before-name" || state === "name") {
2071
2287
  if (styleRule.constructor.name === "CSSNestedDeclarations") {
2072
2288
  if (styleRule.style.length) {
2073
2289
  parentRule.cssRules.push(styleRule);
@@ -2084,9 +2300,11 @@ CSSOM.parse = function parse(token) {
2084
2300
  styleRule.parentStyleSheet = styleSheet;
2085
2301
  }
2086
2302
 
2087
-
2088
2303
  styleRule = new CSSOM.CSSStyleRule();
2089
- styleRule.selectorText = buffer.trim();
2304
+ // In a nested selector, ensure each selector contains '&' at the beginning, except for selectors that already have '&' somewhere
2305
+ styleRule.selectorText = parseAndSplitNestedSelectors(buffer.trim()).map(function(sel) {
2306
+ return sel.indexOf('&') === -1 ? '& ' + sel : sel;
2307
+ }).join(', ');
2090
2308
  styleRule.style.__starts = i - buffer.length;
2091
2309
  styleRule.parentRule = parentRule;
2092
2310
  nestedSelectorRule = styleRule;
@@ -2161,8 +2379,14 @@ CSSOM.parse = function parse(token) {
2161
2379
 
2162
2380
  case ";":
2163
2381
  switch (state) {
2382
+ case "before-value":
2383
+ case "before-name":
2384
+ parseError("Unexpected ;");
2385
+ buffer = "";
2386
+ state = "before-name";
2387
+ break;
2164
2388
  case "value":
2165
- styleRule.style.setProperty(name, buffer.trim(), priority);
2389
+ styleRule.style.setProperty(name, buffer.trim(), priority, parseError);
2166
2390
  priority = "";
2167
2391
  buffer = "";
2168
2392
  state = "before-name";
@@ -2212,9 +2436,10 @@ CSSOM.parse = function parse(token) {
2212
2436
  case "}":
2213
2437
  switch (state) {
2214
2438
  case "value":
2215
- styleRule.style.setProperty(name, buffer.trim(), priority);
2439
+ styleRule.style.setProperty(name, buffer.trim(), priority, parseError);
2216
2440
  priority = "";
2217
2441
  /* falls through */
2442
+ case "before-value":
2218
2443
  case "before-name":
2219
2444
  case "name":
2220
2445
  styleRule.__ends = i + 1;
@@ -2232,7 +2457,14 @@ CSSOM.parse = function parse(token) {
2232
2457
  currentScope = parentRule || styleSheet;
2233
2458
  }
2234
2459
 
2235
- currentScope.cssRules.push(styleRule);
2460
+ if (styleRule.constructor.name === "CSSStyleRule" && !isValidSelectorText(styleRule.selectorText)) {
2461
+ if (styleRule === nestedSelectorRule) {
2462
+ nestedSelectorRule = null;
2463
+ }
2464
+ parseError('Invalid CSSStyleRule.selectorText');
2465
+ } else {
2466
+ currentScope.cssRules.push(styleRule);
2467
+ }
2236
2468
  buffer = "";
2237
2469
  if (currentScope.constructor === CSSOM.CSSKeyframesRule) {
2238
2470
  state = "keyframeRule-begin";
@@ -2254,8 +2486,14 @@ CSSOM.parse = function parse(token) {
2254
2486
  case "selector":
2255
2487
  // End of media/supports/document rule.
2256
2488
  if (!parentRule) {
2489
+ parseError("Unexpected }");
2490
+
2491
+ var hasPreviousStyleRule = currentScope.cssRules.length && currentScope.cssRules[currentScope.cssRules.length - 1].constructor.name === "CSSStyleRule";
2492
+ if (hasPreviousStyleRule) {
2493
+ i = ignoreBalancedBlock(i, token.slice(i), 1);
2494
+ }
2495
+
2257
2496
  break;
2258
- //parseError("Unexpected }");
2259
2497
  }
2260
2498
 
2261
2499
 
@@ -2284,7 +2522,7 @@ CSSOM.parse = function parse(token) {
2284
2522
  } else {
2285
2523
  prevScope = currentScope;
2286
2524
  currentScope = parentRule;
2287
- currentScope.cssRules.push(prevScope);
2525
+ currentScope !== prevScope && currentScope.cssRules.push(prevScope);
2288
2526
  break;
2289
2527
  }
2290
2528
  }
@@ -2,6 +2,28 @@
2
2
  var CSSOM = {};
3
3
  ///CommonJS
4
4
 
5
+ // NOTE: Check viability to add a validation for css values or use a dependency like csstree-validator
6
+ /**
7
+ * Regular expression to detect invalid characters in the value portion of a CSS style declaration.
8
+ *
9
+ * This regex matches a colon (:) that is not inside parentheses and not inside single or double quotes.
10
+ * It is used to ensure that the value part of a CSS property does not contain unexpected colons,
11
+ * which would indicate a malformed declaration (e.g., "color: foo:bar;" is invalid).
12
+ *
13
+ * The negative lookahead `(?![^(]*\))` ensures that the colon is not followed by a closing
14
+ * parenthesis without encountering an opening parenthesis, effectively ignoring colons inside
15
+ * function-like values (e.g., `url(data:image/png;base64,...)`).
16
+ *
17
+ * The lookahead `(?=(?:[^'"]|'[^']*'|"[^"]*")*$)` ensures that the colon is not inside single or double quotes,
18
+ * allowing colons within quoted strings (e.g., `content: ":";` or `background: url("foo:bar.png");`).
19
+ *
20
+ * Example:
21
+ * "color: red;" // valid, does not match
22
+ * "background: url(data:image/png;base64,...);" // valid, does not match
23
+ * "content: ':';" // valid, does not match
24
+ * "color: foo:bar;" // invalid, matches
25
+ */
26
+ var basicStylePropertyValueValidationRegExp = /:(?![^(]*\))(?=(?:[^'"]|'[^']*'|"[^"]*")*$)/;
5
27
 
6
28
  /**
7
29
  * @constructor
@@ -38,8 +60,12 @@ CSSOM.CSSStyleDeclaration.prototype = {
38
60
  * @param {string} [priority=null] "important" or null
39
61
  * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration-setProperty
40
62
  */
41
- setProperty: function(name, value, priority) {
42
- if (this[name]) {
63
+ setProperty: function(name, value, priority, parseErrorHandler)
64
+ {
65
+ // NOTE: Check viability to add a validation for css values or use a dependency like csstree-validator
66
+ if (basicStylePropertyValueValidationRegExp.test(value)) {
67
+ parseErrorHandler && parseErrorHandler('Invalid CSSStyleDeclaration property value');
68
+ } else if (this[name]) {
43
69
  // Property already exist. Overwrite it.
44
70
  var index = Array.prototype.indexOf.call(this, name);
45
71
  if (index < 0) {
package/lib/parse.js CHANGED
@@ -6,7 +6,8 @@ var CSSOM = {};
6
6
  /**
7
7
  * @param {string} token
8
8
  */
9
- CSSOM.parse = function parse(token) {
9
+ CSSOM.parse = function parse(token, errorHandler) {
10
+ errorHandler = errorHandler === undefined && (console && console.error);
10
11
 
11
12
  var i = 0;
12
13
 
@@ -28,6 +29,8 @@ CSSOM.parse = function parse(token) {
28
29
  var valueParenthesisDepth = 0;
29
30
 
30
31
  var SIGNIFICANT_WHITESPACE = {
32
+ "name": true,
33
+ "before-name": true,
31
34
  "selector": true,
32
35
  "value": true,
33
36
  "value-parenthesis": true,
@@ -132,6 +135,29 @@ CSSOM.parse = function parse(token) {
132
135
  return null;
133
136
  }
134
137
 
138
+ /**
139
+ * Advances the index `i` to skip over a balanced block of curly braces in the given string.
140
+ * This is typically used to ignore the contents of a CSS rule block.
141
+ *
142
+ * @param {number} i - The current index in the string to start searching from.
143
+ * @param {string} str - The string containing the CSS code.
144
+ * @param {number} fromIndex - The index in the string where the balanced block search should begin.
145
+ * @returns {number} The updated index after skipping the balanced block.
146
+ */
147
+ function ignoreBalancedBlock(i, str, fromIndex) {
148
+ var ruleClosingMatch = matchBalancedBlock(str, fromIndex);
149
+ if (ruleClosingMatch) {
150
+ var ignoreRange = ruleClosingMatch.index + ruleClosingMatch[0].length;
151
+ i+= ignoreRange;
152
+ if (token.charAt(i) === '}') {
153
+ i -= 1;
154
+ }
155
+ } else {
156
+ i += str.length;
157
+ }
158
+ return i;
159
+ }
160
+
135
161
  var parseError = function(message) {
136
162
  var lines = token.substring(0, i).split('\n');
137
163
  var lineCount = lines.length;
@@ -141,7 +167,12 @@ CSSOM.parse = function parse(token) {
141
167
  /* jshint sub : true */
142
168
  error['char'] = charCount;
143
169
  error.styleSheet = styleSheet;
144
- throw error;
170
+ // Print the error but continue parsing the sheet
171
+ try {
172
+ throw error;
173
+ } catch(e) {
174
+ errorHandler && errorHandler(e);
175
+ }
145
176
  };
146
177
 
147
178
  var validateAtRule = function(atRuleKey, validCallback, cannotBeNested) {
@@ -192,22 +223,169 @@ CSSOM.parse = function parse(token) {
192
223
  }
193
224
 
194
225
  // Ignore the entire rule block (if it's a statement it should ignore the statement plus the next block)
195
- var ruleClosingMatch = matchBalancedBlock(ruleSlice);
196
- if (ruleClosingMatch) {
197
- var ignoreRange = ruleClosingMatch.index + ruleClosingMatch[0].length;
198
- i+= ignoreRange;
199
- if (token.charAt(i) === '}') {
200
- i -= 1;
201
- }
202
- } else {
203
- i += ruleSlice.length;
204
- }
226
+ i = ignoreBalancedBlock(i, ruleSlice);
205
227
  state = "before-selector";
206
228
  } else {
207
229
  validCallback.call(this);
208
230
  }
209
231
  }
210
232
 
233
+ /**
234
+ * Regular expression to match a basic CSS selector.
235
+ *
236
+ * This regex matches the following selector components:
237
+ * - Type selectors (e.g., `div`, `span`)
238
+ * - Universal selector (`*`)
239
+ * - ID selectors (e.g., `#header`)
240
+ * - Class selectors (e.g., `.container`)
241
+ * - Attribute selectors (e.g., `[type="text"]`)
242
+ * - Pseudo-classes and pseudo-elements (e.g., `:hover`, `::before`, `:nth-child(2)`)
243
+ * - The parent selector (`&`)
244
+ * - Combinators (`>`, `+`, `~`) with optional whitespace
245
+ * - Whitespace (descendant combinator)
246
+ *
247
+ * The pattern ensures that a string consists only of valid basic selector components,
248
+ * possibly repeated and combined, but does not match full CSS selector groups separated by commas.
249
+ *
250
+ * @type {RegExp}
251
+ */
252
+ var basicSelectorRegExp = /^([a-zA-Z][a-zA-Z0-9_-]*|\*|#[a-zA-Z0-9_-]+|\.[a-zA-Z0-9_-]+|\[[^\[\]]*(?:\s+[iI])?\]|::?[a-zA-Z0-9_-]+(?:\([^\(\)]*\))?|&|\s*[>+~]\s*|\s+)+$/;
253
+
254
+ /**
255
+ * Regular expression to match CSS pseudo-classes with arguments.
256
+ *
257
+ * Matches patterns like `:pseudo-class(argument)`, capturing the pseudo-class name and its argument.
258
+ *
259
+ * Capture groups:
260
+ * 1. The pseudo-class name (letters and hyphens).
261
+ * 2. The argument inside the parentheses (any characters except a closing parenthesis).
262
+ *
263
+ * Global flag (`g`) is used to find all matches in the input string.
264
+ *
265
+ * Example match: `:nth-child(2n+1)`
266
+ * - Group 1: "nth-child"
267
+ * - Group 2: "2n+1"
268
+ * @type {RegExp}
269
+ */
270
+ var globalPseudoClassRegExp = /:([a-zA-Z-]+)\(([^)]*?(?:\s+[iI])?)\)/g;
271
+
272
+ /**
273
+ * Parses a CSS selector string and splits it into parts, handling nested parentheses.
274
+ *
275
+ * This function is useful for splitting selectors that may contain nested function-like
276
+ * syntax (e.g., :not(.foo, .bar)), ensuring that commas inside parentheses do not split
277
+ * the selector.
278
+ *
279
+ * @param {string} selector - The CSS selector string to parse.
280
+ * @returns {string[]} An array of selector parts, split by top-level commas, with whitespace trimmed.
281
+ */
282
+ function parseAndSplitNestedSelectors(selector) {
283
+ var depth = 0;
284
+ var buffer = "";
285
+ var parts = [];
286
+ var inSingleQuote = false;
287
+ var inDoubleQuote = false;
288
+ var i, char;
289
+
290
+ for (i = 0; i < selector.length; i++) {
291
+ char = selector.charAt(i);
292
+
293
+ if (char === "'" && !inDoubleQuote) {
294
+ inSingleQuote = !inSingleQuote;
295
+ buffer += char;
296
+ } else if (char === '"' && !inSingleQuote) {
297
+ inDoubleQuote = !inDoubleQuote;
298
+ buffer += char;
299
+ } else if (!inSingleQuote && !inDoubleQuote) {
300
+ if (char === '(') {
301
+ depth++;
302
+ buffer += char;
303
+ } else if (char === ')') {
304
+ depth--;
305
+ buffer += char;
306
+ if (depth === 0) {
307
+ parts.push(buffer.replace(/^\s+|\s+$/g, ""));
308
+ buffer = "";
309
+ }
310
+ } else if (char === ',' && depth === 0) {
311
+ if (buffer.replace(/^\s+|\s+$/g, "")) {
312
+ parts.push(buffer.replace(/^\s+|\s+$/g, ""));
313
+ }
314
+ buffer = "";
315
+ } else {
316
+ buffer += char;
317
+ }
318
+ } else {
319
+ buffer += char;
320
+ }
321
+ }
322
+
323
+ if (buffer.replace(/^\s+|\s+$/g, "")) {
324
+ parts.push(buffer.replace(/^\s+|\s+$/g, ""));
325
+ }
326
+
327
+ return parts;
328
+ }
329
+
330
+ /**
331
+ * Validates a CSS selector string, including handling of nested selectors within certain pseudo-classes.
332
+ *
333
+ * This function checks if the provided selector is valid according to the rules defined by
334
+ * `basicSelectorRegExp`. For pseudo-classes that accept selector lists (such as :not, :is, :has, :where),
335
+ * it recursively validates each nested selector using the same validation logic.
336
+ *
337
+ * @param {string} selector - The CSS selector string to validate.
338
+ * @returns {boolean} Returns `true` if the selector is valid, otherwise `false`.
339
+ */
340
+ function validateSelector(selector) {
341
+ var match, nestedSelectors, i;
342
+
343
+ // Only pseudo-classes that accept selector lists should recurse
344
+ var selectorListPseudoClasses = {
345
+ 'not': true,
346
+ 'is': true,
347
+ 'has': true,
348
+ 'where': true
349
+ };
350
+
351
+ // Reset regex lastIndex for global regex in ES5 loop
352
+ var pseudoClassRegExp = new RegExp(globalPseudoClassRegExp.source, globalPseudoClassRegExp.flags);
353
+ while ((match = pseudoClassRegExp.exec(selector)) !== null) {
354
+ var pseudoClass = match[1];
355
+ if (selectorListPseudoClasses.hasOwnProperty(pseudoClass)) {
356
+ nestedSelectors = parseAndSplitNestedSelectors(match[2]);
357
+ // Validate each nested selector
358
+ for (i = 0; i < nestedSelectors.length; i++) {
359
+ if (!validateSelector(nestedSelectors[i])) {
360
+ return false;
361
+ }
362
+ }
363
+ }
364
+ }
365
+
366
+ // Allow "&" anywhere in the selector for nested selectors
367
+ return basicSelectorRegExp.test(selector);
368
+ }
369
+
370
+ /**
371
+ * Checks if a given CSS selector text is valid by splitting it by commas
372
+ * and validating each individual selector using the `validateSelector` function.
373
+ *
374
+ * @param {string} selectorText - The CSS selector text to validate. Can contain multiple selectors separated by commas.
375
+ * @returns {boolean} Returns true if all selectors are valid, otherwise false.
376
+ */
377
+ function isValidSelectorText(selectorText) {
378
+ // Split selectorText by commas and validate each part
379
+ var selectors = parseAndSplitNestedSelectors(selectorText);
380
+ for (var i = 0; i < selectors.length; i++) {
381
+ var processedSelectors = selectors[i].replace(/^\s+|\s+$/g, "");
382
+ if (!validateSelector(processedSelectors)) {
383
+ return false;
384
+ }
385
+ }
386
+ return true;
387
+ }
388
+
211
389
  var endingIndex = token.length - 1;
212
390
 
213
391
  for (var character; (character = token.charAt(i)); i++) {
@@ -240,6 +418,9 @@ CSSOM.parse = function parse(token) {
240
418
  parseError('Unmatched "');
241
419
  }
242
420
  } while (token[index - 2] === '\\');
421
+ if (index === 0) {
422
+ break;
423
+ }
243
424
  buffer += token.slice(i, index);
244
425
  i = index - 1;
245
426
  switch (state) {
@@ -263,6 +444,9 @@ CSSOM.parse = function parse(token) {
263
444
  parseError("Unmatched '");
264
445
  }
265
446
  } while (token[index - 2] === '\\');
447
+ if (index === 0) {
448
+ break;
449
+ }
266
450
  buffer += token.slice(i, index);
267
451
  i = index - 1;
268
452
  switch (state) {
@@ -398,6 +582,11 @@ CSSOM.parse = function parse(token) {
398
582
  if (currentScope === styleSheet) {
399
583
  nestedSelectorRule = null;
400
584
  }
585
+ if (state === 'before-selector') {
586
+ parseError("Unexpected {");
587
+ i = ignoreBalancedBlock(i, token.slice(i));
588
+ break;
589
+ }
401
590
  if (state === "selector" || state === "atRule") {
402
591
  if (!nestedSelectorRule && buffer.indexOf(";") !== -1) {
403
592
  var ruleClosingMatch = token.slice(i).match(forwardRuleClosingBraceRegExp);
@@ -527,7 +716,7 @@ CSSOM.parse = function parse(token) {
527
716
  documentRule.parentStyleSheet = styleSheet;
528
717
  buffer = "";
529
718
  state = "before-selector";
530
- } else if (state === "name") {
719
+ } else if (state === "before-name" || state === "name") {
531
720
  if (styleRule.constructor.name === "CSSNestedDeclarations") {
532
721
  if (styleRule.style.length) {
533
722
  parentRule.cssRules.push(styleRule);
@@ -544,9 +733,11 @@ CSSOM.parse = function parse(token) {
544
733
  styleRule.parentStyleSheet = styleSheet;
545
734
  }
546
735
 
547
-
548
736
  styleRule = new CSSOM.CSSStyleRule();
549
- styleRule.selectorText = buffer.trim();
737
+ // In a nested selector, ensure each selector contains '&' at the beginning, except for selectors that already have '&' somewhere
738
+ styleRule.selectorText = parseAndSplitNestedSelectors(buffer.trim()).map(function(sel) {
739
+ return sel.indexOf('&') === -1 ? '& ' + sel : sel;
740
+ }).join(', ');
550
741
  styleRule.style.__starts = i - buffer.length;
551
742
  styleRule.parentRule = parentRule;
552
743
  nestedSelectorRule = styleRule;
@@ -621,8 +812,14 @@ CSSOM.parse = function parse(token) {
621
812
 
622
813
  case ";":
623
814
  switch (state) {
815
+ case "before-value":
816
+ case "before-name":
817
+ parseError("Unexpected ;");
818
+ buffer = "";
819
+ state = "before-name";
820
+ break;
624
821
  case "value":
625
- styleRule.style.setProperty(name, buffer.trim(), priority);
822
+ styleRule.style.setProperty(name, buffer.trim(), priority, parseError);
626
823
  priority = "";
627
824
  buffer = "";
628
825
  state = "before-name";
@@ -672,9 +869,10 @@ CSSOM.parse = function parse(token) {
672
869
  case "}":
673
870
  switch (state) {
674
871
  case "value":
675
- styleRule.style.setProperty(name, buffer.trim(), priority);
872
+ styleRule.style.setProperty(name, buffer.trim(), priority, parseError);
676
873
  priority = "";
677
874
  /* falls through */
875
+ case "before-value":
678
876
  case "before-name":
679
877
  case "name":
680
878
  styleRule.__ends = i + 1;
@@ -692,7 +890,14 @@ CSSOM.parse = function parse(token) {
692
890
  currentScope = parentRule || styleSheet;
693
891
  }
694
892
 
695
- currentScope.cssRules.push(styleRule);
893
+ if (styleRule.constructor.name === "CSSStyleRule" && !isValidSelectorText(styleRule.selectorText)) {
894
+ if (styleRule === nestedSelectorRule) {
895
+ nestedSelectorRule = null;
896
+ }
897
+ parseError('Invalid CSSStyleRule.selectorText');
898
+ } else {
899
+ currentScope.cssRules.push(styleRule);
900
+ }
696
901
  buffer = "";
697
902
  if (currentScope.constructor === CSSOM.CSSKeyframesRule) {
698
903
  state = "keyframeRule-begin";
@@ -714,8 +919,14 @@ CSSOM.parse = function parse(token) {
714
919
  case "selector":
715
920
  // End of media/supports/document rule.
716
921
  if (!parentRule) {
922
+ parseError("Unexpected }");
923
+
924
+ var hasPreviousStyleRule = currentScope.cssRules.length && currentScope.cssRules[currentScope.cssRules.length - 1].constructor.name === "CSSStyleRule";
925
+ if (hasPreviousStyleRule) {
926
+ i = ignoreBalancedBlock(i, token.slice(i), 1);
927
+ }
928
+
717
929
  break;
718
- //parseError("Unexpected }");
719
930
  }
720
931
 
721
932
 
@@ -744,7 +955,7 @@ CSSOM.parse = function parse(token) {
744
955
  } else {
745
956
  prevScope = currentScope;
746
957
  currentScope = parentRule;
747
- currentScope.cssRules.push(prevScope);
958
+ currentScope !== prevScope && currentScope.cssRules.push(prevScope);
748
959
  break;
749
960
  }
750
961
  }
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "parser",
8
8
  "styleSheet"
9
9
  ],
10
- "version": "0.9.1",
10
+ "version": "0.9.3",
11
11
  "author": "Nikita Vasilyev <me@elv1s.ru>",
12
12
  "contributors": [
13
13
  "Acemir Sousa Mendes <acemirsm@gmail.com>"