@acemir/cssom 0.9.5 → 0.9.6

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.
Files changed (3) hide show
  1. package/build/CSSOM.js +104 -28
  2. package/lib/parse.js +104 -28
  3. package/package.json +1 -1
package/build/CSSOM.js CHANGED
@@ -1798,13 +1798,14 @@ CSSOM.parse = function parse(token, errorHandler) {
1798
1798
  }
1799
1799
 
1800
1800
  /**
1801
- * Regular expression to match a basic CSS selector.
1802
- *
1803
- * This regex matches the following selector components:
1801
+ * Validates a basic CSS selector, allowing for deeply nested balanced parentheses in pseudo-classes.
1802
+ * This function replaces the previous basicSelectorRegExp.
1803
+ *
1804
+ * This function matches:
1804
1805
  * - Type selectors (e.g., `div`, `span`)
1805
1806
  * - Universal selector (`*`)
1806
- * - ID selectors (e.g., `#header`)
1807
- * - Class selectors (e.g., `.container`)
1807
+ * - ID selectors (e.g., `#header`, `#a\ b`, `#åèiöú`)
1808
+ * - Class selectors (e.g., `.container`, `.a\ b`, `.åèiöú`)
1808
1809
  * - Attribute selectors (e.g., `[type="text"]`)
1809
1810
  * - Pseudo-classes and pseudo-elements (e.g., `:hover`, `::before`, `:nth-child(2)`)
1810
1811
  * - Pseudo-classes with nested parentheses, including cases where parentheses are nested inside arguments,
@@ -1813,13 +1814,66 @@ CSSOM.parse = function parse(token, errorHandler) {
1813
1814
  * - Combinators (`>`, `+`, `~`) with optional whitespace
1814
1815
  * - Whitespace (descendant combinator)
1815
1816
  *
1816
- * The pattern ensures that a string consists only of valid basic selector components,
1817
- * possibly repeated and combined, including pseudo-classes with nested parentheses,
1818
- * but does not match full CSS selector groups separated by commas.
1817
+ * Unicode and escape sequences are allowed in identifiers.
1819
1818
  *
1820
- * @type {RegExp}
1819
+ * @param {string} selector
1820
+ * @returns {boolean}
1821
1821
  */
