@acemir/cssom 0.9.24 → 0.9.26

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,17 @@
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.
12
15
  * @param {CSSOM.CSSStyleSheet} [opts.styleSheet] - Reuse a style sheet instead of creating a new one (e.g. as `parentStyleSheet`)
13
16
  * @param {CSSOM.CSSRuleList} [opts.cssRules] - Prepare all rules in this list instead of mutating the style sheet continually
14
17
  * @param {function|boolean} [errorHandler] - Optional error handler function or `true` to use `console.error`.
@@ -62,6 +65,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
62
65
  styleSheet = opts.styleSheet;
63
66
  } else {
64
67
  styleSheet = new CSSOM.CSSStyleSheet()
68
+ styleSheet.__constructed = false;
65
69
  }
66
70
 
67
71
  var topScope;
@@ -72,7 +76,19 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
72
76
  }
73
77
 
74
78
  if (opts && opts.globalObject) {
75
- styleSheet.__globalObject = opts.globalObject;
79
+ CSSOM.setup({ globalObject: opts.globalObject });
80
+ }
81
+
82
+ if (opts && opts.ownerNode) {
83
+ styleSheet.__ownerNode = opts.ownerNode;
84
+ var ownerNodeMedia = opts.ownerNode.media || (opts.ownerNode.getAttribute && opts.ownerNode.getAttribute("media"));
85
+ if (ownerNodeMedia) {
86
+ styleSheet.media.mediaText = ownerNodeMedia;
87
+ }
88
+ }
89
+
90
+ if (opts && opts.ownerRule) {
91
+ styleSheet.__ownerRule = opts.ownerRule;
76
92
  }
77
93
 
78
94
  // @type CSSStyleSheet|CSSMediaRule|CSSContainerRule|CSSSupportsRule|CSSFontFaceRule|CSSKeyframesRule|CSSDocumentRule
@@ -84,7 +100,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
84
100
  var ancestorRules = [];
85
101
  var prevScope;
86
102
 
87
- var name, priority="", styleRule, mediaRule, containerRule, counterStyleRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule, startingStyleRule, scopeRule, pageRule, layerBlockRule, layerStatementRule, nestedSelectorRule, namespaceRule;
103
+ var name, priority = "", styleRule, mediaRule, containerRule, counterStyleRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule, startingStyleRule, scopeRule, pageRule, layerBlockRule, layerStatementRule, nestedSelectorRule, namespaceRule;
88
104
 
89
105
  // Track defined namespace prefixes for validation
90
106
  var definedNamespacePrefixes = {};
@@ -182,14 +198,14 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
182
198
  var ruleClosingMatch = matchBalancedBlock(str, fromIndex);
183
199
  if (ruleClosingMatch) {
184
200
  var ignoreRange = ruleClosingMatch.index + ruleClosingMatch[0].length;
185
- i+= ignoreRange;
201
+ i += ignoreRange;
186
202
  if (token.charAt(i) === '}') {
187
203
  i -= 1;
188
204
  }
189
205
  } else {
190
206
  i += str.length;
191
207
  }
192
- return i;
208
+ return i;
193
209
  }
194
210
 
195
211
  /**
@@ -199,29 +215,29 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
199
215
  */
