@acemir/cssom 0.9.23 → 0.9.25
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 +2113 -1269
- package/lib/CSSConditionRule.js +3 -1
- package/lib/CSSContainerRule.js +20 -9
- package/lib/CSSCounterStyleRule.js +36 -2
- package/lib/CSSDocumentRule.js +10 -2
- package/lib/CSSFontFaceRule.js +9 -2
- package/lib/CSSGroupingRule.js +65 -9
- package/lib/CSSHostRule.js +22 -6
- package/lib/CSSImportRule.js +49 -39
- package/lib/CSSKeyframeRule.js +9 -2
- package/lib/CSSKeyframesRule.js +20 -7
- package/lib/CSSLayerBlockRule.js +20 -9
- package/lib/CSSLayerStatementRule.js +9 -5
- package/lib/CSSMediaRule.js +20 -9
- package/lib/CSSNamespaceRule.js +27 -17
- package/lib/CSSNestedDeclarations.js +9 -5
- package/lib/CSSOM.js +16 -1
- package/lib/CSSPageRule.js +11 -155
- package/lib/CSSRule.js +10 -0
- package/lib/CSSScopeRule.js +13 -5
- package/lib/CSSStartingStyleRule.js +20 -6
- package/lib/CSSStyleRule.js +11 -157
- package/lib/CSSStyleSheet.js +115 -4
- package/lib/CSSSupportsRule.js +19 -8
- package/lib/CSSValueExpression.js +3 -1
- package/lib/MediaList.js +3 -1
- package/lib/StyleSheet.js +24 -1
- package/lib/errorUtils.js +23 -16
- package/lib/index.js +2 -0
- package/lib/parse.js +1521 -784
- package/package.json +1 -1
package/lib/parse.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
//.CommonJS
|
|
2
|
-
var CSSOM = {
|
|
2
|
+
var CSSOM = {
|
|
3
|
+
setup: require('./CSSOM').setup
|
|
4
|
+
};
|
|
3
5
|
///CommonJS
|
|
4
6
|
|
|
5
|
-
|
|
6
7
|
/**
|
|
7
8
|
* Parses a CSS string and returns a CSSOM.CSSStyleSheet object representing the parsed stylesheet.
|
|
8
9
|
*
|
|
9
10
|
* @param {string} token - The CSS string to parse.
|
|
10
11
|
* @param {object} [opts] - Optional parsing options.
|
|
11
|
-
* @param {object} [opts.globalObject] - An optional global object to
|
|
12
|
+
* @param {object} [opts.globalObject] - @deprecated This property will be removed in the next release. Use CSSOM.setup({ globalObject }) instead. - An optional global object to override globals and window. Useful on jsdom webplatform tests.
|
|
13
|
+
* @param {Element | ProcessingInstruction} [opts.ownerNode] - The owner node of the stylesheet.
|
|
14
|
+
* @param {CSSRule} [opts.ownerRule] - The owner rule of the stylesheet.
|
|
15
|
+
* @param {CSSOM.CSSStyleSheet} [opts.styleSheet] - Reuse a style sheet instead of creating a new one (e.g. as `parentStyleSheet`)
|
|
16
|
+
* @param {CSSOM.CSSRuleList} [opts.cssRules] - Prepare all rules in this list instead of mutating the style sheet continually
|
|
12
17
|
* @param {function|boolean} [errorHandler] - Optional error handler function or `true` to use `console.error`.
|
|
13
18
|
* @returns {CSSOM.CSSStyleSheet} The parsed CSSStyleSheet object.
|
|
14
19
|
*/
|
|
@@ -55,14 +60,35 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
55
60
|
"pageBlock": true
|
|
56
61
|
};
|
|
57
62
|
|
|
58
|
-
var styleSheet
|
|
63
|
+
var styleSheet;
|
|
64
|
+
if (opts && opts.styleSheet) {
|
|
65
|
+
styleSheet = opts.styleSheet;
|
|
66
|
+
} else {
|
|
67
|
+
styleSheet = new CSSOM.CSSStyleSheet()
|
|
68
|
+
styleSheet.__constructed = false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
var topScope;
|
|
72
|
+
if (opts && opts.cssRules) {
|
|
73
|
+
topScope = { cssRules: opts.cssRules };
|
|
74
|
+
} else {
|
|
75
|
+
topScope = styleSheet;
|
|
76
|
+
}
|
|
59
77
|
|
|
60
78
|
if (opts && opts.globalObject) {
|
|
61
|
-
|
|
79
|
+
CSSOM.setup({ globalObject: opts.globalObject });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (opts && opts.ownerNode) {
|
|
83
|
+
styleSheet.__ownerNode = opts.ownerNode;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (opts && opts.ownerRule) {
|
|
87
|
+
styleSheet.__ownerRule = opts.ownerRule;
|
|
62
88
|
}
|
|
63
89
|
|
|
64
90
|
// @type CSSStyleSheet|CSSMediaRule|CSSContainerRule|CSSSupportsRule|CSSFontFaceRule|CSSKeyframesRule|CSSDocumentRule
|
|
65
|
-
var currentScope =
|
|
91
|
+
var currentScope = topScope;
|
|
66
92
|
|
|
67
93
|
// @type CSSMediaRule|CSSContainerRule|CSSSupportsRule|CSSKeyframesRule|CSSDocumentRule
|
|
68
94
|
var parentRule;
|
|
@@ -70,7 +96,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
70
96
|
var ancestorRules = [];
|
|
71
97
|
var prevScope;
|
|
72
98
|
|
|
73
|
-
var name, priority="", styleRule, mediaRule, containerRule, counterStyleRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule, startingStyleRule, scopeRule, pageRule, layerBlockRule, layerStatementRule, nestedSelectorRule, namespaceRule;
|
|
99
|
+
var name, priority = "", styleRule, mediaRule, containerRule, counterStyleRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule, startingStyleRule, scopeRule, pageRule, layerBlockRule, layerStatementRule, nestedSelectorRule, namespaceRule;
|
|
74
100
|
|
|
75
101
|
// Track defined namespace prefixes for validation
|
|
76
102
|
var definedNamespacePrefixes = {};
|
|
@@ -168,14 +194,14 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
168
194
|
var ruleClosingMatch = matchBalancedBlock(str, fromIndex);
|
|
169
195
|
if (ruleClosingMatch) {
|
|
170
196
|
var ignoreRange = ruleClosingMatch.index + ruleClosingMatch[0].length;
|
|
171
|
-
i+= ignoreRange;
|
|
197
|
+
i += ignoreRange;
|
|
172
198
|
if (token.charAt(i) === '}') {
|
|
173
199
|
i -= 1;
|
|
174
200
|
}
|
|
175
201
|
} else {
|
|
176
202
|
i += str.length;
|
|
177
203
|
}
|
|
178
|
-
return i;
|
|
204
|
+
return i;
|
|
179
205
|
}
|
|
180
206
|
|
|
181
207
|
/**
|
|
@@ -185,29 +211,29 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
185
211
|
*/
|
|
186
212
|
function parseScopePrelude(preludeContent) {
|
|
187
213
|
var parts = preludeContent.split(/\s*\)\s*to\s+\(/);
|
|
188
|
-
|
|
214
|
+
|
|
189
215
|
// Restore the parentheses that were consumed by the split
|
|
190
216
|
if (parts.length === 2) {
|
|
191
217
|
parts[0] = parts[0] + ')';
|
|
192
218
|
parts[1] = '(' + parts[1];
|
|
193
219
|
}
|
|
194
|
-
|
|
220
|
+
|
|
195
221
|
var hasStart = parts[0] &&
|
|
196
222
|
parts[0].charAt(0) === '(' &&
|
|
197
223
|
parts[0].charAt(parts[0].length - 1) === ')';
|
|
198
224
|
var hasEnd = parts[1] &&
|
|
199
225
|
parts[1].charAt(0) === '(' &&
|
|
200
226
|
parts[1].charAt(parts[1].length - 1) === ')';
|
|
201
|
-
|
|
227
|
+
|
|
202
228
|
// Handle case: @scope to (<end>)
|
|
203
229
|
var hasOnlyEnd = !hasStart &&
|
|
204
230
|
!hasEnd &&
|
|
205
231
|
parts[0].indexOf('to (') === 0 &&
|
|
206
232
|
parts[0].charAt(parts[0].length - 1) === ')';
|
|
207
|
-
|
|
233
|
+
|
|
208
234
|
var startSelector = '';
|
|
209
235
|
var endSelector = '';
|
|
210
|
-
|
|
236
|
+
|
|
211
237
|
if (hasStart) {
|
|
212
238
|
startSelector = parts[0].slice(1, -1).trim();
|
|
213
239
|
}
|
|
@@ -217,7 +243,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
217
243
|
if (hasOnlyEnd) {
|
|
218
244
|
endSelector = parts[0].slice(4, -1).trim();
|
|
219
245
|
}
|
|
220
|
-
|
|
246
|
+
|
|
221
247
|
return {
|
|
222
248
|
startSelector: startSelector,
|
|
223
249
|
endSelector: endSelector,
|
|
@@ -255,11 +281,11 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
255
281
|
var inDoubleQuote = false;
|
|
256
282
|
var inAttr = false;
|
|
257
283
|
var stack = useStack ? [] : null;
|
|
258
|
-
|
|
284
|
+
|
|
259
285
|
for (var i = 0; i < selector.length; i++) {
|
|
260
286
|
var char = selector[i];
|
|
261
287
|
var prevChar = i > 0 ? selector[i - 1] : '';
|
|
262
|
-
|
|
288
|
+
|
|
263
289
|
if (inSingleQuote) {
|
|
264
290
|
if (char === "'" && prevChar !== "\\") {
|
|
265
291
|
inSingleQuote = false;
|
|
@@ -310,7 +336,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
310
336
|
}
|
|
311
337
|
}
|
|
312
338
|
}
|
|
313
|
-
|
|
339
|
+
|
|
314
340
|
// Check if everything is balanced
|
|
315
341
|
if (useStack) {
|
|
316
342
|
return stack.length === 0 && bracketDepth === 0 && !inSingleQuote && !inDoubleQuote && !inAttr;
|
|
@@ -360,7 +386,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
360
386
|
/(?:^|[\s>+~,\[])cue\s*\(/i,
|
|
361
387
|
/(?:^|[\s>+~,\[])cue-region\s*\(/i
|
|
362
388
|
];
|
|
363
|
-
|
|
389
|
+
|
|
364
390
|
for (var i = 0; i < invalidPatterns.length; i++) {
|
|
365
391
|
if (invalidPatterns[i].test(selector)) {
|
|
366
392
|
return true;
|
|
@@ -391,7 +417,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
391
417
|
var ruleRegExp = new RegExp(atRuleKey + sourceRuleRegExp.source, sourceRuleRegExp.flags);
|
|
392
418
|
var ruleSlice = token.slice(i);
|
|
393
419
|
// Not all rules can be nested, if the rule cannot be nested and is in the root scope, do not perform the check
|
|
394
|
-
var shouldPerformCheck = cannotBeNested && currentScope !==
|
|
420
|
+
var shouldPerformCheck = cannotBeNested && currentScope !== topScope ? false : true;
|
|
395
421
|
// First, check if there is no invalid characters just after the at-rule
|
|
396
422
|
if (shouldPerformCheck && ruleSlice.search(ruleRegExp) === 0) {
|
|
397
423
|
// Find the closest allowed character before the at-rule (a opening or closing brace, a semicolon or a comment ending)
|
|
@@ -406,7 +432,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
406
432
|
isValid = true;
|
|
407
433
|
}
|
|
408
434
|
}
|
|
409
|
-
|
|
435
|
+
|
|
410
436
|
// Additional validation for @scope rule
|
|
411
437
|
if (isValid && atRuleKey === "@scope") {
|
|
412
438
|
var openBraceIndex = ruleSlice.indexOf('{');
|
|
@@ -425,7 +451,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
425
451
|
var hasStart = parsedScopePrelude.hasStart;
|
|
426
452
|
var hasEnd = parsedScopePrelude.hasEnd;
|
|
427
453
|
var hasOnlyEnd = parsedScopePrelude.hasOnlyEnd;
|
|
428
|
-
|
|
454
|
+
|
|
429
455
|
// Validation rules for @scope:
|
|
430
456
|
// 1. Empty selectors in parentheses are invalid: @scope () {} or @scope (.a) to () {}
|
|
431
457
|
if ((hasStart && startSelector === '') || (hasEnd && endSelector === '') || (hasOnlyEnd && endSelector === '')) {
|
|
@@ -461,13 +487,13 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
461
487
|
if (openBraceIndex !== -1) {
|
|
462
488
|
// Extract the rule prelude (everything between the at-rule and {)
|
|
463
489
|
var rulePrelude = ruleSlice.slice(0, openBraceIndex).trim();
|
|
464
|
-
|
|
490
|
+
|
|
465
491
|
// Skip past at-rule keyword and whitespace
|
|
466
492
|
var preludeContent = rulePrelude.slice("@page".length).trim();
|
|
467
493
|
|
|
468
494
|
if (preludeContent.length > 0) {
|
|
469
495
|
var trimmedValue = preludeContent.trim();
|
|
470
|
-
|
|
496
|
+
|
|
471
497
|
// Empty selector is valid for @page
|
|
472
498
|
if (trimmedValue !== '') {
|
|
473
499
|
// Parse @page selectorText for page name and pseudo-pages
|
|
@@ -491,7 +517,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
491
517
|
|
|
492
518
|
// Validate pseudo-pages if present
|
|
493
519
|
if (pseudoPages) {
|
|
494
|
-
var pseudos = pseudoPages.split(':').filter(function(p) { return p; });
|
|
520
|
+
var pseudos = pseudoPages.split(':').filter(function (p) { return p; });
|
|
495
521
|
var validPseudos = ['left', 'right', 'first', 'blank'];
|
|
496
522
|
var allValid = true;
|
|
497
523
|
for (var j = 0; j < pseudos.length; j++) {
|
|
@@ -500,7 +526,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
500
526
|
break;
|
|
501
527
|
}
|
|
502
528
|
}
|
|
503
|
-
|
|
529
|
+
|
|
504
530
|
if (!allValid) {
|
|
505
531
|
isValid = false;
|
|
506
532
|
}
|
|
@@ -509,21 +535,21 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
509
535
|
isValid = false;
|
|
510
536
|
}
|
|
511
537
|
}
|
|
512
|
-
|
|
538
|
+
|
|
513
539
|
}
|
|
514
540
|
}
|
|
515
541
|
}
|
|
516
|
-
|
|
542
|
+
|
|
517
543
|
if (!isValid) {
|
|
518
544
|
// If it's invalid the browser will simply ignore the entire invalid block
|
|
519
545
|
// Use regex to find the closing brace of the invalid rule
|
|
520
|
-
|
|
546
|
+
|
|
521
547
|
// Regex used above is not ES5 compliant. Using alternative.
|
|
522
548
|
// var ruleStatementMatch = ruleSlice.match(atRulesStatemenRegExp); //
|
|
523
549
|
var ruleStatementMatch = atRulesStatemenRegExpES5Alternative(ruleSlice);
|
|
524
550
|
|
|
525
551
|
// If it's a statement inside a nested rule, ignore only the statement
|
|
526
|
-
if (ruleStatementMatch && currentScope !==
|
|
552
|
+
if (ruleStatementMatch && currentScope !== topScope) {
|
|
527
553
|
var ignoreEnd = ruleStatementMatch[0].indexOf(";");
|
|
528
554
|
i += ruleStatementMatch.index + ignoreEnd;
|
|
529
555
|
return;
|
|
@@ -532,7 +558,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
532
558
|
// Check if there's a semicolon before the invalid at-rule and the first opening brace
|
|
533
559
|
if (atRuleKey === "@layer") {
|
|
534
560
|
var ruleSemicolonAndOpeningBraceMatch = ruleSlice.match(forwardRuleSemicolonAndOpeningBraceRegExp);
|
|
535
|
-
if (ruleSemicolonAndOpeningBraceMatch && ruleSemicolonAndOpeningBraceMatch[1] === ";"
|
|
561
|
+
if (ruleSemicolonAndOpeningBraceMatch && ruleSemicolonAndOpeningBraceMatch[1] === ";") {
|
|
536
562
|
// Ignore the rule block until the semicolon
|
|
537
563
|
i += ruleSemicolonAndOpeningBraceMatch.index + ruleSemicolonAndOpeningBraceMatch[0].length;
|
|
538
564
|
state = "before-selector";
|
|
@@ -548,6 +574,255 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
548
574
|
}
|
|
549
575
|
}
|
|
550
576
|
|
|
577
|
+
// Helper functions for looseSelectorValidator
|
|
578
|
+
// Defined outside to avoid recreation on every validation call
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Check if character is a valid identifier start
|
|
582
|
+
* @param {string} c - Character to check
|
|
583
|
+
* @returns {boolean}
|
|
584
|
+
*/
|
|
585
|
+
function isIdentStart(c) {
|
|
586
|
+
return /[a-zA-Z_\u00A0-\uFFFF]/.test(c);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Check if character is a valid identifier character
|
|
591
|
+
* @param {string} c - Character to check
|
|
592
|
+
* @returns {boolean}
|
|
593
|
+
*/
|
|
594
|
+
function isIdentChar(c) {
|
|
595
|
+
return /[a-zA-Z0-9_\u00A0-\uFFFF\-]/.test(c);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Helper function to validate CSS selector syntax without regex backtracking.
|
|
600
|
+
* Iteratively parses the selector string to identify valid components.
|
|
601
|
+
*
|
|
602
|
+
* Supports:
|
|
603
|
+
* - Escaped special characters (e.g., .class\!, #id\@name)
|
|
604
|
+
* - Namespace selectors (ns|element, *|element, |element)
|
|
605
|
+
* - All standard CSS selectors (class, ID, type, attribute, pseudo, etc.)
|
|
606
|
+
* - Combinators (>, +, ~, whitespace)
|
|
607
|
+
* - Nesting selector (&)
|
|
608
|
+
*
|
|
609
|
+
* This approach eliminates exponential backtracking by using explicit character-by-character
|
|
610
|
+
* parsing instead of nested quantifiers in regex.
|
|
611
|
+
*
|
|
612
|
+
* @param {string} selector - The selector to validate
|
|
613
|
+
* @returns {boolean} - True if valid selector syntax
|
|
614
|
+
*/
|
|
615
|
+
function looseSelectorValidator(selector) {
|
|
616
|
+
if (!selector || selector.length === 0) {
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
var i = 0;
|
|
621
|
+
var len = selector.length;
|
|
622
|
+
var hasMatchedComponent = false;
|
|
623
|
+
|
|
624
|
+
// Helper: Skip escaped character (backslash + any char)
|
|
625
|
+
function skipEscape() {
|
|
626
|
+
if (i < len && selector[i] === '\\') {
|
|
627
|
+
i += 2; // Skip backslash and next character
|
|
628
|
+
return true;
|
|
629
|
+
}
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Helper: Parse identifier (with possible escapes)
|
|
634
|
+
function parseIdentifier() {
|
|
635
|
+
var start = i;
|
|
636
|
+
while (i < len) {
|
|
637
|
+
if (skipEscape()) {
|
|
638
|
+
continue;
|
|
639
|
+
} else if (isIdentChar(selector[i])) {
|
|
640
|
+
i++;
|
|
641
|
+
} else {
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return i > start;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Helper: Parse namespace prefix (optional)
|
|
649
|
+
function parseNamespace() {
|
|
650
|
+
var start = i;
|
|
651
|
+
|
|
652
|
+
// Match: *| or identifier| or |
|
|
653
|
+
if (i < len && selector[i] === '*') {
|
|
654
|
+
i++;
|
|
655
|
+
} else if (i < len && (isIdentStart(selector[i]) || selector[i] === '\\')) {
|
|
656
|
+
parseIdentifier();
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (i < len && selector[i] === '|') {
|
|
660
|
+
i++;
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Rollback if no pipe found
|
|
665
|
+
i = start;
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Helper: Parse pseudo-class/element arguments (with balanced parens)
|
|
670
|
+
function parsePseudoArgs() {
|
|
671
|
+
if (i >= len || selector[i] !== '(') {
|
|
672
|
+
return false;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
i++; // Skip opening paren
|
|
676
|
+
var depth = 1;
|
|
677
|
+
var inString = false;
|
|
678
|
+
var stringChar = '';
|
|
679
|
+
|
|
680
|
+
while (i < len && depth > 0) {
|
|
681
|
+
var c = selector[i];
|
|
682
|
+
|
|
683
|
+
if (c === '\\' && i + 1 < len) {
|
|
684
|
+
i += 2; // Skip escaped character
|
|
685
|
+
} else if (!inString && (c === '"' || c === '\'')) {
|
|
686
|
+
inString = true;
|
|
687
|
+
stringChar = c;
|
|
688
|
+
i++;
|
|
689
|
+
} else if (inString && c === stringChar) {
|
|
690
|
+
inString = false;
|
|
691
|
+
i++;
|
|
692
|
+
} else if (!inString && c === '(') {
|
|
693
|
+
depth++;
|
|
694
|
+
i++;
|
|
695
|
+
} else if (!inString && c === ')') {
|
|
696
|
+
depth--;
|
|
697
|
+
i++;
|
|
698
|
+
} else {
|
|
699
|
+
i++;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return depth === 0;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Main parsing loop
|
|
707
|
+
while (i < len) {
|
|
708
|
+
var matched = false;
|
|
709
|
+
var start = i;
|
|
710
|
+
|
|
711
|
+
// Skip whitespace
|
|
712
|
+
while (i < len && /\s/.test(selector[i])) {
|
|
713
|
+
i++;
|
|
714
|
+
}
|
|
715
|
+
if (i > start) {
|
|
716
|
+
hasMatchedComponent = true;
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Match combinators: >, +, ~
|
|
721
|
+
if (i < len && /[>+~]/.test(selector[i])) {
|
|
722
|
+
i++;
|
|
723
|
+
hasMatchedComponent = true;
|
|
724
|
+
// Skip trailing whitespace
|
|
725
|
+
while (i < len && /\s/.test(selector[i])) {
|
|
726
|
+
i++;
|
|
727
|
+
}
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Match nesting selector: &
|
|
732
|
+
if (i < len && selector[i] === '&') {
|
|
733
|
+
i++;
|
|
734
|
+
hasMatchedComponent = true;
|
|
735
|
+
matched = true;
|
|
736
|
+
}
|
|
737
|
+
// Match class selector: .identifier
|
|
738
|
+
else if (i < len && selector[i] === '.') {
|
|
739
|
+
i++;
|
|
740
|
+
if (parseIdentifier()) {
|
|
741
|
+
hasMatchedComponent = true;
|
|
742
|
+
matched = true;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
// Match ID selector: #identifier
|
|
746
|
+
else if (i < len && selector[i] === '#') {
|
|
747
|
+
i++;
|
|
748
|
+
if (parseIdentifier()) {
|
|
749
|
+
hasMatchedComponent = true;
|
|
750
|
+
matched = true;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
// Match pseudo-class/element: :identifier or ::identifier
|
|
754
|
+
else if (i < len && selector[i] === ':') {
|
|
755
|
+
i++;
|
|
756
|
+
if (i < len && selector[i] === ':') {
|
|
757
|
+
i++; // Pseudo-element
|
|
758
|
+
}
|
|
759
|
+
if (parseIdentifier()) {
|
|
760
|
+
parsePseudoArgs(); // Optional arguments
|
|
761
|
+
hasMatchedComponent = true;
|
|
762
|
+
matched = true;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
// Match attribute selector: [...]
|
|
766
|
+
else if (i < len && selector[i] === '[') {
|
|
767
|
+
i++;
|
|
768
|
+
var depth = 1;
|
|
769
|
+
while (i < len && depth > 0) {
|
|
770
|
+
if (selector[i] === '\\') {
|
|
771
|
+
i += 2;
|
|
772
|
+
} else if (selector[i] === '\'') {
|
|
773
|
+
i++;
|
|
774
|
+
while (i < len && selector[i] !== '\'') {
|
|
775
|
+
if (selector[i] === '\\') i += 2;
|
|
776
|
+
else i++;
|
|
777
|
+
}
|
|
778
|
+
if (i < len) i++; // Skip closing quote
|
|
779
|
+
} else if (selector[i] === '"') {
|
|
780
|
+
i++;
|
|
781
|
+
while (i < len && selector[i] !== '"') {
|
|
782
|
+
if (selector[i] === '\\') i += 2;
|
|
783
|
+
else i++;
|
|
784
|
+
}
|
|
785
|
+
if (i < len) i++; // Skip closing quote
|
|
786
|
+
} else if (selector[i] === '[') {
|
|
787
|
+
depth++;
|
|
788
|
+
i++;
|
|
789
|
+
} else if (selector[i] === ']') {
|
|
790
|
+
depth--;
|
|
791
|
+
i++;
|
|
792
|
+
} else {
|
|
793
|
+
i++;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
if (depth === 0) {
|
|
797
|
+
hasMatchedComponent = true;
|
|
798
|
+
matched = true;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
// Match type selector with optional namespace: [namespace|]identifier
|
|
802
|
+
else if (i < len && (isIdentStart(selector[i]) || selector[i] === '\\' || selector[i] === '*' || selector[i] === '|')) {
|
|
803
|
+
parseNamespace(); // Optional namespace prefix
|
|
804
|
+
|
|
805
|
+
if (i < len && selector[i] === '*') {
|
|
806
|
+
i++; // Universal selector
|
|
807
|
+
hasMatchedComponent = true;
|
|
808
|
+
matched = true;
|
|
809
|
+
} else if (i < len && (isIdentStart(selector[i]) || selector[i] === '\\')) {
|
|
810
|
+
if (parseIdentifier()) {
|
|
811
|
+
hasMatchedComponent = true;
|
|
812
|
+
matched = true;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// If no match found, invalid selector
|
|
818
|
+
if (!matched && i === start) {
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
return hasMatchedComponent;
|
|
824
|
+
}
|
|
825
|
+
|
|
551
826
|
/**
|
|
552
827
|
* Validates a basic CSS selector, allowing for deeply nested balanced parentheses in pseudo-classes.
|
|
553
828
|
* This function replaces the previous basicSelectorRegExp.
|
|
@@ -572,6 +847,12 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
572
847
|
* @returns {boolean}
|
|
573
848
|
*/
|
|
574
849
|
function basicSelectorValidator(selector) {
|
|
850
|
+
// Guard against extremely long selectors to prevent potential regex performance issues
|
|
851
|
+
// Reasonable selectors are typically under 1000 characters
|
|
852
|
+
if (selector.length > 10000) {
|
|
853
|
+
return false;
|
|
854
|
+
}
|
|
855
|
+
|
|
575
856
|
// Validate balanced syntax with attribute tracking and stack-based parentheses matching
|
|
576
857
|
if (!validateBalancedSyntax(selector, true, true)) {
|
|
577
858
|
return false;
|
|
@@ -594,33 +875,71 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
594
875
|
|
|
595
876
|
// Check for invalid pseudo-class usage with quoted strings
|
|
596
877
|
// Pseudo-classes like :lang(), :dir(), :nth-*() should not accept quoted strings
|
|
597
|
-
|
|
598
|
-
var
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
878
|
+
// Using iterative parsing instead of regex to avoid exponential backtracking
|
|
879
|
+
var noQuotesPseudos = ['lang', 'dir', 'nth-child', 'nth-last-child', 'nth-of-type', 'nth-last-of-type'];
|
|
880
|
+
|
|
881
|
+
for (var idx = 0; idx < selector.length; idx++) {
|
|
882
|
+
// Look for pseudo-class/element start
|
|
883
|
+
if (selector[idx] === ':') {
|
|
884
|
+
var pseudoStart = idx;
|
|
885
|
+
idx++;
|
|
886
|
+
|
|
887
|
+
// Skip second colon for pseudo-elements
|
|
888
|
+
if (idx < selector.length && selector[idx] === ':') {
|
|
889
|
+
idx++;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Extract pseudo name
|
|
893
|
+
var nameStart = idx;
|
|
894
|
+
while (idx < selector.length && /[a-zA-Z0-9\-]/.test(selector[idx])) {
|
|
895
|
+
idx++;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
if (idx === nameStart) {
|
|
899
|
+
continue; // No name found
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
var pseudoName = selector.substring(nameStart, idx).toLowerCase();
|
|
903
|
+
|
|
904
|
+
// Check if this pseudo has arguments
|
|
905
|
+
if (idx < selector.length && selector[idx] === '(') {
|
|
906
|
+
idx++;
|
|
907
|
+
var contentStart = idx;
|
|
908
|
+
var depth = 1;
|
|
909
|
+
|
|
910
|
+
// Find matching closing paren (handle nesting)
|
|
911
|
+
while (idx < selector.length && depth > 0) {
|
|
912
|
+
if (selector[idx] === '\\') {
|
|
913
|
+
idx += 2; // Skip escaped character
|
|
914
|
+
} else if (selector[idx] === '(') {
|
|
915
|
+
depth++;
|
|
916
|
+
idx++;
|
|
917
|
+
} else if (selector[idx] === ')') {
|
|
918
|
+
depth--;
|
|
919
|
+
idx++;
|
|
920
|
+
} else {
|
|
921
|
+
idx++;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
if (depth === 0) {
|
|
926
|
+
var pseudoContent = selector.substring(contentStart, idx - 1);
|
|
927
|
+
|
|
928
|
+
// Check if this pseudo should not have quoted strings
|
|
929
|
+
for (var j = 0; j < noQuotesPseudos.length; j++) {
|
|
930
|
+
if (pseudoName === noQuotesPseudos[j] && /['"]/.test(pseudoContent)) {
|
|
931
|
+
return false;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
612
935
|
}
|
|
613
936
|
}
|
|
614
937
|
}
|
|
615
938
|
|
|
616
|
-
//
|
|
617
|
-
|
|
618
|
-
// Modified to support namespace selectors: *|element, prefix|element, |element
|
|
619
|
-
// Fixed attribute selector regex to properly handle |=, ~=, ^=, $=, *= operators
|
|
620
|
-
var looseSelectorRegExp = /^((?:(?:\*|[a-zA-Z_\u00A0-\uFFFF\\][a-zA-Z0-9_\u00A0-\uFFFF\-\\]*|)\|)?[a-zA-Z_\u00A0-\uFFFF\\][a-zA-Z0-9_\u00A0-\uFFFF\-\\]*|(?:(?:\*|[a-zA-Z_\u00A0-\uFFFF\\][a-zA-Z0-9_\u00A0-\uFFFF\-\\]*|)\|)?\*|#[a-zA-Z0-9_\u00A0-\uFFFF\-\\]+|\.[a-zA-Z0-9_\u00A0-\uFFFF\-\\]+|\[(?:[^\[\]'"]|'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*")*(?:\s+[iI])?\]|::?[a-zA-Z0-9_\u00A0-\uFFFF\-\\]+(?:\((.*)\))?|&|\s*[>+~]\s*|\s+)+$/;
|
|
621
|
-
return looseSelectorRegExp.test(selector);
|
|
939
|
+
// Use the iterative validator to avoid regex backtracking issues
|
|
940
|
+
return looseSelectorValidator(selector);
|
|
622
941
|
}
|
|
623
|
-
|
|
942
|
+
|
|
624
943
|
/**
|
|
625
944
|
* Regular expression to match CSS pseudo-classes with arguments.
|
|
626
945
|
*
|
|
@@ -636,9 +955,96 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
636
955
|
* - :nth-child(2n+1)
|
|
637
956
|
* - :has(.sel:nth-child(3n))
|
|
638
957
|
* - :not(".foo, .bar")
|
|
639
|
-
*
|
|
958
|
+
*
|
|
959
|
+
* REPLACED WITH FUNCTION to avoid exponential backtracking.
|
|
960
|
+
*/
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Extract pseudo-classes with arguments from a selector using iterative parsing.
|
|
964
|
+
* Replaces the previous globalPseudoClassRegExp to avoid exponential backtracking.
|
|
965
|
+
*
|
|
966
|
+
* Handles:
|
|
967
|
+
* - Regular content without parentheses or quotes
|
|
968
|
+
* - Single-quoted strings
|
|
969
|
+
* - Double-quoted strings
|
|
970
|
+
* - Nested parentheses (arbitrary depth)
|
|
971
|
+
*
|
|
972
|
+
* @param {string} selector - The CSS selector to parse
|
|
973
|
+
* @returns {Array} Array of matches, each with: [fullMatch, pseudoName, pseudoArgs, startIndex]
|
|
640
974
|
*/
|
|
641
|
-
|
|
975
|
+
function extractPseudoClasses(selector) {
|
|
976
|
+
var matches = [];
|
|
977
|
+
|
|
978
|
+
for (var i = 0; i < selector.length; i++) {
|
|
979
|
+
// Look for pseudo-class start (single or double colon)
|
|
980
|
+
if (selector[i] === ':') {
|
|
981
|
+
var pseudoStart = i;
|
|
982
|
+
i++;
|
|
983
|
+
|
|
984
|
+
// Skip second colon for pseudo-elements (::)
|
|
985
|
+
if (i < selector.length && selector[i] === ':') {
|
|
986
|
+
i++;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Extract pseudo name
|
|
990
|
+
var nameStart = i;
|
|
991
|
+
while (i < selector.length && /[a-zA-Z\-]/.test(selector[i])) {
|
|
992
|
+
i++;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (i === nameStart) {
|
|
996
|
+
continue; // No name found
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
var pseudoName = selector.substring(nameStart, i);
|
|
1000
|
+
|
|
1001
|
+
// Check if this pseudo has arguments
|
|
1002
|
+
if (i < selector.length && selector[i] === '(') {
|
|
1003
|
+
i++;
|
|
1004
|
+
var argsStart = i;
|
|
1005
|
+
var depth = 1;
|
|
1006
|
+
var inSingleQuote = false;
|
|
1007
|
+
var inDoubleQuote = false;
|
|
1008
|
+
|
|
1009
|
+
// Find matching closing paren (handle nesting and strings)
|
|
1010
|
+
while (i < selector.length && depth > 0) {
|
|
1011
|
+
var ch = selector[i];
|
|
1012
|
+
|
|
1013
|
+
if (ch === '\\') {
|
|
1014
|
+
i += 2; // Skip escaped character
|
|
1015
|
+
} else if (ch === "'" && !inDoubleQuote) {
|
|
1016
|
+
inSingleQuote = !inSingleQuote;
|
|
1017
|
+
i++;
|
|
1018
|
+
} else if (ch === '"' && !inSingleQuote) {
|
|
1019
|
+
inDoubleQuote = !inDoubleQuote;
|
|
1020
|
+
i++;
|
|
1021
|
+
} else if (ch === '(' && !inSingleQuote && !inDoubleQuote) {
|
|
1022
|
+
depth++;
|
|
1023
|
+
i++;
|
|
1024
|
+
} else if (ch === ')' && !inSingleQuote && !inDoubleQuote) {
|
|
1025
|
+
depth--;
|
|
1026
|
+
i++;
|
|
1027
|
+
} else {
|
|
1028
|
+
i++;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
if (depth === 0) {
|
|
1033
|
+
var pseudoArgs = selector.substring(argsStart, i - 1);
|
|
1034
|
+
var fullMatch = selector.substring(pseudoStart, i);
|
|
1035
|
+
|
|
1036
|
+
// Store match in same format as regex: [fullMatch, pseudoName, pseudoArgs, startIndex]
|
|
1037
|
+
matches.push([fullMatch, pseudoName, pseudoArgs, pseudoStart]);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// Move back one since loop will increment
|
|
1041
|
+
i--;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
return matches;
|
|
1047
|
+
}
|
|
642
1048
|
|
|
643
1049
|
/**
|
|
644
1050
|
* Parses a CSS selector string and splits it into parts, handling nested parentheses.
|
|
@@ -733,18 +1139,30 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
733
1139
|
return validatedSelectorsCache[selector];
|
|
734
1140
|
}
|
|
735
1141
|
|
|
736
|
-
// Use
|
|
737
|
-
var pseudoClassMatches =
|
|
738
|
-
var pseudoClassRegExp = new RegExp(globalPseudoClassRegExp.source, globalPseudoClassRegExp.flags);
|
|
739
|
-
var match;
|
|
740
|
-
while ((match = pseudoClassRegExp.exec(selector)) !== null) {
|
|
741
|
-
pseudoClassMatches.push(match);
|
|
742
|
-
}
|
|
1142
|
+
// Use function-based parsing to extract pseudo-classes (avoids backtracking)
|
|
1143
|
+
var pseudoClassMatches = extractPseudoClasses(selector);
|
|
743
1144
|
|
|
744
1145
|
for (var j = 0; j < pseudoClassMatches.length; j++) {
|
|
745
1146
|
var pseudoClass = pseudoClassMatches[j][1];
|
|
746
1147
|
if (selectorListPseudoClasses.hasOwnProperty(pseudoClass)) {
|
|
747
1148
|
var nestedSelectors = parseAndSplitNestedSelectors(pseudoClassMatches[j][2]);
|
|
1149
|
+
|
|
1150
|
+
// Check if ANY selector in the list contains & (nesting selector)
|
|
1151
|
+
// If so, skip validation for the entire selector list since & will be replaced at runtime
|
|
1152
|
+
var hasAmpersand = false;
|
|
1153
|
+
for (var k = 0; k < nestedSelectors.length; k++) {
|
|
1154
|
+
if (/&/.test(nestedSelectors[k])) {
|
|
1155
|
+
hasAmpersand = true;
|
|
1156
|
+
break;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// If any selector has &, skip validation for this entire pseudo-class
|
|
1161
|
+
if (hasAmpersand) {
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// Otherwise, validate each selector normally
|
|
748
1166
|
for (var i = 0; i < nestedSelectors.length; i++) {
|
|
749
1167
|
var nestedSelector = nestedSelectors[i];
|
|
750
1168
|
if (!validatedSelectorsCache.hasOwnProperty(nestedSelector)) {
|
|
@@ -781,10 +1199,10 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
781
1199
|
var inAttr = false;
|
|
782
1200
|
var inSingleQuote = false;
|
|
783
1201
|
var inDoubleQuote = false;
|
|
784
|
-
|
|
1202
|
+
|
|
785
1203
|
for (var i = 0; i < selector.length; i++) {
|
|
786
1204
|
var char = selector[i];
|
|
787
|
-
|
|
1205
|
+
|
|
788
1206
|
if (inSingleQuote) {
|
|
789
1207
|
if (char === "'" && selector[i - 1] !== "\\") {
|
|
790
1208
|
inSingleQuote = false;
|
|
@@ -811,18 +1229,18 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
811
1229
|
}
|
|
812
1230
|
}
|
|
813
1231
|
}
|
|
814
|
-
|
|
1232
|
+
|
|
815
1233
|
if (pipeIndex === -1) {
|
|
816
1234
|
return true; // No namespace, always valid
|
|
817
1235
|
}
|
|
818
|
-
|
|
1236
|
+
|
|
819
1237
|
var namespacePrefix = selector.substring(0, pipeIndex);
|
|
820
|
-
|
|
1238
|
+
|
|
821
1239
|
// Universal namespace (*|) and default namespace (|) are always valid
|
|
822
1240
|
if (namespacePrefix === '*' || namespacePrefix === '') {
|
|
823
1241
|
return true;
|
|
824
1242
|
}
|
|
825
|
-
|
|
1243
|
+
|
|
826
1244
|
// Check if the custom namespace prefix is defined
|
|
827
1245
|
return definedNamespacePrefixes.hasOwnProperty(namespacePrefix);
|
|
828
1246
|
}
|
|
@@ -831,22 +1249,92 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
831
1249
|
* Processes a CSS selector text
|
|
832
1250
|
*
|
|
833
1251
|
* @param {string} selectorText - The CSS selector text to process
|
|
834
|
-
* @returns {string} The processed selector text with normalized whitespace
|
|
1252
|
+
* @returns {string} The processed selector text with normalized whitespace and invalid selectors removed
|
|
835
1253
|
*/
|
|
836
1254
|
function processSelectorText(selectorText) {
|
|
837
|
-
//
|
|
838
|
-
|
|
839
|
-
// TODO: Move these validation logic to a shared function to be reused in CSSStyleRule.selectorText setter
|
|
840
|
-
|
|
841
|
-
/**
|
|
842
|
-
* Normalizes whitespace and preserving quoted strings.
|
|
843
|
-
* Replaces all newline characters (CRLF, CR, or LF) with spaces while keeping quoted
|
|
844
|
-
* strings (single or double quotes) intact, including any escaped characters within them.
|
|
845
|
-
*/
|
|
846
|
-
return selectorText.replace(/(['"])(?:\\.|[^\\])*?\1|(\r\n|\r|\n)/g, function(match, _, newline) {
|
|
1255
|
+
// Normalize whitespace first
|
|
1256
|
+
var normalized = selectorText.replace(/(['"])(?:\\.|[^\\])*?\1|(\r\n|\r|\n)/g, function (match, _, newline) {
|
|
847
1257
|
if (newline) return " ";
|
|
848
1258
|
return match;
|
|
849
1259
|
});
|
|
1260
|
+
|
|
1261
|
+
// Recursively process pseudo-classes to handle nesting
|
|
1262
|
+
return processNestedPseudoClasses(normalized);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
/**
|
|
1266
|
+
* Recursively processes pseudo-classes to filter invalid selectors
|
|
1267
|
+
*
|
|
1268
|
+
* @param {string} selectorText - The CSS selector text to process
|
|
1269
|
+
* @param {number} depth - Current recursion depth (to prevent infinite loops)
|
|
1270
|
+
* @returns {string} The processed selector text with invalid selectors removed
|
|
1271
|
+
*/
|
|
1272
|
+
function processNestedPseudoClasses(selectorText, depth) {
|
|
1273
|
+
// Prevent infinite recursion
|
|
1274
|
+
if (typeof depth === 'undefined') {
|
|
1275
|
+
depth = 0;
|
|
1276
|
+
}
|
|
1277
|
+
if (depth > 10) {
|
|
1278
|
+
return selectorText;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
var pseudoClassMatches = extractPseudoClasses(selectorText);
|
|
1282
|
+
|
|
1283
|
+
// If no pseudo-classes found, return as-is
|
|
1284
|
+
if (pseudoClassMatches.length === 0) {
|
|
1285
|
+
return selectorText;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// Build result by processing matches from right to left (to preserve positions)
|
|
1289
|
+
var result = selectorText;
|
|
1290
|
+
|
|
1291
|
+
for (var j = pseudoClassMatches.length - 1; j >= 0; j--) {
|
|
1292
|
+
var pseudoClass = pseudoClassMatches[j][1];
|
|
1293
|
+
if (selectorListPseudoClasses.hasOwnProperty(pseudoClass)) {
|
|
1294
|
+
var fullMatch = pseudoClassMatches[j][0];
|
|
1295
|
+
var pseudoArgs = pseudoClassMatches[j][2];
|
|
1296
|
+
var matchStart = pseudoClassMatches[j][3];
|
|
1297
|
+
|
|
1298
|
+
// Check if ANY selector contains & BEFORE processing
|
|
1299
|
+
var nestedSelectorsRaw = parseAndSplitNestedSelectors(pseudoArgs);
|
|
1300
|
+
var hasAmpersand = false;
|
|
1301
|
+
for (var k = 0; k < nestedSelectorsRaw.length; k++) {
|
|
1302
|
+
if (/&/.test(nestedSelectorsRaw[k])) {
|
|
1303
|
+
hasAmpersand = true;
|
|
1304
|
+
break;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// If & is present, skip all processing (keep everything unchanged)
|
|
1309
|
+
if (hasAmpersand) {
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// Recursively process the arguments
|
|
1314
|
+
var processedArgs = processNestedPseudoClasses(pseudoArgs, depth + 1);
|
|
1315
|
+
var nestedSelectors = parseAndSplitNestedSelectors(processedArgs);
|
|
1316
|
+
|
|
1317
|
+
// Filter out invalid selectors
|
|
1318
|
+
var validSelectors = [];
|
|
1319
|
+
for (var i = 0; i < nestedSelectors.length; i++) {
|
|
1320
|
+
var nestedSelector = nestedSelectors[i];
|
|
1321
|
+
if (basicSelectorValidator(nestedSelector)) {
|
|
1322
|
+
validSelectors.push(nestedSelector);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// Reconstruct the pseudo-class with only valid selectors
|
|
1327
|
+
var newArgs = validSelectors.join(', ');
|
|
1328
|
+
var newPseudoClass = ':' + pseudoClass + '(' + newArgs + ')';
|
|
1329
|
+
|
|
1330
|
+
// Replace in the result string using position (processing right to left preserves positions)
|
|
1331
|
+
result = result.substring(0, matchStart) + newPseudoClass + result.substring(matchStart + fullMatch.length);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
return result;
|
|
1336
|
+
|
|
1337
|
+
return normalized;
|
|
850
1338
|
}
|
|
851
1339
|
|
|
852
1340
|
/**
|
|
@@ -860,6 +1348,12 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
860
1348
|
// TODO: The same validations here needs to be reused in CSSStyleRule.selectorText setter
|
|
861
1349
|
// TODO: Move these validation logic to a shared function to be reused in CSSStyleRule.selectorText setter
|
|
862
1350
|
|
|
1351
|
+
// Check for empty selector lists in pseudo-classes (e.g., :is(), :not(), :where(), :has())
|
|
1352
|
+
// These are invalid after filtering out invalid selectors
|
|
1353
|
+
if (/:(?:is|not|where|has)\(\s*\)/.test(selectorText)) {
|
|
1354
|
+
return false;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
863
1357
|
// Check for newlines inside single or double quotes using regex
|
|
864
1358
|
// This matches any quoted string (single or double) containing a newline
|
|
865
1359
|
var quotedNewlineRegExp = /(['"])(?:\\.|[^\\])*?\1/g;
|
|
@@ -880,6 +1374,12 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
880
1374
|
return true;
|
|
881
1375
|
}
|
|
882
1376
|
|
|
1377
|
+
function pushToAncestorRules(rule) {
|
|
1378
|
+
if (ancestorRules.indexOf(rule) === -1) {
|
|
1379
|
+
ancestorRules.push(rule);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
883
1383
|
function parseError(message, isNested) {
|
|
884
1384
|
var lines = token.substring(0, i).split('\n');
|
|
885
1385
|
var lineCount = lines.length;
|
|
@@ -893,12 +1393,22 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
893
1393
|
// Print the error but continue parsing the sheet
|
|
894
1394
|
try {
|
|
895
1395
|
throw error;
|
|
896
|
-
} catch(e) {
|
|
1396
|
+
} catch (e) {
|
|
897
1397
|
errorHandler && errorHandler(e);
|
|
898
1398
|
}
|
|
899
1399
|
};
|
|
900
1400
|
|
|
1401
|
+
// Helper functions to check character types
|
|
1402
|
+
function isSelectorStartChar(char) {
|
|
1403
|
+
return '.:#&*['.indexOf(char) !== -1;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function isWhitespaceChar(char) {
|
|
1407
|
+
return ' \t\n\r'.indexOf(char) !== -1;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
901
1410
|
var endingIndex = token.length - 1;
|
|
1411
|
+
var initialEndingIndex = endingIndex;
|
|
902
1412
|
|
|
903
1413
|
for (var character; (character = token.charAt(i)); i++) {
|
|
904
1414
|
if (i === endingIndex) {
|
|
@@ -907,804 +1417,1031 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
907
1417
|
case "namespaceRule":
|
|
908
1418
|
case "layerBlock":
|
|
909
1419
|
if (character !== ";") {
|
|
910
|
-
token += ";"
|
|
1420
|
+
token += ";";
|
|
1421
|
+
endingIndex += 1;
|
|
1422
|
+
}
|
|
1423
|
+
break;
|
|
1424
|
+
case "value":
|
|
1425
|
+
if (character !== "}") {
|
|
1426
|
+
if (character === ";") {
|
|
1427
|
+
token += "}"
|
|
1428
|
+
} else {
|
|
1429
|
+
token += ";";
|
|
1430
|
+
}
|
|
1431
|
+
endingIndex += 1;
|
|
1432
|
+
break;
|
|
1433
|
+
}
|
|
1434
|
+
case "name":
|
|
1435
|
+
case "before-name":
|
|
1436
|
+
if (character === "}") {
|
|
1437
|
+
token += " "
|
|
1438
|
+
} else {
|
|
1439
|
+
token += "}"
|
|
1440
|
+
}
|
|
1441
|
+
endingIndex += 1
|
|
1442
|
+
break;
|
|
1443
|
+
case "before-selector":
|
|
1444
|
+
if (character !== "}" && currentScope !== styleSheet) {
|
|
1445
|
+
token += "}"
|
|
1446
|
+
endingIndex += 1
|
|
1447
|
+
break;
|
|
911
1448
|
}
|
|
912
1449
|
}
|
|
913
1450
|
}
|
|
914
|
-
|
|
1451
|
+
|
|
1452
|
+
// Handle escape sequences before processing special characters
|
|
1453
|
+
// If we encounter a backslash, add both the backslash and the next character to buffer
|
|
1454
|
+
// and skip the next iteration to prevent the escaped character from being interpreted
|
|
1455
|
+
if (character === '\\' && i + 1 < token.length) {
|
|
1456
|
+
buffer += character + token.charAt(i + 1);
|
|
1457
|
+
i++; // Skip the next character
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
915
1461
|
switch (character) {
|
|
916
1462
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
}
|
|
925
|
-
break;
|
|
926
|
-
|
|
927
|
-
// String
|
|
928
|
-
case '"':
|
|
929
|
-
index = i + 1;
|
|
930
|
-
do {
|
|
931
|
-
index = token.indexOf('"', index) + 1;
|
|
932
|
-
if (!index) {
|
|
933
|
-
parseError('Unmatched "');
|
|
1463
|
+
case " ":
|
|
1464
|
+
case "\t":
|
|
1465
|
+
case "\r":
|
|
1466
|
+
case "\n":
|
|
1467
|
+
case "\f":
|
|
1468
|
+
if (SIGNIFICANT_WHITESPACE[state]) {
|
|
1469
|
+
buffer += character;
|
|
934
1470
|
}
|
|
935
|
-
} while (token[index - 2] === '\\');
|
|
936
|
-
if (index === 0) {
|
|
937
1471
|
break;
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
state = 'importRule';
|
|
947
|
-
if (i === endingIndex) {
|
|
948
|
-
token += ';'
|
|
1472
|
+
|
|
1473
|
+
// String
|
|
1474
|
+
case '"':
|
|
1475
|
+
index = i + 1;
|
|
1476
|
+
do {
|
|
1477
|
+
index = token.indexOf('"', index) + 1;
|
|
1478
|
+
if (!index) {
|
|
1479
|
+
parseError('Unmatched "');
|
|
949
1480
|
}
|
|
1481
|
+
} while (token[index - 2] === '\\');
|
|
1482
|
+
if (index === 0) {
|
|
950
1483
|
break;
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1484
|
+
}
|
|
1485
|
+
buffer += token.slice(i, index);
|
|
1486
|
+
i = index - 1;
|
|
1487
|
+
switch (state) {
|
|
1488
|
+
case 'before-value':
|
|
1489
|
+
state = 'value';
|
|
1490
|
+
break;
|
|
1491
|
+
case 'importRule-begin':
|
|
1492
|
+
state = 'importRule';
|
|
1493
|
+
if (i === endingIndex) {
|
|
1494
|
+
token += ';'
|
|
1495
|
+
}
|
|
1496
|
+
break;
|
|
1497
|
+
case 'namespaceRule-begin':
|
|
1498
|
+
state = 'namespaceRule';
|
|
1499
|
+
if (i === endingIndex) {
|
|
1500
|
+
token += ';'
|
|
1501
|
+
}
|
|
1502
|
+
break;
|
|
1503
|
+
}
|
|
1504
|
+
break;
|
|
1505
|
+
|
|
1506
|
+
case "'":
|
|
1507
|
+
index = i + 1;
|
|
1508
|
+
do {
|
|
1509
|
+
index = token.indexOf("'", index) + 1;
|
|
1510
|
+
if (!index) {
|
|
1511
|
+
parseError("Unmatched '");
|
|
955
1512
|
}
|
|
1513
|
+
} while (token[index - 2] === '\\');
|
|
1514
|
+
if (index === 0) {
|
|
956
1515
|
break;
|
|
957
|
-
}
|
|
958
|
-
break;
|
|
959
|
-
|
|
960
|
-
case "'":
|
|
961
|
-
index = i + 1;
|
|
962
|
-
do {
|
|
963
|
-
index = token.indexOf("'", index) + 1;
|
|
964
|
-
if (!index) {
|
|
965
|
-
parseError("Unmatched '");
|
|
966
1516
|
}
|
|
967
|
-
|
|
968
|
-
|
|
1517
|
+
buffer += token.slice(i, index);
|
|
1518
|
+
i = index - 1;
|
|
1519
|
+
switch (state) {
|
|
1520
|
+
case 'before-value':
|
|
1521
|
+
state = 'value';
|
|
1522
|
+
break;
|
|
1523
|
+
case 'importRule-begin':
|
|
1524
|
+
state = 'importRule';
|
|
1525
|
+
break;
|
|
1526
|
+
case 'namespaceRule-begin':
|
|
1527
|
+
state = 'namespaceRule';
|
|
1528
|
+
break;
|
|
1529
|
+
}
|
|
969
1530
|
break;
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
break;
|
|
983
|
-
}
|
|
984
|
-
break;
|
|
985
|
-
|
|
986
|
-
// Comment
|
|
987
|
-
case "/":
|
|
988
|
-
if (token.charAt(i + 1) === "*") {
|
|
989
|
-
i += 2;
|
|
990
|
-
index = token.indexOf("*/", i);
|
|
991
|
-
if (index === -1) {
|
|
992
|
-
i = token.length - 1;
|
|
993
|
-
buffer = "";
|
|
1531
|
+
|
|
1532
|
+
// Comment
|
|
1533
|
+
case "/":
|
|
1534
|
+
if (token.charAt(i + 1) === "*") {
|
|
1535
|
+
i += 2;
|
|
1536
|
+
index = token.indexOf("*/", i);
|
|
1537
|
+
if (index === -1) {
|
|
1538
|
+
i = token.length - 1;
|
|
1539
|
+
buffer = "";
|
|
1540
|
+
} else {
|
|
1541
|
+
i = index + 1;
|
|
1542
|
+
}
|
|
994
1543
|
} else {
|
|
995
|
-
|
|
1544
|
+
buffer += character;
|
|
1545
|
+
}
|
|
1546
|
+
if (state === "importRule-begin") {
|
|
1547
|
+
buffer += " ";
|
|
1548
|
+
state = "importRule";
|
|
1549
|
+
}
|
|
1550
|
+
if (state === "namespaceRule-begin") {
|
|
1551
|
+
buffer += " ";
|
|
1552
|
+
state = "namespaceRule";
|
|
996
1553
|
}
|
|
997
|
-
} else {
|
|
998
|
-
buffer += character;
|
|
999
|
-
}
|
|
1000
|
-
if (state === "importRule-begin") {
|
|
1001
|
-
buffer += " ";
|
|
1002
|
-
state = "importRule";
|
|
1003
|
-
}
|
|
1004
|
-
if (state === "namespaceRule-begin") {
|
|
1005
|
-
buffer += " ";
|
|
1006
|
-
state = "namespaceRule";
|
|
1007
|
-
}
|
|
1008
|
-
break;
|
|
1009
|
-
|
|
1010
|
-
// At-rule
|
|
1011
|
-
case "@":
|
|
1012
|
-
if (token.indexOf("@-moz-document", i) === i) {
|
|
1013
|
-
validateAtRule("@-moz-document", function(){
|
|
1014
|
-
state = "documentRule-begin";
|
|
1015
|
-
documentRule = new CSSOM.CSSDocumentRule();
|
|
1016
|
-
documentRule.__starts = i;
|
|
1017
|
-
i += "-moz-document".length;
|
|
1018
|
-
});
|
|
1019
|
-
buffer = "";
|
|
1020
|
-
break;
|
|
1021
|
-
} else if (token.indexOf("@media", i) === i) {
|
|
1022
|
-
validateAtRule("@media", function(){
|
|
1023
|
-
state = "atBlock";
|
|
1024
|
-
mediaRule = new CSSOM.CSSMediaRule();
|
|
1025
|
-
mediaRule.__starts = i;
|
|
1026
|
-
i += "media".length;
|
|
1027
|
-
});
|
|
1028
|
-
buffer = "";
|
|
1029
|
-
break;
|
|
1030
|
-
} else if (token.indexOf("@container", i) === i) {
|
|
1031
|
-
validateAtRule("@container", function(){
|
|
1032
|
-
state = "containerBlock";
|
|
1033
|
-
containerRule = new CSSOM.CSSContainerRule();
|
|
1034
|
-
containerRule.__starts = i;
|
|
1035
|
-
i += "container".length;
|
|
1036
|
-
});
|
|
1037
|
-
buffer = "";
|
|
1038
|
-
break;
|
|
1039
|
-
} else if (token.indexOf("@counter-style", i) === i) {
|
|
1040
|
-
validateAtRule("@counter-style", function(){
|
|
1041
|
-
state = "counterStyleBlock"
|
|
1042
|
-
counterStyleRule = new CSSOM.CSSCounterStyleRule();
|
|
1043
|
-
counterStyleRule.__starts = i;
|
|
1044
|
-
i += "counter-style".length;
|
|
1045
|
-
}, true);
|
|
1046
|
-
buffer = "";
|
|
1047
|
-
break;
|
|
1048
|
-
} else if (token.indexOf("@scope", i) === i) {
|
|
1049
|
-
validateAtRule("@scope", function(){
|
|
1050
|
-
state = "scopeBlock";
|
|
1051
|
-
scopeRule = new CSSOM.CSSScopeRule();
|
|
1052
|
-
scopeRule.__starts = i;
|
|
1053
|
-
i += "scope".length;
|
|
1054
|
-
});
|
|
1055
|
-
buffer = "";
|
|
1056
|
-
break;
|
|
1057
|
-
} else if (token.indexOf("@layer", i) === i) {
|
|
1058
|
-
validateAtRule("@layer", function(){
|
|
1059
|
-
state = "layerBlock"
|
|
1060
|
-
layerBlockRule = new CSSOM.CSSLayerBlockRule();
|
|
1061
|
-
layerBlockRule.__starts = i;
|
|
1062
|
-
i += "layer".length;
|
|
1063
|
-
});
|
|
1064
|
-
buffer = "";
|
|
1065
|
-
break;
|
|
1066
|
-
} else if (token.indexOf("@page", i) === i) {
|
|
1067
|
-
validateAtRule("@page", function(){
|
|
1068
|
-
state = "pageBlock"
|
|
1069
|
-
pageRule = new CSSOM.CSSPageRule();
|
|
1070
|
-
pageRule.__starts = i;
|
|
1071
|
-
i += "page".length;
|
|
1072
|
-
});
|
|
1073
|
-
buffer = "";
|
|
1074
|
-
break;
|
|
1075
|
-
} else if (token.indexOf("@supports", i) === i) {
|
|
1076
|
-
validateAtRule("@supports", function(){
|
|
1077
|
-
state = "conditionBlock";
|
|
1078
|
-
supportsRule = new CSSOM.CSSSupportsRule();
|
|
1079
|
-
supportsRule.__starts = i;
|
|
1080
|
-
i += "supports".length;
|
|
1081
|
-
});
|
|
1082
|
-
buffer = "";
|
|
1083
|
-
break;
|
|
1084
|
-
} else if (token.indexOf("@host", i) === i) {
|
|
1085
|
-
validateAtRule("@host", function(){
|
|
1086
|
-
state = "hostRule-begin";
|
|
1087
|
-
i += "host".length;
|
|
1088
|
-
hostRule = new CSSOM.CSSHostRule();
|
|
1089
|
-
hostRule.__starts = i;
|
|
1090
|
-
});
|
|
1091
|
-
buffer = "";
|
|
1092
|
-
break;
|
|
1093
|
-
} else if (token.indexOf("@starting-style", i) === i) {
|
|
1094
|
-
validateAtRule("@starting-style", function(){
|
|
1095
|
-
state = "startingStyleRule-begin";
|
|
1096
|
-
i += "starting-style".length;
|
|
1097
|
-
startingStyleRule = new CSSOM.CSSStartingStyleRule();
|
|
1098
|
-
startingStyleRule.__starts = i;
|
|
1099
|
-
});
|
|
1100
|
-
buffer = "";
|
|
1101
|
-
break;
|
|
1102
|
-
} else if (token.indexOf("@import", i) === i) {
|
|
1103
|
-
buffer = "";
|
|
1104
|
-
validateAtRule("@import", function(){
|
|
1105
|
-
state = "importRule-begin";
|
|
1106
|
-
i += "import".length;
|
|
1107
|
-
buffer += "@import";
|
|
1108
|
-
}, true);
|
|
1109
|
-
break;
|
|
1110
|
-
} else if (token.indexOf("@namespace", i) === i) {
|
|
1111
|
-
buffer = "";
|
|
1112
|
-
validateAtRule("@namespace", function(){
|
|
1113
|
-
state = "namespaceRule-begin";
|
|
1114
|
-
i += "namespace".length;
|
|
1115
|
-
buffer += "@namespace";
|
|
1116
|
-
}, true);
|
|
1117
|
-
break;
|
|
1118
|
-
} else if (token.indexOf("@font-face", i) === i) {
|
|
1119
|
-
buffer = "";
|
|
1120
|
-
validateAtRule("@font-face", function(){
|
|
1121
|
-
state = "fontFaceRule-begin";
|
|
1122
|
-
i += "font-face".length;
|
|
1123
|
-
fontFaceRule = new CSSOM.CSSFontFaceRule();
|
|
1124
|
-
fontFaceRule.__starts = i;
|
|
1125
|
-
}, true);
|
|
1126
1554
|
break;
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
if (
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1555
|
+
|
|
1556
|
+
// At-rule
|
|
1557
|
+
case "@":
|
|
1558
|
+
if (nestedSelectorRule) {
|
|
1559
|
+
if (styleRule && styleRule.constructor.name === "CSSNestedDeclarations") {
|
|
1560
|
+
currentScope.cssRules.push(styleRule);
|
|
1561
|
+
}
|
|
1562
|
+
if (nestedSelectorRule.parentRule && nestedSelectorRule.parentRule.constructor.name === "CSSStyleRule") {
|
|
1563
|
+
styleRule = nestedSelectorRule.parentRule;
|
|
1564
|
+
}
|
|
1565
|
+
// Don't reset nestedSelectorRule here - preserve it through @-rules
|
|
1566
|
+
}
|
|
1567
|
+
if (token.indexOf("@-moz-document", i) === i) {
|
|
1568
|
+
validateAtRule("@-moz-document", function () {
|
|
1569
|
+
state = "documentRule-begin";
|
|
1570
|
+
documentRule = new CSSOM.CSSDocumentRule();
|
|
1571
|
+
documentRule.__starts = i;
|
|
1572
|
+
i += "-moz-document".length;
|
|
1573
|
+
});
|
|
1136
1574
|
buffer = "";
|
|
1137
1575
|
break;
|
|
1138
|
-
} else if (
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1576
|
+
} else if (token.indexOf("@media", i) === i) {
|
|
1577
|
+
validateAtRule("@media", function () {
|
|
1578
|
+
state = "atBlock";
|
|
1579
|
+
mediaRule = new CSSOM.CSSMediaRule();
|
|
1580
|
+
mediaRule.__starts = i;
|
|
1581
|
+
i += "media".length;
|
|
1582
|
+
});
|
|
1583
|
+
buffer = "";
|
|
1584
|
+
break;
|
|
1585
|
+
} else if (token.indexOf("@container", i) === i) {
|
|
1586
|
+
validateAtRule("@container", function () {
|
|
1587
|
+
state = "containerBlock";
|
|
1588
|
+
containerRule = new CSSOM.CSSContainerRule();
|
|
1589
|
+
containerRule.__starts = i;
|
|
1590
|
+
i += "container".length;
|
|
1591
|
+
});
|
|
1592
|
+
buffer = "";
|
|
1593
|
+
break;
|
|
1594
|
+
} else if (token.indexOf("@counter-style", i) === i) {
|
|
1595
|
+
validateAtRule("@counter-style", function () {
|
|
1596
|
+
state = "counterStyleBlock"
|
|
1597
|
+
counterStyleRule = new CSSOM.CSSCounterStyleRule();
|
|
1598
|
+
counterStyleRule.__starts = i;
|
|
1599
|
+
i += "counter-style".length;
|
|
1600
|
+
}, true);
|
|
1601
|
+
buffer = "";
|
|
1602
|
+
break;
|
|
1603
|
+
} else if (token.indexOf("@scope", i) === i) {
|
|
1604
|
+
validateAtRule("@scope", function () {
|
|
1605
|
+
state = "scopeBlock";
|
|
1606
|
+
scopeRule = new CSSOM.CSSScopeRule();
|
|
1607
|
+
scopeRule.__starts = i;
|
|
1608
|
+
i += "scope".length;
|
|
1609
|
+
});
|
|
1610
|
+
buffer = "";
|
|
1611
|
+
break;
|
|
1612
|
+
} else if (token.indexOf("@layer", i) === i) {
|
|
1613
|
+
validateAtRule("@layer", function () {
|
|
1614
|
+
state = "layerBlock"
|
|
1615
|
+
layerBlockRule = new CSSOM.CSSLayerBlockRule();
|
|
1616
|
+
layerBlockRule.__starts = i;
|
|
1617
|
+
i += "layer".length;
|
|
1618
|
+
});
|
|
1619
|
+
buffer = "";
|
|
1620
|
+
break;
|
|
1621
|
+
} else if (token.indexOf("@page", i) === i) {
|
|
1622
|
+
validateAtRule("@page", function () {
|
|
1623
|
+
state = "pageBlock"
|
|
1624
|
+
pageRule = new CSSOM.CSSPageRule();
|
|
1625
|
+
pageRule.__starts = i;
|
|
1626
|
+
i += "page".length;
|
|
1627
|
+
});
|
|
1628
|
+
buffer = "";
|
|
1629
|
+
break;
|
|
1630
|
+
} else if (token.indexOf("@supports", i) === i) {
|
|
1631
|
+
validateAtRule("@supports", function () {
|
|
1632
|
+
state = "conditionBlock";
|
|
1633
|
+
supportsRule = new CSSOM.CSSSupportsRule();
|
|
1634
|
+
supportsRule.__starts = i;
|
|
1635
|
+
i += "supports".length;
|
|
1636
|
+
});
|
|
1637
|
+
buffer = "";
|
|
1638
|
+
break;
|
|
1639
|
+
} else if (token.indexOf("@host", i) === i) {
|
|
1640
|
+
validateAtRule("@host", function () {
|
|
1641
|
+
state = "hostRule-begin";
|
|
1642
|
+
i += "host".length;
|
|
1643
|
+
hostRule = new CSSOM.CSSHostRule();
|
|
1644
|
+
hostRule.__starts = i;
|
|
1645
|
+
});
|
|
1646
|
+
buffer = "";
|
|
1647
|
+
break;
|
|
1648
|
+
} else if (token.indexOf("@starting-style", i) === i) {
|
|
1649
|
+
validateAtRule("@starting-style", function () {
|
|
1650
|
+
state = "startingStyleRule-begin";
|
|
1651
|
+
i += "starting-style".length;
|
|
1652
|
+
startingStyleRule = new CSSOM.CSSStartingStyleRule();
|
|
1653
|
+
startingStyleRule.__starts = i;
|
|
1654
|
+
});
|
|
1655
|
+
buffer = "";
|
|
1656
|
+
break;
|
|
1657
|
+
} else if (token.indexOf("@import", i) === i) {
|
|
1658
|
+
buffer = "";
|
|
1659
|
+
validateAtRule("@import", function () {
|
|
1660
|
+
state = "importRule-begin";
|
|
1661
|
+
i += "import".length;
|
|
1662
|
+
buffer += "@import";
|
|
1663
|
+
}, true);
|
|
1664
|
+
break;
|
|
1665
|
+
} else if (token.indexOf("@namespace", i) === i) {
|
|
1666
|
+
buffer = "";
|
|
1667
|
+
validateAtRule("@namespace", function () {
|
|
1668
|
+
state = "namespaceRule-begin";
|
|
1669
|
+
i += "namespace".length;
|
|
1670
|
+
buffer += "@namespace";
|
|
1671
|
+
}, true);
|
|
1672
|
+
break;
|
|
1673
|
+
} else if (token.indexOf("@font-face", i) === i) {
|
|
1674
|
+
buffer = "";
|
|
1675
|
+
// @font-face can be nested only inside CSSScopeRule or CSSConditionRule
|
|
1676
|
+
// and only if there's no CSSStyleRule in the parent chain
|
|
1677
|
+
var cannotBeNested = true;
|
|
1678
|
+
if (currentScope !== topScope) {
|
|
1679
|
+
var hasStyleRuleInChain = false;
|
|
1680
|
+
var hasValidParent = false;
|
|
1681
|
+
|
|
1682
|
+
// Check currentScope
|
|
1683
|
+
if (currentScope.constructor.name === 'CSSStyleRule') {
|
|
1684
|
+
hasStyleRuleInChain = true;
|
|
1685
|
+
} else if (currentScope instanceof CSSOM.CSSScopeRule || currentScope instanceof CSSOM.CSSConditionRule) {
|
|
1686
|
+
hasValidParent = true;
|
|
1687
|
+
}
|
|
1144
1688
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1689
|
+
// Check ancestorRules for CSSStyleRule
|
|
1690
|
+
if (!hasStyleRuleInChain) {
|
|
1691
|
+
for (var j = 0; j < ancestorRules.length; j++) {
|
|
1692
|
+
if (ancestorRules[j].constructor.name === 'CSSStyleRule') {
|
|
1693
|
+
hasStyleRuleInChain = true;
|
|
1694
|
+
break;
|
|
1695
|
+
}
|
|
1696
|
+
if (ancestorRules[j] instanceof CSSOM.CSSScopeRule || ancestorRules[j] instanceof CSSOM.CSSConditionRule) {
|
|
1697
|
+
hasValidParent = true;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// Allow nesting if we have a valid parent and no style rule in the chain
|
|
1703
|
+
if (hasValidParent && !hasStyleRuleInChain) {
|
|
1704
|
+
cannotBeNested = false;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
validateAtRule("@font-face", function () {
|
|
1708
|
+
state = "fontFaceRule-begin";
|
|
1709
|
+
i += "font-face".length;
|
|
1710
|
+
fontFaceRule = new CSSOM.CSSFontFaceRule();
|
|
1711
|
+
fontFaceRule.__starts = i;
|
|
1712
|
+
}, cannotBeNested);
|
|
1713
|
+
break;
|
|
1714
|
+
} else {
|
|
1715
|
+
atKeyframesRegExp.lastIndex = i;
|
|
1716
|
+
var matchKeyframes = atKeyframesRegExp.exec(token);
|
|
1717
|
+
if (matchKeyframes && matchKeyframes.index === i) {
|
|
1718
|
+
state = "keyframesRule-begin";
|
|
1719
|
+
keyframesRule = new CSSOM.CSSKeyframesRule();
|
|
1720
|
+
keyframesRule.__starts = i;
|
|
1721
|
+
keyframesRule._vendorPrefix = matchKeyframes[1]; // Will come out as undefined if no prefix was found
|
|
1722
|
+
i += matchKeyframes[0].length - 1;
|
|
1159
1723
|
buffer = "";
|
|
1160
|
-
state = "before-selector";
|
|
1161
|
-
i += ruleClosingMatch.index + ruleClosingMatch[0].length;
|
|
1162
1724
|
break;
|
|
1725
|
+
} else if (state === "selector") {
|
|
1726
|
+
state = "atRule";
|
|
1163
1727
|
}
|
|
1164
1728
|
}
|
|
1729
|
+
buffer += character;
|
|
1730
|
+
break;
|
|
1165
1731
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1732
|
+
case "{":
|
|
1733
|
+
if (currentScope === topScope) {
|
|
1734
|
+
nestedSelectorRule = null;
|
|
1169
1735
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
styleRule.__parentStyleSheet = styleSheet;
|
|
1175
|
-
buffer = "";
|
|
1176
|
-
state = "before-name";
|
|
1177
|
-
} else if (state === "atBlock") {
|
|
1178
|
-
mediaRule.media.mediaText = buffer.trim();
|
|
1179
|
-
|
|
1180
|
-
if (parentRule) {
|
|
1181
|
-
mediaRule.__parentRule = parentRule;
|
|
1182
|
-
ancestorRules.push(parentRule);
|
|
1736
|
+
if (state === 'before-selector') {
|
|
1737
|
+
parseError("Unexpected {");
|
|
1738
|
+
i = ignoreBalancedBlock(i, token.slice(i));
|
|
1739
|
+
break;
|
|
1183
1740
|
}
|
|
1741
|
+
if (state === "selector" || state === "atRule") {
|
|
1742
|
+
if (!nestedSelectorRule && buffer.indexOf(";") !== -1) {
|
|
1743
|
+
var ruleClosingMatch = token.slice(i).match(forwardRuleClosingBraceRegExp);
|
|
1744
|
+
if (ruleClosingMatch) {
|
|
1745
|
+
styleRule = null;
|
|
1746
|
+
buffer = "";
|
|
1747
|
+
state = "before-selector";
|
|
1748
|
+
i += ruleClosingMatch.index + ruleClosingMatch[0].length;
|
|
1749
|
+
break;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1184
1752
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
containerRule.__conditionText = buffer.trim();
|
|
1191
|
-
|
|
1192
|
-
if (parentRule) {
|
|
1193
|
-
containerRule.__parentRule = parentRule;
|
|
1194
|
-
ancestorRules.push(parentRule);
|
|
1195
|
-
}
|
|
1196
|
-
currentScope = parentRule = containerRule;
|
|
1197
|
-
containerRule.__parentStyleSheet = styleSheet;
|
|
1198
|
-
buffer = "";
|
|
1199
|
-
state = "before-selector";
|
|
1200
|
-
} else if (state === "counterStyleBlock") {
|
|
1201
|
-
// TODO: Validate counter-style name. At least that it cannot be empty nor multiple
|
|
1202
|
-
counterStyleRule.name = buffer.trim().replace(/\n/g, "");
|
|
1203
|
-
currentScope = parentRule = counterStyleRule;
|
|
1204
|
-
counterStyleRule.__parentStyleSheet = styleSheet;
|
|
1205
|
-
buffer = "";
|
|
1206
|
-
} else if (state === "conditionBlock") {
|
|
1207
|
-
supportsRule.__conditionText = buffer.trim();
|
|
1208
|
-
|
|
1209
|
-
if (parentRule) {
|
|
1210
|
-
supportsRule.__parentRule = parentRule;
|
|
1211
|
-
ancestorRules.push(parentRule);
|
|
1212
|
-
}
|
|
1753
|
+
// Ensure styleRule exists before trying to set properties on it
|
|
1754
|
+
if (!styleRule) {
|
|
1755
|
+
styleRule = new CSSOM.CSSStyleRule();
|
|
1756
|
+
styleRule.__starts = i;
|
|
1757
|
+
}
|
|
1213
1758
|
|
|
1214
|
-
|
|
1215
|
-
supportsRule.__parentStyleSheet = styleSheet;
|
|
1216
|
-
buffer = "";
|
|
1217
|
-
state = "before-selector";
|
|
1218
|
-
} else if (state === "scopeBlock") {
|
|
1219
|
-
var parsedScopePrelude = parseScopePrelude(buffer.trim());
|
|
1220
|
-
|
|
1221
|
-
if (parsedScopePrelude.hasStart) {
|
|
1222
|
-
scopeRule.__start = parsedScopePrelude.startSelector;
|
|
1223
|
-
}
|
|
1224
|
-
if (parsedScopePrelude.hasEnd) {
|
|
1225
|
-
scopeRule.__end = parsedScopePrelude.endSelector;
|
|
1226
|
-
}
|
|
1227
|
-
if (parsedScopePrelude.hasOnlyEnd) {
|
|
1228
|
-
scopeRule.__end = parsedScopePrelude.endSelector;
|
|
1229
|
-
}
|
|
1759
|
+
var originalParentRule = parentRule;
|
|
1230
1760
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
currentScope = parentRule = scopeRule;
|
|
1236
|
-
scopeRule.__parentStyleSheet = styleSheet;
|
|
1237
|
-
buffer = "";
|
|
1238
|
-
state = "before-selector";
|
|
1239
|
-
} else if (state === "layerBlock") {
|
|
1240
|
-
layerBlockRule.name = buffer.trim();
|
|
1761
|
+
if (parentRule) {
|
|
1762
|
+
styleRule.__parentRule = parentRule;
|
|
1763
|
+
pushToAncestorRules(parentRule);
|
|
1764
|
+
}
|
|
1241
1765
|
|
|
1242
|
-
|
|
1766
|
+
currentScope = parentRule = styleRule;
|
|
1767
|
+
var processedSelectorText = processSelectorText(buffer.trim());
|
|
1768
|
+
// In a nested selector, ensure each selector contains '&' at the beginning, except for selectors that already have '&' somewhere
|
|
1769
|
+
if (originalParentRule && originalParentRule.constructor.name === "CSSStyleRule") {
|
|
1770
|
+
styleRule.selectorText = parseAndSplitNestedSelectors(processedSelectorText).map(function (sel) {
|
|
1771
|
+
// Add & at the beginning if there's no & in the selector, or if it starts with a combinator
|
|
1772
|
+
return (sel.indexOf('&') === -1 || startsWithCombinatorRegExp.test(sel)) ? '& ' + sel : sel;
|
|
1773
|
+
}).join(', ');
|
|
1774
|
+
} else {
|
|
1775
|
+
styleRule.selectorText = processedSelectorText;
|
|
1776
|
+
}
|
|
1777
|
+
styleRule.style.__starts = i;
|
|
1778
|
+
styleRule.__parentStyleSheet = styleSheet;
|
|
1779
|
+
buffer = "";
|
|
1780
|
+
state = "before-name";
|
|
1781
|
+
} else if (state === "atBlock") {
|
|
1782
|
+
mediaRule.media.mediaText = buffer.trim();
|
|
1243
1783
|
|
|
1244
|
-
if (isValidName) {
|
|
1245
1784
|
if (parentRule) {
|
|
1246
|
-
|
|
1247
|
-
|
|
1785
|
+
mediaRule.__parentRule = parentRule;
|
|
1786
|
+
pushToAncestorRules(parentRule);
|
|
1787
|
+
// If entering @media from within a CSSStyleRule, set nestedSelectorRule
|
|
1788
|
+
// so that & selectors and declarations work correctly inside
|
|
1789
|
+
if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) {
|
|
1790
|
+
nestedSelectorRule = parentRule;
|
|
1791
|
+
}
|
|
1248
1792
|
}
|
|
1249
|
-
|
|
1250
|
-
currentScope = parentRule = layerBlockRule;
|
|
1251
|
-
layerBlockRule.__parentStyleSheet = styleSheet;
|
|
1252
|
-
}
|
|
1253
|
-
buffer = "";
|
|
1254
|
-
state = "before-selector";
|
|
1255
|
-
} else if (state === "pageBlock") {
|
|
1256
|
-
pageRule.selectorText = buffer.trim();
|
|
1257
|
-
|
|
1258
|
-
if (parentRule) {
|
|
1259
|
-
pageRule.__parentRule = parentRule;
|
|
1260
|
-
ancestorRules.push(parentRule);
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
currentScope = parentRule = pageRule;
|
|
1264
|
-
pageRule.__parentStyleSheet = styleSheet;
|
|
1265
|
-
styleRule = pageRule;
|
|
1266
|
-
buffer = "";
|
|
1267
|
-
state = "before-name";
|
|
1268
|
-
} else if (state === "hostRule-begin") {
|
|
1269
|
-
if (parentRule) {
|
|
1270
|
-
ancestorRules.push(parentRule);
|
|
1271
|
-
}
|
|
1272
1793
|
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
}
|
|
1794
|
+
currentScope = parentRule = mediaRule;
|
|
1795
|
+
pushToAncestorRules(mediaRule);
|
|
1796
|
+
mediaRule.__parentStyleSheet = styleSheet;
|
|
1797
|
+
styleRule = null; // Reset styleRule when entering @-rule
|
|
1798
|
+
buffer = "";
|
|
1799
|
+
state = "before-selector";
|
|
1800
|
+
} else if (state === "containerBlock") {
|
|
1801
|
+
containerRule.__conditionText = buffer.trim();
|
|
1282
1802
|
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1803
|
+
if (parentRule) {
|
|
1804
|
+
containerRule.__parentRule = parentRule;
|
|
1805
|
+
pushToAncestorRules(parentRule);
|
|
1806
|
+
if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) {
|
|
1807
|
+
nestedSelectorRule = parentRule;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
currentScope = parentRule = containerRule;
|
|
1811
|
+
pushToAncestorRules(containerRule);
|
|
1812
|
+
containerRule.__parentStyleSheet = styleSheet;
|
|
1813
|
+
styleRule = null; // Reset styleRule when entering @-rule
|
|
1814
|
+
buffer = "";
|
|
1815
|
+
state = "before-selector";
|
|
1816
|
+
} else if (state === "counterStyleBlock") {
|
|
1817
|
+
var counterStyleName = buffer.trim().replace(/\n/g, "");
|
|
1818
|
+
// Validate: name cannot be empty, contain whitespace, or contain dots
|
|
1819
|
+
var isValidCounterStyleName = counterStyleName.length > 0 && !/[\s.]/.test(counterStyleName);
|
|
1820
|
+
|
|
1821
|
+
if (isValidCounterStyleName) {
|
|
1822
|
+
counterStyleRule.name = counterStyleName;
|
|
1823
|
+
currentScope = parentRule = counterStyleRule;
|
|
1824
|
+
counterStyleRule.__parentStyleSheet = styleSheet;
|
|
1825
|
+
}
|
|
1826
|
+
buffer = "";
|
|
1827
|
+
} else if (state === "conditionBlock") {
|
|
1828
|
+
supportsRule.__conditionText = buffer.trim();
|
|
1287
1829
|
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
buffer = "";
|
|
1295
|
-
state = "before-name";
|
|
1296
|
-
} else if (state === "keyframesRule-begin") {
|
|
1297
|
-
keyframesRule.name = buffer.trim();
|
|
1298
|
-
if (parentRule) {
|
|
1299
|
-
ancestorRules.push(parentRule);
|
|
1300
|
-
keyframesRule.__parentRule = parentRule;
|
|
1301
|
-
}
|
|
1302
|
-
keyframesRule.__parentStyleSheet = styleSheet;
|
|
1303
|
-
currentScope = parentRule = keyframesRule;
|
|
1304
|
-
buffer = "";
|
|
1305
|
-
state = "keyframeRule-begin";
|
|
1306
|
-
} else if (state === "keyframeRule-begin") {
|
|
1307
|
-
styleRule = new CSSOM.CSSKeyframeRule();
|
|
1308
|
-
styleRule.keyText = buffer.trim();
|
|
1309
|
-
styleRule.__starts = i;
|
|
1310
|
-
buffer = "";
|
|
1311
|
-
state = "before-name";
|
|
1312
|
-
} else if (state === "documentRule-begin") {
|
|
1313
|
-
// FIXME: what if this '{' is in the url text of the match function?
|
|
1314
|
-
documentRule.matcher.matcherText = buffer.trim();
|
|
1315
|
-
if (parentRule) {
|
|
1316
|
-
ancestorRules.push(parentRule);
|
|
1317
|
-
documentRule.__parentRule = parentRule;
|
|
1318
|
-
}
|
|
1319
|
-
currentScope = parentRule = documentRule;
|
|
1320
|
-
documentRule.__parentStyleSheet = styleSheet;
|
|
1321
|
-
buffer = "";
|
|
1322
|
-
state = "before-selector";
|
|
1323
|
-
} else if (state === "before-name" || state === "name") {
|
|
1324
|
-
if (styleRule.constructor.name === "CSSNestedDeclarations") {
|
|
1325
|
-
if (styleRule.style.length) {
|
|
1326
|
-
parentRule.cssRules.push(styleRule);
|
|
1327
|
-
styleRule.__parentRule = parentRule;
|
|
1328
|
-
styleRule.__parentStyleSheet = styleSheet;
|
|
1329
|
-
ancestorRules.push(parentRule);
|
|
1330
|
-
} else {
|
|
1331
|
-
// If the styleRule is empty, we can assume that it's a nested selector
|
|
1332
|
-
ancestorRules.push(parentRule);
|
|
1830
|
+
if (parentRule) {
|
|
1831
|
+
supportsRule.__parentRule = parentRule;
|
|
1832
|
+
pushToAncestorRules(parentRule);
|
|
1833
|
+
if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) {
|
|
1834
|
+
nestedSelectorRule = parentRule;
|
|
1835
|
+
}
|
|
1333
1836
|
}
|
|
1334
|
-
} else {
|
|
1335
|
-
currentScope = parentRule = styleRule;
|
|
1336
|
-
ancestorRules.push(parentRule);
|
|
1337
|
-
styleRule.__parentStyleSheet = styleSheet;
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
styleRule = new CSSOM.CSSStyleRule();
|
|
1341
|
-
var processedSelectorText = processSelectorText(buffer.trim());
|
|
1342
|
-
// In a nested selector, ensure each selector contains '&' at the beginning, except for selectors that already have '&' somewhere
|
|
1343
|
-
if (parentRule.constructor.name !== "CSSStyleRule" && parentRule.parentRule === null) {
|
|
1344
|
-
styleRule.selectorText = processedSelectorText;
|
|
1345
|
-
} else {
|
|
1346
|
-
styleRule.selectorText = parseAndSplitNestedSelectors(processedSelectorText).map(function(sel) {
|
|
1347
|
-
// Add & at the beginning if there's no & in the selector, or if it starts with a combinator
|
|
1348
|
-
return (sel.indexOf('&') === -1 || startsWithCombinatorRegExp.test(sel)) ? '& ' + sel : sel;
|
|
1349
|
-
}).join(', ');
|
|
1350
|
-
}
|
|
1351
|
-
styleRule.style.__starts = i - buffer.length;
|
|
1352
|
-
styleRule.__parentRule = parentRule;
|
|
1353
|
-
nestedSelectorRule = styleRule;
|
|
1354
1837
|
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
case ":":
|
|
1361
|
-
if (state === "name") {
|
|
1362
|
-
// It can be a nested selector, let's check
|
|
1363
|
-
var openBraceBeforeMatch = token.slice(i).match(/[{;}]/);
|
|
1364
|
-
var hasOpenBraceBefore = openBraceBeforeMatch && openBraceBeforeMatch[0] === '{';
|
|
1365
|
-
if (hasOpenBraceBefore) {
|
|
1366
|
-
// Is a selector
|
|
1367
|
-
buffer += character;
|
|
1368
|
-
} else {
|
|
1369
|
-
// Is a declaration
|
|
1370
|
-
name = buffer.trim();
|
|
1838
|
+
currentScope = parentRule = supportsRule;
|
|
1839
|
+
pushToAncestorRules(supportsRule);
|
|
1840
|
+
supportsRule.__parentStyleSheet = styleSheet;
|
|
1841
|
+
styleRule = null; // Reset styleRule when entering @-rule
|
|
1371
1842
|
buffer = "";
|
|
1372
|
-
state = "before-
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
buffer += character;
|
|
1376
|
-
}
|
|
1377
|
-
break;
|
|
1843
|
+
state = "before-selector";
|
|
1844
|
+
} else if (state === "scopeBlock") {
|
|
1845
|
+
var parsedScopePrelude = parseScopePrelude(buffer.trim());
|
|
1378
1846
|
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1847
|
+
if (parsedScopePrelude.hasStart) {
|
|
1848
|
+
scopeRule.__start = parsedScopePrelude.startSelector;
|
|
1849
|
+
}
|
|
1850
|
+
if (parsedScopePrelude.hasEnd) {
|
|
1851
|
+
scopeRule.__end = parsedScopePrelude.endSelector;
|
|
1852
|
+
}
|
|
1853
|
+
if (parsedScopePrelude.hasOnlyEnd) {
|
|
1854
|
+
scopeRule.__end = parsedScopePrelude.endSelector;
|
|
1855
|
+
}
|
|
1384
1856
|
|
|
1385
|
-
if (
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1857
|
+
if (parentRule) {
|
|
1858
|
+
scopeRule.__parentRule = parentRule;
|
|
1859
|
+
pushToAncestorRules(parentRule);
|
|
1860
|
+
if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) {
|
|
1861
|
+
nestedSelectorRule = parentRule;
|
|
1862
|
+
}
|
|
1390
1863
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
//
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
valueParenthesisDepth++;
|
|
1400
|
-
buffer += character;
|
|
1401
|
-
} else {
|
|
1402
|
-
buffer += character;
|
|
1403
|
-
}
|
|
1404
|
-
break;
|
|
1864
|
+
currentScope = parentRule = scopeRule;
|
|
1865
|
+
pushToAncestorRules(scopeRule);
|
|
1866
|
+
scopeRule.__parentStyleSheet = styleSheet;
|
|
1867
|
+
styleRule = null; // Reset styleRule when entering @-rule
|
|
1868
|
+
buffer = "";
|
|
1869
|
+
state = "before-selector";
|
|
1870
|
+
} else if (state === "layerBlock") {
|
|
1871
|
+
layerBlockRule.name = buffer.trim();
|
|
1405
1872
|
|
|
1406
|
-
|
|
1407
|
-
if (state === 'value-parenthesis') {
|
|
1408
|
-
valueParenthesisDepth--;
|
|
1409
|
-
if (valueParenthesisDepth === 0) state = 'value';
|
|
1410
|
-
}
|
|
1411
|
-
buffer += character;
|
|
1412
|
-
break;
|
|
1873
|
+
var isValidName = layerBlockRule.name.length === 0 || layerBlockRule.name.match(cssCustomIdentifierRegExp) !== null;
|
|
1413
1874
|
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1875
|
+
if (isValidName) {
|
|
1876
|
+
if (parentRule) {
|
|
1877
|
+
layerBlockRule.__parentRule = parentRule;
|
|
1878
|
+
pushToAncestorRules(parentRule);
|
|
1879
|
+
if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) {
|
|
1880
|
+
nestedSelectorRule = parentRule;
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1422
1883
|
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1884
|
+
currentScope = parentRule = layerBlockRule;
|
|
1885
|
+
pushToAncestorRules(layerBlockRule);
|
|
1886
|
+
layerBlockRule.__parentStyleSheet = styleSheet;
|
|
1887
|
+
}
|
|
1888
|
+
styleRule = null; // Reset styleRule when entering @-rule
|
|
1428
1889
|
buffer = "";
|
|
1429
|
-
state = "before-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1890
|
+
state = "before-selector";
|
|
1891
|
+
} else if (state === "pageBlock") {
|
|
1892
|
+
pageRule.selectorText = buffer.trim();
|
|
1893
|
+
|
|
1894
|
+
if (parentRule) {
|
|
1895
|
+
pageRule.__parentRule = parentRule;
|
|
1896
|
+
pushToAncestorRules(parentRule);
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
currentScope = parentRule = pageRule;
|
|
1900
|
+
pageRule.__parentStyleSheet = styleSheet;
|
|
1901
|
+
styleRule = pageRule;
|
|
1434
1902
|
buffer = "";
|
|
1435
1903
|
state = "before-name";
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
state = "before-selector";
|
|
1440
|
-
break;
|
|
1441
|
-
case "importRule":
|
|
1442
|
-
var isValid = styleSheet.cssRules.length === 0 || styleSheet.cssRules.some(function (rule) {
|
|
1443
|
-
return ['CSSImportRule', 'CSSLayerStatementRule'].indexOf(rule.constructor.name) !== -1
|
|
1444
|
-
});
|
|
1445
|
-
if (isValid) {
|
|
1446
|
-
importRule = new CSSOM.CSSImportRule();
|
|
1447
|
-
importRule.__parentStyleSheet = importRule.styleSheet.__parentStyleSheet = styleSheet;
|
|
1448
|
-
importRule.cssText = buffer + character;
|
|
1449
|
-
styleSheet.cssRules.push(importRule);
|
|
1904
|
+
} else if (state === "hostRule-begin") {
|
|
1905
|
+
if (parentRule) {
|
|
1906
|
+
pushToAncestorRules(parentRule);
|
|
1450
1907
|
}
|
|
1908
|
+
|
|
1909
|
+
currentScope = parentRule = hostRule;
|
|
1910
|
+
pushToAncestorRules(hostRule);
|
|
1911
|
+
hostRule.__parentStyleSheet = styleSheet;
|
|
1451
1912
|
buffer = "";
|
|
1452
1913
|
state = "before-selector";
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
try {
|
|
1460
|
-
// Validate namespace syntax before creating the rule
|
|
1461
|
-
var testNamespaceRule = new CSSOM.CSSNamespaceRule();
|
|
1462
|
-
testNamespaceRule.cssText = buffer + character;
|
|
1463
|
-
|
|
1464
|
-
namespaceRule = testNamespaceRule;
|
|
1465
|
-
namespaceRule.__parentStyleSheet = styleSheet;
|
|
1466
|
-
styleSheet.cssRules.push(namespaceRule);
|
|
1467
|
-
|
|
1468
|
-
// Track the namespace prefix for validation
|
|
1469
|
-
if (namespaceRule.prefix) {
|
|
1470
|
-
definedNamespacePrefixes[namespaceRule.prefix] = namespaceRule.namespaceURI;
|
|
1471
|
-
}
|
|
1472
|
-
} catch(e) {
|
|
1473
|
-
parseError(e.message);
|
|
1914
|
+
} else if (state === "startingStyleRule-begin") {
|
|
1915
|
+
if (parentRule) {
|
|
1916
|
+
startingStyleRule.__parentRule = parentRule;
|
|
1917
|
+
pushToAncestorRules(parentRule);
|
|
1918
|
+
if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) {
|
|
1919
|
+
nestedSelectorRule = parentRule;
|
|
1474
1920
|
}
|
|
1475
1921
|
}
|
|
1922
|
+
|
|
1923
|
+
currentScope = parentRule = startingStyleRule;
|
|
1924
|
+
pushToAncestorRules(startingStyleRule);
|
|
1925
|
+
startingStyleRule.__parentStyleSheet = styleSheet;
|
|
1926
|
+
styleRule = null; // Reset styleRule when entering @-rule
|
|
1476
1927
|
buffer = "";
|
|
1477
1928
|
state = "before-selector";
|
|
1478
|
-
break;
|
|
1479
|
-
case "layerBlock":
|
|
1480
|
-
var nameListStr = buffer.trim().split(",").map(function (name) {
|
|
1481
|
-
return name.trim();
|
|
1482
|
-
});
|
|
1483
|
-
var isInvalid = parentRule !== undefined || nameListStr.some(function (name) {
|
|
1484
|
-
return name.trim().match(cssCustomIdentifierRegExp) === null;
|
|
1485
|
-
});
|
|
1486
1929
|
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
layerStatementRule.__starts = layerBlockRule.__starts;
|
|
1491
|
-
layerStatementRule.__ends = i;
|
|
1492
|
-
layerStatementRule.nameList = nameListStr;
|
|
1493
|
-
styleSheet.cssRules.push(layerStatementRule);
|
|
1930
|
+
} else if (state === "fontFaceRule-begin") {
|
|
1931
|
+
if (parentRule) {
|
|
1932
|
+
fontFaceRule.__parentRule = parentRule;
|
|
1494
1933
|
}
|
|
1934
|
+
fontFaceRule.__parentStyleSheet = styleSheet;
|
|
1935
|
+
styleRule = fontFaceRule;
|
|
1495
1936
|
buffer = "";
|
|
1496
|
-
state = "before-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
break;
|
|
1503
|
-
|
|
1504
|
-
case "}":
|
|
1505
|
-
if (state === "counterStyleBlock") {
|
|
1506
|
-
// FIXME : Implement cssText get setter that parses the real implementation
|
|
1507
|
-
counterStyleRule.cssText = "@counter-style " + counterStyleRule.name + " { " + buffer.trim().replace(/\n/g, " ").replace(/(['"])(?:\\.|[^\\])*?\1|(\s{2,})/g, function(match, quote) {
|
|
1508
|
-
return quote ? match : ' ';
|
|
1509
|
-
}) + " }";
|
|
1510
|
-
buffer = "";
|
|
1511
|
-
state = "before-selector";
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
switch (state) {
|
|
1515
|
-
case "value":
|
|
1516
|
-
styleRule.style.setProperty(name, buffer.trim(), priority, parseError);
|
|
1517
|
-
priority = "";
|
|
1518
|
-
/* falls through */
|
|
1519
|
-
case "before-value":
|
|
1520
|
-
case "before-name":
|
|
1521
|
-
case "name":
|
|
1522
|
-
styleRule.__ends = i + 1;
|
|
1523
|
-
|
|
1524
|
-
if (parentRule === styleRule) {
|
|
1525
|
-
parentRule = ancestorRules.pop()
|
|
1937
|
+
state = "before-name";
|
|
1938
|
+
} else if (state === "keyframesRule-begin") {
|
|
1939
|
+
keyframesRule.name = buffer.trim();
|
|
1940
|
+
if (parentRule) {
|
|
1941
|
+
pushToAncestorRules(parentRule);
|
|
1942
|
+
keyframesRule.__parentRule = parentRule;
|
|
1526
1943
|
}
|
|
1527
|
-
|
|
1944
|
+
keyframesRule.__parentStyleSheet = styleSheet;
|
|
1945
|
+
currentScope = parentRule = keyframesRule;
|
|
1946
|
+
buffer = "";
|
|
1947
|
+
state = "keyframeRule-begin";
|
|
1948
|
+
} else if (state === "keyframeRule-begin") {
|
|
1949
|
+
styleRule = new CSSOM.CSSKeyframeRule();
|
|
1950
|
+
styleRule.keyText = buffer.trim();
|
|
1951
|
+
styleRule.__starts = i;
|
|
1952
|
+
buffer = "";
|
|
1953
|
+
state = "before-name";
|
|
1954
|
+
} else if (state === "documentRule-begin") {
|
|
1955
|
+
// FIXME: what if this '{' is in the url text of the match function?
|
|
1956
|
+
documentRule.matcher.matcherText = buffer.trim();
|
|
1528
1957
|
if (parentRule) {
|
|
1529
|
-
|
|
1958
|
+
pushToAncestorRules(parentRule);
|
|
1959
|
+
documentRule.__parentRule = parentRule;
|
|
1530
1960
|
}
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1961
|
+
currentScope = parentRule = documentRule;
|
|
1962
|
+
pushToAncestorRules(documentRule);
|
|
1963
|
+
documentRule.__parentStyleSheet = styleSheet;
|
|
1964
|
+
buffer = "";
|
|
1965
|
+
state = "before-selector";
|
|
1966
|
+
} else if (state === "before-name" || state === "name") {
|
|
1967
|
+
// @font-face and similar rules don't support nested selectors
|
|
1968
|
+
// If we encounter a nested selector block inside them, skip it
|
|
1969
|
+
if (styleRule.constructor.name === "CSSFontFaceRule" ||
|
|
1970
|
+
styleRule.constructor.name === "CSSKeyframeRule" ||
|
|
1971
|
+
(styleRule.constructor.name === "CSSPageRule" && parentRule === styleRule)) {
|
|
1972
|
+
// Skip the nested block
|
|
1973
|
+
var ruleClosingMatch = token.slice(i).match(forwardRuleClosingBraceRegExp);
|
|
1974
|
+
if (ruleClosingMatch) {
|
|
1975
|
+
i += ruleClosingMatch.index + ruleClosingMatch[0].length - 1;
|
|
1976
|
+
buffer = "";
|
|
1977
|
+
state = "before-name";
|
|
1978
|
+
break;
|
|
1979
|
+
}
|
|
1535
1980
|
}
|
|
1536
1981
|
|
|
1537
|
-
if (styleRule.constructor.name === "
|
|
1538
|
-
if (styleRule
|
|
1539
|
-
|
|
1982
|
+
if (styleRule.constructor.name === "CSSNestedDeclarations") {
|
|
1983
|
+
if (styleRule.style.length) {
|
|
1984
|
+
parentRule.cssRules.push(styleRule);
|
|
1985
|
+
styleRule.__parentRule = parentRule;
|
|
1986
|
+
styleRule.__parentStyleSheet = styleSheet;
|
|
1987
|
+
pushToAncestorRules(parentRule);
|
|
1988
|
+
} else {
|
|
1989
|
+
// If the styleRule is empty, we can assume that it's a nested selector
|
|
1990
|
+
pushToAncestorRules(parentRule);
|
|
1540
1991
|
}
|
|
1541
|
-
parseError('Invalid CSSStyleRule (selectorText = "' + styleRule.selectorText + '")', styleRule.parentRule !== null);
|
|
1542
1992
|
} else {
|
|
1543
|
-
currentScope
|
|
1993
|
+
currentScope = parentRule = styleRule;
|
|
1994
|
+
pushToAncestorRules(parentRule);
|
|
1995
|
+
styleRule.__parentStyleSheet = styleSheet;
|
|
1544
1996
|
}
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1997
|
+
|
|
1998
|
+
styleRule = new CSSOM.CSSStyleRule();
|
|
1999
|
+
var processedSelectorText = processSelectorText(buffer.trim());
|
|
2000
|
+
// In a nested selector, ensure each selector contains '&' at the beginning, except for selectors that already have '&' somewhere
|
|
2001
|
+
if (parentRule.constructor.name === "CSSScopeRule" || (parentRule.constructor.name !== "CSSStyleRule" && parentRule.parentRule === null)) {
|
|
2002
|
+
styleRule.selectorText = processedSelectorText;
|
|
1548
2003
|
} else {
|
|
1549
|
-
|
|
2004
|
+
styleRule.selectorText = parseAndSplitNestedSelectors(processedSelectorText).map(function (sel) {
|
|
2005
|
+
// Add & at the beginning if there's no & in the selector, or if it starts with a combinator
|
|
2006
|
+
return (sel.indexOf('&') === -1 || startsWithCombinatorRegExp.test(sel)) ? '& ' + sel : sel;
|
|
2007
|
+
}).join(', ');
|
|
2008
|
+
}
|
|
2009
|
+
styleRule.style.__starts = i - buffer.length;
|
|
2010
|
+
styleRule.__parentRule = parentRule;
|
|
2011
|
+
// Only set nestedSelectorRule if we're directly inside a CSSStyleRule or CSSScopeRule,
|
|
2012
|
+
// not inside other grouping rules like @media/@supports
|
|
2013
|
+
if (parentRule.constructor.name === "CSSStyleRule" || parentRule.constructor.name === "CSSScopeRule") {
|
|
2014
|
+
nestedSelectorRule = styleRule;
|
|
1550
2015
|
}
|
|
1551
2016
|
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
2017
|
+
buffer = "";
|
|
2018
|
+
state = "before-name";
|
|
2019
|
+
}
|
|
2020
|
+
break;
|
|
2021
|
+
|
|
2022
|
+
case ":":
|
|
2023
|
+
if (state === "name") {
|
|
2024
|
+
// It can be a nested selector, let's check
|
|
2025
|
+
var openBraceBeforeMatch = token.slice(i).match(/[{;}]/);
|
|
2026
|
+
var hasOpenBraceBefore = openBraceBeforeMatch && openBraceBeforeMatch[0] === '{';
|
|
2027
|
+
if (hasOpenBraceBefore) {
|
|
2028
|
+
// Is a selector
|
|
2029
|
+
buffer += character;
|
|
1557
2030
|
} else {
|
|
1558
|
-
|
|
1559
|
-
|
|
2031
|
+
// Is a declaration
|
|
2032
|
+
name = buffer.trim();
|
|
2033
|
+
buffer = "";
|
|
2034
|
+
state = "before-value";
|
|
1560
2035
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
2036
|
+
} else {
|
|
2037
|
+
buffer += character;
|
|
2038
|
+
}
|
|
2039
|
+
break;
|
|
2040
|
+
|
|
2041
|
+
case "(":
|
|
2042
|
+
if (state === 'value') {
|
|
2043
|
+
// ie css expression mode
|
|
2044
|
+
if (buffer.trim() === 'expression') {
|
|
2045
|
+
var info = (new CSSOM.CSSValueExpression(token, i)).parse();
|
|
2046
|
+
|
|
2047
|
+
if (info.error) {
|
|
2048
|
+
parseError(info.error);
|
|
2049
|
+
} else {
|
|
2050
|
+
buffer += info.expression;
|
|
2051
|
+
i = info.idx;
|
|
1571
2052
|
}
|
|
1572
|
-
|
|
1573
|
-
|
|
2053
|
+
} else {
|
|
2054
|
+
state = 'value-parenthesis';
|
|
2055
|
+
//always ensure this is reset to 1 on transition
|
|
2056
|
+
//from value to value-parenthesis
|
|
2057
|
+
valueParenthesisDepth = 1;
|
|
2058
|
+
buffer += character;
|
|
1574
2059
|
}
|
|
2060
|
+
} else if (state === 'value-parenthesis') {
|
|
2061
|
+
valueParenthesisDepth++;
|
|
2062
|
+
buffer += character;
|
|
2063
|
+
} else {
|
|
2064
|
+
buffer += character;
|
|
2065
|
+
}
|
|
2066
|
+
break;
|
|
1575
2067
|
|
|
2068
|
+
case ")":
|
|
2069
|
+
if (state === 'value-parenthesis') {
|
|
2070
|
+
valueParenthesisDepth--;
|
|
2071
|
+
if (valueParenthesisDepth === 0) state = 'value';
|
|
2072
|
+
}
|
|
2073
|
+
buffer += character;
|
|
2074
|
+
break;
|
|
1576
2075
|
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
2076
|
+
case "!":
|
|
2077
|
+
if (state === "value" && token.indexOf("!important", i) === i) {
|
|
2078
|
+
priority = "important";
|
|
2079
|
+
i += "important".length;
|
|
2080
|
+
} else {
|
|
2081
|
+
buffer += character;
|
|
2082
|
+
}
|
|
2083
|
+
break;
|
|
2084
|
+
|
|
2085
|
+
case ";":
|
|
2086
|
+
switch (state) {
|
|
2087
|
+
case "before-value":
|
|
2088
|
+
case "before-name":
|
|
2089
|
+
parseError("Unexpected ;");
|
|
2090
|
+
buffer = "";
|
|
2091
|
+
state = "before-name";
|
|
2092
|
+
break;
|
|
2093
|
+
case "value":
|
|
2094
|
+
styleRule.style.setProperty(name, buffer.trim(), priority, parseError);
|
|
2095
|
+
priority = "";
|
|
2096
|
+
buffer = "";
|
|
2097
|
+
state = "before-name";
|
|
2098
|
+
break;
|
|
2099
|
+
case "atRule":
|
|
2100
|
+
buffer = "";
|
|
2101
|
+
state = "before-selector";
|
|
2102
|
+
break;
|
|
2103
|
+
case "importRule":
|
|
2104
|
+
var isValid = topScope.cssRules.length === 0 || topScope.cssRules.some(function (rule) {
|
|
2105
|
+
return ['CSSImportRule', 'CSSLayerStatementRule'].indexOf(rule.constructor.name) !== -1
|
|
2106
|
+
});
|
|
2107
|
+
if (isValid) {
|
|
2108
|
+
importRule = new CSSOM.CSSImportRule();
|
|
2109
|
+
importRule.__parentStyleSheet = importRule.styleSheet.__parentStyleSheet = styleSheet;
|
|
2110
|
+
importRule.parse(buffer + character);
|
|
2111
|
+
topScope.cssRules.push(importRule);
|
|
2112
|
+
}
|
|
2113
|
+
buffer = "";
|
|
2114
|
+
state = "before-selector";
|
|
2115
|
+
break;
|
|
2116
|
+
case "namespaceRule":
|
|
2117
|
+
var isValid = topScope.cssRules.length === 0 || topScope.cssRules.every(function (rule) {
|
|
2118
|
+
return ['CSSImportRule', 'CSSLayerStatementRule', 'CSSNamespaceRule'].indexOf(rule.constructor.name) !== -1
|
|
2119
|
+
});
|
|
2120
|
+
if (isValid) {
|
|
2121
|
+
try {
|
|
2122
|
+
// Validate namespace syntax before creating the rule
|
|
2123
|
+
var testNamespaceRule = new CSSOM.CSSNamespaceRule();
|
|
2124
|
+
testNamespaceRule.parse(buffer + character);
|
|
2125
|
+
|
|
2126
|
+
namespaceRule = testNamespaceRule;
|
|
2127
|
+
namespaceRule.__parentStyleSheet = styleSheet;
|
|
2128
|
+
topScope.cssRules.push(namespaceRule);
|
|
2129
|
+
|
|
2130
|
+
// Track the namespace prefix for validation
|
|
2131
|
+
if (namespaceRule.prefix) {
|
|
2132
|
+
definedNamespacePrefixes[namespaceRule.prefix] = namespaceRule.namespaceURI;
|
|
2133
|
+
}
|
|
2134
|
+
} catch (e) {
|
|
2135
|
+
parseError(e.message);
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
buffer = "";
|
|
2139
|
+
state = "before-selector";
|
|
2140
|
+
break;
|
|
2141
|
+
case "layerBlock":
|
|
2142
|
+
var nameListStr = buffer.trim().split(",").map(function (name) {
|
|
2143
|
+
return name.trim();
|
|
2144
|
+
});
|
|
2145
|
+
var isInvalid = nameListStr.some(function (name) {
|
|
2146
|
+
return name.trim().match(cssCustomIdentifierRegExp) === null;
|
|
2147
|
+
});
|
|
2148
|
+
|
|
2149
|
+
// Check if there's a CSSStyleRule in the parent chain
|
|
2150
|
+
var hasStyleRuleParent = false;
|
|
2151
|
+
if (parentRule) {
|
|
2152
|
+
var checkParent = parentRule;
|
|
2153
|
+
while (checkParent) {
|
|
2154
|
+
if (checkParent.constructor.name === "CSSStyleRule") {
|
|
2155
|
+
hasStyleRuleParent = true;
|
|
2156
|
+
break;
|
|
1599
2157
|
}
|
|
2158
|
+
checkParent = checkParent.__parentRule;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
if (!isInvalid && !hasStyleRuleParent) {
|
|
2163
|
+
layerStatementRule = new CSSOM.CSSLayerStatementRule();
|
|
2164
|
+
layerStatementRule.__parentStyleSheet = styleSheet;
|
|
2165
|
+
layerStatementRule.__starts = layerBlockRule.__starts;
|
|
2166
|
+
layerStatementRule.__ends = i;
|
|
2167
|
+
layerStatementRule.nameList = nameListStr;
|
|
2168
|
+
|
|
2169
|
+
// Add to parent rule if nested, otherwise to top scope
|
|
2170
|
+
if (parentRule) {
|
|
2171
|
+
layerStatementRule.__parentRule = parentRule;
|
|
2172
|
+
parentRule.cssRules.push(layerStatementRule);
|
|
1600
2173
|
} else {
|
|
1601
|
-
|
|
1602
|
-
currentScope = parentRule;
|
|
1603
|
-
currentScope !== prevScope && currentScope.cssRules.push(prevScope);
|
|
1604
|
-
break;
|
|
2174
|
+
topScope.cssRules.push(layerStatementRule);
|
|
1605
2175
|
}
|
|
1606
2176
|
}
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
2177
|
+
buffer = "";
|
|
2178
|
+
state = "before-selector";
|
|
2179
|
+
break;
|
|
2180
|
+
default:
|
|
2181
|
+
buffer += character;
|
|
2182
|
+
break;
|
|
2183
|
+
}
|
|
2184
|
+
break;
|
|
2185
|
+
|
|
2186
|
+
case "}":
|
|
2187
|
+
if (state === "counterStyleBlock") {
|
|
2188
|
+
// FIXME : Implement missing properties on CSSCounterStyleRule interface and update parse method
|
|
2189
|
+
// For now it's just assigning entire rule text
|
|
2190
|
+
counterStyleRule.parse("@counter-style " + counterStyleRule.name + " { " + buffer + " }");
|
|
2191
|
+
buffer = "";
|
|
2192
|
+
state = "before-selector";
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
switch (state) {
|
|
2196
|
+
case "value":
|
|
2197
|
+
styleRule.style.setProperty(name, buffer.trim(), priority, parseError);
|
|
2198
|
+
priority = "";
|
|
2199
|
+
/* falls through */
|
|
2200
|
+
case "before-value":
|
|
2201
|
+
case "before-name":
|
|
2202
|
+
case "name":
|
|
2203
|
+
styleRule.__ends = i + 1;
|
|
2204
|
+
|
|
2205
|
+
if (parentRule === styleRule) {
|
|
2206
|
+
parentRule = ancestorRules.pop()
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
if (parentRule) {
|
|
2210
|
+
styleRule.__parentRule = parentRule;
|
|
2211
|
+
}
|
|
2212
|
+
styleRule.__parentStyleSheet = styleSheet;
|
|
2213
|
+
|
|
2214
|
+
if (currentScope === styleRule) {
|
|
2215
|
+
currentScope = parentRule || topScope;
|
|
1615
2216
|
}
|
|
1616
|
-
|
|
1617
|
-
if (
|
|
1618
|
-
|
|
1619
|
-
var nestedSelectorTokenToCurrentSelectorToken = token.slice(nestedSelectorRule.__starts, i + 1);
|
|
1620
|
-
var openingBraceMatch = nestedSelectorTokenToCurrentSelectorToken.match(/{/g);
|
|
1621
|
-
var closingBraceMatch = nestedSelectorTokenToCurrentSelectorToken.match(/}/g);
|
|
1622
|
-
var openingBraceLen = openingBraceMatch && openingBraceMatch.length;
|
|
1623
|
-
var closingBraceLen = closingBraceMatch && closingBraceMatch.length;
|
|
1624
|
-
|
|
1625
|
-
if (openingBraceLen === closingBraceLen) {
|
|
1626
|
-
// If the number of opening and closing braces are equal, we can assume that the new selector is starting outside the nestedSelectorRule
|
|
1627
|
-
nestedSelectorRule.__ends = i + 1;
|
|
2217
|
+
|
|
2218
|
+
if (styleRule.constructor.name === "CSSStyleRule" && !isValidSelectorText(styleRule.selectorText)) {
|
|
2219
|
+
if (styleRule === nestedSelectorRule) {
|
|
1628
2220
|
nestedSelectorRule = null;
|
|
1629
|
-
parentRule = null;
|
|
1630
2221
|
}
|
|
2222
|
+
parseError('Invalid CSSStyleRule (selectorText = "' + styleRule.selectorText + '")', styleRule.parentRule !== null);
|
|
2223
|
+
} else {
|
|
2224
|
+
if (styleRule.parentRule) {
|
|
2225
|
+
styleRule.parentRule.cssRules.push(styleRule);
|
|
2226
|
+
} else {
|
|
2227
|
+
currentScope.cssRules.push(styleRule);
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
buffer = "";
|
|
2231
|
+
if (currentScope.constructor === CSSOM.CSSKeyframesRule) {
|
|
2232
|
+
state = "keyframeRule-begin";
|
|
1631
2233
|
} else {
|
|
1632
|
-
|
|
2234
|
+
state = "before-selector";
|
|
2235
|
+
}
|
|
1633
2236
|
|
|
2237
|
+
if (styleRule.constructor.name === "CSSNestedDeclarations") {
|
|
2238
|
+
if (currentScope !== topScope) {
|
|
2239
|
+
// Only set nestedSelectorRule if currentScope is CSSStyleRule or CSSScopeRule
|
|
2240
|
+
// Not for other grouping rules like @media/@supports
|
|
2241
|
+
if (currentScope.constructor.name === "CSSStyleRule" || currentScope.constructor.name === "CSSScopeRule") {
|
|
2242
|
+
nestedSelectorRule = currentScope;
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
styleRule = null;
|
|
2246
|
+
} else {
|
|
2247
|
+
// Update nestedSelectorRule when closing a CSSStyleRule
|
|
2248
|
+
if (styleRule === nestedSelectorRule) {
|
|
2249
|
+
var selector = styleRule.selectorText && styleRule.selectorText.trim();
|
|
2250
|
+
// Check if this is proper nesting (&.class, &:pseudo) vs prepended & (& :is, & .class with space)
|
|
2251
|
+
// Prepended & has pattern "& X" where X starts with : or .
|
|
2252
|
+
var isPrependedAmpersand = selector && selector.match(/^&\s+[:\.]/);
|
|
2253
|
+
|
|
2254
|
+
// Check if parent is a grouping rule that can contain nested selectors
|
|
2255
|
+
var isGroupingRule = currentScope && currentScope instanceof CSSOM.CSSGroupingRule;
|
|
2256
|
+
|
|
2257
|
+
if (!isPrependedAmpersand && isGroupingRule) {
|
|
2258
|
+
// Proper nesting - set nestedSelectorRule to parent for more nested selectors
|
|
2259
|
+
// But only if it's a CSSStyleRule or CSSScopeRule, not other grouping rules like @media
|
|
2260
|
+
if (currentScope.constructor.name === "CSSStyleRule" || currentScope.constructor.name === "CSSScopeRule") {
|
|
2261
|
+
nestedSelectorRule = currentScope;
|
|
2262
|
+
}
|
|
2263
|
+
// If currentScope is another type of grouping rule (like @media), keep nestedSelectorRule unchanged
|
|
2264
|
+
} else {
|
|
2265
|
+
// Prepended & or not nested in grouping rule - reset to prevent CSSNestedDeclarations
|
|
2266
|
+
nestedSelectorRule = null;
|
|
2267
|
+
}
|
|
2268
|
+
} else if (nestedSelectorRule && currentScope instanceof CSSOM.CSSGroupingRule) {
|
|
2269
|
+
// When closing a nested rule that's not the nestedSelectorRule itself,
|
|
2270
|
+
// maintain nestedSelectorRule if we're still inside a grouping rule
|
|
2271
|
+
// This ensures declarations after nested selectors inside @media/@supports etc. work correctly
|
|
2272
|
+
}
|
|
2273
|
+
styleRule = null;
|
|
2274
|
+
break;
|
|
1634
2275
|
}
|
|
1635
|
-
|
|
2276
|
+
case "keyframeRule-begin":
|
|
2277
|
+
case "before-selector":
|
|
2278
|
+
case "selector":
|
|
2279
|
+
// End of media/supports/document rule.
|
|
2280
|
+
if (!parentRule) {
|
|
2281
|
+
parseError("Unexpected }");
|
|
2282
|
+
|
|
2283
|
+
var hasPreviousStyleRule = currentScope.cssRules.length && currentScope.cssRules[currentScope.cssRules.length - 1].constructor.name === "CSSStyleRule";
|
|
2284
|
+
if (hasPreviousStyleRule) {
|
|
2285
|
+
i = ignoreBalancedBlock(i, token.slice(i), 1);
|
|
2286
|
+
}
|
|
1636
2287
|
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
break;
|
|
1640
|
-
}
|
|
1641
|
-
break;
|
|
2288
|
+
break;
|
|
2289
|
+
}
|
|
1642
2290
|
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
2291
|
+
while (ancestorRules.length > 0) {
|
|
2292
|
+
parentRule = ancestorRules.pop();
|
|
2293
|
+
|
|
2294
|
+
if (parentRule instanceof CSSOM.CSSGroupingRule && (parentRule.constructor.name !== 'CSSStyleRule' || parentRule.__parentRule)) {
|
|
2295
|
+
if (nestedSelectorRule) {
|
|
2296
|
+
if (nestedSelectorRule.parentRule) {
|
|
2297
|
+
prevScope = nestedSelectorRule;
|
|
2298
|
+
currentScope = nestedSelectorRule.parentRule;
|
|
2299
|
+
if (currentScope.cssRules.findIndex(function (rule) {
|
|
2300
|
+
return rule === prevScope
|
|
2301
|
+
}) === -1) {
|
|
2302
|
+
currentScope.cssRules.push(prevScope);
|
|
2303
|
+
}
|
|
2304
|
+
nestedSelectorRule = currentScope;
|
|
2305
|
+
} else {
|
|
2306
|
+
// If nestedSelectorRule doesn't have a parentRule, we're closing a grouping rule
|
|
2307
|
+
// inside a top-level CSSStyleRule. We need to push currentScope to the parentRule.
|
|
2308
|
+
prevScope = currentScope;
|
|
2309
|
+
// Push to actual parent from ancestorRules if available
|
|
2310
|
+
var actualParent = ancestorRules.length > 0 ? ancestorRules[ancestorRules.length - 1] : nestedSelectorRule;
|
|
2311
|
+
actualParent !== prevScope && actualParent.cssRules.push(prevScope);
|
|
2312
|
+
// Update currentScope to the nestedSelectorRule before breaking
|
|
2313
|
+
currentScope = actualParent;
|
|
2314
|
+
parentRule = actualParent;
|
|
2315
|
+
break;
|
|
2316
|
+
}
|
|
2317
|
+
} else {
|
|
2318
|
+
prevScope = currentScope;
|
|
2319
|
+
parentRule !== prevScope && parentRule.cssRules.push(prevScope);
|
|
2320
|
+
break;
|
|
2321
|
+
}
|
|
1664
2322
|
}
|
|
1665
2323
|
}
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
if
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
2324
|
+
|
|
2325
|
+
// If currentScope has a __parentRule and wasn't added yet, add it
|
|
2326
|
+
if (ancestorRules.length === 0 && currentScope.__parentRule && currentScope.__parentRule.cssRules) {
|
|
2327
|
+
if (currentScope.__parentRule.cssRules.findIndex(function (rule) {
|
|
2328
|
+
return rule === currentScope
|
|
2329
|
+
}) === -1) {
|
|
2330
|
+
currentScope.__parentRule.cssRules.push(currentScope);
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
// Only handle top-level rule closing if we processed all ancestors
|
|
2335
|
+
if (ancestorRules.length === 0 && currentScope.parentRule == null) {
|
|
2336
|
+
currentScope.__ends = i + 1;
|
|
2337
|
+
if (currentScope !== topScope && topScope.cssRules.findIndex(function (rule) {
|
|
2338
|
+
return rule === currentScope
|
|
2339
|
+
}) === -1) {
|
|
2340
|
+
topScope.cssRules.push(currentScope);
|
|
2341
|
+
}
|
|
2342
|
+
currentScope = topScope;
|
|
2343
|
+
if (nestedSelectorRule === parentRule) {
|
|
2344
|
+
// Check if this selector is really starting inside another selector
|
|
2345
|
+
var nestedSelectorTokenToCurrentSelectorToken = token.slice(nestedSelectorRule.__starts, i + 1);
|
|
2346
|
+
var openingBraceMatch = nestedSelectorTokenToCurrentSelectorToken.match(/{/g);
|
|
2347
|
+
var closingBraceMatch = nestedSelectorTokenToCurrentSelectorToken.match(/}/g);
|
|
2348
|
+
var openingBraceLen = openingBraceMatch && openingBraceMatch.length;
|
|
2349
|
+
var closingBraceLen = closingBraceMatch && closingBraceMatch.length;
|
|
2350
|
+
|
|
2351
|
+
if (openingBraceLen === closingBraceLen) {
|
|
2352
|
+
// If the number of opening and closing braces are equal, we can assume that the new selector is starting outside the nestedSelectorRule
|
|
2353
|
+
nestedSelectorRule.__ends = i + 1;
|
|
2354
|
+
nestedSelectorRule = null;
|
|
2355
|
+
parentRule = null;
|
|
2356
|
+
}
|
|
1683
2357
|
} else {
|
|
2358
|
+
parentRule = null;
|
|
2359
|
+
}
|
|
2360
|
+
} else {
|
|
2361
|
+
currentScope = parentRule;
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
buffer = "";
|
|
2365
|
+
state = "before-selector";
|
|
2366
|
+
break;
|
|
2367
|
+
}
|
|
2368
|
+
break;
|
|
2369
|
+
|
|
2370
|
+
default:
|
|
2371
|
+
switch (state) {
|
|
2372
|
+
case "before-selector":
|
|
2373
|
+
state = "selector";
|
|
2374
|
+
if ((styleRule || scopeRule) && parentRule) {
|
|
2375
|
+
// Assuming it's a declaration inside Nested Selector OR a Nested Declaration
|
|
2376
|
+
// If Declaration inside Nested Selector let's keep the same styleRule
|
|
2377
|
+
if (!isSelectorStartChar(character) && !isWhitespaceChar(character) && parentRule instanceof CSSOM.CSSGroupingRule) {
|
|
2378
|
+
// parentRule.__parentRule = styleRule;
|
|
2379
|
+
state = "before-name";
|
|
2380
|
+
if (styleRule !== parentRule) {
|
|
2381
|
+
styleRule = new CSSOM.CSSNestedDeclarations();
|
|
2382
|
+
styleRule.__starts = i;
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
} else if (nestedSelectorRule && parentRule && parentRule instanceof CSSOM.CSSGroupingRule) {
|
|
2387
|
+
if (isSelectorStartChar(character)) {
|
|
2388
|
+
// If starting with a selector character, create CSSStyleRule instead of CSSNestedDeclarations
|
|
1684
2389
|
styleRule = new CSSOM.CSSStyleRule();
|
|
1685
|
-
styleRule.__starts = i;
|
|
2390
|
+
styleRule.__starts = i;
|
|
2391
|
+
} else if (!isWhitespaceChar(character)) {
|
|
2392
|
+
// Starting a declaration (not whitespace, not a selector)
|
|
2393
|
+
state = "before-name";
|
|
2394
|
+
// Check if we should create CSSNestedDeclarations
|
|
2395
|
+
// This happens if: parent has cssRules OR nestedSelectorRule exists (indicating CSSStyleRule in hierarchy)
|
|
2396
|
+
if (parentRule.cssRules.length || nestedSelectorRule) {
|
|
2397
|
+
currentScope = parentRule;
|
|
2398
|
+
// Only set nestedSelectorRule if parentRule is CSSStyleRule or CSSScopeRule
|
|
2399
|
+
if (parentRule.constructor.name === "CSSStyleRule" || parentRule.constructor.name === "CSSScopeRule") {
|
|
2400
|
+
nestedSelectorRule = parentRule;
|
|
2401
|
+
}
|
|
2402
|
+
styleRule = new CSSOM.CSSNestedDeclarations();
|
|
2403
|
+
styleRule.__starts = i;
|
|
2404
|
+
} else {
|
|
2405
|
+
if (parentRule.constructor.name === "CSSStyleRule") {
|
|
2406
|
+
styleRule = parentRule;
|
|
2407
|
+
} else {
|
|
2408
|
+
styleRule = new CSSOM.CSSStyleRule();
|
|
2409
|
+
styleRule.__starts = i;
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
1686
2412
|
}
|
|
1687
2413
|
}
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
2414
|
+
break;
|
|
2415
|
+
case "before-name":
|
|
2416
|
+
state = "name";
|
|
2417
|
+
break;
|
|
2418
|
+
case "before-value":
|
|
2419
|
+
state = "value";
|
|
2420
|
+
break;
|
|
2421
|
+
case "importRule-begin":
|
|
2422
|
+
state = "importRule";
|
|
2423
|
+
break;
|
|
2424
|
+
case "namespaceRule-begin":
|
|
2425
|
+
state = "namespaceRule";
|
|
2426
|
+
break;
|
|
2427
|
+
}
|
|
2428
|
+
buffer += character;
|
|
2429
|
+
break;
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
// Auto-close all unclosed nested structures
|
|
2433
|
+
// Check AFTER processing the character, at the ORIGINAL ending index
|
|
2434
|
+
// Only add closing braces if CSS is incomplete (not at top scope)
|
|
2435
|
+
if (i === initialEndingIndex && (currentScope !== topScope || ancestorRules.length > 0)) {
|
|
2436
|
+
var needsClosing = ancestorRules.length;
|
|
2437
|
+
if (currentScope !== topScope && ancestorRules.indexOf(currentScope) === -1) {
|
|
2438
|
+
needsClosing += 1;
|
|
2439
|
+
}
|
|
2440
|
+
// Add closing braces for all unclosed structures
|
|
2441
|
+
for (var closeIdx = 0; closeIdx < needsClosing; closeIdx++) {
|
|
2442
|
+
token += "}";
|
|
2443
|
+
endingIndex += 1;
|
|
1705
2444
|
}
|
|
1706
|
-
buffer += character;
|
|
1707
|
-
break;
|
|
1708
2445
|
}
|
|
1709
2446
|
}
|
|
1710
2447
|
|