@acemir/cssom 0.9.0 → 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 +485 -70
- package/lib/CSSImportRule.js +55 -4
- package/lib/CSSLayerBlockRule.js +2 -12
- package/lib/CSSLayerStatementRule.js +32 -0
- package/lib/CSSStyleDeclaration.js +41 -4
- package/lib/CSSStyleSheet.js +4 -1
- package/lib/clone.js +18 -1
- package/lib/index.js +1 -0
- package/lib/parse.js +344 -50
- package/package.json +1 -1
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,
|
|
@@ -50,17 +53,57 @@ CSSOM.parse = function parse(token) {
|
|
|
50
53
|
var parentRule;
|
|
51
54
|
|
|
52
55
|
var ancestorRules = [];
|
|
53
|
-
var hasAncestors = false;
|
|
54
56
|
var prevScope;
|
|
55
57
|
|
|
56
|
-
var name, priority="", styleRule, mediaRule, containerRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule, startingStyleRule, layerBlockRule, nestedSelectorRule;
|
|
58
|
+
var name, priority="", styleRule, mediaRule, containerRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule, startingStyleRule, layerBlockRule, layerStatementRule, nestedSelectorRule;
|
|
57
59
|
|
|
58
60
|
var atKeyframesRegExp = /@(-(?:\w+-)+)?keyframes/g; // Match @keyframes and vendor-prefixed @keyframes
|
|
59
|
-
|
|
61
|
+
// Regex above is not ES5 compliant
|
|
62
|
+
// var atRulesStatemenRegExp = /(?<!{.*)[;}]\s*/; // Match a statement by verifying it finds a semicolon or closing brace not followed by another semicolon or closing brace
|
|
60
63
|
var beforeRulePortionRegExp = /{(?!.*{)|}(?!.*})|;(?!.*;)|\*\/(?!.*\*\/)/g; // Match the closest allowed character (a opening or closing brace, a semicolon or a comment ending) before the rule
|
|
61
64
|
var beforeRuleValidationRegExp = /^[\s{};]*(\*\/\s*)?$/; // Match that the portion before the rule is empty or contains only whitespace, semicolons, opening/closing braces, and optionally a comment ending (*/) followed by whitespace
|
|
62
65
|
var forwardRuleValidationRegExp = /(?:\(|\s|\/\*)/; // Match that the rule is followed by any whitespace, a opening comment or a condition opening parenthesis
|
|
66
|
+
var forwardImportRuleValidationRegExp = /(?:\s|\/\*|'|")/; // Match that the rule is followed by any whitespace, an opening comment, a single quote or double quote
|
|
63
67
|
var forwardRuleClosingBraceRegExp = /{[^{}]*}|}/; // Finds the next closing brace of a rule block
|
|
68
|
+
var forwardRuleSemicolonAndOpeningBraceRegExp = /^.*?({|;)/; // Finds the next semicolon or opening brace after the at-rule
|
|
69
|
+
var layerRuleNameRegExp = /^(-?[_a-zA-Z]+[_a-zA-Z0-9-]*)$/; // Validates a single @layer name
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Searches for the first occurrence of a CSS at-rule statement terminator (`;` or `}`)
|
|
73
|
+
* that is not inside a brace block within the given string. Mimics the behavior of a
|
|
74
|
+
* regular expression match for such terminators, including any trailing whitespace.
|
|
75
|
+
* @param {string} str - The string to search for at-rule statement terminators.
|
|
76
|
+
* @returns {object | null} {0: string, index: number} or null if no match is found.
|
|
77
|
+
*/
|
|
78
|
+
function atRulesStatemenRegExpES5Alternative(ruleSlice) {
|
|
79
|
+
for (var i = 0; i < ruleSlice.length; i++) {
|
|
80
|
+
var char = ruleSlice[i];
|
|
81
|
+
|
|
82
|
+
if (char === ';' || char === '}') {
|
|
83
|
+
// Simulate negative lookbehind: check if there is a { before this position
|
|
84
|
+
var sliceBefore = ruleSlice.substring(0, i);
|
|
85
|
+
var openBraceIndex = sliceBefore.indexOf('{');
|
|
86
|
+
|
|
87
|
+
if (openBraceIndex === -1) {
|
|
88
|
+
// No { found before, so we treat it as a valid match
|
|
89
|
+
var match = char;
|
|
90
|
+
var j = i + 1;
|
|
91
|
+
|
|
92
|
+
while (j < ruleSlice.length && /\s/.test(ruleSlice[j])) {
|
|
93
|
+
match += ruleSlice[j];
|
|
94
|
+
j++;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
var matchObj = [match];
|
|
98
|
+
matchObj.index = i;
|
|
99
|
+
matchObj.input = ruleSlice;
|
|
100
|
+
return matchObj;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
64
107
|
|
|
65
108
|
/**
|
|
66
109
|
* Finds the first balanced block (including nested braces) in the string, starting from fromIndex.
|
|
@@ -69,17 +112,18 @@ CSSOM.parse = function parse(token) {
|
|
|
69
112
|
* @param {number} [fromIndex=0] - The index to start searching from.
|
|
70
113
|
* @returns {object|null} - { 0: matchedString, index: startIndex, input: str } or null if not found.
|
|
71
114
|
*/
|
|
72
|
-
function matchBalancedBlock(str, fromIndex
|
|
73
|
-
|
|
115
|
+
function matchBalancedBlock(str, fromIndex) {
|
|
116
|
+
fromIndex = fromIndex || 0;
|
|
117
|
+
var openIndex = str.indexOf('{', fromIndex);
|
|
74
118
|
if (openIndex === -1) return null;
|
|
75
|
-
|
|
76
|
-
for (
|
|
119
|
+
var depth = 0;
|
|
120
|
+
for (var i = openIndex; i < str.length; i++) {
|
|
77
121
|
if (str[i] === '{') {
|
|
78
122
|
depth++;
|
|
79
123
|
} else if (str[i] === '}') {
|
|
80
124
|
depth--;
|
|
81
125
|
if (depth === 0) {
|
|
82
|
-
|
|
126
|
+
var matchedString = str.slice(openIndex, i + 1);
|
|
83
127
|
return {
|
|
84
128
|
0: matchedString,
|
|
85
129
|
index: openIndex,
|
|
@@ -91,6 +135,29 @@ CSSOM.parse = function parse(token) {
|
|
|
91
135
|
return null;
|
|
92
136
|
}
|
|
93
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
|
+
|
|
94
161
|
var parseError = function(message) {
|
|
95
162
|
var lines = token.substring(0, i).split('\n');
|
|
96
163
|
var lineCount = lines.length;
|
|
@@ -100,12 +167,18 @@ CSSOM.parse = function parse(token) {
|
|
|
100
167
|
/* jshint sub : true */
|
|
101
168
|
error['char'] = charCount;
|
|
102
169
|
error.styleSheet = styleSheet;
|
|
103
|
-
|
|
170
|
+
// Print the error but continue parsing the sheet
|
|
171
|
+
try {
|
|
172
|
+
throw error;
|
|
173
|
+
} catch(e) {
|
|
174
|
+
errorHandler && errorHandler(e);
|
|
175
|
+
}
|
|
104
176
|
};
|
|
105
177
|
|
|
106
178
|
var validateAtRule = function(atRuleKey, validCallback, cannotBeNested) {
|
|
107
179
|
var isValid = false;
|
|
108
|
-
var
|
|
180
|
+
var sourceRuleRegExp = atRuleKey === "@import" ? forwardImportRuleValidationRegExp : forwardRuleValidationRegExp;
|
|
181
|
+
var ruleRegExp = new RegExp(atRuleKey + sourceRuleRegExp.source, sourceRuleRegExp.flags);
|
|
109
182
|
var ruleSlice = token.slice(i);
|
|
110
183
|
// Not all rules can be nested, if the rule cannot be nested and is in the root scope, do not perform the check
|
|
111
184
|
var shouldPerformCheck = cannotBeNested && currentScope !== styleSheet ? false : true;
|
|
@@ -127,7 +200,9 @@ CSSOM.parse = function parse(token) {
|
|
|
127
200
|
// If it's invalid the browser will simply ignore the entire invalid block
|
|
128
201
|
// Use regex to find the closing brace of the invalid rule
|
|
129
202
|
|
|
130
|
-
|
|
203
|
+
// Regex used above is not ES5 compliant. Using alternative.
|
|
204
|
+
// var ruleStatementMatch = ruleSlice.match(atRulesStatemenRegExp); //
|
|
205
|
+
var ruleStatementMatch = atRulesStatemenRegExpES5Alternative(ruleSlice);
|
|
131
206
|
|
|
132
207
|
// If it's a statement inside a nested rule, ignore only the statement
|
|
133
208
|
if (ruleStatementMatch && currentScope !== styleSheet) {
|
|
@@ -136,25 +211,179 @@ CSSOM.parse = function parse(token) {
|
|
|
136
211
|
return;
|
|
137
212
|
}
|
|
138
213
|
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
214
|
+
// Check if there's a semicolon before the invalid at-rule and the first opening brace
|
|
215
|
+
if (atRuleKey === "@layer") {
|
|
216
|
+
var ruleSemicolonAndOpeningBraceMatch = ruleSlice.match(forwardRuleSemicolonAndOpeningBraceRegExp);
|
|
217
|
+
if (ruleSemicolonAndOpeningBraceMatch && ruleSemicolonAndOpeningBraceMatch[1] === ";" ) {
|
|
218
|
+
// Ignore the rule block until the semicolon
|
|
219
|
+
i += ruleSemicolonAndOpeningBraceMatch.index + ruleSemicolonAndOpeningBraceMatch[0].length;
|
|
220
|
+
state = "before-selector";
|
|
221
|
+
return;
|
|
146
222
|
}
|
|
147
|
-
} else {
|
|
148
|
-
i += ruleSlice.length;
|
|
149
223
|
}
|
|
224
|
+
|
|
225
|
+
// Ignore the entire rule block (if it's a statement it should ignore the statement plus the next block)
|
|
226
|
+
i = ignoreBalancedBlock(i, ruleSlice);
|
|
150
227
|
state = "before-selector";
|
|
151
228
|
} else {
|
|
152
229
|
validCallback.call(this);
|
|
153
230
|
}
|
|
154
231
|
}
|
|
155
232
|
|
|
156
|
-
|
|
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
|
+
};
|
|
157
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
|
+
|
|
376
|
+
var endingIndex = token.length - 1;
|
|
377
|
+
|
|
378
|
+
for (var character; (character = token.charAt(i)); i++) {
|
|
379
|
+
if (i === endingIndex) {
|
|
380
|
+
switch (state) {
|
|
381
|
+
case "importRule":
|
|
382
|
+
case "layerBlock":
|
|
383
|
+
token += ";"
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
158
387
|
switch (character) {
|
|
159
388
|
|
|
160
389
|
case " ":
|
|
@@ -176,6 +405,9 @@ CSSOM.parse = function parse(token) {
|
|
|
176
405
|
parseError('Unmatched "');
|
|
177
406
|
}
|
|
178
407
|
} while (token[index - 2] === '\\');
|
|
408
|
+
if (index === 0) {
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
179
411
|
buffer += token.slice(i, index);
|
|
180
412
|
i = index - 1;
|
|
181
413
|
switch (state) {
|
|
@@ -184,6 +416,9 @@ CSSOM.parse = function parse(token) {
|
|
|
184
416
|
break;
|
|
185
417
|
case 'importRule-begin':
|
|
186
418
|
state = 'importRule';
|
|
419
|
+
if (i === endingIndex) {
|
|
420
|
+
token += ';'
|
|
421
|
+
}
|
|
187
422
|
break;
|
|
188
423
|
}
|
|
189
424
|
break;
|
|
@@ -196,6 +431,9 @@ CSSOM.parse = function parse(token) {
|
|
|
196
431
|
parseError("Unmatched '");
|
|
197
432
|
}
|
|
198
433
|
} while (token[index - 2] === '\\');
|
|
434
|
+
if (index === 0) {
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
199
437
|
buffer += token.slice(i, index);
|
|
200
438
|
i = index - 1;
|
|
201
439
|
switch (state) {
|
|
@@ -331,8 +569,13 @@ CSSOM.parse = function parse(token) {
|
|
|
331
569
|
if (currentScope === styleSheet) {
|
|
332
570
|
nestedSelectorRule = null;
|
|
333
571
|
}
|
|
572
|
+
if (state === 'before-selector') {
|
|
573
|
+
parseError("Unexpected {");
|
|
574
|
+
i = ignoreBalancedBlock(i, token.slice(i));
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
334
577
|
if (state === "selector" || state === "atRule") {
|
|
335
|
-
if (!nestedSelectorRule && buffer.
|
|
578
|
+
if (!nestedSelectorRule && buffer.indexOf(";") !== -1) {
|
|
336
579
|
var ruleClosingMatch = token.slice(i).match(forwardRuleClosingBraceRegExp);
|
|
337
580
|
if (ruleClosingMatch) {
|
|
338
581
|
styleRule = null;
|
|
@@ -349,6 +592,7 @@ CSSOM.parse = function parse(token) {
|
|
|
349
592
|
}
|
|
350
593
|
|
|
351
594
|
currentScope = parentRule = styleRule;
|
|
595
|
+
console.log('sel out', buffer);
|
|
352
596
|
styleRule.selectorText = buffer.trim();
|
|
353
597
|
styleRule.style.__starts = i;
|
|
354
598
|
styleRule.parentStyleSheet = styleSheet;
|
|
@@ -390,15 +634,19 @@ CSSOM.parse = function parse(token) {
|
|
|
390
634
|
buffer = "";
|
|
391
635
|
state = "before-selector";
|
|
392
636
|
} else if (state === "layerBlock") {
|
|
393
|
-
layerBlockRule.
|
|
637
|
+
layerBlockRule.name = buffer.trim();
|
|
394
638
|
|
|
395
|
-
|
|
396
|
-
layerBlockRule.parentRule = parentRule;
|
|
397
|
-
ancestorRules.push(parentRule);
|
|
398
|
-
}
|
|
639
|
+
var isValidName = layerBlockRule.name.length === 0 || layerBlockRule.name.match(layerRuleNameRegExp) !== null;
|
|
399
640
|
|
|
400
|
-
|
|
401
|
-
|
|
641
|
+
if (isValidName) {
|
|
642
|
+
if (parentRule) {
|
|
643
|
+
layerBlockRule.parentRule = parentRule;
|
|
644
|
+
ancestorRules.push(parentRule);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
currentScope = parentRule = layerBlockRule;
|
|
648
|
+
layerBlockRule.parentStyleSheet = styleSheet;
|
|
649
|
+
}
|
|
402
650
|
buffer = "";
|
|
403
651
|
state = "before-selector";
|
|
404
652
|
} else if (state === "hostRule-begin") {
|
|
@@ -456,7 +704,7 @@ CSSOM.parse = function parse(token) {
|
|
|
456
704
|
documentRule.parentStyleSheet = styleSheet;
|
|
457
705
|
buffer = "";
|
|
458
706
|
state = "before-selector";
|
|
459
|
-
} else if (state === "name") {
|
|
707
|
+
} else if (state === "before-name" || state === "name") {
|
|
460
708
|
if (styleRule.constructor.name === "CSSNestedDeclarations") {
|
|
461
709
|
if (styleRule.style.length) {
|
|
462
710
|
parentRule.cssRules.push(styleRule);
|
|
@@ -473,9 +721,12 @@ CSSOM.parse = function parse(token) {
|
|
|
473
721
|
styleRule.parentStyleSheet = styleSheet;
|
|
474
722
|
}
|
|
475
723
|
|
|
476
|
-
|
|
477
724
|
styleRule = new CSSOM.CSSStyleRule();
|
|
478
|
-
|
|
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(', ');
|
|
479
730
|
styleRule.style.__starts = i - buffer.length;
|
|
480
731
|
styleRule.parentRule = parentRule;
|
|
481
732
|
nestedSelectorRule = styleRule;
|
|
@@ -550,8 +801,14 @@ CSSOM.parse = function parse(token) {
|
|
|
550
801
|
|
|
551
802
|
case ";":
|
|
552
803
|
switch (state) {
|
|
804
|
+
case "before-value":
|
|
805
|
+
case "before-name":
|
|
806
|
+
parseError("Unexpected ;");
|
|
807
|
+
buffer = "";
|
|
808
|
+
state = "before-name";
|
|
809
|
+
break;
|
|
553
810
|
case "value":
|
|
554
|
-
styleRule.style.setProperty(name, buffer.trim(), priority);
|
|
811
|
+
styleRule.style.setProperty(name, buffer.trim(), priority, parseError);
|
|
555
812
|
priority = "";
|
|
556
813
|
buffer = "";
|
|
557
814
|
state = "before-name";
|
|
@@ -561,10 +818,34 @@ CSSOM.parse = function parse(token) {
|
|
|
561
818
|
state = "before-selector";
|
|
562
819
|
break;
|
|
563
820
|
case "importRule":
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
821
|
+
var isValid = styleSheet.cssRules.length === 0 || styleSheet.cssRules.some(function (rule) {
|
|
822
|
+
return ['CSSImportRule', 'CSSLayerStatementRule'].indexOf(rule.constructor.name) !== -1
|
|
823
|
+
});
|
|
824
|
+
if (isValid) {
|
|
825
|
+
importRule = new CSSOM.CSSImportRule();
|
|
826
|
+
importRule.parentStyleSheet = importRule.styleSheet.parentStyleSheet = styleSheet;
|
|
827
|
+
importRule.cssText = buffer + character;
|
|
828
|
+
styleSheet.cssRules.push(importRule);
|
|
829
|
+
}
|
|
830
|
+
buffer = "";
|
|
831
|
+
state = "before-selector";
|
|
832
|
+
break;
|
|
833
|
+
case "layerBlock":
|
|
834
|
+
var nameListStr = buffer.trim().split(",").map(function (name) {
|
|
835
|
+
return name.trim();
|
|
836
|
+
});
|
|
837
|
+
var isInvalid = parentRule !== undefined || nameListStr.some(function (name) {
|
|
838
|
+
return name.trim().match(layerRuleNameRegExp) === null;
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
if (!isInvalid) {
|
|
842
|
+
layerStatementRule = new CSSOM.CSSLayerStatementRule();
|
|
843
|
+
layerStatementRule.parentStyleSheet = styleSheet;
|
|
844
|
+
layerStatementRule.__starts = layerBlockRule.__starts;
|
|
845
|
+
layerStatementRule.__ends = i;
|
|
846
|
+
layerStatementRule.nameList = nameListStr;
|
|
847
|
+
styleSheet.cssRules.push(layerStatementRule);
|
|
848
|
+
}
|
|
568
849
|
buffer = "";
|
|
569
850
|
state = "before-selector";
|
|
570
851
|
break;
|
|
@@ -577,9 +858,10 @@ CSSOM.parse = function parse(token) {
|
|
|
577
858
|
case "}":
|
|
578
859
|
switch (state) {
|
|
579
860
|
case "value":
|
|
580
|
-
styleRule.style.setProperty(name, buffer.trim(), priority);
|
|
861
|
+
styleRule.style.setProperty(name, buffer.trim(), priority, parseError);
|
|
581
862
|
priority = "";
|
|
582
863
|
/* falls through */
|
|
864
|
+
case "before-value":
|
|
583
865
|
case "before-name":
|
|
584
866
|
case "name":
|
|
585
867
|
styleRule.__ends = i + 1;
|
|
@@ -597,7 +879,14 @@ CSSOM.parse = function parse(token) {
|
|
|
597
879
|
currentScope = parentRule || styleSheet;
|
|
598
880
|
}
|
|
599
881
|
|
|
600
|
-
|
|
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
|
+
}
|
|
601
890
|
buffer = "";
|
|
602
891
|
if (currentScope.constructor === CSSOM.CSSKeyframesRule) {
|
|
603
892
|
state = "keyframeRule-begin";
|
|
@@ -619,12 +908,16 @@ CSSOM.parse = function parse(token) {
|
|
|
619
908
|
case "selector":
|
|
620
909
|
// End of media/supports/document rule.
|
|
621
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
|
+
|
|
622
918
|
break;
|
|
623
|
-
//parseError("Unexpected }");
|
|
624
919
|
}
|
|
625
920
|
|
|
626
|
-
// Handle rules nested in @media or @supports
|
|
627
|
-
hasAncestors = ancestorRules.length > 0;
|
|
628
921
|
|
|
629
922
|
while (ancestorRules.length > 0) {
|
|
630
923
|
parentRule = ancestorRules.pop();
|
|
@@ -651,14 +944,10 @@ CSSOM.parse = function parse(token) {
|
|
|
651
944
|
} else {
|
|
652
945
|
prevScope = currentScope;
|
|
653
946
|
currentScope = parentRule;
|
|
654
|
-
currentScope.cssRules.push(prevScope);
|
|
947
|
+
currentScope !== prevScope && currentScope.cssRules.push(prevScope);
|
|
655
948
|
break;
|
|
656
949
|
}
|
|
657
950
|
}
|
|
658
|
-
|
|
659
|
-
if (ancestorRules.length === 0) {
|
|
660
|
-
hasAncestors = false;
|
|
661
|
-
}
|
|
662
951
|
}
|
|
663
952
|
|
|
664
953
|
if (currentScope.parentRule == null) {
|
|
@@ -672,8 +961,12 @@ CSSOM.parse = function parse(token) {
|
|
|
672
961
|
if (nestedSelectorRule === parentRule) {
|
|
673
962
|
// Check if this selector is really starting inside another selector
|
|
674
963
|
var nestedSelectorTokenToCurrentSelectorToken = token.slice(nestedSelectorRule.__starts, i + 1);
|
|
675
|
-
|
|
676
|
-
|
|
964
|
+
var openingBraceMatch = nestedSelectorTokenToCurrentSelectorToken.match(/{/g);
|
|
965
|
+
var closingBraceMatch = nestedSelectorTokenToCurrentSelectorToken.match(/}/g);
|
|
966
|
+
var openingBraceLen = openingBraceMatch && openingBraceMatch.length;
|
|
967
|
+
var closingBraceLen = closingBraceMatch && closingBraceMatch.length;
|
|
968
|
+
|
|
969
|
+
if (openingBraceLen === closingBraceLen) {
|
|
677
970
|
// If the number of opening and closing braces are equal, we can assume that the new selector is starting outside the nestedSelectorRule
|
|
678
971
|
nestedSelectorRule.__ends = i + 1;
|
|
679
972
|
nestedSelectorRule = null;
|
|
@@ -780,4 +1073,5 @@ CSSOM.CSSKeyframesRule = require('./CSSKeyframesRule').CSSKeyframesRule;
|
|
|
780
1073
|
CSSOM.CSSValueExpression = require('./CSSValueExpression').CSSValueExpression;
|
|
781
1074
|
CSSOM.CSSDocumentRule = require('./CSSDocumentRule').CSSDocumentRule;
|
|
782
1075
|
CSSOM.CSSLayerBlockRule = require("./CSSLayerBlockRule").CSSLayerBlockRule;
|
|
1076
|
+
CSSOM.CSSLayerStatementRule = require("./CSSLayerStatementRule").CSSLayerStatementRule;
|
|
783
1077
|
///CommonJS
|