200
216
  function parseScopePrelude(preludeContent) {
201
217
  var parts = preludeContent.split(/\s*\)\s*to\s+\(/);
202
-
218
+
203
219
  // Restore the parentheses that were consumed by the split
204
220
  if (parts.length === 2) {
205
221
  parts[0] = parts[0] + ')';
206
222
  parts[1] = '(' + parts[1];
207
223
  }
208
-
224
+
209
225
  var hasStart = parts[0] &&
210
226
  parts[0].charAt(0) === '(' &&
211
227
  parts[0].charAt(parts[0].length - 1) === ')';
212
228
  var hasEnd = parts[1] &&
213
229
  parts[1].charAt(0) === '(' &&
214
230
  parts[1].charAt(parts[1].length - 1) === ')';
215
-
231
+
216
232
  // Handle case: @scope to (<end>)
217
233
  var hasOnlyEnd = !hasStart &&
218
234
  !hasEnd &&
219
235
  parts[0].indexOf('to (') === 0 &&
220
236
  parts[0].charAt(parts[0].length - 1) === ')';
221
-
237
+
222
238
  var startSelector = '';
223
239
  var endSelector = '';
224
-
240
+
225
241
  if (hasStart) {
226
242
  startSelector = parts[0].slice(1, -1).trim();
227
243
  }
@@ -231,7 +247,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
231
247
  if (hasOnlyEnd) {
232
248
  endSelector = parts[0].slice(4, -1).trim();
233
249
  }
234
-
250
+
235
251
  return {
236
252
  startSelector: startSelector,
237
253
  endSelector: endSelector,
@@ -269,11 +285,11 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
269
285
  var inDoubleQuote = false;
270
286
  var inAttr = false;
271
287
  var stack = useStack ? [] : null;
272
-
288
+
273
289
  for (var i = 0; i < selector.length; i++) {
274
290
  var char = selector[i];
275
291
  var prevChar = i > 0 ? selector[i - 1] : '';
276
-
292
+
277
293
  if (inSingleQuote) {
278
294
  if (char === "'" && prevChar !== "\\") {
279
295
  inSingleQuote = false;
@@ -324,7 +340,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
324
340
  }
325
341
  }
326
342
  }
327
-
343
+
328
344
  // Check if everything is balanced
329
345
  if (useStack) {
330
346
  return stack.length === 0 && bracketDepth === 0 && !inSingleQuote && !inDoubleQuote && !inAttr;
@@ -374,7 +390,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
374
390
  /(?:^|[\s>+~,\[])cue\s*\(/i,
375
391
  /(?:^|[\s>+~,\[])cue-region\s*\(/i
376
392
  ];
377
-
393
+
378
394
  for (var i = 0; i < invalidPatterns.length; i++) {
379
395
  if (invalidPatterns[i].test(selector)) {
380
396
  return true;
@@ -420,7 +436,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
420
436
  isValid = true;
421
437
  }
422
438
  }
423
-
439
+
424
440
  // Additional validation for @scope rule
425
441
  if (isValid && atRuleKey === "@scope") {
426
442
  var openBraceIndex = ruleSlice.indexOf('{');
@@ -439,7 +455,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
439
455
  var hasStart = parsedScopePrelude.hasStart;
440
456
  var hasEnd = parsedScopePrelude.hasEnd;
441
457
  var hasOnlyEnd = parsedScopePrelude.hasOnlyEnd;
442
-
458
+
443
459
  // Validation rules for @scope:
444
460
  // 1. Empty selectors in parentheses are invalid: @scope () {} or @scope (.a) to () {}
445
461
  if ((hasStart && startSelector === '') || (hasEnd && endSelector === '') || (hasOnlyEnd && endSelector === '')) {
@@ -475,13 +491,13 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
475
491
  if (openBraceIndex !== -1) {
476
492
  // Extract the rule prelude (everything between the at-rule and {)
477
493
  var rulePrelude = ruleSlice.slice(0, openBraceIndex).trim();
478
-
494
+
479
495
  // Skip past at-rule keyword and whitespace
480
496
  var preludeContent = rulePrelude.slice("@page".length).trim();
481
497
 
482
498
  if (preludeContent.length > 0) {
483
499
  var trimmedValue = preludeContent.trim();
484
-
500
+
485
501
  // Empty selector is valid for @page
486
502
  if (trimmedValue !== '') {
487
503
  // Parse @page selectorText for page name and pseudo-pages
@@ -505,7 +521,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
505
521
 
506
522
  // Validate pseudo-pages if present
507
523
  if (pseudoPages) {
508
- var pseudos = pseudoPages.split(':').filter(function(p) { return p; });
524
+ var pseudos = pseudoPages.split(':').filter(function (p) { return p; });
509
525
  var validPseudos = ['left', 'right', 'first', 'blank'];
510
526
  var allValid = true;
511
527
  for (var j = 0; j < pseudos.length; j++) {
@@ -514,7 +530,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
514
530
  break;
515
531
  }
516
532
  }
517
-
533
+
518
534
  if (!allValid) {
519
535
  isValid = false;
520
536
  }
@@ -523,15 +539,15 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
523
539
  isValid = false;
524
540
  }
525
541
  }
526
-
542
+
527
543
  }
528
544
  }
529
545
  }
530
-
546
+
531
547
  if (!isValid) {
532
548
  // If it's invalid the browser will simply ignore the entire invalid block
533
549
  // Use regex to find the closing brace of the invalid rule
534
-
550
+
535
551
  // Regex used above is not ES5 compliant. Using alternative.
536
552
  // var ruleStatementMatch = ruleSlice.match(atRulesStatemenRegExp); //
537
553
  var ruleStatementMatch = atRulesStatemenRegExpES5Alternative(ruleSlice);
@@ -546,7 +562,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
546
562
  // Check if there's a semicolon before the invalid at-rule and the first opening brace
547
563
  if (atRuleKey === "@layer") {
548
564
  var ruleSemicolonAndOpeningBraceMatch = ruleSlice.match(forwardRuleSemicolonAndOpeningBraceRegExp);
549
- if (ruleSemicolonAndOpeningBraceMatch && ruleSemicolonAndOpeningBraceMatch[1] === ";" ) {
565
+ if (ruleSemicolonAndOpeningBraceMatch && ruleSemicolonAndOpeningBraceMatch[1] === ";") {
550
566
  // Ignore the rule block until the semicolon
551
567
  i += ruleSemicolonAndOpeningBraceMatch.index + ruleSemicolonAndOpeningBraceMatch[0].length;
552
568
  state = "before-selector";
@@ -564,7 +580,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
564
580
 
565
581
  // Helper functions for looseSelectorValidator
566
582
  // Defined outside to avoid recreation on every validation call
567
-
583
+
568
584
  /**
569
585
  * Check if character is a valid identifier start
570
586
  * @param {string} c - Character to check
@@ -636,19 +652,19 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
636
652
  // Helper: Parse namespace prefix (optional)
637
653
  function parseNamespace() {
638
654
  var start = i;
639
-
655
+
640
656
  // Match: *| or identifier| or |
641
657
  if (i < len && selector[i] === '*') {
642
658
  i++;
643
659
  } else if (i < len && (isIdentStart(selector[i]) || selector[i] === '\\')) {
644
660
  parseIdentifier();
645
661
  }
646
-
662
+
647
663
  if (i < len && selector[i] === '|') {
648
664
  i++;
649
665
  return true;
650
666
  }
651
-
667
+
652
668
  // Rollback if no pipe found
653
669
  i = start;
654
670
  return false;
@@ -659,15 +675,15 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
659
675
  if (i >= len || selector[i] !== '(') {
660
676
  return false;
661
677
  }
662
-
678
+
663
679
  i++; // Skip opening paren
664
680
  var depth = 1;
665
681
  var inString = false;
666
682
  var stringChar = '';
667
-
683
+
668
684
  while (i < len && depth > 0) {
669
685
  var c = selector[i];
670
-
686
+
671
687
  if (c === '\\' && i + 1 < len) {
672
688
  i += 2; // Skip escaped character
673
689
  } else if (!inString && (c === '"' || c === '\'')) {
@@ -687,7 +703,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
687
703
  i++;
688
704
  }
689
705
  }
690
-
706
+
691
707
  return depth === 0;
692
708
  }
693
709
 
@@ -789,7 +805,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
789
805
  // Match type selector with optional namespace: [namespace|]identifier
790
806
  else if (i < len && (isIdentStart(selector[i]) || selector[i] === '\\' || selector[i] === '*' || selector[i] === '|')) {
791
807
  parseNamespace(); // Optional namespace prefix
792
-
808
+
793
809
  if (i < len && selector[i] === '*') {
794
810
  i++; // Universal selector
795
811
  hasMatchedComponent = true;
@@ -865,36 +881,36 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
865
881
  // Pseudo-classes like :lang(), :dir(), :nth-*() should not accept quoted strings
866
882
  // Using iterative parsing instead of regex to avoid exponential backtracking
867
883
  var noQuotesPseudos = ['lang', 'dir', 'nth-child', 'nth-last-child', 'nth-of-type', 'nth-last-of-type'];
868
-
884
+
869
885
  for (var idx = 0; idx < selector.length; idx++) {
870
886
  // Look for pseudo-class/element start
871
887
  if (selector[idx] === ':') {
872
888
  var pseudoStart = idx;
873
889
  idx++;
874
-
890
+
875
891
  // Skip second colon for pseudo-elements
876
892
  if (idx < selector.length && selector[idx] === ':') {
877
893
  idx++;
878
894
  }
879
-
895
+
880
896
  // Extract pseudo name
881
897
  var nameStart = idx;
882
898
  while (idx < selector.length && /[a-zA-Z0-9\-]/.test(selector[idx])) {
883
899
  idx++;
884
900
  }
885
-
901
+
886
902
  if (idx === nameStart) {
887
903
  continue; // No name found
888
904
  }
889
-
905
+
890
906
  var pseudoName = selector.substring(nameStart, idx).toLowerCase();
891
-
907
+
892
908
  // Check if this pseudo has arguments
893
909
  if (idx < selector.length && selector[idx] === '(') {
894
910
  idx++;
895
911
  var contentStart = idx;
896
912
  var depth = 1;
897
-
913
+
898
914
  // Find matching closing paren (handle nesting)
899
915
  while (idx < selector.length && depth > 0) {
900
916
  if (selector[idx] === '\\') {
@@ -909,10 +925,10 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
909
925
  idx++;
910
926
  }
911
927
  }
912
-
928
+
913
929
  if (depth === 0) {
914
930
  var pseudoContent = selector.substring(contentStart, idx - 1);
915
-
931
+
916
932
  // Check if this pseudo should not have quoted strings
917
933
  for (var j = 0; j < noQuotesPseudos.length; j++) {
918
934
  if (pseudoName === noQuotesPseudos[j] && /['"]/.test(pseudoContent)) {
@@ -927,7 +943,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
927
943
  // Use the iterative validator to avoid regex backtracking issues
928
944
  return looseSelectorValidator(selector);
929
945
  }
930
-
946
+
931
947
  /**
932
948
  * Regular expression to match CSS pseudo-classes with arguments.
933
949
  *
@@ -946,7 +962,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
946
962
  *
947
963
  * REPLACED WITH FUNCTION to avoid exponential backtracking.
948
964
  */
949
-
965
+
950
966
  /**
951
967
  * Extract pseudo-classes with arguments from a selector using iterative parsing.
952
968
  * Replaces the previous globalPseudoClassRegExp to avoid exponential backtracking.
@@ -962,30 +978,30 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
962
978
  */
963
979
  function extractPseudoClasses(selector) {
964
980
  var matches = [];
965
-
981
+
966
982
  for (var i = 0; i < selector.length; i++) {
967
983
  // Look for pseudo-class start (single or double colon)
968
984
  if (selector[i] === ':') {
969
985
  var pseudoStart = i;
970
986
  i++;
971
-
987
+
972
988
  // Skip second colon for pseudo-elements (::)
973
989
  if (i < selector.length && selector[i] === ':') {
974
990
  i++;
975
991
  }
976
-
992
+
977
993
  // Extract pseudo name
978
994
  var nameStart = i;
979
995
  while (i < selector.length && /[a-zA-Z\-]/.test(selector[i])) {
980
996
  i++;
981
997
  }
982
-
998
+
983
999
  if (i === nameStart) {
984
1000
  continue; // No name found
985
1001
  }
986
-
1002
+
987
1003
  var pseudoName = selector.substring(nameStart, i);
988
-
1004
+
989
1005
  // Check if this pseudo has arguments
990
1006
  if (i < selector.length && selector[i] === '(') {
991
1007
  i++;
@@ -993,11 +1009,11 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
993
1009
  var depth = 1;
994
1010
  var inSingleQuote = false;
995
1011
  var inDoubleQuote = false;
996
-
1012
+
997
1013
  // Find matching closing paren (handle nesting and strings)
998
1014
  while (i < selector.length && depth > 0) {
999
1015
  var ch = selector[i];
1000
-
1016
+
1001
1017
  if (ch === '\\') {
1002
1018
  i += 2; // Skip escaped character
1003
1019
  } else if (ch === "'" && !inDoubleQuote) {
@@ -1016,21 +1032,21 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1016
1032
  i++;
1017
1033
  }
1018
1034
  }
1019
-
1035
+
1020
1036
  if (depth === 0) {
1021
1037
  var pseudoArgs = selector.substring(argsStart, i - 1);
1022
1038
  var fullMatch = selector.substring(pseudoStart, i);
1023
-
1039
+
1024
1040
  // Store match in same format as regex: [fullMatch, pseudoName, pseudoArgs, startIndex]
1025
1041
  matches.push([fullMatch, pseudoName, pseudoArgs, pseudoStart]);
1026
1042
  }
1027
-
1043
+
1028
1044
  // Move back one since loop will increment
1029
1045
  i--;
1030
1046
  }
1031
1047
  }
1032
1048
  }
1033
-
1049
+
1034
1050
  return matches;
1035
1051
  }
1036
1052
 
@@ -1134,6 +1150,23 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1134
1150
  var pseudoClass = pseudoClassMatches[j][1];
1135
1151
  if (selectorListPseudoClasses.hasOwnProperty(pseudoClass)) {
1136
1152
  var nestedSelectors = parseAndSplitNestedSelectors(pseudoClassMatches[j][2]);
1153
+
1154
+ // Check if ANY selector in the list contains & (nesting selector)
1155
+ // If so, skip validation for the entire selector list since & will be replaced at runtime
1156
+ var hasAmpersand = false;
1157
+ for (var k = 0; k < nestedSelectors.length; k++) {
1158
+ if (/&/.test(nestedSelectors[k])) {
1159
+ hasAmpersand = true;
1160
+ break;
1161
+ }
1162
+ }
1163
+
1164
+ // If any selector has &, skip validation for this entire pseudo-class
1165
+ if (hasAmpersand) {
1166
+ continue;
1167
+ }
1168
+
1169
+ // Otherwise, validate each selector normally
1137
1170
  for (var i = 0; i < nestedSelectors.length; i++) {
1138
1171
  var nestedSelector = nestedSelectors[i];
1139
1172
  if (!validatedSelectorsCache.hasOwnProperty(nestedSelector)) {
@@ -1170,10 +1203,10 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1170
1203
  var inAttr = false;
1171
1204
  var inSingleQuote = false;
1172
1205
  var inDoubleQuote = false;
1173
-
1206
+
1174
1207
  for (var i = 0; i < selector.length; i++) {
1175
1208
  var char = selector[i];
1176
-
1209
+
1177
1210
  if (inSingleQuote) {
1178
1211
  if (char === "'" && selector[i - 1] !== "\\") {
1179
1212
  inSingleQuote = false;
@@ -1200,18 +1233,18 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1200
1233
  }
1201
1234
  }
1202
1235
  }
1203
-
1236
+
1204
1237
  if (pipeIndex === -1) {
1205
1238
  return true; // No namespace, always valid
1206
1239
  }
1207
-
1240
+
1208
1241
  var namespacePrefix = selector.substring(0, pipeIndex);
1209
-
1242
+
1210
1243
  // Universal namespace (*|) and default namespace (|) are always valid
1211
1244
  if (namespacePrefix === '*' || namespacePrefix === '') {
1212
1245
  return true;
1213
1246
  }
1214
-
1247
+
1215
1248
  // Check if the custom namespace prefix is defined
1216
1249
  return definedNamespacePrefixes.hasOwnProperty(namespacePrefix);
1217
1250
  }
@@ -1220,22 +1253,92 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1220
1253
  * Processes a CSS selector text
1221
1254
  *
1222
1255
  * @param {string} selectorText - The CSS selector text to process
1223
- * @returns {string} The processed selector text with normalized whitespace
1256
+ * @returns {string} The processed selector text with normalized whitespace and invalid selectors removed
1224
1257
  */
1225
1258
  function processSelectorText(selectorText) {
1226
- // TODO: Remove invalid selectors that appears inside pseudo classes
1227
- // TODO: The same processing here needs to be reused in CSSStyleRule.selectorText setter
1228
- // TODO: Move these validation logic to a shared function to be reused in CSSStyleRule.selectorText setter
1229
-
1230
- /**
1231
- * Normalizes whitespace and preserving quoted strings.
1232
- * Replaces all newline characters (CRLF, CR, or LF) with spaces while keeping quoted
1233
- * strings (single or double quotes) intact, including any escaped characters within them.
1234
- */
1235
- return selectorText.replace(/(['"])(?:\\.|[^\\])*?\1|(\r\n|\r|\n)/g, function(match, _, newline) {
1259
+ // Normalize whitespace first
1260
+ var normalized = selectorText.replace(/(['"])(?:\\.|[^\\])*?\1|(\r\n|\r|\n)/g, function (match, _, newline) {
1236
1261
  if (newline) return " ";
1237
1262
  return match;
1238
1263
  });
1264
+
1265
+ // Recursively process pseudo-classes to handle nesting
1266
+ return processNestedPseudoClasses(normalized);
1267
+ }
1268
+
1269
+ /**
1270
+ * Recursively processes pseudo-classes to filter invalid selectors
1271
+ *
1272
+ * @param {string} selectorText - The CSS selector text to process
1273
+ * @param {number} depth - Current recursion depth (to prevent infinite loops)
1274
+ * @returns {string} The processed selector text with invalid selectors removed
1275
+ */
1276
+ function processNestedPseudoClasses(selectorText, depth) {
1277
+ // Prevent infinite recursion
1278
+ if (typeof depth === 'undefined') {
1279
+ depth = 0;
1280
+ }
1281
+ if (depth > 10) {
1282
+ return selectorText;
1283
+ }
1284
+
1285
+ var pseudoClassMatches = extractPseudoClasses(selectorText);
1286
+
1287
+ // If no pseudo-classes found, return as-is
1288
+ if (pseudoClassMatches.length === 0) {
1289
+ return selectorText;
1290
+ }
1291
+
1292
+ // Build result by processing matches from right to left (to preserve positions)
1293
+ var result = selectorText;
1294
+
1295
+ for (var j = pseudoClassMatches.length - 1; j >= 0; j--) {
1296
+ var pseudoClass = pseudoClassMatches[j][1];
1297
+ if (selectorListPseudoClasses.hasOwnProperty(pseudoClass)) {
1298
+ var fullMatch = pseudoClassMatches[j][0];
1299
+ var pseudoArgs = pseudoClassMatches[j][2];
1300
+ var matchStart = pseudoClassMatches[j][3];
1301
+
1302
+ // Check if ANY selector contains & BEFORE processing
1303
+ var nestedSelectorsRaw = parseAndSplitNestedSelectors(pseudoArgs);
1304
+ var hasAmpersand = false;
1305
+ for (var k = 0; k < nestedSelectorsRaw.length; k++) {
1306
+ if (/&/.test(nestedSelectorsRaw[k])) {
1307
+ hasAmpersand = true;
1308
+ break;
1309
+ }
1310
+ }
1311
+
1312
+ // If & is present, skip all processing (keep everything unchanged)
1313
+ if (hasAmpersand) {
1314
+ continue;
1315
+ }
1316
+
1317
+ // Recursively process the arguments
1318
+ var processedArgs = processNestedPseudoClasses(pseudoArgs, depth + 1);
1319
+ var nestedSelectors = parseAndSplitNestedSelectors(processedArgs);
1320
+
1321
+ // Filter out invalid selectors
1322
+ var validSelectors = [];
1323
+ for (var i = 0; i < nestedSelectors.length; i++) {
1324
+ var nestedSelector = nestedSelectors[i];
1325
+ if (basicSelectorValidator(nestedSelector)) {
1326
+ validSelectors.push(nestedSelector);
1327
+ }
1328
+ }
1329
+
1330
+ // Reconstruct the pseudo-class with only valid selectors
1331
+ var newArgs = validSelectors.join(', ');
1332
+ var newPseudoClass = ':' + pseudoClass + '(' + newArgs + ')';
1333
+
1334
+ // Replace in the result string using position (processing right to left preserves positions)
1335
+ result = result.substring(0, matchStart) + newPseudoClass + result.substring(matchStart + fullMatch.length);
1336
+ }
1337
+ }
1338
+
1339
+ return result;
1340
+
1341
+ return normalized;
1239
1342
  }
1240
1343
 
1241
1344
  /**
@@ -1249,6 +1352,12 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1249
1352
  // TODO: The same validations here needs to be reused in CSSStyleRule.selectorText setter
1250
1353
  // TODO: Move these validation logic to a shared function to be reused in CSSStyleRule.selectorText setter
1251
1354
 
1355
+ // Check for empty selector lists in pseudo-classes (e.g., :is(), :not(), :where(), :has())
1356
+ // These are invalid after filtering out invalid selectors
1357
+ if (/:(?:is|not|where|has)\(\s*\)/.test(selectorText)) {
1358
+ return false;
1359
+ }
1360
+
1252
1361
  // Check for newlines inside single or double quotes using regex
1253
1362
  // This matches any quoted string (single or double) containing a newline
1254
1363
  var quotedNewlineRegExp = /(['"])(?:\\.|[^\\])*?\1/g;
@@ -1288,12 +1397,22 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1288
1397
  // Print the error but continue parsing the sheet
1289
1398
  try {
1290
1399
  throw error;
1291
- } catch(e) {
1400
+ } catch (e) {
1292
1401
  errorHandler && errorHandler(e);
1293
1402
  }
1294
1403
  };
1295
1404
 
1405
+ // Helper functions to check character types
1406
+ function isSelectorStartChar(char) {
1407
+ return '.:#&*['.indexOf(char) !== -1;
1408
+ }
1409
+
1410
+ function isWhitespaceChar(char) {
1411
+ return ' \t\n\r'.indexOf(char) !== -1;
1412
+ }
1413
+
1296
1414
  var endingIndex = token.length - 1;
1415
+ var initialEndingIndex = endingIndex;
1297
1416
 
1298
1417
  for (var character; (character = token.charAt(i)); i++) {
1299
1418
  if (i === endingIndex) {
@@ -1303,8 +1422,9 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1303
1422
  case "layerBlock":
1304
1423
  if (character !== ";") {
1305
1424
  token += ";";
1306
- break;
1425
+ endingIndex += 1;
1307
1426
  }
1427
+ break;
1308
1428
  case "value":
1309
1429
  if (character !== "}") {
1310
1430
  if (character === ";") {
@@ -1332,7 +1452,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1332
1452
  }
1333
1453
  }
1334
1454
  }
1335
-
1455
+
1336
1456
  // Handle escape sequences before processing special characters
1337
1457
  // If we encounter a backslash, add both the backslash and the next character to buffer
1338
1458
  // and skip the next iteration to prevent the escaped character from being interpreted
@@ -1341,813 +1461,991 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1341
1461
  i++; // Skip the next character
1342
1462
  continue;
1343
1463
  }
1344
-
1464
+
1345
1465
  switch (character) {
1346
1466
 
1347
- case " ":
1348
- case "\t":
1349
- case "\r":
1350
- case "\n":
1351
- case "\f":
1352
- if (SIGNIFICANT_WHITESPACE[state]) {
1353
- buffer += character;
1354
- }
1355
- break;
1356
-
1357
- // String
1358
- case '"':
1359
- index = i + 1;
1360
- do {
1361
- index = token.indexOf('"', index) + 1;
1362
- if (!index) {
1363
- parseError('Unmatched "');
1467
+ case " ":
1468
+ case "\t":
1469
+ case "\r":
1470
+ case "\n":
1471
+ case "\f":
1472
+ if (SIGNIFICANT_WHITESPACE[state]) {
1473
+ buffer += character;
1364
1474
  }
1365
- } while (token[index - 2] === '\\');
1366
- if (index === 0) {
1367
1475
  break;
1368
- }
1369
- buffer += token.slice(i, index);
1370
- i = index - 1;
1371
- switch (state) {
1372
- case 'before-value':
1373
- state = 'value';
1374
- break;
1375
- case 'importRule-begin':
1376
- state = 'importRule';
1377
- if (i === endingIndex) {
1378
- token += ';'
1476
+
1477
+ // String
1478
+ case '"':
1479
+ index = i + 1;
1480
+ do {
1481
+ index = token.indexOf('"', index) + 1;
1482
+ if (!index) {
1483
+ parseError('Unmatched "');
1379
1484
  }
1485
+ } while (token[index - 2] === '\\');
1486
+ if (index === 0) {
1380
1487
  break;
1381
- case 'namespaceRule-begin':
1382
- state = 'namespaceRule';
1383
- if (i === endingIndex) {
1384
- token += ';'
1488
+ }
1489
+ buffer += token.slice(i, index);
1490
+ i = index - 1;
1491
+ switch (state) {
1492
+ case 'before-value':
1493
+ state = 'value';
1494
+ break;
1495
+ case 'importRule-begin':
1496
+ state = 'importRule';
1497
+ if (i === endingIndex) {
1498
+ token += ';'
1499
+ }
1500
+ break;
1501
+ case 'namespaceRule-begin':
1502
+ state = 'namespaceRule';
1503
+ if (i === endingIndex) {
1504
+ token += ';'
1505
+ }
1506
+ break;
1507
+ }
1508
+ break;
1509
+
1510
+ case "'":
1511
+ index = i + 1;
1512
+ do {
1513
+ index = token.indexOf("'", index) + 1;
1514
+ if (!index) {
1515
+ parseError("Unmatched '");
1385
1516
  }
1517
+ } while (token[index - 2] === '\\');
1518
+ if (index === 0) {
1386
1519
  break;
1387
- }
1388
- break;
1389
-
1390
- case "'":
1391
- index = i + 1;
1392
- do {
1393
- index = token.indexOf("'", index) + 1;
1394
- if (!index) {
1395
- parseError("Unmatched '");
1396
1520
  }
1397
- } while (token[index - 2] === '\\');
1398
- if (index === 0) {
1521
+ buffer += token.slice(i, index);
1522
+ i = index - 1;
1523
+ switch (state) {
1524
+ case 'before-value':
1525
+ state = 'value';
1526
+ break;
1527
+ case 'importRule-begin':
1528
+ state = 'importRule';
1529
+ break;
1530
+ case 'namespaceRule-begin':
1531
+ state = 'namespaceRule';
1532
+ break;
1533
+ }
1399
1534
  break;
1400
- }
1401
- buffer += token.slice(i, index);
1402
- i = index - 1;
1403
- switch (state) {
1404
- case 'before-value':
1405
- state = 'value';
1406
- break;
1407
- case 'importRule-begin':
1408
- state = 'importRule';
1409
- break;
1410
- case 'namespaceRule-begin':
1411
- state = 'namespaceRule';
1412
- break;
1413
- }
1414
- break;
1415
-
1416
- // Comment
1417
- case "/":
1418
- if (token.charAt(i + 1) === "*") {
1419
- i += 2;
1420
- index = token.indexOf("*/", i);
1421
- if (index === -1) {
1422
- i = token.length - 1;
1423
- buffer = "";
1535
+
1536
+ // Comment
1537
+ case "/":
1538
+ if (token.charAt(i + 1) === "*") {
1539
+ i += 2;
1540
+ index = token.indexOf("*/", i);
1541
+ if (index === -1) {
1542
+ i = token.length - 1;
1543
+ buffer = "";
1544
+ } else {
1545
+ i = index + 1;
1546
+ }
1424
1547
  } else {
1425
- i = index + 1;
1548
+ buffer += character;
1426
1549
  }
1427
- } else {
1428
- buffer += character;
1429
- }
1430
- if (state === "importRule-begin") {
1431
- buffer += " ";
1432
- state = "importRule";
1433
- }
1434
- if (state === "namespaceRule-begin") {
1435
- buffer += " ";
1436
- state = "namespaceRule";
1437
- }
1438
- break;
1439
-
1440
- // At-rule
1441
- case "@":
1442
- if (nestedSelectorRule) {
1443
- if (styleRule && styleRule.constructor.name === "CSSNestedDeclarations") {
1444
- currentScope.cssRules.push(styleRule);
1550
+ if (state === "importRule-begin") {
1551
+ buffer += " ";
1552
+ state = "importRule";
1445
1553
  }
1446
- if (nestedSelectorRule.parentRule.constructor.name === "CSSStyleRule") {
1447
- styleRule = nestedSelectorRule.parentRule;
1554
+ if (state === "namespaceRule-begin") {
1555
+ buffer += " ";
1556
+ state = "namespaceRule";
1448
1557
  }
1449
- nestedSelectorRule = null;
1450
- }
1451
- if (token.indexOf("@-moz-document", i) === i) {
1452
- validateAtRule("@-moz-document", function(){
1453
- state = "documentRule-begin";
1454
- documentRule = new CSSOM.CSSDocumentRule();
1455
- documentRule.__starts = i;
1456
- i += "-moz-document".length;
1457
- });
1458
- buffer = "";
1459
- break;
1460
- } else if (token.indexOf("@media", i) === i) {
1461
- validateAtRule("@media", function(){
1462
- state = "atBlock";
1463
- mediaRule = new CSSOM.CSSMediaRule();
1464
- mediaRule.__starts = i;
1465
- i += "media".length;
1466
- });
1467
- buffer = "";
1468
- break;
1469
- } else if (token.indexOf("@container", i) === i) {
1470
- validateAtRule("@container", function(){
1471
- state = "containerBlock";
1472
- containerRule = new CSSOM.CSSContainerRule();
1473
- containerRule.__starts = i;
1474
- i += "container".length;
1475
- });
1476
- buffer = "";
1477
- break;
1478
- } else if (token.indexOf("@counter-style", i) === i) {
1479
- validateAtRule("@counter-style", function(){
1480
- state = "counterStyleBlock"
1481
- counterStyleRule = new CSSOM.CSSCounterStyleRule();
1482
- counterStyleRule.__starts = i;
1483
- i += "counter-style".length;
1484
- }, true);
1485
- buffer = "";
1486
- break;
1487
- } else if (token.indexOf("@scope", i) === i) {
1488
- validateAtRule("@scope", function(){
1489
- state = "scopeBlock";
1490
- scopeRule = new CSSOM.CSSScopeRule();
1491
- scopeRule.__starts = i;
1492
- i += "scope".length;
1493
- });
1494
- buffer = "";
1495
- break;
1496
- } else if (token.indexOf("@layer", i) === i) {
1497
- validateAtRule("@layer", function(){
1498
- state = "layerBlock"
1499
- layerBlockRule = new CSSOM.CSSLayerBlockRule();
1500
- layerBlockRule.__starts = i;
1501
- i += "layer".length;
1502
- });
1503
- buffer = "";
1504
- break;
1505
- } else if (token.indexOf("@page", i) === i) {
1506
- validateAtRule("@page", function(){
1507
- state = "pageBlock"
1508
- pageRule = new CSSOM.CSSPageRule();
1509
- pageRule.__starts = i;
1510
- i += "page".length;
1511
- });
1512
- buffer = "";
1513
- break;
1514
- } else if (token.indexOf("@supports", i) === i) {
1515
- validateAtRule("@supports", function(){
1516
- state = "conditionBlock";
1517
- supportsRule = new CSSOM.CSSSupportsRule();
1518
- supportsRule.__starts = i;
1519
- i += "supports".length;
1520
- });
1521
- buffer = "";
1522
1558
  break;
1523
- } else if (token.indexOf("@host", i) === i) {
1524
- validateAtRule("@host", function(){
1525
- state = "hostRule-begin";
1526
- i += "host".length;
1527
- hostRule = new CSSOM.CSSHostRule();
1528
- hostRule.__starts = i;
1529
- });
1530
- buffer = "";
1531
- break;
1532
- } else if (token.indexOf("@starting-style", i) === i) {
1533
- validateAtRule("@starting-style", function(){
1534
- state = "startingStyleRule-begin";
1535
- i += "starting-style".length;
1536
- startingStyleRule = new CSSOM.CSSStartingStyleRule();
1537
- startingStyleRule.__starts = i;
1538
- });
1539
- buffer = "";
1540
- break;
1541
- } else if (token.indexOf("@import", i) === i) {
1542
- buffer = "";
1543
- validateAtRule("@import", function(){
1544
- state = "importRule-begin";
1545
- i += "import".length;
1546
- buffer += "@import";
1547
- }, true);
1548
- break;
1549
- } else if (token.indexOf("@namespace", i) === i) {
1550
- buffer = "";
1551
- validateAtRule("@namespace", function(){
1552
- state = "namespaceRule-begin";
1553
- i += "namespace".length;
1554
- buffer += "@namespace";
1555
- }, true);
1556
- break;
1557
- } else if (token.indexOf("@font-face", i) === i) {
1558
- buffer = "";
1559
- validateAtRule("@font-face", function(){
1560
- state = "fontFaceRule-begin";
1561
- i += "font-face".length;
1562
- fontFaceRule = new CSSOM.CSSFontFaceRule();
1563
- fontFaceRule.__starts = i;
1564
- }, true);
1565
- break;
1566
- } else {
1567
- atKeyframesRegExp.lastIndex = i;
1568
- var matchKeyframes = atKeyframesRegExp.exec(token);
1569
- if (matchKeyframes && matchKeyframes.index === i) {
1570
- state = "keyframesRule-begin";
1571
- keyframesRule = new CSSOM.CSSKeyframesRule();
1572
- keyframesRule.__starts = i;
1573
- keyframesRule._vendorPrefix = matchKeyframes[1]; // Will come out as undefined if no prefix was found
1574
- i += matchKeyframes[0].length - 1;
1559
+
1560
+ // At-rule
1561
+ case "@":
1562
+ if (nestedSelectorRule) {
1563
+ if (styleRule && styleRule.constructor.name === "CSSNestedDeclarations") {
1564
+ currentScope.cssRules.push(styleRule);
1565
+ }
1566
+ if (nestedSelectorRule.parentRule && nestedSelectorRule.parentRule.constructor.name === "CSSStyleRule") {
1567
+ styleRule = nestedSelectorRule.parentRule;
1568
+ }
1569
+ // Don't reset nestedSelectorRule here - preserve it through @-rules
1570
+ }
1571
+ if (token.indexOf("@-moz-document", i) === i) {
1572
+ validateAtRule("@-moz-document", function () {
1573
+ state = "documentRule-begin";
1574
+ documentRule = new CSSOM.CSSDocumentRule();
1575
+ documentRule.__starts = i;
1576
+ i += "-moz-document".length;
1577
+ });
1575
1578
  buffer = "";
1576
1579
  break;
1577
- } else if (state === "selector") {
1578
- state = "atRule";
1579
- }
1580
- }
1581
- buffer += character;
1582
- break;
1580
+ } else if (token.indexOf("@media", i) === i) {
1581
+ validateAtRule("@media", function () {
1582
+ state = "atBlock";
1583
+ mediaRule = new CSSOM.CSSMediaRule();
1584
+ mediaRule.__starts = i;
1585
+ i += "media".length;
1586
+ });
1587
+ buffer = "";
1588
+ break;
1589
+ } else if (token.indexOf("@container", i) === i) {
1590
+ validateAtRule("@container", function () {
1591
+ state = "containerBlock";
1592
+ containerRule = new CSSOM.CSSContainerRule();
1593
+ containerRule.__starts = i;
1594
+ i += "container".length;
1595
+ });
1596
+ buffer = "";
1597
+ break;
1598
+ } else if (token.indexOf("@counter-style", i) === i) {
1599
+ validateAtRule("@counter-style", function () {
1600
+ state = "counterStyleBlock"
1601
+ counterStyleRule = new CSSOM.CSSCounterStyleRule();
1602
+ counterStyleRule.__starts = i;
1603
+ i += "counter-style".length;
1604
+ }, true);
1605
+ buffer = "";
1606
+ break;
1607
+ } else if (token.indexOf("@scope", i) === i) {
1608
+ validateAtRule("@scope", function () {
1609
+ state = "scopeBlock";
1610
+ scopeRule = new CSSOM.CSSScopeRule();
1611
+ scopeRule.__starts = i;
1612
+ i += "scope".length;
1613
+ });
1614
+ buffer = "";
1615
+ break;
1616
+ } else if (token.indexOf("@layer", i) === i) {
1617
+ validateAtRule("@layer", function () {
1618
+ state = "layerBlock"
1619
+ layerBlockRule = new CSSOM.CSSLayerBlockRule();
1620
+ layerBlockRule.__starts = i;
1621
+ i += "layer".length;
1622
+ });
1623
+ buffer = "";
1624
+ break;
1625
+ } else if (token.indexOf("@page", i) === i) {
1626
+ validateAtRule("@page", function () {
1627
+ state = "pageBlock"
1628
+ pageRule = new CSSOM.CSSPageRule();
1629
+ pageRule.__starts = i;
1630
+ i += "page".length;
1631
+ });
1632
+ buffer = "";
1633
+ break;
1634
+ } else if (token.indexOf("@supports", i) === i) {
1635
+ validateAtRule("@supports", function () {
1636
+ state = "conditionBlock";
1637
+ supportsRule = new CSSOM.CSSSupportsRule();
1638
+ supportsRule.__starts = i;
1639
+ i += "supports".length;
1640
+ });
1641
+ buffer = "";
1642
+ break;
1643
+ } else if (token.indexOf("@host", i) === i) {
1644
+ validateAtRule("@host", function () {
1645
+ state = "hostRule-begin";
1646
+ i += "host".length;
1647
+ hostRule = new CSSOM.CSSHostRule();
1648
+ hostRule.__starts = i;
1649
+ });
1650
+ buffer = "";
1651
+ break;
1652
+ } else if (token.indexOf("@starting-style", i) === i) {
1653
+ validateAtRule("@starting-style", function () {
1654
+ state = "startingStyleRule-begin";
1655
+ i += "starting-style".length;
1656
+ startingStyleRule = new CSSOM.CSSStartingStyleRule();
1657
+ startingStyleRule.__starts = i;
1658
+ });
1659
+ buffer = "";
1660
+ break;
1661
+ } else if (token.indexOf("@import", i) === i) {
1662
+ buffer = "";
1663
+ validateAtRule("@import", function () {
1664
+ state = "importRule-begin";
1665
+ i += "import".length;
1666
+ buffer += "@import";
1667
+ }, true);
1668
+ break;
1669
+ } else if (token.indexOf("@namespace", i) === i) {
1670
+ buffer = "";
1671
+ validateAtRule("@namespace", function () {
1672
+ state = "namespaceRule-begin";
1673
+ i += "namespace".length;
1674
+ buffer += "@namespace";
1675
+ }, true);
1676
+ break;
1677
+ } else if (token.indexOf("@font-face", i) === i) {
1678
+ buffer = "";
1679
+ // @font-face can be nested only inside CSSScopeRule or CSSConditionRule
1680
+ // and only if there's no CSSStyleRule in the parent chain
1681
+ var cannotBeNested = true;
1682
+ if (currentScope !== topScope) {
1683
+ var hasStyleRuleInChain = false;
1684
+ var hasValidParent = false;
1685
+
1686
+ // Check currentScope
1687
+ if (currentScope.constructor.name === 'CSSStyleRule') {
1688
+ hasStyleRuleInChain = true;
1689
+ } else if (currentScope instanceof CSSOM.CSSScopeRule || currentScope instanceof CSSOM.CSSConditionRule) {
1690
+ hasValidParent = true;
1691
+ }
1583
1692
 
1584
- case "{":
1585
- if (currentScope === topScope) {
1586
- nestedSelectorRule = null;
1587
- }
1588
- if (state === 'before-selector') {
1589
- parseError("Unexpected {");
1590
- i = ignoreBalancedBlock(i, token.slice(i));
1591
- break;
1592
- }
1593
- if (state === "selector" || state === "atRule") {
1594
- if (!nestedSelectorRule && buffer.indexOf(";") !== -1) {
1595
- var ruleClosingMatch = token.slice(i).match(forwardRuleClosingBraceRegExp);
1596
- if (ruleClosingMatch) {
1597
- styleRule = null;
1693
+ // Check ancestorRules for CSSStyleRule
1694
+ if (!hasStyleRuleInChain) {
1695
+ for (var j = 0; j < ancestorRules.length; j++) {
1696
+ if (ancestorRules[j].constructor.name === 'CSSStyleRule') {
1697
+ hasStyleRuleInChain = true;
1698
+ break;
1699
+ }
1700
+ if (ancestorRules[j] instanceof CSSOM.CSSScopeRule || ancestorRules[j] instanceof CSSOM.CSSConditionRule) {
1701
+ hasValidParent = true;
1702
+ }
1703
+ }
1704
+ }
1705
+
1706
+ // Allow nesting if we have a valid parent and no style rule in the chain
1707
+ if (hasValidParent && !hasStyleRuleInChain) {
1708
+ cannotBeNested = false;
1709
+ }
1710
+ }
1711
+ validateAtRule("@font-face", function () {
1712
+ state = "fontFaceRule-begin";
1713
+ i += "font-face".length;
1714
+ fontFaceRule = new CSSOM.CSSFontFaceRule();
1715
+ fontFaceRule.__starts = i;
1716
+ }, cannotBeNested);
1717
+ break;
1718
+ } else {
1719
+ atKeyframesRegExp.lastIndex = i;
1720
+ var matchKeyframes = atKeyframesRegExp.exec(token);
1721
+ if (matchKeyframes && matchKeyframes.index === i) {
1722
+ state = "keyframesRule-begin";
1723
+ keyframesRule = new CSSOM.CSSKeyframesRule();
1724
+ keyframesRule.__starts = i;
1725
+ keyframesRule._vendorPrefix = matchKeyframes[1]; // Will come out as undefined if no prefix was found
1726
+ i += matchKeyframes[0].length - 1;
1598
1727
  buffer = "";
1599
- state = "before-selector";
1600
- i += ruleClosingMatch.index + ruleClosingMatch[0].length;
1601
1728
  break;
1729
+ } else if (state === "selector") {
1730
+ state = "atRule";
1602
1731
  }
1603
1732
  }
1733
+ buffer += character;
1734
+ break;
1604
1735
 
1605
- if (parentRule) {
1606
- styleRule.__parentRule = parentRule;
1607
- pushToAncestorRules(parentRule);
1736
+ case "{":
1737
+ if (currentScope === topScope) {
1738
+ nestedSelectorRule = null;
1608
1739
  }
1609
-
1610
- currentScope = parentRule = styleRule;
1611
- styleRule.selectorText = processSelectorText(buffer.trim());
1612
- styleRule.style.__starts = i;
1613
- styleRule.__parentStyleSheet = styleSheet;
1614
- buffer = "";
1615
- state = "before-name";
1616
- } else if (state === "atBlock") {
1617
- mediaRule.media.mediaText = buffer.trim();
1618
-
1619
- if (parentRule) {
1620
- mediaRule.__parentRule = parentRule;
1621
- pushToAncestorRules(parentRule);
1740
+ if (state === 'before-selector') {
1741
+ parseError("Unexpected {");
1742
+ i = ignoreBalancedBlock(i, token.slice(i));
1743
+ break;
1622
1744
  }
1745
+ if (state === "selector" || state === "atRule") {
1746
+ if (!nestedSelectorRule && buffer.indexOf(";") !== -1) {
1747
+ var ruleClosingMatch = token.slice(i).match(forwardRuleClosingBraceRegExp);
1748
+ if (ruleClosingMatch) {
1749
+ styleRule = null;
1750
+ buffer = "";
1751
+ state = "before-selector";
1752
+ i += ruleClosingMatch.index + ruleClosingMatch[0].length;
1753
+ break;
1754
+ }
1755
+ }
1623
1756
 
1624
- currentScope = parentRule = mediaRule;
1625
- mediaRule.__parentStyleSheet = styleSheet;
1626
- buffer = "";
1627
- state = "before-selector";
1628
- } else if (state === "containerBlock") {
1629
- containerRule.__conditionText = buffer.trim();
1630
-
1631
- if (parentRule) {
1632
- containerRule.__parentRule = parentRule;
1633
- pushToAncestorRules(parentRule);
1634
- }
1635
- currentScope = parentRule = containerRule;
1636
- containerRule.__parentStyleSheet = styleSheet;
1637
- buffer = "";
1638
- state = "before-selector";
1639
- } else if (state === "counterStyleBlock") {
1640
- // TODO: Validate counter-style name. At least that it cannot be empty nor multiple
1641
- counterStyleRule.name = buffer.trim().replace(/\n/g, "");
1642
- currentScope = parentRule = counterStyleRule;
1643
- counterStyleRule.__parentStyleSheet = styleSheet;
1644
- buffer = "";
1645
- } else if (state === "conditionBlock") {
1646
- supportsRule.__conditionText = buffer.trim();
1647
-
1648
- if (parentRule) {
1649
- supportsRule.__parentRule = parentRule;
1650
- pushToAncestorRules(parentRule);
1651
- }
1757
+ // Ensure styleRule exists before trying to set properties on it
1758
+ if (!styleRule) {
1759
+ styleRule = new CSSOM.CSSStyleRule();
1760
+ styleRule.__starts = i;
1761
+ }
1652
1762
 
1653
- currentScope = parentRule = supportsRule;
1654
- supportsRule.__parentStyleSheet = styleSheet;
1655
- buffer = "";
1656
- state = "before-selector";
1657
- } else if (state === "scopeBlock") {
1658
- var parsedScopePrelude = parseScopePrelude(buffer.trim());
1659
-
1660
- if (parsedScopePrelude.hasStart) {
1661
- scopeRule.__start = parsedScopePrelude.startSelector;
1662
- }
1663
- if (parsedScopePrelude.hasEnd) {
1664
- scopeRule.__end = parsedScopePrelude.endSelector;
1665
- }
1666
- if (parsedScopePrelude.hasOnlyEnd) {
1667
- scopeRule.__end = parsedScopePrelude.endSelector;
1668
- }
1763
+ var originalParentRule = parentRule;
1669
1764
 
1670
- if (parentRule) {
1671
- scopeRule.__parentRule = parentRule;
1672
- pushToAncestorRules(parentRule);
1673
- }
1674
- currentScope = parentRule = scopeRule;
1675
- scopeRule.__parentStyleSheet = styleSheet;
1676
- buffer = "";
1677
- state = "before-selector";
1678
- } else if (state === "layerBlock") {
1679
- layerBlockRule.name = buffer.trim();
1765
+ if (parentRule) {
1766
+ styleRule.__parentRule = parentRule;
1767
+ pushToAncestorRules(parentRule);
1768
+ }
1680
1769
 
1681
- var isValidName = layerBlockRule.name.length === 0 || layerBlockRule.name.match(cssCustomIdentifierRegExp) !== null;
1770
+ currentScope = parentRule = styleRule;
1771
+ var processedSelectorText = processSelectorText(buffer.trim());
1772
+ // In a nested selector, ensure each selector contains '&' at the beginning, except for selectors that already have '&' somewhere
1773
+ if (originalParentRule && originalParentRule.constructor.name === "CSSStyleRule") {
1774
+ styleRule.selectorText = parseAndSplitNestedSelectors(processedSelectorText).map(function (sel) {
1775
+ // Add & at the beginning if there's no & in the selector, or if it starts with a combinator
1776
+ return (sel.indexOf('&') === -1 || startsWithCombinatorRegExp.test(sel)) ? '& ' + sel : sel;
1777
+ }).join(', ');
1778
+ } else {
1779
+ styleRule.selectorText = processedSelectorText;
1780
+ }
1781
+ styleRule.style.__starts = i;
1782
+ styleRule.__parentStyleSheet = styleSheet;
1783
+ buffer = "";
1784
+ state = "before-name";
1785
+ } else if (state === "atBlock") {
1786
+ mediaRule.media.mediaText = buffer.trim();
1682
1787
 
1683
- if (isValidName) {
1684
1788
  if (parentRule) {
1685
- layerBlockRule.__parentRule = parentRule;
1789
+ mediaRule.__parentRule = parentRule;
1686
1790
  pushToAncestorRules(parentRule);
1791
+ // If entering @media from within a CSSStyleRule, set nestedSelectorRule
1792
+ // so that & selectors and declarations work correctly inside
1793
+ if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) {
1794
+ nestedSelectorRule = parentRule;
1795
+ }
1687
1796
  }
1688
-
1689
- currentScope = parentRule = layerBlockRule;
1690
- layerBlockRule.__parentStyleSheet = styleSheet;
1691
- }
1692
- buffer = "";
1693
- state = "before-selector";
1694
- } else if (state === "pageBlock") {
1695
- pageRule.selectorText = buffer.trim();
1696
-
1697
- if (parentRule) {
1698
- pageRule.__parentRule = parentRule;
1699
- pushToAncestorRules(parentRule);
1700
- }
1701
1797
 
1702
- currentScope = parentRule = pageRule;
1703
- pageRule.__parentStyleSheet = styleSheet;
1704
- styleRule = pageRule;
1705
- buffer = "";
1706
- state = "before-name";
1707
- } else if (state === "hostRule-begin") {
1708
- if (parentRule) {
1709
- pushToAncestorRules(parentRule);
1710
- }
1711
-
1712
- currentScope = parentRule = hostRule;
1713
- hostRule.__parentStyleSheet = styleSheet;
1714
- buffer = "";
1715
- state = "before-selector";
1716
- } else if (state === "startingStyleRule-begin") {
1717
- if (parentRule) {
1718
- startingStyleRule.__parentRule = parentRule;
1719
- pushToAncestorRules(parentRule);
1720
- }
1721
-
1722
- currentScope = parentRule = startingStyleRule;
1723
- startingStyleRule.__parentStyleSheet = styleSheet;
1724
- buffer = "";
1725
- state = "before-selector";
1798
+ currentScope = parentRule = mediaRule;
1799
+ pushToAncestorRules(mediaRule);
1800
+ mediaRule.__parentStyleSheet = styleSheet;
1801
+ styleRule = null; // Reset styleRule when entering @-rule
1802
+ buffer = "";
1803
+ state = "before-selector";
1804
+ } else if (state === "containerBlock") {
1805
+ containerRule.__conditionText = buffer.trim();
1726
1806
 
1727
- } else if (state === "fontFaceRule-begin") {
1728
- if (parentRule) {
1729
- fontFaceRule.__parentRule = parentRule;
1730
- }
1731
- fontFaceRule.__parentStyleSheet = styleSheet;
1732
- styleRule = fontFaceRule;
1733
- buffer = "";
1734
- state = "before-name";
1735
- } else if (state === "keyframesRule-begin") {
1736
- keyframesRule.name = buffer.trim();
1737
- if (parentRule) {
1738
- pushToAncestorRules(parentRule);
1739
- keyframesRule.__parentRule = parentRule;
1740
- }
1741
- keyframesRule.__parentStyleSheet = styleSheet;
1742
- currentScope = parentRule = keyframesRule;
1743
- buffer = "";
1744
- state = "keyframeRule-begin";
1745
- } else if (state === "keyframeRule-begin") {
1746
- styleRule = new CSSOM.CSSKeyframeRule();
1747
- styleRule.keyText = buffer.trim();
1748
- styleRule.__starts = i;
1749
- buffer = "";
1750
- state = "before-name";
1751
- } else if (state === "documentRule-begin") {
1752
- // FIXME: what if this '{' is in the url text of the match function?
1753
- documentRule.matcher.matcherText = buffer.trim();
1754
- if (parentRule) {
1755
- pushToAncestorRules(parentRule);
1756
- documentRule.__parentRule = parentRule;
1757
- }
1758
- currentScope = parentRule = documentRule;
1759
- documentRule.__parentStyleSheet = styleSheet;
1760
- buffer = "";
1761
- state = "before-selector";
1762
- } else if (state === "before-name" || state === "name") {
1763
- if (styleRule.constructor.name === "CSSNestedDeclarations") {
1764
- if (styleRule.style.length) {
1765
- parentRule.cssRules.push(styleRule);
1766
- styleRule.__parentRule = parentRule;
1767
- styleRule.__parentStyleSheet = styleSheet;
1807
+ if (parentRule) {
1808
+ containerRule.__parentRule = parentRule;
1768
1809
  pushToAncestorRules(parentRule);
1769
- } else {
1770
- // If the styleRule is empty, we can assume that it's a nested selector
1810
+ if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) {
1811
+ nestedSelectorRule = parentRule;
1812
+ }
1813
+ }
1814
+ currentScope = parentRule = containerRule;
1815
+ pushToAncestorRules(containerRule);
1816
+ containerRule.__parentStyleSheet = styleSheet;
1817
+ styleRule = null; // Reset styleRule when entering @-rule
1818
+ buffer = "";
1819
+ state = "before-selector";
1820
+ } else if (state === "counterStyleBlock") {
1821
+ var counterStyleName = buffer.trim().replace(/\n/g, "");
1822
+ // Validate: name cannot be empty, contain whitespace, or contain dots
1823
+ var isValidCounterStyleName = counterStyleName.length > 0 && !/[\s.]/.test(counterStyleName);
1824
+
1825
+ if (isValidCounterStyleName) {
1826
+ counterStyleRule.name = counterStyleName;
1827
+ currentScope = parentRule = counterStyleRule;
1828
+ counterStyleRule.__parentStyleSheet = styleSheet;
1829
+ }
1830
+ buffer = "";
1831
+ } else if (state === "conditionBlock") {
1832
+ supportsRule.__conditionText = buffer.trim();
1833
+
1834
+ if (parentRule) {
1835
+ supportsRule.__parentRule = parentRule;
1771
1836
  pushToAncestorRules(parentRule);
1837
+ if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) {
1838
+ nestedSelectorRule = parentRule;
1839
+ }
1772
1840
  }
1773
- } else {
1774
- currentScope = parentRule = styleRule;
1775
- pushToAncestorRules(parentRule);
1776
- styleRule.__parentStyleSheet = styleSheet;
1777
- }
1778
-
1779
- styleRule = new CSSOM.CSSStyleRule();
1780
- var processedSelectorText = processSelectorText(buffer.trim());
1781
- // In a nested selector, ensure each selector contains '&' at the beginning, except for selectors that already have '&' somewhere
1782
- if (parentRule.constructor.name === "CSSScopeRule" || (parentRule.constructor.name !== "CSSStyleRule" && parentRule.parentRule === null)) {
1783
- styleRule.selectorText = processedSelectorText;
1784
- } else {
1785
- styleRule.selectorText = parseAndSplitNestedSelectors(processedSelectorText).map(function(sel) {
1786
- // Add & at the beginning if there's no & in the selector, or if it starts with a combinator
1787
- return (sel.indexOf('&') === -1 || startsWithCombinatorRegExp.test(sel)) ? '& ' + sel : sel;
1788
- }).join(', ');
1789
- }
1790
- styleRule.style.__starts = i - buffer.length;
1791
- styleRule.__parentRule = parentRule;
1792
- nestedSelectorRule = styleRule;
1793
1841
 
1794
- buffer = "";
1795
- state = "before-name";
1796
- }
1797
- break;
1798
-
1799
- case ":":
1800
- if (state === "name") {
1801
- // It can be a nested selector, let's check
1802
- var openBraceBeforeMatch = token.slice(i).match(/[{;}]/);
1803
- var hasOpenBraceBefore = openBraceBeforeMatch && openBraceBeforeMatch[0] === '{';
1804
- if (hasOpenBraceBefore) {
1805
- // Is a selector
1806
- buffer += character;
1807
- } else {
1808
- // Is a declaration
1809
- name = buffer.trim();
1842
+ currentScope = parentRule = supportsRule;
1843
+ pushToAncestorRules(supportsRule);
1844
+ supportsRule.__parentStyleSheet = styleSheet;
1845
+ styleRule = null; // Reset styleRule when entering @-rule
1810
1846
  buffer = "";
1811
- state = "before-value";
1812
- }
1813
- } else {
1814
- buffer += character;
1815
- }
1816
- break;
1847
+ state = "before-selector";
1848
+ } else if (state === "scopeBlock") {
1849
+ var parsedScopePrelude = parseScopePrelude(buffer.trim());
1817
1850
 
1818
- case "(":
1819
- if (state === 'value') {
1820
- // ie css expression mode
1821
- if (buffer.trim() === 'expression') {
1822
- var info = (new CSSOM.CSSValueExpression(token, i)).parse();
1851
+ if (parsedScopePrelude.hasStart) {
1852
+ scopeRule.__start = parsedScopePrelude.startSelector;
1853
+ }
1854
+ if (parsedScopePrelude.hasEnd) {
1855
+ scopeRule.__end = parsedScopePrelude.endSelector;
1856
+ }
1857
+ if (parsedScopePrelude.hasOnlyEnd) {
1858
+ scopeRule.__end = parsedScopePrelude.endSelector;
1859
+ }
1823
1860
 
1824
- if (info.error) {
1825
- parseError(info.error);
1826
- } else {
1827
- buffer += info.expression;
1828
- i = info.idx;
1861
+ if (parentRule) {
1862
+ scopeRule.__parentRule = parentRule;
1863
+ pushToAncestorRules(parentRule);
1864
+ if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) {
1865
+ nestedSelectorRule = parentRule;
1866
+ }
1829
1867
  }
1830
- } else {
1831
- state = 'value-parenthesis';
1832
- //always ensure this is reset to 1 on transition
1833
- //from value to value-parenthesis
1834
- valueParenthesisDepth = 1;
1835
- buffer += character;
1836
- }
1837
- } else if (state === 'value-parenthesis') {
1838
- valueParenthesisDepth++;
1839
- buffer += character;
1840
- } else {
1841
- buffer += character;
1842
- }
1843
- break;
1868
+ currentScope = parentRule = scopeRule;
1869
+ pushToAncestorRules(scopeRule);
1870
+ scopeRule.__parentStyleSheet = styleSheet;
1871
+ styleRule = null; // Reset styleRule when entering @-rule
1872
+ buffer = "";
1873
+ state = "before-selector";
1874
+ } else if (state === "layerBlock") {
1875
+ layerBlockRule.name = buffer.trim();
1844
1876
 
1845
- case ")":
1846
- if (state === 'value-parenthesis') {
1847
- valueParenthesisDepth--;
1848
- if (valueParenthesisDepth === 0) state = 'value';
1849
- }
1850
- buffer += character;
1851
- break;
1877
+ var isValidName = layerBlockRule.name.length === 0 || layerBlockRule.name.match(cssCustomIdentifierRegExp) !== null;
1852
1878
 
1853
- case "!":
1854
- if (state === "value" && token.indexOf("!important", i) === i) {
1855
- priority = "important";
1856
- i += "important".length;
1857
- } else {
1858
- buffer += character;
1859
- }
1860
- break;
1879
+ if (isValidName) {
1880
+ if (parentRule) {
1881
+ layerBlockRule.__parentRule = parentRule;
1882
+ pushToAncestorRules(parentRule);
1883
+ if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) {
1884
+ nestedSelectorRule = parentRule;
1885
+ }
1886
+ }
1861
1887
 
1862
- case ";":
1863
- switch (state) {
1864
- case "before-value":
1865
- case "before-name":
1866
- parseError("Unexpected ;");
1888
+ currentScope = parentRule = layerBlockRule;
1889
+ pushToAncestorRules(layerBlockRule);
1890
+ layerBlockRule.__parentStyleSheet = styleSheet;
1891
+ }
1892
+ styleRule = null; // Reset styleRule when entering @-rule
1867
1893
  buffer = "";
1868
- state = "before-name";
1869
- break;
1870
- case "value":
1871
- styleRule.style.setProperty(name, buffer.trim(), priority, parseError);
1872
- priority = "";
1894
+ state = "before-selector";
1895
+ } else if (state === "pageBlock") {
1896
+ pageRule.selectorText = buffer.trim();
1897
+
1898
+ if (parentRule) {
1899
+ pageRule.__parentRule = parentRule;
1900
+ pushToAncestorRules(parentRule);
1901
+ }
1902
+
1903
+ currentScope = parentRule = pageRule;
1904
+ pageRule.__parentStyleSheet = styleSheet;
1905
+ styleRule = pageRule;
1873
1906
  buffer = "";
1874
1907
  state = "before-name";
1875
- break;
1876
- case "atRule":
1877
- buffer = "";
1878
- state = "before-selector";
1879
- break;
1880
- case "importRule":
1881
- var isValid = topScope.cssRules.length === 0 || topScope.cssRules.some(function (rule) {
1882
- return ['CSSImportRule', 'CSSLayerStatementRule'].indexOf(rule.constructor.name) !== -1
1883
- });
1884
- if (isValid) {
1885
- importRule = new CSSOM.CSSImportRule();
1886
- importRule.__parentStyleSheet = importRule.styleSheet.__parentStyleSheet = styleSheet;
1887
- importRule.cssText = buffer + character;
1888
- topScope.cssRules.push(importRule);
1908
+ } else if (state === "hostRule-begin") {
1909
+ if (parentRule) {
1910
+ pushToAncestorRules(parentRule);
1889
1911
  }
1912
+
1913
+ currentScope = parentRule = hostRule;
1914
+ pushToAncestorRules(hostRule);
1915
+ hostRule.__parentStyleSheet = styleSheet;
1890
1916
  buffer = "";
1891
1917
  state = "before-selector";
1892
- break;
1893
- case "namespaceRule":
1894
- var isValid = topScope.cssRules.length === 0 || topScope.cssRules.every(function (rule) {
1895
- return ['CSSImportRule','CSSLayerStatementRule','CSSNamespaceRule'].indexOf(rule.constructor.name) !== -1
1896
- });
1897
- if (isValid) {
1898
- try {
1899
- // Validate namespace syntax before creating the rule
1900
- var testNamespaceRule = new CSSOM.CSSNamespaceRule();
1901
- testNamespaceRule.cssText = buffer + character;
1902
-
1903
- namespaceRule = testNamespaceRule;
1904
- namespaceRule.__parentStyleSheet = styleSheet;
1905
- topScope.cssRules.push(namespaceRule);
1906
-
1907
- // Track the namespace prefix for validation
1908
- if (namespaceRule.prefix) {
1909
- definedNamespacePrefixes[namespaceRule.prefix] = namespaceRule.namespaceURI;
1910
- }
1911
- } catch(e) {
1912
- parseError(e.message);
1918
+ } else if (state === "startingStyleRule-begin") {
1919
+ if (parentRule) {
1920
+ startingStyleRule.__parentRule = parentRule;
1921
+ pushToAncestorRules(parentRule);
1922
+ if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) {
1923
+ nestedSelectorRule = parentRule;
1913
1924
  }
1914
1925
  }
1926
+
1927
+ currentScope = parentRule = startingStyleRule;
1928
+ pushToAncestorRules(startingStyleRule);
1929
+ startingStyleRule.__parentStyleSheet = styleSheet;
1930
+ styleRule = null; // Reset styleRule when entering @-rule
1915
1931
  buffer = "";
1916
1932
  state = "before-selector";
1917
- break;
1918
- case "layerBlock":
1919
- var nameListStr = buffer.trim().split(",").map(function (name) {
1920
- return name.trim();
1921
- });
1922
- var isInvalid = parentRule !== undefined || nameListStr.some(function (name) {
1923
- return name.trim().match(cssCustomIdentifierRegExp) === null;
1924
- });
1925
1933
 
1926
- if (!isInvalid) {
1927
- layerStatementRule = new CSSOM.CSSLayerStatementRule();
1928
- layerStatementRule.__parentStyleSheet = styleSheet;
1929
- layerStatementRule.__starts = layerBlockRule.__starts;
1930
- layerStatementRule.__ends = i;
1931
- layerStatementRule.nameList = nameListStr;
1932
- topScope.cssRules.push(layerStatementRule);
1934
+ } else if (state === "fontFaceRule-begin") {
1935
+ if (parentRule) {
1936
+ fontFaceRule.__parentRule = parentRule;
1933
1937
  }
1938
+ fontFaceRule.__parentStyleSheet = styleSheet;
1939
+ styleRule = fontFaceRule;
1934
1940
  buffer = "";
1935
- state = "before-selector";
1936
- break;
1937
- default:
1938
- buffer += character;
1939
- break;
1940
- }
1941
- break;
1942
-
1943
- case "}":
1944
- if (state === "counterStyleBlock") {
1945
- // FIXME : Implement cssText get setter that parses the real implementation
1946
- counterStyleRule.cssText = "@counter-style " + counterStyleRule.name + " { " + buffer.trim().replace(/\n/g, " ").replace(/(['"])(?:\\.|[^\\])*?\1|(\s{2,})/g, function(match, quote) {
1947
- return quote ? match : ' ';
1948
- }) + " }";
1949
- buffer = "";
1950
- state = "before-selector";
1951
- }
1952
-
1953
- switch (state) {
1954
- case "value":
1955
- styleRule.style.setProperty(name, buffer.trim(), priority, parseError);
1956
- priority = "";
1957
- /* falls through */
1958
- case "before-value":
1959
- case "before-name":
1960
- case "name":
1961
- styleRule.__ends = i + 1;
1962
-
1963
- if (parentRule === styleRule) {
1964
- parentRule = ancestorRules.pop()
1941
+ state = "before-name";
1942
+ } else if (state === "keyframesRule-begin") {
1943
+ keyframesRule.name = buffer.trim();
1944
+ if (parentRule) {
1945
+ pushToAncestorRules(parentRule);
1946
+ keyframesRule.__parentRule = parentRule;
1965
1947
  }
1966
-
1948
+ keyframesRule.__parentStyleSheet = styleSheet;
1949
+ currentScope = parentRule = keyframesRule;
1950
+ buffer = "";
1951
+ state = "keyframeRule-begin";
1952
+ } else if (state === "keyframeRule-begin") {
1953
+ styleRule = new CSSOM.CSSKeyframeRule();
1954
+ styleRule.keyText = buffer.trim();
1955
+ styleRule.__starts = i;
1956
+ buffer = "";
1957
+ state = "before-name";
1958
+ } else if (state === "documentRule-begin") {
1959
+ // FIXME: what if this '{' is in the url text of the match function?
1960
+ documentRule.matcher.matcherText = buffer.trim();
1967
1961
  if (parentRule) {
1968
- styleRule.__parentRule = parentRule;
1962
+ pushToAncestorRules(parentRule);
1963
+ documentRule.__parentRule = parentRule;
1969
1964
  }
1970
- styleRule.__parentStyleSheet = styleSheet;
1971
-
1972
- if (currentScope === styleRule) {
1973
- currentScope = parentRule || topScope;
1965
+ currentScope = parentRule = documentRule;
1966
+ pushToAncestorRules(documentRule);
1967
+ documentRule.__parentStyleSheet = styleSheet;
1968
+ buffer = "";
1969
+ state = "before-selector";
1970
+ } else if (state === "before-name" || state === "name") {
1971
+ // @font-face and similar rules don't support nested selectors
1972
+ // If we encounter a nested selector block inside them, skip it
1973
+ if (styleRule.constructor.name === "CSSFontFaceRule" ||
1974
+ styleRule.constructor.name === "CSSKeyframeRule" ||
1975
+ (styleRule.constructor.name === "CSSPageRule" && parentRule === styleRule)) {
1976
+ // Skip the nested block
1977
+ var ruleClosingMatch = token.slice(i).match(forwardRuleClosingBraceRegExp);
1978
+ if (ruleClosingMatch) {
1979
+ i += ruleClosingMatch.index + ruleClosingMatch[0].length - 1;
1980
+ buffer = "";
1981
+ state = "before-name";
1982
+ break;
1983
+ }
1974
1984
  }
1975
1985
 
1976
- if (styleRule.constructor.name === "CSSStyleRule" && !isValidSelectorText(styleRule.selectorText)) {
1977
- if (styleRule === nestedSelectorRule) {
1978
- nestedSelectorRule = null;
1979
- }
1980
- parseError('Invalid CSSStyleRule (selectorText = "' + styleRule.selectorText + '")', styleRule.parentRule !== null);
1981
- } else {
1982
- if (styleRule.parentRule) {
1983
- styleRule.parentRule.cssRules.push(styleRule);
1986
+ if (styleRule.constructor.name === "CSSNestedDeclarations") {
1987
+ if (styleRule.style.length) {
1988
+ parentRule.cssRules.push(styleRule);
1989
+ styleRule.__parentRule = parentRule;
1990
+ styleRule.__parentStyleSheet = styleSheet;
1991
+ pushToAncestorRules(parentRule);
1984
1992
  } else {
1985
- currentScope.cssRules.push(styleRule);
1993
+ // If the styleRule is empty, we can assume that it's a nested selector
1994
+ pushToAncestorRules(parentRule);
1986
1995
  }
1996
+ } else {
1997
+ currentScope = parentRule = styleRule;
1998
+ pushToAncestorRules(parentRule);
1999
+ styleRule.__parentStyleSheet = styleSheet;
1987
2000
  }
2001
+
2002
+ styleRule = new CSSOM.CSSStyleRule();
2003
+ var processedSelectorText = processSelectorText(buffer.trim());
2004
+ // In a nested selector, ensure each selector contains '&' at the beginning, except for selectors that already have '&' somewhere
2005
+ if (parentRule.constructor.name === "CSSScopeRule" || (parentRule.constructor.name !== "CSSStyleRule" && parentRule.parentRule === null)) {
2006
+ styleRule.selectorText = processedSelectorText;
2007
+ } else {
2008
+ styleRule.selectorText = parseAndSplitNestedSelectors(processedSelectorText).map(function (sel) {
2009
+ // Add & at the beginning if there's no & in the selector, or if it starts with a combinator
2010
+ return (sel.indexOf('&') === -1 || startsWithCombinatorRegExp.test(sel)) ? '& ' + sel : sel;
2011
+ }).join(', ');
2012
+ }
2013
+ styleRule.style.__starts = i - buffer.length;
2014
+ styleRule.__parentRule = parentRule;
2015
+ // Only set nestedSelectorRule if we're directly inside a CSSStyleRule or CSSScopeRule,
2016
+ // not inside other grouping rules like @media/@supports
2017
+ if (parentRule.constructor.name === "CSSStyleRule" || parentRule.constructor.name === "CSSScopeRule") {
2018
+ nestedSelectorRule = styleRule;
2019
+ }
2020
+
1988
2021
  buffer = "";
1989
- if (currentScope.constructor === CSSOM.CSSKeyframesRule) {
1990
- state = "keyframeRule-begin";
2022
+ state = "before-name";
2023
+ }
2024
+ break;
2025
+
2026
+ case ":":
2027
+ if (state === "name") {
2028
+ // It can be a nested selector, let's check
2029
+ var openBraceBeforeMatch = token.slice(i).match(/[{;}]/);
2030
+ var hasOpenBraceBefore = openBraceBeforeMatch && openBraceBeforeMatch[0] === '{';
2031
+ if (hasOpenBraceBefore) {
2032
+ // Is a selector
2033
+ buffer += character;
1991
2034
  } else {
1992
- state = "before-selector";
2035
+ // Is a declaration
2036
+ name = buffer.trim();
2037
+ buffer = "";
2038
+ state = "before-value";
1993
2039
  }
2040
+ } else {
2041
+ buffer += character;
2042
+ }
2043
+ break;
1994
2044
 
1995
- if (styleRule.constructor.name === "CSSNestedDeclarations") {
1996
- if (currentScope !== topScope) {
1997
- nestedSelectorRule = currentScope;
2045
+ case "(":
2046
+ if (state === 'value') {
2047
+ // ie css expression mode
2048
+ if (buffer.trim() === 'expression') {
2049
+ var info = (new CSSOM.CSSValueExpression(token, i)).parse();
2050
+
2051
+ if (info.error) {
2052
+ parseError(info.error);
2053
+ } else {
2054
+ buffer += info.expression;
2055
+ i = info.idx;
1998
2056
  }
1999
- styleRule = null;
2000
2057
  } else {
2001
- styleRule = null;
2002
- break;
2058
+ state = 'value-parenthesis';
2059
+ //always ensure this is reset to 1 on transition
2060
+ //from value to value-parenthesis
2061
+ valueParenthesisDepth = 1;
2062
+ buffer += character;
2003
2063
  }
2004
- case "keyframeRule-begin":
2005
- case "before-selector":
2006
- case "selector":
2007
- // End of media/supports/document rule.
2008
- if (!parentRule) {
2009
- parseError("Unexpected }");
2010
-
2011
- var hasPreviousStyleRule = currentScope.cssRules.length && currentScope.cssRules[currentScope.cssRules.length - 1].constructor.name === "CSSStyleRule";
2012
- if (hasPreviousStyleRule) {
2013
- i = ignoreBalancedBlock(i, token.slice(i), 1);
2064
+ } else if (state === 'value-parenthesis') {
2065
+ valueParenthesisDepth++;
2066
+ buffer += character;
2067
+ } else {
2068
+ buffer += character;
2069
+ }
2070
+ break;
2071
+
2072
+ case ")":
2073
+ if (state === 'value-parenthesis') {
2074
+ valueParenthesisDepth--;
2075
+ if (valueParenthesisDepth === 0) state = 'value';
2076
+ }
2077
+ buffer += character;
2078
+ break;
2079
+
2080
+ case "!":
2081
+ if (state === "value" && token.indexOf("!important", i) === i) {
2082
+ priority = "important";
2083
+ i += "important".length;
2084
+ } else {
2085
+ buffer += character;
2086
+ }
2087
+ break;
2088
+
2089
+ case ";":
2090
+ switch (state) {
2091
+ case "before-value":
2092
+ case "before-name":
2093
+ parseError("Unexpected ;");
2094
+ buffer = "";
2095
+ state = "before-name";
2096
+ break;
2097
+ case "value":
2098
+ styleRule.style.setProperty(name, buffer.trim(), priority, parseError);
2099
+ priority = "";
2100
+ buffer = "";
2101
+ state = "before-name";
2102
+ break;
2103
+ case "atRule":
2104
+ buffer = "";
2105
+ state = "before-selector";
2106
+ break;
2107
+ case "importRule":
2108
+ var isValid = topScope.cssRules.length === 0 || topScope.cssRules.some(function (rule) {
2109
+ return ['CSSImportRule', 'CSSLayerStatementRule'].indexOf(rule.constructor.name) !== -1
2110
+ });
2111
+ if (isValid) {
2112
+ importRule = new CSSOM.CSSImportRule();
2113
+ importRule.__parentStyleSheet = importRule.styleSheet.__parentStyleSheet = styleSheet;
2114
+ importRule.parse(buffer + character);
2115
+ topScope.cssRules.push(importRule);
2014
2116
  }
2015
-
2117
+ buffer = "";
2118
+ state = "before-selector";
2016
2119
  break;
2017
- }
2018
-
2019
- while (ancestorRules.length > 0) {
2020
- parentRule = ancestorRules.pop();
2021
-
2022
- if (
2023
- parentRule.constructor.name === "CSSStyleRule"
2024
- || parentRule.constructor.name === "CSSMediaRule"
2025
- || parentRule.constructor.name === "CSSSupportsRule"
2026
- || parentRule.constructor.name === "CSSContainerRule"
2027
- || parentRule.constructor.name === "CSSScopeRule"
2028
- || parentRule.constructor.name === "CSSLayerBlockRule"
2029
- || parentRule.constructor.name === "CSSStartingStyleRule"
2030
- ) {
2031
- if (nestedSelectorRule) {
2032
- if (nestedSelectorRule.parentRule) {
2033
- prevScope = nestedSelectorRule;
2034
- currentScope = nestedSelectorRule.parentRule;
2035
- if (currentScope.cssRules.findIndex(function (rule) {
2036
- return rule === prevScope
2037
- }) === -1) {
2038
- currentScope.cssRules.push(prevScope);
2039
- }
2040
- nestedSelectorRule = currentScope;
2120
+ case "namespaceRule":
2121
+ var isValid = topScope.cssRules.length === 0 || topScope.cssRules.every(function (rule) {
2122
+ return ['CSSImportRule', 'CSSLayerStatementRule', 'CSSNamespaceRule'].indexOf(rule.constructor.name) !== -1
2123
+ });
2124
+ if (isValid) {
2125
+ try {
2126
+ // Validate namespace syntax before creating the rule
2127
+ var testNamespaceRule = new CSSOM.CSSNamespaceRule();
2128
+ testNamespaceRule.parse(buffer + character);
2129
+
2130
+ namespaceRule = testNamespaceRule;
2131
+ namespaceRule.__parentStyleSheet = styleSheet;
2132
+ topScope.cssRules.push(namespaceRule);
2133
+
2134
+ // Track the namespace prefix for validation
2135
+ if (namespaceRule.prefix) {
2136
+ definedNamespacePrefixes[namespaceRule.prefix] = namespaceRule.namespaceURI;
2041
2137
  }
2138
+ } catch (e) {
2139
+ parseError(e.message);
2140
+ }
2141
+ }
2142
+ buffer = "";
2143
+ state = "before-selector";
2144
+ break;
2145
+ case "layerBlock":
2146
+ var nameListStr = buffer.trim().split(",").map(function (name) {
2147
+ return name.trim();
2148
+ });
2149
+ var isInvalid = nameListStr.some(function (name) {
2150
+ return name.trim().match(cssCustomIdentifierRegExp) === null;
2151
+ });
2152
+
2153
+ // Check if there's a CSSStyleRule in the parent chain
2154
+ var hasStyleRuleParent = false;
2155
+ if (parentRule) {
2156
+ var checkParent = parentRule;
2157
+ while (checkParent) {
2158
+ if (checkParent.constructor.name === "CSSStyleRule") {
2159
+ hasStyleRuleParent = true;
2160
+ break;
2161
+ }
2162
+ checkParent = checkParent.__parentRule;
2163
+ }
2164
+ }
2165
+
2166
+ if (!isInvalid && !hasStyleRuleParent) {
2167
+ layerStatementRule = new CSSOM.CSSLayerStatementRule();
2168
+ layerStatementRule.__parentStyleSheet = styleSheet;
2169
+ layerStatementRule.__starts = layerBlockRule.__starts;
2170
+ layerStatementRule.__ends = i;
2171
+ layerStatementRule.nameList = nameListStr;
2172
+
2173
+ // Add to parent rule if nested, otherwise to top scope
2174
+ if (parentRule) {
2175
+ layerStatementRule.__parentRule = parentRule;
2176
+ parentRule.cssRules.push(layerStatementRule);
2042
2177
  } else {
2043
- prevScope = currentScope;
2044
- parentRule !== prevScope && parentRule.cssRules.push(prevScope);
2045
- break;
2178
+ topScope.cssRules.push(layerStatementRule);
2046
2179
  }
2047
2180
  }
2048
- }
2049
-
2050
- if (currentScope.parentRule == null) {
2051
- currentScope.__ends = i + 1;
2052
- if (currentScope !== topScope && topScope.cssRules.findIndex(function (rule) {
2053
- return rule === currentScope
2054
- }) === -1) {
2055
- topScope.cssRules.push(currentScope);
2181
+ buffer = "";
2182
+ state = "before-selector";
2183
+ break;
2184
+ default:
2185
+ buffer += character;
2186
+ break;
2187
+ }
2188
+ break;
2189
+
2190
+ case "}":
2191
+ if (state === "counterStyleBlock") {
2192
+ // FIXME : Implement missing properties on CSSCounterStyleRule interface and update parse method
2193
+ // For now it's just assigning entire rule text
2194
+ counterStyleRule.parse("@counter-style " + counterStyleRule.name + " { " + buffer + " }");
2195
+ buffer = "";
2196
+ state = "before-selector";
2197
+ }
2198
+
2199
+ switch (state) {
2200
+ case "value":
2201
+ styleRule.style.setProperty(name, buffer.trim(), priority, parseError);
2202
+ priority = "";
2203
+ /* falls through */
2204
+ case "before-value":
2205
+ case "before-name":
2206
+ case "name":
2207
+ styleRule.__ends = i + 1;
2208
+
2209
+ if (parentRule === styleRule) {
2210
+ parentRule = ancestorRules.pop()
2056
2211
  }
2057
- currentScope = topScope;
2058
- if (nestedSelectorRule === parentRule) {
2059
- // Check if this selector is really starting inside another selector
2060
- var nestedSelectorTokenToCurrentSelectorToken = token.slice(nestedSelectorRule.__starts, i + 1);
2061
- var openingBraceMatch = nestedSelectorTokenToCurrentSelectorToken.match(/{/g);
2062
- var closingBraceMatch = nestedSelectorTokenToCurrentSelectorToken.match(/}/g);
2063
- var openingBraceLen = openingBraceMatch && openingBraceMatch.length;
2064
- var closingBraceLen = closingBraceMatch && closingBraceMatch.length;
2065
-
2066
- if (openingBraceLen === closingBraceLen) {
2067
- // If the number of opening and closing braces are equal, we can assume that the new selector is starting outside the nestedSelectorRule
2068
- nestedSelectorRule.__ends = i + 1;
2212
+
2213
+ if (parentRule) {
2214
+ styleRule.__parentRule = parentRule;
2215
+ }
2216
+ styleRule.__parentStyleSheet = styleSheet;
2217
+
2218
+ if (currentScope === styleRule) {
2219
+ currentScope = parentRule || topScope;
2220
+ }
2221
+
2222
+ if (styleRule.constructor.name === "CSSStyleRule" && !isValidSelectorText(styleRule.selectorText)) {
2223
+ if (styleRule === nestedSelectorRule) {
2069
2224
  nestedSelectorRule = null;
2070
- parentRule = null;
2071
2225
  }
2226
+ parseError('Invalid CSSStyleRule (selectorText = "' + styleRule.selectorText + '")', styleRule.parentRule !== null);
2227
+ } else {
2228
+ if (styleRule.parentRule) {
2229
+ styleRule.parentRule.cssRules.push(styleRule);
2230
+ } else {
2231
+ currentScope.cssRules.push(styleRule);
2232
+ }
2233
+ }
2234
+ buffer = "";
2235
+ if (currentScope.constructor === CSSOM.CSSKeyframesRule) {
2236
+ state = "keyframeRule-begin";
2237
+ } else {
2238
+ state = "before-selector";
2239
+ }
2240
+
2241
+ if (styleRule.constructor.name === "CSSNestedDeclarations") {
2242
+ if (currentScope !== topScope) {
2243
+ // Only set nestedSelectorRule if currentScope is CSSStyleRule or CSSScopeRule
2244
+ // Not for other grouping rules like @media/@supports
2245
+ if (currentScope.constructor.name === "CSSStyleRule" || currentScope.constructor.name === "CSSScopeRule") {
2246
+ nestedSelectorRule = currentScope;
2247
+ }
2248
+ }
2249
+ styleRule = null;
2072
2250
  } else {
2073
- parentRule = null;
2251
+ // Update nestedSelectorRule when closing a CSSStyleRule
2252
+ if (styleRule === nestedSelectorRule) {
2253
+ var selector = styleRule.selectorText && styleRule.selectorText.trim();
2254
+ // Check if this is proper nesting (&.class, &:pseudo) vs prepended & (& :is, & .class with space)
2255
+ // Prepended & has pattern "& X" where X starts with : or .
2256
+ var isPrependedAmpersand = selector && selector.match(/^&\s+[:\.]/);
2257
+
2258
+ // Check if parent is a grouping rule that can contain nested selectors
2259
+ var isGroupingRule = currentScope && currentScope instanceof CSSOM.CSSGroupingRule;
2260
+
2261
+ if (!isPrependedAmpersand && isGroupingRule) {
2262
+ // Proper nesting - set nestedSelectorRule to parent for more nested selectors
2263
+ // But only if it's a CSSStyleRule or CSSScopeRule, not other grouping rules like @media
2264
+ if (currentScope.constructor.name === "CSSStyleRule" || currentScope.constructor.name === "CSSScopeRule") {
2265
+ nestedSelectorRule = currentScope;
2266
+ }
2267
+ // If currentScope is another type of grouping rule (like @media), keep nestedSelectorRule unchanged
2268
+ } else {
2269
+ // Prepended & or not nested in grouping rule - reset to prevent CSSNestedDeclarations
2270
+ nestedSelectorRule = null;
2271
+ }
2272
+ } else if (nestedSelectorRule && currentScope instanceof CSSOM.CSSGroupingRule) {
2273
+ // When closing a nested rule that's not the nestedSelectorRule itself,
2274
+ // maintain nestedSelectorRule if we're still inside a grouping rule
2275
+ // This ensures declarations after nested selectors inside @media/@supports etc. work correctly
2276
+ }
2277
+ styleRule = null;
2278
+ break;
2279
+ }
2280
+ case "keyframeRule-begin":
2281
+ case "before-selector":
2282
+ case "selector":
2283
+ // End of media/supports/document rule.
2284
+ if (!parentRule) {
2285
+ parseError("Unexpected }");
2286
+
2287
+ var hasPreviousStyleRule = currentScope.cssRules.length && currentScope.cssRules[currentScope.cssRules.length - 1].constructor.name === "CSSStyleRule";
2288
+ if (hasPreviousStyleRule) {
2289
+ i = ignoreBalancedBlock(i, token.slice(i), 1);
2290
+ }
2074
2291
 
2292
+ break;
2075
2293
  }
2076
- } else {
2077
- currentScope = parentRule;
2078
- }
2079
2294
 
2080
- buffer = "";
2081
- state = "before-selector";
2082
- break;
2083
- }
2084
- break;
2295
+ while (ancestorRules.length > 0) {
2296
+ parentRule = ancestorRules.pop();
2297
+
2298
+ if (parentRule instanceof CSSOM.CSSGroupingRule && (parentRule.constructor.name !== 'CSSStyleRule' || parentRule.__parentRule)) {
2299
+ if (nestedSelectorRule) {
2300
+ if (nestedSelectorRule.parentRule) {
2301
+ prevScope = nestedSelectorRule;
2302
+ currentScope = nestedSelectorRule.parentRule;
2303
+ if (currentScope.cssRules.findIndex(function (rule) {
2304
+ return rule === prevScope
2305
+ }) === -1) {
2306
+ currentScope.cssRules.push(prevScope);
2307
+ }
2308
+ nestedSelectorRule = currentScope;
2309
+ } else {
2310
+ // If nestedSelectorRule doesn't have a parentRule, we're closing a grouping rule
2311
+ // inside a top-level CSSStyleRule. We need to push currentScope to the parentRule.
2312
+ prevScope = currentScope;
2313
+ // Push to actual parent from ancestorRules if available
2314
+ var actualParent = ancestorRules.length > 0 ? ancestorRules[ancestorRules.length - 1] : nestedSelectorRule;
2315
+ actualParent !== prevScope && actualParent.cssRules.push(prevScope);
2316
+ // Update currentScope to the nestedSelectorRule before breaking
2317
+ currentScope = actualParent;
2318
+ parentRule = actualParent;
2319
+ break;
2320
+ }
2321
+ } else {
2322
+ prevScope = currentScope;
2323
+ parentRule !== prevScope && parentRule.cssRules.push(prevScope);
2324
+ break;
2325
+ }
2326
+ }
2327
+ }
2085
2328
 
2086
- default:
2087
- switch (state) {
2088
- case "before-selector":
2089
- state = "selector";
2090
- if ((styleRule || scopeRule) && parentRule) {
2091
- // Assuming it's a declaration inside Nested Selector OR a Nested Declaration
2092
- // If Declaration inside Nested Selector let's keep the same styleRule
2093
- if (
2094
- parentRule.constructor.name === "CSSStyleRule"
2095
- || parentRule.constructor.name === "CSSMediaRule"
2096
- || parentRule.constructor.name === "CSSSupportsRule"
2097
- || parentRule.constructor.name === "CSSContainerRule"
2098
- || parentRule.constructor.name === "CSSScopeRule"
2099
- || parentRule.constructor.name === "CSSLayerBlockRule"
2100
- || parentRule.constructor.name === "CSSStartingStyleRule"
2101
- ) {
2102
- // parentRule.__parentRule = styleRule;
2103
- state = "before-name";
2104
- if (styleRule !== parentRule) {
2105
- styleRule = new CSSOM.CSSNestedDeclarations();
2106
- styleRule.__starts = i;
2329
+ // If currentScope has a __parentRule and wasn't added yet, add it
2330
+ if (ancestorRules.length === 0 && currentScope.__parentRule && currentScope.__parentRule.cssRules) {
2331
+ if (currentScope.__parentRule.cssRules.findIndex(function (rule) {
2332
+ return rule === currentScope
2333
+ }) === -1) {
2334
+ currentScope.__parentRule.cssRules.push(currentScope);
2107
2335
  }
2108
2336
  }
2109
-
2110
- } else if (nestedSelectorRule && parentRule && (
2111
- parentRule.constructor.name === "CSSStyleRule"
2112
- || parentRule.constructor.name === "CSSMediaRule"
2113
- || parentRule.constructor.name === "CSSSupportsRule"
2114
- || parentRule.constructor.name === "CSSContainerRule"
2115
- || parentRule.constructor.name === "CSSLayerBlockRule"
2116
- || parentRule.constructor.name === "CSSStartingStyleRule"
2117
- )) {
2118
- state = "before-name";
2119
- if (parentRule.cssRules.length) {
2120
- currentScope = nestedSelectorRule = parentRule;
2121
- styleRule = new CSSOM.CSSNestedDeclarations();
2122
- styleRule.__starts = i;
2123
- } else {
2124
- if (parentRule.constructor.name === "CSSStyleRule") {
2125
- styleRule = parentRule;
2337
+
2338
+ // Only handle top-level rule closing if we processed all ancestors
2339
+ if (ancestorRules.length === 0 && currentScope.parentRule == null) {
2340
+ currentScope.__ends = i + 1;
2341
+ if (currentScope !== topScope && topScope.cssRules.findIndex(function (rule) {
2342
+ return rule === currentScope
2343
+ }) === -1) {
2344
+ topScope.cssRules.push(currentScope);
2345
+ }
2346
+ currentScope = topScope;
2347
+ if (nestedSelectorRule === parentRule) {
2348
+ // Check if this selector is really starting inside another selector
2349
+ var nestedSelectorTokenToCurrentSelectorToken = token.slice(nestedSelectorRule.__starts, i + 1);
2350
+ var openingBraceMatch = nestedSelectorTokenToCurrentSelectorToken.match(/{/g);
2351
+ var closingBraceMatch = nestedSelectorTokenToCurrentSelectorToken.match(/}/g);
2352
+ var openingBraceLen = openingBraceMatch && openingBraceMatch.length;
2353
+ var closingBraceLen = closingBraceMatch && closingBraceMatch.length;
2354
+
2355
+ if (openingBraceLen === closingBraceLen) {
2356
+ // If the number of opening and closing braces are equal, we can assume that the new selector is starting outside the nestedSelectorRule
2357
+ nestedSelectorRule.__ends = i + 1;
2358
+ nestedSelectorRule = null;
2359
+ parentRule = null;
2360
+ }
2126
2361
  } else {
2362
+ parentRule = null;
2363
+ }
2364
+ } else {
2365
+ currentScope = parentRule;
2366
+ }
2367
+
2368
+ buffer = "";
2369
+ state = "before-selector";
2370
+ break;
2371
+ }
2372
+ break;
2373
+
2374
+ default:
2375
+ switch (state) {
2376
+ case "before-selector":
2377
+ state = "selector";
2378
+ if ((styleRule || scopeRule) && parentRule) {
2379
+ // Assuming it's a declaration inside Nested Selector OR a Nested Declaration
2380
+ // If Declaration inside Nested Selector let's keep the same styleRule
2381
+ if (!isSelectorStartChar(character) && !isWhitespaceChar(character) && parentRule instanceof CSSOM.CSSGroupingRule) {
2382
+ // parentRule.__parentRule = styleRule;
2383
+ state = "before-name";
2384
+ if (styleRule !== parentRule) {
2385
+ styleRule = new CSSOM.CSSNestedDeclarations();
2386
+ styleRule.__starts = i;
2387
+ }
2388
+ }
2389
+
2390
+ } else if (nestedSelectorRule && parentRule && parentRule instanceof CSSOM.CSSGroupingRule) {
2391
+ if (isSelectorStartChar(character)) {
2392
+ // If starting with a selector character, create CSSStyleRule instead of CSSNestedDeclarations
2127
2393
  styleRule = new CSSOM.CSSStyleRule();
2128
- styleRule.__starts = i;
2394
+ styleRule.__starts = i;
2395
+ } else if (!isWhitespaceChar(character)) {
2396
+ // Starting a declaration (not whitespace, not a selector)
2397
+ state = "before-name";
2398
+ // Check if we should create CSSNestedDeclarations
2399
+ // This happens if: parent has cssRules OR nestedSelectorRule exists (indicating CSSStyleRule in hierarchy)
2400
+ if (parentRule.cssRules.length || nestedSelectorRule) {
2401
+ currentScope = parentRule;
2402
+ // Only set nestedSelectorRule if parentRule is CSSStyleRule or CSSScopeRule
2403
+ if (parentRule.constructor.name === "CSSStyleRule" || parentRule.constructor.name === "CSSScopeRule") {
2404
+ nestedSelectorRule = parentRule;
2405
+ }
2406
+ styleRule = new CSSOM.CSSNestedDeclarations();
2407
+ styleRule.__starts = i;
2408
+ } else {
2409
+ if (parentRule.constructor.name === "CSSStyleRule") {
2410
+ styleRule = parentRule;
2411
+ } else {
2412
+ styleRule = new CSSOM.CSSStyleRule();
2413
+ styleRule.__starts = i;
2414
+ }
2415
+ }
2129
2416
  }
2130
2417
  }
2131
- } else {
2132
- styleRule = new CSSOM.CSSStyleRule();
2133
- styleRule.__starts = i;
2134
- }
2135
- break;
2136
- case "before-name":
2137
- state = "name";
2138
- break;
2139
- case "before-value":
2140
- state = "value";
2141
- break;
2142
- case "importRule-begin":
2143
- state = "importRule";
2144
- break;
2145
- case "namespaceRule-begin":
2146
- state = "namespaceRule";
2147
- break;
2418
+ break;
2419
+ case "before-name":
2420
+ state = "name";
2421
+ break;
2422
+ case "before-value":
2423
+ state = "value";
2424
+ break;
2425
+ case "importRule-begin":
2426
+ state = "importRule";
2427
+ break;
2428
+ case "namespaceRule-begin":
2429
+ state = "namespaceRule";
2430
+ break;
2431
+ }
2432
+ buffer += character;
2433
+ break;
2434
+ }
2435
+
2436
+ // Auto-close all unclosed nested structures
2437
+ // Check AFTER processing the character, at the ORIGINAL ending index
2438
+ // Only add closing braces if CSS is incomplete (not at top scope)
2439
+ if (i === initialEndingIndex && (currentScope !== topScope || ancestorRules.length > 0)) {
2440
+ var needsClosing = ancestorRules.length;
2441
+ if (currentScope !== topScope && ancestorRules.indexOf(currentScope) === -1) {
2442
+ needsClosing += 1;
2443
+ }
2444
+ // Add closing braces for all unclosed structures
2445
+ for (var closeIdx = 0; closeIdx < needsClosing; closeIdx++) {
2446
+ token += "}";
2447
+ endingIndex += 1;
2148
2448
  }
2149
- buffer += character;
2150
- break;
2151
2449
  }
2152
2450
  }
2153
2451