@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 +249 -22
- package/lib/CSSStyleDeclaration.js +28 -2
- package/lib/parse.js +220 -20
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|