1822
- 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+)+$/;
1822
+ function basicSelectorValidator(selector) {
1823
+ var length = selector.length;
1824
+ var i = 0;
1825
+ var stack = [];
1826
+ var inAttr = false;
1827
+ var inSingleQuote = false;
1828
+ var inDoubleQuote = false;
1829
+
1830
+ while (i < length) {
1831
+ var char = selector[i];
1832
+
1833
+ if (inSingleQuote) {
1834
+ if (char === "'" && selector[i - 1] !== "\\") {
1835
+ inSingleQuote = false;
1836
+ }
1837
+ } else if (inDoubleQuote) {
1838
+ if (char === '"' && selector[i - 1] !== "\\") {
1839
+ inDoubleQuote = false;
1840
+ }
1841
+ } else if (inAttr) {
1842
+ if (char === "]") {
1843
+ inAttr = false;
1844
+ } else if (char === "'") {
1845
+ inSingleQuote = true;
1846
+ } else if (char === '"') {
1847
+ inDoubleQuote = true;
1848
+ }
1849
+ } else {
1850
+ if (char === "[") {
1851
+ inAttr = true;
1852
+ } else if (char === "'") {
1853
+ inSingleQuote = true;
1854
+ } else if (char === '"') {
1855
+ inDoubleQuote = true;
1856
+ } else if (char === "(") {
1857
+ stack.push("(");
1858
+ } else if (char === ")") {
1859
+ if (!stack.length || stack.pop() !== "(") {
1860
+ return false;
1861
+ }
1862
+ }
1863
+ }
1864
+ i++;
1865
+ }
1866
+
1867
+ // If any stack or quote/attr context remains, it's invalid
1868
+ if (stack.length || inAttr || inSingleQuote || inDoubleQuote) {
1869
+ return false;
1870
+ }
1871
+
1872
+ // Fallback to a loose regexp for the overall selector structure (without deep paren matching)
1873
+ // This is similar to the original, but without nested paren limitations
1874
+ var looseSelectorRegExp = /^([a-zA-Z_\u00A0-\uFFFF\\][a-zA-Z0-9_\u00A0-\uFFFF\-\\]*|\*|#[a-zA-Z0-9_\u00A0-\uFFFF\-\\]+|\.[a-zA-Z0-9_\u00A0-\uFFFF\-\\]+|\[[^\[\]]*(?:\s+[iI])?\]|::?[a-zA-Z0-9_\u00A0-\uFFFF\-\\]+(?:\((.*)\))?|&|\s*[>+~]\s*|\s+)+$/;
1875
+ return looseSelectorRegExp.test(selector);
1876
+ }
1823
1877
 
1824
1878
  /**
1825
1879
  * Regular expression to match CSS pseudo-classes with arguments.
@@ -1902,40 +1956,62 @@ CSSOM.parse = function parse(token, errorHandler) {
1902
1956
  * Validates a CSS selector string, including handling of nested selectors within certain pseudo-classes.
1903
1957
  *
1904
1958
  * This function checks if the provided selector is valid according to the rules defined by
1905
- * `basicSelectorRegExp`. For pseudo-classes that accept selector lists (such as :not, :is, :has, :where),
1959
+ * `basicSelectorValidator`. For pseudo-classes that accept selector lists (such as :not, :is, :has, :where),
1906
1960
  * it recursively validates each nested selector using the same validation logic.
1907
1961
  *
1908
1962
  * @param {string} selector - The CSS selector string to validate.
1909
1963
  * @returns {boolean} Returns `true` if the selector is valid, otherwise `false`.
1910
1964
  */
1965
+
1966
+ // Cache to store validated selectors
1967
+ var validatedSelectorsCache = new Map();
1968
+
1969
+ // Only pseudo-classes that accept selector lists should recurse
1970
+ var selectorListPseudoClasses = {
1971
+ 'not': true,
1972
+ 'is': true,
1973
+ 'has': true,
1974
+ 'where': true
1975
+ };
1976
+
1911
1977
  function validateSelector(selector) {
1912
- var match, nestedSelectors, i;
1913
-
1914
- // Only pseudo-classes that accept selector lists should recurse
1915
- var selectorListPseudoClasses = {
1916
- 'not': true,
1917
- 'is': true,
1918
- 'has': true,
1919
- 'where': true
1920
- };
1978
+ if (validatedSelectorsCache.has(selector)) {
1979
+ return validatedSelectorsCache.get(selector);
1980
+ }
1921
1981
 
1922
- // Reset regex lastIndex for global regex in ES5 loop
1982
+ // Use a non-global regex to find all pseudo-classes with arguments
1983
+ var pseudoClassMatches = [];
1923
1984
  var pseudoClassRegExp = new RegExp(globalPseudoClassRegExp.source, globalPseudoClassRegExp.flags);
1985
+ var match;
1924
1986
  while ((match = pseudoClassRegExp.exec(selector)) !== null) {
1925
- var pseudoClass = match[1];
1987
+ pseudoClassMatches.push(match);
1988
+ }
1989
+
1990
+ for (var j = 0; j < pseudoClassMatches.length; j++) {
1991
+ var pseudoClass = pseudoClassMatches[j][1];
1926
1992
  if (selectorListPseudoClasses.hasOwnProperty(pseudoClass)) {
1927
- nestedSelectors = parseAndSplitNestedSelectors(match[2]);
1928
- // Validate each nested selector
1929
- for (i = 0; i < nestedSelectors.length; i++) {
1930
- if (!validateSelector(nestedSelectors[i])) {
1993
+ var nestedSelectors = parseAndSplitNestedSelectors(pseudoClassMatches[j][2]);
1994
+ for (var i = 0; i < nestedSelectors.length; i++) {
1995
+ var nestedSelector = nestedSelectors[i];
1996
+ if (!validatedSelectorsCache.has(nestedSelector)) {
1997
+ var nestedSelectorValidation = validateSelector(nestedSelector);
1998
+ validatedSelectorsCache.set(nestedSelector, nestedSelectorValidation);
1999
+ if (!nestedSelectorValidation) {
2000
+ validatedSelectorsCache.set(selector, false);
2001
+ return false;
2002
+ }
2003
+ } else if (!validatedSelectorsCache.get(nestedSelector)) {
2004
+ validatedSelectorsCache.set(selector, false);
1931
2005
  return false;
1932
2006
  }
1933
2007
  }
1934
2008
  }
1935
2009
  }
1936
2010
 
1937
- // Allow "&" anywhere in the selector for nested selectors
1938
- return basicSelectorRegExp.test(selector);
2011
+ var basicSelectorValidation = basicSelectorValidator(selector);
2012
+ validatedSelectorsCache.set(selector, basicSelectorValidation);
2013
+
2014
+ return basicSelectorValidation;
1939
2015
  }
1940
2016
 
1941
2017
  /**
package/lib/parse.js CHANGED
@@ -231,13 +231,14 @@ CSSOM.parse = function parse(token, errorHandler) {
231
231
  }
232
232
 
233
233
  /**
234
- * Regular expression to match a basic CSS selector.
235
- *
236
- * This regex matches the following selector components:
234
+ * Validates a basic CSS selector, allowing for deeply nested balanced parentheses in pseudo-classes.
235
+ * This function replaces the previous basicSelectorRegExp.
236
+ *
237
+ * This function matches:
237
238
  * - Type selectors (e.g., `div`, `span`)
238
239
  * - Universal selector (`*`)
239
- * - ID selectors (e.g., `#header`)
240
- * - Class selectors (e.g., `.container`)
240
+ * - ID selectors (e.g., `#header`, `#a\ b`, `#åèiöú`)
241
+ * - Class selectors (e.g., `.container`, `.a\ b`, `.åèiöú`)
241
242
  * - Attribute selectors (e.g., `[type="text"]`)
242
243
  * - Pseudo-classes and pseudo-elements (e.g., `:hover`, `::before`, `:nth-child(2)`)
243
244
  * - Pseudo-classes with nested parentheses, including cases where parentheses are nested inside arguments,
@@ -246,13 +247,66 @@ CSSOM.parse = function parse(token, errorHandler) {
246
247
  * - Combinators (`>`, `+`, `~`) with optional whitespace
247
248
  * - Whitespace (descendant combinator)
248
249
  *
249
- * The pattern ensures that a string consists only of valid basic selector components,
250
- * possibly repeated and combined, including pseudo-classes with nested parentheses,
251
- * but does not match full CSS selector groups separated by commas.
250
+ * Unicode and escape sequences are allowed in identifiers.
252
251
  *
253
- * @type {RegExp}
252
+ * @param {string} selector
253
+ * @returns {boolean}
254
254
  */
255
- 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+)+$/;
255
+ function basicSelectorValidator(selector) {
256
+ var length = selector.length;
257
+ var i = 0;
258
+ var stack = [];
259
+ var inAttr = false;
260
+ var inSingleQuote = false;
261
+ var inDoubleQuote = false;
262
+
263
+ while (i < length) {
264
+ var char = selector[i];
265
+
266
+ if (inSingleQuote) {
267
+ if (char === "'" && selector[i - 1] !== "\\") {
268
+ inSingleQuote = false;
269
+ }
270
+ } else if (inDoubleQuote) {
271
+ if (char === '"' && selector[i - 1] !== "\\") {
272
+ inDoubleQuote = false;
273
+ }
274
+ } else if (inAttr) {
275
+ if (char === "]") {
276
+ inAttr = false;
277
+ } else if (char === "'") {
278
+ inSingleQuote = true;
279
+ } else if (char === '"') {
280
+ inDoubleQuote = true;
281
+ }
282
+ } else {
283
+ if (char === "[") {
284
+ inAttr = true;
285
+ } else if (char === "'") {
286
+ inSingleQuote = true;
287
+ } else if (char === '"') {
288
+ inDoubleQuote = true;
289
+ } else if (char === "(") {
290
+ stack.push("(");
291
+ } else if (char === ")") {
292
+ if (!stack.length || stack.pop() !== "(") {
293
+ return false;
294
+ }
295
+ }
296
+ }
297
+ i++;
298
+ }
299
+
300
+ // If any stack or quote/attr context remains, it's invalid
301
+ if (stack.length || inAttr || inSingleQuote || inDoubleQuote) {
302
+ return false;
303
+ }
304
+
305
+ // Fallback to a loose regexp for the overall selector structure (without deep paren matching)
306
+ // This is similar to the original, but without nested paren limitations
307
+ var looseSelectorRegExp = /^([a-zA-Z_\u00A0-\uFFFF\\][a-zA-Z0-9_\u00A0-\uFFFF\-\\]*|\*|#[a-zA-Z0-9_\u00A0-\uFFFF\-\\]+|\.[a-zA-Z0-9_\u00A0-\uFFFF\-\\]+|\[[^\[\]]*(?:\s+[iI])?\]|::?[a-zA-Z0-9_\u00A0-\uFFFF\-\\]+(?:\((.*)\))?|&|\s*[>+~]\s*|\s+)+$/;
308
+ return looseSelectorRegExp.test(selector);
309
+ }
256
310
 
257
311
  /**
258
312
  * Regular expression to match CSS pseudo-classes with arguments.
@@ -335,40 +389,62 @@ CSSOM.parse = function parse(token, errorHandler) {
335
389
  * Validates a CSS selector string, including handling of nested selectors within certain pseudo-classes.
336
390
  *
337
391
  * This function checks if the provided selector is valid according to the rules defined by
338
- * `basicSelectorRegExp`. For pseudo-classes that accept selector lists (such as :not, :is, :has, :where),
392
+ * `basicSelectorValidator`. For pseudo-classes that accept selector lists (such as :not, :is, :has, :where),
339
393
  * it recursively validates each nested selector using the same validation logic.
340
394
  *
341
395
  * @param {string} selector - The CSS selector string to validate.
342
396
  * @returns {boolean} Returns `true` if the selector is valid, otherwise `false`.
343
397
  */
344
- function validateSelector(selector) {
345
- var match, nestedSelectors, i;
346
398
 
347
- // Only pseudo-classes that accept selector lists should recurse
348
- var selectorListPseudoClasses = {
349
- 'not': true,
350
- 'is': true,
351
- 'has': true,
352
- 'where': true
353
- };
399
+ // Cache to store validated selectors
400
+ var validatedSelectorsCache = new Map();
401
+
402
+ // Only pseudo-classes that accept selector lists should recurse
403
+ var selectorListPseudoClasses = {
404
+ 'not': true,
405
+ 'is': true,
406
+ 'has': true,
407
+ 'where': true
408
+ };
409
+
410
+ function validateSelector(selector) {
411
+ if (validatedSelectorsCache.has(selector)) {
412
+ return validatedSelectorsCache.get(selector);
413
+ }
354
414
 
355
- // Reset regex lastIndex for global regex in ES5 loop
415
+ // Use a non-global regex to find all pseudo-classes with arguments
416
+ var pseudoClassMatches = [];
356
417
  var pseudoClassRegExp = new RegExp(globalPseudoClassRegExp.source, globalPseudoClassRegExp.flags);
418
+ var match;
357
419
  while ((match = pseudoClassRegExp.exec(selector)) !== null) {
358
- var pseudoClass = match[1];
420
+ pseudoClassMatches.push(match);
421
+ }
422
+
423
+ for (var j = 0; j < pseudoClassMatches.length; j++) {
424
+ var pseudoClass = pseudoClassMatches[j][1];
359
425
  if (selectorListPseudoClasses.hasOwnProperty(pseudoClass)) {
360
- nestedSelectors = parseAndSplitNestedSelectors(match[2]);
361
- // Validate each nested selector
362
- for (i = 0; i < nestedSelectors.length; i++) {
363
- if (!validateSelector(nestedSelectors[i])) {
426
+ var nestedSelectors = parseAndSplitNestedSelectors(pseudoClassMatches[j][2]);
427
+ for (var i = 0; i < nestedSelectors.length; i++) {
428
+ var nestedSelector = nestedSelectors[i];
429
+ if (!validatedSelectorsCache.has(nestedSelector)) {
430
+ var nestedSelectorValidation = validateSelector(nestedSelector);
431
+ validatedSelectorsCache.set(nestedSelector, nestedSelectorValidation);
432
+ if (!nestedSelectorValidation) {
433
+ validatedSelectorsCache.set(selector, false);
434
+ return false;
435
+ }
436
+ } else if (!validatedSelectorsCache.get(nestedSelector)) {
437
+ validatedSelectorsCache.set(selector, false);
364
438
  return false;
365
439
  }
366
440
  }
367
441
  }
368
442
  }
369
443
 
370
- // Allow "&" anywhere in the selector for nested selectors
371
- return basicSelectorRegExp.test(selector);
444
+ var basicSelectorValidation = basicSelectorValidator(selector);
445
+ validatedSelectorsCache.set(selector, basicSelectorValidation);
446
+
447
+ return basicSelectorValidation;
372
448
  }
373
449
 
374
450
  /**
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "parser",
8
8
  "styleSheet"
9
9
  ],
10
- "version": "0.9.5",
10
+ "version": "0.9.6",
11
11
  "author": "Nikita Vasilyev <me@elv1s.ru>",
12
12
  "contributors": [
13
13
  "Acemir Sousa Mendes <acemirsm@gmail.com>"