@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 +260 -22
- package/lib/CSSStyleDeclaration.js +28 -2
- package/lib/parse.js +231 -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,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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|