@acemir/cssom 0.9.1 → 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,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,156 @@ 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_-]+|\[[^\[\]]*\]|::?[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);
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
+
1751
1943
  var endingIndex = token.length - 1;
1752
1944
 
1753
1945
  for (var character; (character = token.charAt(i)); i++) {
@@ -1780,6 +1972,9 @@ CSSOM.parse = function parse(token) {
1780
1972
  parseError('Unmatched "');
1781
1973
  }
1782
1974
  } while (token[index - 2] === '\\');
1975
+ if (index === 0) {
1976
+ break;
1977
+ }
1783
1978
  buffer += token.slice(i, index);
1784
1979
  i = index - 1;
1785
1980
  switch (state) {
@@ -1803,6 +1998,9 @@ CSSOM.parse = function parse(token) {
1803
1998
  parseError("Unmatched '");
1804
1999
  }
1805
2000
  } while (token[index - 2] === '\\');
2001
+ if (index === 0) {
2002
+ break;
2003
+ }
1806
2004
  buffer += token.slice(i, index);
1807
2005
  i = index - 1;
1808
2006
  switch (state) {
@@ -1938,6 +2136,11 @@ CSSOM.parse = function parse(token) {
1938
2136
  if (currentScope === styleSheet) {
1939
2137
  nestedSelectorRule = null;
1940
2138
  }
2139
+ if (state === 'before-selector') {
2140
+ parseError("Unexpected {");
2141
+ i = ignoreBalancedBlock(i, token.slice(i));
2142
+ break;
2143
+ }
1941
2144
  if (state === "selector" || state === "atRule") {
1942
2145
  if (!nestedSelectorRule && buffer.indexOf(";") !== -1) {
1943
2146
  var ruleClosingMatch = token.slice(i).match(forwardRuleClosingBraceRegExp);
@@ -1956,6 +2159,7 @@ CSSOM.parse = function parse(token) {
1956
2159
  }
1957
2160
 
1958
2161
  currentScope = parentRule = styleRule;
2162
+ console.log('sel out', buffer);
1959
2163
  styleRule.selectorText = buffer.trim();
1960
2164
  styleRule.style.__starts = i;
1961
2165
  styleRule.parentStyleSheet = styleSheet;
@@ -2067,7 +2271,7 @@ CSSOM.parse = function parse(token) {
2067
2271
  documentRule.parentStyleSheet = styleSheet;
2068
2272
  buffer = "";
2069
2273
  state = "before-selector";
2070
- } else if (state === "name") {
2274
+ } else if (state === "before-name" || state === "name") {
2071
2275
  if (styleRule.constructor.name === "CSSNestedDeclarations") {
2072
2276
  if (styleRule.style.length) {
2073
2277
  parentRule.cssRules.push(styleRule);
@@ -2084,9 +2288,12 @@ CSSOM.parse = function parse(token) {
2084
2288
  styleRule.parentStyleSheet = styleSheet;
2085
2289
  }
2086
2290
 
2087
-
2088
2291
  styleRule = new CSSOM.CSSStyleRule();
2089
- 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(', ');
2090
2297
  styleRule.style.__starts = i - buffer.length;
2091
2298
  styleRule.parentRule = parentRule;
2092
2299
  nestedSelectorRule = styleRule;
@@ -2161,8 +2368,14 @@ CSSOM.parse = function parse(token) {
2161
2368
 
2162
2369
  case ";":
2163
2370
  switch (state) {
2371
+ case "before-value":
2372
+ case "before-name":
2373
+ parseError("Unexpected ;");
2374
+ buffer = "";
2375
+ state = "before-name";
2376
+ break;
2164
2377
  case "value":
2165
- styleRule.style.setProperty(name, buffer.trim(), priority);
2378
+ styleRule.style.setProperty(name, buffer.trim(), priority, parseError);
2166
2379
  priority = "";
2167
2380
  buffer = "";
2168
2381
  state = "before-name";
@@ -2212,9 +2425,10 @@ CSSOM.parse = function parse(token) {
2212
2425
  case "}":
2213
2426
  switch (state) {
2214
2427
  case "value":
2215
- styleRule.style.setProperty(name, buffer.trim(), priority);
2428
+ styleRule.style.setProperty(name, buffer.trim(), priority, parseError);
2216
2429
  priority = "";
2217
2430
  /* falls through */
2431
+ case "before-value":
2218
2432
  case "before-name":
2219
2433
  case "name":
2220
2434
  styleRule.__ends = i + 1;
@@ -2232,7 +2446,14 @@ CSSOM.parse = function parse(token) {
2232
2446
  currentScope = parentRule || styleSheet;
2233
2447
  }
2234
2448
 
2235
- 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
+ }
2236
2457
  buffer = "";
2237
2458
  if (currentScope.constructor === CSSOM.CSSKeyframesRule) {
2238
2459
  state = "keyframeRule-begin";
@@ -2254,8 +2475,14 @@ CSSOM.parse = function parse(token) {
2254
2475
  case "selector":
2255
2476
  // End of media/supports/document rule.
2256
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
+
2257
2485
  break;
2258
- //parseError("Unexpected }");
2259
2486
  }
2260
2487
 
2261
2488
 
@@ -2284,7 +2511,7 @@ CSSOM.parse = function parse(token) {
2284
2511
  } else {
2285
2512
  prevScope = currentScope;
2286
2513
  currentScope = parentRule;
2287
- currentScope.cssRules.push(prevScope);
2514
+ currentScope !== prevScope && currentScope.cssRules.push(prevScope);
2288
2515
  break;
2289
2516
  }
2290
2517
  }
@@ -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,156 @@ 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_-]+|\[[^\[\]]*\]|::?[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-]+)\(([^)]*)\)/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 parseNestedSelectors(selector) {
283
+ var depth = 0;
284
+ var buffer = "";
285
+ var parts = [];
286
+ var i, char;
287
+
288
+ for (i = 0; i < selector.length; i++) {
289
+ char = selector.charAt(i);
290
+
291
+ if (char === '(') {
292
+ depth++;
293
+ buffer += char;
294
+ } else if (char === ')') {
295
+ depth--;
296
+ buffer += char;
297
+ if (depth === 0) {
298
+ parts.push(buffer.replace(/^\s+|\s+$/g, ""));
299
+ buffer = "";
300
+ }
301
+ } else if (char === ',' && depth === 0) {
302
+ if (buffer.replace(/^\s+|\s+$/g, "")) {
303
+ parts.push(buffer.replace(/^\s+|\s+$/g, ""));
304
+ }
305
+ buffer = "";
306
+ } else {
307
+ buffer += char;
308
+ }
309
+ }
310
+
311
+ if (buffer.replace(/^\s+|\s+$/g, "")) {
312
+ parts.push(buffer.replace(/^\s+|\s+$/g, ""));
313
+ }
314
+
315
+ return parts;
316
+ }
317
+
318
+ /**
319
+ * Validates a CSS selector string, including handling of nested selectors within certain pseudo-classes.
320
+ *
321
+ * This function checks if the provided selector is valid according to the rules defined by
322
+ * `basicSelectorRegExp`. For pseudo-classes that accept selector lists (such as :not, :is, :has, :where),
323
+ * it recursively validates each nested selector using the same validation logic.
324
+ *
325
+ * @param {string} selector - The CSS selector string to validate.
326
+ * @returns {boolean} Returns `true` if the selector is valid, otherwise `false`.
327
+ */
328
+ function validateSelector(selector) {
329
+ var match, nestedSelectors, i;
330
+
331
+ // Only pseudo-classes that accept selector lists should recurse
332
+ var selectorListPseudoClasses = {
333
+ 'not': true,
334
+ 'is': true,
335
+ 'has': true,
336
+ 'where': true
337
+ };
338
+
339
+ // Reset regex lastIndex for global regex in ES5 loop
340
+ var pseudoClassRegExp = new RegExp(globalPseudoClassRegExp.source, globalPseudoClassRegExp.flags);
341
+ while ((match = pseudoClassRegExp.exec(selector)) !== null) {
342
+ var pseudoClass = match[1];
343
+ if (selectorListPseudoClasses.hasOwnProperty(pseudoClass)) {
344
+ nestedSelectors = parseNestedSelectors(match[2]);
345
+ // Validate each nested selector
346
+ for (i = 0; i < nestedSelectors.length; i++) {
347
+ if (!validateSelector(nestedSelectors[i])) {
348
+ return false;
349
+ }
350
+ }
351
+ }
352
+ }
353
+
354
+ // Allow "&" anywhere in the selector for nested selectors
355
+ return basicSelectorRegExp.test(selector);
356
+ }
357
+
358
+ /**
359
+ * Checks if a given CSS selector text is valid by splitting it by commas
360
+ * and validating each individual selector using the `validateSelector` function.
361
+ *
362
+ * @param {string} selectorText - The CSS selector text to validate. Can contain multiple selectors separated by commas.
363
+ * @returns {boolean} Returns true if all selectors are valid, otherwise false.
364
+ */
365
+ function isValidSelectorText(selectorText) {
366
+ // Split selectorText by commas and validate each part
367
+ var selectors = selectorText.split(',');
368
+ for (var i = 0; i < selectors.length; i++) {
369
+ if (!validateSelector(selectors[i].replace(/^\s+|\s+$/g, ""))) {
370
+ return false;
371
+ }
372
+ }
373
+ return true;
374
+ }
375
+
211
376
  var endingIndex = token.length - 1;
212
377
 
213
378
  for (var character; (character = token.charAt(i)); i++) {
@@ -240,6 +405,9 @@ CSSOM.parse = function parse(token) {
240
405
  parseError('Unmatched "');
241
406
  }
242
407
  } while (token[index - 2] === '\\');
408
+ if (index === 0) {
409
+ break;
410
+ }
243
411
  buffer += token.slice(i, index);
244
412
  i = index - 1;
245
413
  switch (state) {
@@ -263,6 +431,9 @@ CSSOM.parse = function parse(token) {
263
431
  parseError("Unmatched '");
264
432
  }
265
433
  } while (token[index - 2] === '\\');
434
+ if (index === 0) {
435
+ break;
436
+ }
266
437
  buffer += token.slice(i, index);
267
438
  i = index - 1;
268
439
  switch (state) {
@@ -398,6 +569,11 @@ CSSOM.parse = function parse(token) {
398
569
  if (currentScope === styleSheet) {
399
570
  nestedSelectorRule = null;
400
571
  }
572
+ if (state === 'before-selector') {
573
+ parseError("Unexpected {");
574
+ i = ignoreBalancedBlock(i, token.slice(i));
575
+ break;
576
+ }
401
577
  if (state === "selector" || state === "atRule") {
402
578
  if (!nestedSelectorRule && buffer.indexOf(";") !== -1) {
403
579
  var ruleClosingMatch = token.slice(i).match(forwardRuleClosingBraceRegExp);
@@ -416,6 +592,7 @@ CSSOM.parse = function parse(token) {
416
592
  }
417
593
 
418
594
  currentScope = parentRule = styleRule;
595
+ console.log('sel out', buffer);
419
596
  styleRule.selectorText = buffer.trim();
420
597
  styleRule.style.__starts = i;
421
598
  styleRule.parentStyleSheet = styleSheet;
@@ -527,7 +704,7 @@ CSSOM.parse = function parse(token) {
527
704
  documentRule.parentStyleSheet = styleSheet;
528
705
  buffer = "";
529
706
  state = "before-selector";
530
- } else if (state === "name") {
707
+ } else if (state === "before-name" || state === "name") {
531
708
  if (styleRule.constructor.name === "CSSNestedDeclarations") {
532
709
  if (styleRule.style.length) {
533
710
  parentRule.cssRules.push(styleRule);
@@ -544,9 +721,12 @@ CSSOM.parse = function parse(token) {
544
721
  styleRule.parentStyleSheet = styleSheet;
545
722
  }
546
723
 
547
-
548
724
  styleRule = new CSSOM.CSSStyleRule();
549
- styleRule.selectorText = buffer.trim();
725
+ console.log('sel in', buffer);
726
+ // In a nested selector, ensure each selector contains '&' at the beginning, except for selectors that already have '&' somewhere
727
+ styleRule.selectorText = parseNestedSelectors(buffer.trim()).map(function(sel) {
728
+ return sel.indexOf('&') === -1 ? '& ' + sel : sel;
729
+ }).join(', ');
550
730
  styleRule.style.__starts = i - buffer.length;
551
731
  styleRule.parentRule = parentRule;
552
732
  nestedSelectorRule = styleRule;
@@ -621,8 +801,14 @@ CSSOM.parse = function parse(token) {
621
801
 
622
802
  case ";":
623
803
  switch (state) {
804
+ case "before-value":
805
+ case "before-name":
806
+ parseError("Unexpected ;");
807
+ buffer = "";
808
+ state = "before-name";
809
+ break;
624
810
  case "value":
625
- styleRule.style.setProperty(name, buffer.trim(), priority);
811
+ styleRule.style.setProperty(name, buffer.trim(), priority, parseError);
626
812
  priority = "";
627
813
  buffer = "";
628
814
  state = "before-name";
@@ -672,9 +858,10 @@ CSSOM.parse = function parse(token) {
672
858
  case "}":
673
859
  switch (state) {
674
860
  case "value":
675
- styleRule.style.setProperty(name, buffer.trim(), priority);
861
+ styleRule.style.setProperty(name, buffer.trim(), priority, parseError);
676
862
  priority = "";
677
863
  /* falls through */
864
+ case "before-value":
678
865
  case "before-name":
679
866
  case "name":
680
867
  styleRule.__ends = i + 1;
@@ -692,7 +879,14 @@ CSSOM.parse = function parse(token) {
692
879
  currentScope = parentRule || styleSheet;
693
880
  }
694
881
 
695
- currentScope.cssRules.push(styleRule);
882
+ if (styleRule.constructor.name === "CSSStyleRule" && !isValidSelectorText(styleRule.selectorText)) {
883
+ if (styleRule === nestedSelectorRule) {
884
+ nestedSelectorRule = null;
885
+ }
886
+ parseError('Invalid CSSStyleRule.selectorText');
887
+ } else {
888
+ currentScope.cssRules.push(styleRule);
889
+ }
696
890
  buffer = "";
697
891
  if (currentScope.constructor === CSSOM.CSSKeyframesRule) {
698
892
  state = "keyframeRule-begin";
@@ -714,8 +908,14 @@ CSSOM.parse = function parse(token) {
714
908
  case "selector":
715
909
  // End of media/supports/document rule.
716
910
  if (!parentRule) {
911
+ parseError("Unexpected }");
912
+
913
+ var hasPreviousStyleRule = currentScope.cssRules.length && currentScope.cssRules[currentScope.cssRules.length - 1].constructor.name === "CSSStyleRule";
914
+ if (hasPreviousStyleRule) {
915
+ i = ignoreBalancedBlock(i, token.slice(i), 1);
916
+ }
917
+
717
918
  break;
718
- //parseError("Unexpected }");
719
919
  }
720
920
 
721
921
 
@@ -744,7 +944,7 @@ CSSOM.parse = function parse(token) {
744
944
  } else {
745
945
  prevScope = currentScope;
746
946
  currentScope = parentRule;
747
- currentScope.cssRules.push(prevScope);
947
+ currentScope !== prevScope && currentScope.cssRules.push(prevScope);
748
948
  break;
749
949
  }
750
950
  }
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.2",
11
11
  "author": "Nikita Vasilyev <me@elv1s.ru>",
12
12
  "contributors": [
13
13
  "Acemir Sousa Mendes <acemirsm@gmail.com>"