@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/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 attach to the stylesheet. Useful on jsdom webplatform tests.
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 = new CSSOM.CSSStyleSheet();
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
- styleSheet.__globalObject = opts.globalObject;
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 = styleSheet;
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 !== styleSheet ? false : true;
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 !== styleSheet) {
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
- var pseudoPattern = /::?([a-zA-Z][\w-]*)\(([^)]+)\)/g;
598
- var pseudoMatch;
599
- while ((pseudoMatch = pseudoPattern.exec(selector)) !== null) {
600
- var pseudoName = pseudoMatch[1];
601
- var pseudoContent = pseudoMatch[2];
602
-
603
- // List of pseudo-classes that should not accept quoted strings
604
- // :lang() - accepts language codes: en, fr-CA
605
- // :dir() - accepts direction: ltr, rtl
606
- // :nth-*() - accepts An+B notation: 2n+1, odd, even
607
- var noQuotesPseudos = ['lang', 'dir', 'nth-child', 'nth-last-child', 'nth-of-type', 'nth-last-of-type'];
608
-
609
- for (var i = 0; i < noQuotesPseudos.length; i++) {
610
- if (pseudoName === noQuotesPseudos[i] && /['"]/.test(pseudoContent)) {
611
- return false;
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
- // Fallback to a loose regexp for the overall selector structure (without deep paren matching)
617
- // This is similar to the original, but without nested paren limitations
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
- * @type {RegExp}
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
- var globalPseudoClassRegExp = /:([a-zA-Z-]+)\(((?:[^()"]+|"[^"]*"|'[^']*'|\((?:[^()"]+|"[^"]*"|'[^']*')*\))*?)\)/g;
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 a non-global regex to find all pseudo-classes with arguments
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
- // TODO: Remove invalid selectors that appears inside pseudo classes
838
- // TODO: The same processing here needs to be reused in CSSStyleRule.selectorText setter
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
- case " ":
918
- case "\t":
919
- case "\r":
920
- case "\n":
921
- case "\f":
922
- if (SIGNIFICANT_WHITESPACE[state]) {
923
- buffer += character;
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
- buffer += token.slice(i, index);
940
- i = index - 1;
941
- switch (state) {
942
- case 'before-value':
943
- state = 'value';
944
- break;
945
- case 'importRule-begin':
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
- case 'namespaceRule-begin':
952
- state = 'namespaceRule';
953
- if (i === endingIndex) {
954
- token += ';'
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
- } while (token[index - 2] === '\\');
968
- if (index === 0) {
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
- buffer += token.slice(i, index);
972
- i = index - 1;
973
- switch (state) {
974
- case 'before-value':
975
- state = 'value';
976
- break;
977
- case 'importRule-begin':
978
- state = 'importRule';
979
- break;
980
- case 'namespaceRule-begin':
981
- state = 'namespaceRule';
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
- i = index + 1;
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
- } else {
1128
- atKeyframesRegExp.lastIndex = i;
1129
- var matchKeyframes = atKeyframesRegExp.exec(token);
1130
- if (matchKeyframes && matchKeyframes.index === i) {
1131
- state = "keyframesRule-begin";
1132
- keyframesRule = new CSSOM.CSSKeyframesRule();
1133
- keyframesRule.__starts = i;
1134
- keyframesRule._vendorPrefix = matchKeyframes[1]; // Will come out as undefined if no prefix was found
1135
- i += matchKeyframes[0].length - 1;
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 (state === "selector") {
1139
- state = "atRule";
1140
- }
1141
- }
1142
- buffer += character;
1143
- break;
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
- case "{":
1146
- if (currentScope === styleSheet) {
1147
- nestedSelectorRule = null;
1148
- }
1149
- if (state === 'before-selector') {
1150
- parseError("Unexpected {");
1151
- i = ignoreBalancedBlock(i, token.slice(i));
1152
- break;
1153
- }
1154
- if (state === "selector" || state === "atRule") {
1155
- if (!nestedSelectorRule && buffer.indexOf(";") !== -1) {
1156
- var ruleClosingMatch = token.slice(i).match(forwardRuleClosingBraceRegExp);
1157
- if (ruleClosingMatch) {
1158
- styleRule = null;
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
- if (parentRule) {
1167
- styleRule.__parentRule = parentRule;
1168
- ancestorRules.push(parentRule);
1732
+ case "{":
1733
+ if (currentScope === topScope) {
1734
+ nestedSelectorRule = null;
1169
1735
  }
1170
-
1171
- currentScope = parentRule = styleRule;
1172
- styleRule.selectorText = processSelectorText(buffer.trim());
1173
- styleRule.style.__starts = i;
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
- currentScope = parentRule = mediaRule;
1186
- mediaRule.__parentStyleSheet = styleSheet;
1187
- buffer = "";
1188
- state = "before-selector";
1189
- } else if (state === "containerBlock") {
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
- currentScope = parentRule = supportsRule;
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
- if (parentRule) {
1232
- scopeRule.__parentRule = parentRule;
1233
- ancestorRules.push(parentRule);
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
- var isValidName = layerBlockRule.name.length === 0 || layerBlockRule.name.match(cssCustomIdentifierRegExp) !== null;
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
- layerBlockRule.__parentRule = parentRule;
1247
- ancestorRules.push(parentRule);
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
- currentScope = parentRule = hostRule;
1274
- hostRule.__parentStyleSheet = styleSheet;
1275
- buffer = "";
1276
- state = "before-selector";
1277
- } else if (state === "startingStyleRule-begin") {
1278
- if (parentRule) {
1279
- startingStyleRule.__parentRule = parentRule;
1280
- ancestorRules.push(parentRule);
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
- currentScope = parentRule = startingStyleRule;
1284
- startingStyleRule.__parentStyleSheet = styleSheet;
1285
- buffer = "";
1286
- state = "before-selector";
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
- } else if (state === "fontFaceRule-begin") {
1289
- if (parentRule) {
1290
- fontFaceRule.__parentRule = parentRule;
1291
- }
1292
- fontFaceRule.__parentStyleSheet = styleSheet;
1293
- styleRule = fontFaceRule;
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
- buffer = "";
1356
- state = "before-name";
1357
- }
1358
- break;
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-value";
1373
- }
1374
- } else {
1375
- buffer += character;
1376
- }
1377
- break;
1843
+ state = "before-selector";
1844
+ } else if (state === "scopeBlock") {
1845
+ var parsedScopePrelude = parseScopePrelude(buffer.trim());
1378
1846
 
1379
- case "(":
1380
- if (state === 'value') {
1381
- // ie css expression mode
1382
- if (buffer.trim() === 'expression') {
1383
- var info = (new CSSOM.CSSValueExpression(token, i)).parse();
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 (info.error) {
1386
- parseError(info.error);
1387
- } else {
1388
- buffer += info.expression;
1389
- i = info.idx;
1857
+ if (parentRule) {
1858
+ scopeRule.__parentRule = parentRule;
1859
+ pushToAncestorRules(parentRule);
1860
+ if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) {
1861
+ nestedSelectorRule = parentRule;
1862
+ }
1390
1863
  }
1391
- } else {
1392
- state = 'value-parenthesis';
1393
- //always ensure this is reset to 1 on transition
1394
- //from value to value-parenthesis
1395
- valueParenthesisDepth = 1;
1396
- buffer += character;
1397
- }
1398
- } else if (state === 'value-parenthesis') {
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
- case ")":
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
- case "!":
1415
- if (state === "value" && token.indexOf("!important", i) === i) {
1416
- priority = "important";
1417
- i += "important".length;
1418
- } else {
1419
- buffer += character;
1420
- }
1421
- break;
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
- case ";":
1424
- switch (state) {
1425
- case "before-value":
1426
- case "before-name":
1427
- parseError("Unexpected ;");
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-name";
1430
- break;
1431
- case "value":
1432
- styleRule.style.setProperty(name, buffer.trim(), priority, parseError);
1433
- priority = "";
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
- break;
1437
- case "atRule":
1438
- buffer = "";
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
- break;
1454
- case "namespaceRule":
1455
- var isValid = styleSheet.cssRules.length === 0 || styleSheet.cssRules.every(function (rule) {
1456
- return ['CSSImportRule','CSSLayerStatementRule','CSSNamespaceRule'].indexOf(rule.constructor.name) !== -1
1457
- });
1458
- if (isValid) {
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
- if (!isInvalid) {
1488
- layerStatementRule = new CSSOM.CSSLayerStatementRule();
1489
- layerStatementRule.__parentStyleSheet = styleSheet;
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-selector";
1497
- break;
1498
- default:
1499
- buffer += character;
1500
- break;
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
- styleRule.__parentRule = parentRule;
1958
+ pushToAncestorRules(parentRule);
1959
+ documentRule.__parentRule = parentRule;
1530
1960
  }
1531
- styleRule.__parentStyleSheet = styleSheet;
1532
-
1533
- if (currentScope === styleRule) {
1534
- currentScope = parentRule || styleSheet;
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 === "CSSStyleRule" && !isValidSelectorText(styleRule.selectorText)) {
1538
- if (styleRule === nestedSelectorRule) {
1539
- nestedSelectorRule = null;
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.cssRules.push(styleRule);
1993
+ currentScope = parentRule = styleRule;
1994
+ pushToAncestorRules(parentRule);
1995
+ styleRule.__parentStyleSheet = styleSheet;
1544
1996
  }
1545
- buffer = "";
1546
- if (currentScope.constructor === CSSOM.CSSKeyframesRule) {
1547
- state = "keyframeRule-begin";
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
- state = "before-selector";
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
- if (styleRule.constructor.name === "CSSNestedDeclarations") {
1553
- if (currentScope !== styleSheet) {
1554
- nestedSelectorRule = currentScope;
1555
- }
1556
- styleRule = null;
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
- styleRule = null;
1559
- break;
2031
+ // Is a declaration
2032
+ name = buffer.trim();
2033
+ buffer = "";
2034
+ state = "before-value";
1560
2035
  }
1561
- case "keyframeRule-begin":
1562
- case "before-selector":
1563
- case "selector":
1564
- // End of media/supports/document rule.
1565
- if (!parentRule) {
1566
- parseError("Unexpected }");
1567
-
1568
- var hasPreviousStyleRule = currentScope.cssRules.length && currentScope.cssRules[currentScope.cssRules.length - 1].constructor.name === "CSSStyleRule";
1569
- if (hasPreviousStyleRule) {
1570
- i = ignoreBalancedBlock(i, token.slice(i), 1);
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
- break;
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
- while (ancestorRules.length > 0) {
1578
- parentRule = ancestorRules.pop();
1579
-
1580
- if (
1581
- parentRule.constructor.name === "CSSStyleRule"
1582
- || parentRule.constructor.name === "CSSMediaRule"
1583
- || parentRule.constructor.name === "CSSSupportsRule"
1584
- || parentRule.constructor.name === "CSSContainerRule"
1585
- || parentRule.constructor.name === "CSSScopeRule"
1586
- || parentRule.constructor.name === "CSSLayerBlockRule"
1587
- || parentRule.constructor.name === "CSSStartingStyleRule"
1588
- ) {
1589
- if (nestedSelectorRule) {
1590
- if (nestedSelectorRule.parentRule) {
1591
- prevScope = nestedSelectorRule;
1592
- currentScope = nestedSelectorRule.parentRule;
1593
- if (currentScope.cssRules.findIndex(function (rule) {
1594
- return rule === prevScope
1595
- }) === -1) {
1596
- currentScope.cssRules.push(prevScope);
1597
- }
1598
- nestedSelectorRule = currentScope;
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
- prevScope = currentScope;
1602
- currentScope = parentRule;
1603
- currentScope !== prevScope && currentScope.cssRules.push(prevScope);
1604
- break;
2174
+ topScope.cssRules.push(layerStatementRule);
1605
2175
  }
1606
2176
  }
1607
- }
1608
-
1609
- if (currentScope.parentRule == null) {
1610
- currentScope.__ends = i + 1;
1611
- if (currentScope !== styleSheet && styleSheet.cssRules.findIndex(function (rule) {
1612
- return rule === currentScope
1613
- }) === -1) {
1614
- styleSheet.cssRules.push(currentScope);
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
- currentScope = styleSheet;
1617
- if (nestedSelectorRule === parentRule) {
1618
- // Check if this selector is really starting inside another selector
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
- parentRule = null;
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
- buffer = "";
1638
- state = "before-selector";
1639
- break;
1640
- }
1641
- break;
2288
+ break;
2289
+ }
1642
2290
 
1643
- default:
1644
- switch (state) {
1645
- case "before-selector":
1646
- state = "selector";
1647
- if (styleRule && parentRule) {
1648
- // Assuming it's a declaration inside Nested Selector OR a Nested Declaration
1649
- // If Declaration inside Nested Selector let's keep the same styleRule
1650
- if (
1651
- parentRule.constructor.name === "CSSStyleRule"
1652
- || parentRule.constructor.name === "CSSMediaRule"
1653
- || parentRule.constructor.name === "CSSSupportsRule"
1654
- || parentRule.constructor.name === "CSSContainerRule"
1655
- || parentRule.constructor.name === "CSSScopeRule"
1656
- || parentRule.constructor.name === "CSSLayerBlockRule"
1657
- || parentRule.constructor.name === "CSSStartingStyleRule"
1658
- ) {
1659
- // parentRule.__parentRule = styleRule;
1660
- state = "before-name";
1661
- if (styleRule !== parentRule) {
1662
- styleRule = new CSSOM.CSSNestedDeclarations();
1663
- styleRule.__starts = i;
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
- } else if (nestedSelectorRule && parentRule && (
1668
- parentRule.constructor.name === "CSSStyleRule"
1669
- || parentRule.constructor.name === "CSSMediaRule"
1670
- || parentRule.constructor.name === "CSSSupportsRule"
1671
- || parentRule.constructor.name === "CSSContainerRule"
1672
- || parentRule.constructor.name === "CSSLayerBlockRule"
1673
- || parentRule.constructor.name === "CSSStartingStyleRule"
1674
- )) {
1675
- state = "before-name";
1676
- if (parentRule.cssRules.length) {
1677
- currentScope = nestedSelectorRule = parentRule;
1678
- styleRule = new CSSOM.CSSNestedDeclarations();
1679
- styleRule.__starts = i;
1680
- } else {
1681
- if (parentRule.constructor.name === "CSSStyleRule") {
1682
- styleRule = parentRule;
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
- } else {
1689
- styleRule = new CSSOM.CSSStyleRule();
1690
- styleRule.__starts = i;
1691
- }
1692
- break;
1693
- case "before-name":
1694
- state = "name";
1695
- break;
1696
- case "before-value":
1697
- state = "value";
1698
- break;
1699
- case "importRule-begin":
1700
- state = "importRule";
1701
- break;
1702
- case "namespaceRule-begin":
1703
- state = "namespaceRule";
1704
- break;
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