@acemir/cssom 0.9.24 → 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,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,15 @@ 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
+ }
85
+
86
+ if (opts && opts.ownerRule) {
87
+ styleSheet.__ownerRule = opts.ownerRule;
76
88
  }
77
89
 
78
90
  // @type CSSStyleSheet|CSSMediaRule|CSSContainerRule|CSSSupportsRule|CSSFontFaceRule|CSSKeyframesRule|CSSDocumentRule
@@ -84,7 +96,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
84
96
  var ancestorRules = [];
85
97
  var prevScope;
86
98
 
87
- 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;
88
100
 
89
101
  // Track defined namespace prefixes for validation
90
102
  var definedNamespacePrefixes = {};
@@ -182,14 +194,14 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
182
194
  var ruleClosingMatch = matchBalancedBlock(str, fromIndex);
183
195
  if (ruleClosingMatch) {
184
196
  var ignoreRange = ruleClosingMatch.index + ruleClosingMatch[0].length;
185
- i+= ignoreRange;
197
+ i += ignoreRange;
186
198
  if (token.charAt(i) === '}') {
187
199
  i -= 1;
188
200
  }
189
201
  } else {
190
202
  i += str.length;
191
203
  }
192
- return i;
204
+ return i;
193
205
  }
194
206
 
195
207
  /**
@@ -199,29 +211,29 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
199
211
  */
200
212
  function parseScopePrelude(preludeContent) {
201
213
  var parts = preludeContent.split(/\s*\)\s*to\s+\(/);
202
-
214
+
203
215
  // Restore the parentheses that were consumed by the split
204
216
  if (parts.length === 2) {
205
217
  parts[0] = parts[0] + ')';
206
218
  parts[1] = '(' + parts[1];
207
219
  }
208
-
220
+
209
221
  var hasStart = parts[0] &&
210
222
  parts[0].charAt(0) === '(' &&
211
223
  parts[0].charAt(parts[0].length - 1) === ')';
212
224
  var hasEnd = parts[1] &&
213
225
  parts[1].charAt(0) === '(' &&
214
226
  parts[1].charAt(parts[1].length - 1) === ')';
215
-
227
+
216
228
  // Handle case: @scope to (<end>)
217
229
  var hasOnlyEnd = !hasStart &&
218
230
  !hasEnd &&
219
231
  parts[0].indexOf('to (') === 0 &&
220
232
  parts[0].charAt(parts[0].length - 1) === ')';
221
-
233
+
222
234
  var startSelector = '';
223
235
  var endSelector = '';
224
-
236
+
225
237
  if (hasStart) {
226
238
  startSelector = parts[0].slice(1, -1).trim();
227
239
  }
@@ -231,7 +243,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
231
243
  if (hasOnlyEnd) {
232
244
  endSelector = parts[0].slice(4, -1).trim();
233
245
  }
234
-
246
+
235
247
  return {
236
248
  startSelector: startSelector,
237
249
  endSelector: endSelector,
@@ -269,11 +281,11 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
269
281
  var inDoubleQuote = false;
270
282
  var inAttr = false;
271
283
  var stack = useStack ? [] : null;
272
-
284
+
273
285
  for (var i = 0; i < selector.length; i++) {
274
286
  var char = selector[i];
275
287
  var prevChar = i > 0 ? selector[i - 1] : '';
276
-
288
+
277
289
  if (inSingleQuote) {
278
290
  if (char === "'" && prevChar !== "\\") {
279
291
  inSingleQuote = false;
@@ -324,7 +336,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
324
336
  }
325
337
  }
326
338
  }
327
-
339
+
328
340
  // Check if everything is balanced
329
341
  if (useStack) {
330
342
  return stack.length === 0 && bracketDepth === 0 && !inSingleQuote && !inDoubleQuote && !inAttr;
@@ -374,7 +386,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
374
386
  /(?:^|[\s>+~,\[])cue\s*\(/i,
375
387
  /(?:^|[\s>+~,\[])cue-region\s*\(/i
376
388
  ];
377
-
389
+
378
390
  for (var i = 0; i < invalidPatterns.length; i++) {
379
391
  if (invalidPatterns[i].test(selector)) {
380
392
  return true;
@@ -420,7 +432,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
420
432
  isValid = true;
421
433
  }
422
434
  }
423
-
435
+
424
436
  // Additional validation for @scope rule
425
437
  if (isValid && atRuleKey === "@scope") {
426
438
  var openBraceIndex = ruleSlice.indexOf('{');
@@ -439,7 +451,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
439
451
  var hasStart = parsedScopePrelude.hasStart;
440
452
  var hasEnd = parsedScopePrelude.hasEnd;
441
453
  var hasOnlyEnd = parsedScopePrelude.hasOnlyEnd;
442
-
454
+
443
455
  // Validation rules for @scope:
444
456
  // 1. Empty selectors in parentheses are invalid: @scope () {} or @scope (.a) to () {}
445
457
  if ((hasStart && startSelector === '') || (hasEnd && endSelector === '') || (hasOnlyEnd && endSelector === '')) {
@@ -475,13 +487,13 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
475
487
  if (openBraceIndex !== -1) {
476
488
  // Extract the rule prelude (everything between the at-rule and {)
477
489
  var rulePrelude = ruleSlice.slice(0, openBraceIndex).trim();
478
-
490
+
479
491
  // Skip past at-rule keyword and whitespace
480
492
  var preludeContent = rulePrelude.slice("@page".length).trim();
481
493
 
482
494
  if (preludeContent.length > 0) {
483
495
  var trimmedValue = preludeContent.trim();
484
-
496
+
485
497
  // Empty selector is valid for @page
486
498
  if (trimmedValue !== '') {
487
499
  // Parse @page selectorText for page name and pseudo-pages
@@ -505,7 +517,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
505
517
 
506
518
  // Validate pseudo-pages if present
507
519
  if (pseudoPages) {
508
- var pseudos = pseudoPages.split(':').filter(function(p) { return p; });
520
+ var pseudos = pseudoPages.split(':').filter(function (p) { return p; });
509
521
  var validPseudos = ['left', 'right', 'first', 'blank'];
510
522
  var allValid = true;
511
523
  for (var j = 0; j < pseudos.length; j++) {
@@ -514,7 +526,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
514
526
  break;
515
527
  }
516
528
  }
517
-
529
+
518
530
  if (!allValid) {
519
531
  isValid = false;
520
532
  }
@@ -523,15 +535,15 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
523
535
  isValid = false;
524
536
  }
525
537
  }
526
-
538
+
527
539
  }
528
540
  }
529
541
  }
530
-
542
+
531
543
  if (!isValid) {
532
544
  // If it's invalid the browser will simply ignore the entire invalid block
533
545
  // Use regex to find the closing brace of the invalid rule
534
-
546
+
535
547
  // Regex used above is not ES5 compliant. Using alternative.
536
548
  // var ruleStatementMatch = ruleSlice.match(atRulesStatemenRegExp); //
537
549
  var ruleStatementMatch = atRulesStatemenRegExpES5Alternative(ruleSlice);
@@ -546,7 +558,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
546
558
  // Check if there's a semicolon before the invalid at-rule and the first opening brace
547
559
  if (atRuleKey === "@layer") {
548
560
  var ruleSemicolonAndOpeningBraceMatch = ruleSlice.match(forwardRuleSemicolonAndOpeningBraceRegExp);
549
- if (ruleSemicolonAndOpeningBraceMatch && ruleSemicolonAndOpeningBraceMatch[1] === ";" ) {
561
+ if (ruleSemicolonAndOpeningBraceMatch && ruleSemicolonAndOpeningBraceMatch[1] === ";") {
550
562
  // Ignore the rule block until the semicolon
551
563
  i += ruleSemicolonAndOpeningBraceMatch.index + ruleSemicolonAndOpeningBraceMatch[0].length;
552
564
  state = "before-selector";
@@ -564,7 +576,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
564
576
 
565
577
  // Helper functions for looseSelectorValidator
566
578
  // Defined outside to avoid recreation on every validation call
567
-
579
+
568
580
  /**
569
581
  * Check if character is a valid identifier start
570
582
  * @param {string} c - Character to check
@@ -636,19 +648,19 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
636
648
  // Helper: Parse namespace prefix (optional)
637
649
  function parseNamespace() {
638
650
  var start = i;
639
-
651
+
640
652
  // Match: *| or identifier| or |
641
653
  if (i < len && selector[i] === '*') {
642
654
  i++;
643
655
  } else if (i < len && (isIdentStart(selector[i]) || selector[i] === '\\')) {
644
656
  parseIdentifier();
645
657
  }
646
-
658
+
647
659
  if (i < len && selector[i] === '|') {
648
660
  i++;
649
661
  return true;
650
662
  }
651
-
663
+
652
664
  // Rollback if no pipe found
653
665
  i = start;
654
666
  return false;
@@ -659,15 +671,15 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
659
671
  if (i >= len || selector[i] !== '(') {
660
672
  return false;
661
673
  }
662
-
674
+
663
675
  i++; // Skip opening paren
664
676
  var depth = 1;
665
677
  var inString = false;
666
678
  var stringChar = '';
667
-
679
+
668
680
  while (i < len && depth > 0) {
669
681
  var c = selector[i];
670
-
682
+
671
683
  if (c === '\\' && i + 1 < len) {
672
684
  i += 2; // Skip escaped character
673
685
  } else if (!inString && (c === '"' || c === '\'')) {
@@ -687,7 +699,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
687
699
  i++;
688
700
  }
689
701
  }
690
-
702
+
691
703
  return depth === 0;
692
704
  }
693
705
 
@@ -789,7 +801,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
789
801
  // Match type selector with optional namespace: [namespace|]identifier
790
802
  else if (i < len && (isIdentStart(selector[i]) || selector[i] === '\\' || selector[i] === '*' || selector[i] === '|')) {
791
803
  parseNamespace(); // Optional namespace prefix
792
-
804
+
793
805
  if (i < len && selector[i] === '*') {
794
806
  i++; // Universal selector
795
807
  hasMatchedComponent = true;
@@ -865,36 +877,36 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
865
877
  // Pseudo-classes like :lang(), :dir(), :nth-*() should not accept quoted strings
866
878
  // Using iterative parsing instead of regex to avoid exponential backtracking
867
879
  var noQuotesPseudos = ['lang', 'dir', 'nth-child', 'nth-last-child', 'nth-of-type', 'nth-last-of-type'];
868
-
880
+
869
881
  for (var idx = 0; idx < selector.length; idx++) {
870
882
  // Look for pseudo-class/element start
871
883
  if (selector[idx] === ':') {
872
884
  var pseudoStart = idx;
873
885
  idx++;
874
-
886
+
875
887
  // Skip second colon for pseudo-elements
876
888
  if (idx < selector.length && selector[idx] === ':') {
877
889
  idx++;
878
890
  }
879
-
891
+
880
892
  // Extract pseudo name
881
893
  var nameStart = idx;
882
894
  while (idx < selector.length && /[a-zA-Z0-9\-]/.test(selector[idx])) {
883
895
  idx++;
884
896
  }
885
-
897
+
886
898
  if (idx === nameStart) {
887
899
  continue; // No name found
888
900
  }
889
-
901
+
890
902
  var pseudoName = selector.substring(nameStart, idx).toLowerCase();
891
-
903
+
892
904
  // Check if this pseudo has arguments
893
905
  if (idx < selector.length && selector[idx] === '(') {
894
906
  idx++;
895
907
  var contentStart = idx;
896
908
  var depth = 1;
897
-
909
+
898
910
  // Find matching closing paren (handle nesting)
899
911
  while (idx < selector.length && depth > 0) {
900
912
  if (selector[idx] === '\\') {
@@ -909,10 +921,10 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
909
921
  idx++;
910
922
  }
911
923
  }
912
-
924
+
913
925
  if (depth === 0) {
914
926
  var pseudoContent = selector.substring(contentStart, idx - 1);
915
-
927
+
916
928
  // Check if this pseudo should not have quoted strings
917
929
  for (var j = 0; j < noQuotesPseudos.length; j++) {
918
930
  if (pseudoName === noQuotesPseudos[j] && /['"]/.test(pseudoContent)) {
@@ -927,7 +939,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
927
939
  // Use the iterative validator to avoid regex backtracking issues
928
940
  return looseSelectorValidator(selector);
929
941
  }
930
-
942
+
931
943
  /**
932
944
  * Regular expression to match CSS pseudo-classes with arguments.
933
945
  *
@@ -946,7 +958,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
946
958
  *
947
959
  * REPLACED WITH FUNCTION to avoid exponential backtracking.
948
960
  */
949
-
961
+
950
962
  /**
951
963
  * Extract pseudo-classes with arguments from a selector using iterative parsing.
952
964
  * Replaces the previous globalPseudoClassRegExp to avoid exponential backtracking.
@@ -962,30 +974,30 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
962
974
  */
963
975
  function extractPseudoClasses(selector) {
964
976
  var matches = [];
965
-
977
+
966
978
  for (var i = 0; i < selector.length; i++) {
967
979
  // Look for pseudo-class start (single or double colon)
968
980
  if (selector[i] === ':') {
969
981
  var pseudoStart = i;
970
982
  i++;
971
-
983
+
972
984
  // Skip second colon for pseudo-elements (::)
973
985
  if (i < selector.length && selector[i] === ':') {
974
986
  i++;
975
987
  }
976
-
988
+
977
989
  // Extract pseudo name
978
990
  var nameStart = i;
979
991
  while (i < selector.length && /[a-zA-Z\-]/.test(selector[i])) {
980
992
  i++;
981
993
  }
982
-
994
+
983
995
  if (i === nameStart) {
984
996
  continue; // No name found
985
997
  }
986
-
998
+
987
999
  var pseudoName = selector.substring(nameStart, i);
988
-
1000
+
989
1001
  // Check if this pseudo has arguments
990
1002
  if (i < selector.length && selector[i] === '(') {
991
1003
  i++;
@@ -993,11 +1005,11 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
993
1005
  var depth = 1;
994
1006
  var inSingleQuote = false;
995
1007
  var inDoubleQuote = false;
996
-
1008
+
997
1009
  // Find matching closing paren (handle nesting and strings)
998
1010
  while (i < selector.length && depth > 0) {
999
1011
  var ch = selector[i];
1000
-
1012
+
1001
1013
  if (ch === '\\') {
1002
1014
  i += 2; // Skip escaped character
1003
1015
  } else if (ch === "'" && !inDoubleQuote) {
@@ -1016,21 +1028,21 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1016
1028
  i++;
1017
1029
  }
1018
1030
  }
1019
-
1031
+
1020
1032
  if (depth === 0) {
1021
1033
  var pseudoArgs = selector.substring(argsStart, i - 1);
1022
1034
  var fullMatch = selector.substring(pseudoStart, i);
1023
-
1035
+
1024
1036
  // Store match in same format as regex: [fullMatch, pseudoName, pseudoArgs, startIndex]
1025
1037
  matches.push([fullMatch, pseudoName, pseudoArgs, pseudoStart]);
1026
1038
  }
1027
-
1039
+
1028
1040
  // Move back one since loop will increment
1029
1041
  i--;
1030
1042
  }
1031
1043
  }
1032
1044
  }
1033
-
1045
+
1034
1046
  return matches;
1035
1047
  }
1036
1048
 
@@ -1134,6 +1146,23 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1134
1146
  var pseudoClass = pseudoClassMatches[j][1];
1135
1147
  if (selectorListPseudoClasses.hasOwnProperty(pseudoClass)) {
1136
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
1137
1166
  for (var i = 0; i < nestedSelectors.length; i++) {
1138
1167
  var nestedSelector = nestedSelectors[i];
1139
1168
  if (!validatedSelectorsCache.hasOwnProperty(nestedSelector)) {
@@ -1170,10 +1199,10 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1170
1199
  var inAttr = false;
1171
1200
  var inSingleQuote = false;
1172
1201
  var inDoubleQuote = false;
1173
-
1202
+
1174
1203
  for (var i = 0; i < selector.length; i++) {
1175
1204
  var char = selector[i];
1176
-
1205
+
1177
1206
  if (inSingleQuote) {
1178
1207
  if (char === "'" && selector[i - 1] !== "\\") {
1179
1208
  inSingleQuote = false;
@@ -1200,18 +1229,18 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1200
1229
  }
1201
1230
  }
1202
1231
  }
1203
-
1232
+
1204
1233
  if (pipeIndex === -1) {
1205
1234
  return true; // No namespace, always valid
1206
1235
  }
1207
-
1236
+
1208
1237
  var namespacePrefix = selector.substring(0, pipeIndex);
1209
-
1238
+
1210
1239
  // Universal namespace (*|) and default namespace (|) are always valid
1211
1240
  if (namespacePrefix === '*' || namespacePrefix === '') {
1212
1241
  return true;
1213
1242
  }
1214
-
1243
+
1215
1244
  // Check if the custom namespace prefix is defined
1216
1245
  return definedNamespacePrefixes.hasOwnProperty(namespacePrefix);
1217
1246
  }
@@ -1220,22 +1249,92 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1220
1249
  * Processes a CSS selector text
1221
1250
  *
1222
1251
  * @param {string} selectorText - The CSS selector text to process
1223
- * @returns {string} The processed selector text with normalized whitespace
1252
+ * @returns {string} The processed selector text with normalized whitespace and invalid selectors removed
1224
1253
  */
1225
1254
  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) {
1255
+ // Normalize whitespace first
1256
+ var normalized = selectorText.replace(/(['"])(?:\\.|[^\\])*?\1|(\r\n|\r|\n)/g, function (match, _, newline) {
1236
1257
  if (newline) return " ";
1237
1258
  return match;
1238
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;
1239
1338
  }
1240
1339
 
1241
1340
  /**
@@ -1249,6 +1348,12 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1249
1348
  // TODO: The same validations here needs to be reused in CSSStyleRule.selectorText setter
1250
1349
  // TODO: Move these validation logic to a shared function to be reused in CSSStyleRule.selectorText setter
1251
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
+
1252
1357
  // Check for newlines inside single or double quotes using regex
1253
1358
  // This matches any quoted string (single or double) containing a newline
1254
1359
  var quotedNewlineRegExp = /(['"])(?:\\.|[^\\])*?\1/g;
@@ -1288,12 +1393,22 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1288
1393
  // Print the error but continue parsing the sheet
1289
1394
  try {
1290
1395
  throw error;
1291
- } catch(e) {
1396
+ } catch (e) {
1292
1397
  errorHandler && errorHandler(e);
1293
1398
  }
1294
1399
  };
1295
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
+
1296
1410
  var endingIndex = token.length - 1;
1411
+ var initialEndingIndex = endingIndex;
1297
1412
 
1298
1413
  for (var character; (character = token.charAt(i)); i++) {
1299
1414
  if (i === endingIndex) {
@@ -1303,8 +1418,9 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1303
1418
  case "layerBlock":
1304
1419
  if (character !== ";") {
1305
1420
  token += ";";
1306
- break;
1421
+ endingIndex += 1;
1307
1422
  }
1423
+ break;
1308
1424
  case "value":
1309
1425
  if (character !== "}") {
1310
1426
  if (character === ";") {
@@ -1332,7 +1448,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1332
1448
  }
1333
1449
  }
1334
1450
  }
1335
-
1451
+
1336
1452
  // Handle escape sequences before processing special characters
1337
1453
  // If we encounter a backslash, add both the backslash and the next character to buffer
1338
1454
  // and skip the next iteration to prevent the escaped character from being interpreted
@@ -1341,813 +1457,991 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1341
1457
  i++; // Skip the next character
1342
1458
  continue;
1343
1459
  }
1344
-
1460
+
1345
1461
  switch (character) {
1346
1462
 
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 "');
1463
+ case " ":
1464
+ case "\t":
1465
+ case "\r":
1466
+ case "\n":
1467
+ case "\f":
1468
+ if (SIGNIFICANT_WHITESPACE[state]) {
1469
+ buffer += character;
1364
1470
  }
1365
- } while (token[index - 2] === '\\');
1366
- if (index === 0) {
1367
1471
  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 += ';'
1472
+
1473
+ // String
1474
+ case '"':
1475
+ index = i + 1;
1476
+ do {
1477
+ index = token.indexOf('"', index) + 1;
1478
+ if (!index) {
1479
+ parseError('Unmatched "');
1379
1480
  }
1481
+ } while (token[index - 2] === '\\');
1482
+ if (index === 0) {
1380
1483
  break;
1381
- case 'namespaceRule-begin':
1382
- state = 'namespaceRule';
1383
- if (i === endingIndex) {
1384
- 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 '");
1385
1512
  }
1513
+ } while (token[index - 2] === '\\');
1514
+ if (index === 0) {
1386
1515
  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
1516
  }
1397
- } while (token[index - 2] === '\\');
1398
- 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
+ }
1399
1530
  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 = "";
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
+ }
1424
1543
  } else {
1425
- i = index + 1;
1544
+ buffer += character;
1426
1545
  }
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);
1546
+ if (state === "importRule-begin") {
1547
+ buffer += " ";
1548
+ state = "importRule";
1445
1549
  }
1446
- if (nestedSelectorRule.parentRule.constructor.name === "CSSStyleRule") {
1447
- styleRule = nestedSelectorRule.parentRule;
1550
+ if (state === "namespaceRule-begin") {
1551
+ buffer += " ";
1552
+ state = "namespaceRule";
1448
1553
  }
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
1554
  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
- 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;
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
+ });
1575
1574
  buffer = "";
1576
1575
  break;
1577
- } else if (state === "selector") {
1578
- state = "atRule";
1579
- }
1580
- }
1581
- buffer += character;
1582
- 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
+ }
1583
1688
 
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;
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;
1598
1723
  buffer = "";
1599
- state = "before-selector";
1600
- i += ruleClosingMatch.index + ruleClosingMatch[0].length;
1601
1724
  break;
1725
+ } else if (state === "selector") {
1726
+ state = "atRule";
1602
1727
  }
1603
1728
  }
1729
+ buffer += character;
1730
+ break;
1604
1731
 
1605
- if (parentRule) {
1606
- styleRule.__parentRule = parentRule;
1607
- pushToAncestorRules(parentRule);
1732
+ case "{":
1733
+ if (currentScope === topScope) {
1734
+ nestedSelectorRule = null;
1608
1735
  }
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);
1736
+ if (state === 'before-selector') {
1737
+ parseError("Unexpected {");
1738
+ i = ignoreBalancedBlock(i, token.slice(i));
1739
+ break;
1622
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
+ }
1623
1752
 
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
- }
1753
+ // Ensure styleRule exists before trying to set properties on it
1754
+ if (!styleRule) {
1755
+ styleRule = new CSSOM.CSSStyleRule();
1756
+ styleRule.__starts = i;
1757
+ }
1652
1758
 
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
- }
1759
+ var originalParentRule = parentRule;
1669
1760
 
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();
1761
+ if (parentRule) {
1762
+ styleRule.__parentRule = parentRule;
1763
+ pushToAncestorRules(parentRule);
1764
+ }
1680
1765
 
1681
- 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();
1682
1783
 
1683
- if (isValidName) {
1684
1784
  if (parentRule) {
1685
- layerBlockRule.__parentRule = parentRule;
1785
+ mediaRule.__parentRule = parentRule;
1686
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
+ }
1687
1792
  }
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
1793
 
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";
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();
1726
1802
 
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;
1803
+ if (parentRule) {
1804
+ containerRule.__parentRule = parentRule;
1768
1805
  pushToAncestorRules(parentRule);
1769
- } else {
1770
- // If the styleRule is empty, we can assume that it's a nested selector
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();
1829
+
1830
+ if (parentRule) {
1831
+ supportsRule.__parentRule = parentRule;
1771
1832
  pushToAncestorRules(parentRule);
1833
+ if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) {
1834
+ nestedSelectorRule = parentRule;
1835
+ }
1772
1836
  }
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
1837
 
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();
1838
+ currentScope = parentRule = supportsRule;
1839
+ pushToAncestorRules(supportsRule);
1840
+ supportsRule.__parentStyleSheet = styleSheet;
1841
+ styleRule = null; // Reset styleRule when entering @-rule
1810
1842
  buffer = "";
1811
- state = "before-value";
1812
- }
1813
- } else {
1814
- buffer += character;
1815
- }
1816
- break;
1843
+ state = "before-selector";
1844
+ } else if (state === "scopeBlock") {
1845
+ var parsedScopePrelude = parseScopePrelude(buffer.trim());
1817
1846
 
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();
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
+ }
1823
1856
 
1824
- if (info.error) {
1825
- parseError(info.error);
1826
- } else {
1827
- buffer += info.expression;
1828
- i = info.idx;
1857
+ if (parentRule) {
1858
+ scopeRule.__parentRule = parentRule;
1859
+ pushToAncestorRules(parentRule);
1860
+ if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) {
1861
+ nestedSelectorRule = parentRule;
1862
+ }
1829
1863
  }
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;
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();
1844
1872
 
1845
- case ")":
1846
- if (state === 'value-parenthesis') {
1847
- valueParenthesisDepth--;
1848
- if (valueParenthesisDepth === 0) state = 'value';
1849
- }
1850
- buffer += character;
1851
- break;
1873
+ var isValidName = layerBlockRule.name.length === 0 || layerBlockRule.name.match(cssCustomIdentifierRegExp) !== null;
1852
1874
 
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;
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
+ }
1861
1883
 
1862
- case ";":
1863
- switch (state) {
1864
- case "before-value":
1865
- case "before-name":
1866
- parseError("Unexpected ;");
1884
+ currentScope = parentRule = layerBlockRule;
1885
+ pushToAncestorRules(layerBlockRule);
1886
+ layerBlockRule.__parentStyleSheet = styleSheet;
1887
+ }
1888
+ styleRule = null; // Reset styleRule when entering @-rule
1867
1889
  buffer = "";
1868
- state = "before-name";
1869
- break;
1870
- case "value":
1871
- styleRule.style.setProperty(name, buffer.trim(), priority, parseError);
1872
- 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;
1873
1902
  buffer = "";
1874
1903
  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);
1904
+ } else if (state === "hostRule-begin") {
1905
+ if (parentRule) {
1906
+ pushToAncestorRules(parentRule);
1889
1907
  }
1908
+
1909
+ currentScope = parentRule = hostRule;
1910
+ pushToAncestorRules(hostRule);
1911
+ hostRule.__parentStyleSheet = styleSheet;
1890
1912
  buffer = "";
1891
1913
  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);
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;
1913
1920
  }
1914
1921
  }
1922
+
1923
+ currentScope = parentRule = startingStyleRule;
1924
+ pushToAncestorRules(startingStyleRule);
1925
+ startingStyleRule.__parentStyleSheet = styleSheet;
1926
+ styleRule = null; // Reset styleRule when entering @-rule
1915
1927
  buffer = "";
1916
1928
  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
1929
 
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);
1930
+ } else if (state === "fontFaceRule-begin") {
1931
+ if (parentRule) {
1932
+ fontFaceRule.__parentRule = parentRule;
1933
1933
  }
1934
+ fontFaceRule.__parentStyleSheet = styleSheet;
1935
+ styleRule = fontFaceRule;
1934
1936
  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()
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;
1965
1943
  }
1966
-
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();
1967
1957
  if (parentRule) {
1968
- styleRule.__parentRule = parentRule;
1958
+ pushToAncestorRules(parentRule);
1959
+ documentRule.__parentRule = parentRule;
1969
1960
  }
1970
- styleRule.__parentStyleSheet = styleSheet;
1971
-
1972
- if (currentScope === styleRule) {
1973
- currentScope = parentRule || topScope;
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
+ }
1974
1980
  }
1975
1981
 
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);
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);
1984
1988
  } else {
1985
- currentScope.cssRules.push(styleRule);
1989
+ // If the styleRule is empty, we can assume that it's a nested selector
1990
+ pushToAncestorRules(parentRule);
1986
1991
  }
1992
+ } else {
1993
+ currentScope = parentRule = styleRule;
1994
+ pushToAncestorRules(parentRule);
1995
+ styleRule.__parentStyleSheet = styleSheet;
1987
1996
  }
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;
2003
+ } else {
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;
2015
+ }
2016
+
1988
2017
  buffer = "";
1989
- if (currentScope.constructor === CSSOM.CSSKeyframesRule) {
1990
- state = "keyframeRule-begin";
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;
1991
2030
  } else {
1992
- state = "before-selector";
2031
+ // Is a declaration
2032
+ name = buffer.trim();
2033
+ buffer = "";
2034
+ state = "before-value";
1993
2035
  }
2036
+ } else {
2037
+ buffer += character;
2038
+ }
2039
+ break;
1994
2040
 
1995
- if (styleRule.constructor.name === "CSSNestedDeclarations") {
1996
- if (currentScope !== topScope) {
1997
- nestedSelectorRule = currentScope;
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;
1998
2052
  }
1999
- styleRule = null;
2000
2053
  } else {
2001
- styleRule = null;
2002
- break;
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;
2003
2059
  }
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);
2060
+ } else if (state === 'value-parenthesis') {
2061
+ valueParenthesisDepth++;
2062
+ buffer += character;
2063
+ } else {
2064
+ buffer += character;
2065
+ }
2066
+ break;
2067
+
2068
+ case ")":
2069
+ if (state === 'value-parenthesis') {
2070
+ valueParenthesisDepth--;
2071
+ if (valueParenthesisDepth === 0) state = 'value';
2072
+ }
2073
+ buffer += character;
2074
+ break;
2075
+
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);
2014
2112
  }
2015
-
2113
+ buffer = "";
2114
+ state = "before-selector";
2016
2115
  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;
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;
2041
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);
2042
2173
  } else {
2043
- prevScope = currentScope;
2044
- parentRule !== prevScope && parentRule.cssRules.push(prevScope);
2045
- break;
2174
+ topScope.cssRules.push(layerStatementRule);
2046
2175
  }
2047
2176
  }
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);
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;
2056
2216
  }
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;
2217
+
2218
+ if (styleRule.constructor.name === "CSSStyleRule" && !isValidSelectorText(styleRule.selectorText)) {
2219
+ if (styleRule === nestedSelectorRule) {
2069
2220
  nestedSelectorRule = null;
2070
- parentRule = null;
2071
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";
2072
2233
  } else {
2073
- parentRule = null;
2234
+ state = "before-selector";
2235
+ }
2074
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;
2075
2275
  }
2076
- } else {
2077
- currentScope = parentRule;
2078
- }
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
+ }
2079
2287
 
2080
- buffer = "";
2081
- state = "before-selector";
2082
- break;
2083
- }
2084
- break;
2288
+ break;
2289
+ }
2085
2290
 
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;
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
+ }
2107
2322
  }
2108
2323
  }
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;
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
+ }
2126
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
2127
2389
  styleRule = new CSSOM.CSSStyleRule();
2128
- 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
+ }
2129
2412
  }
2130
2413
  }
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;
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;
2148
2444
  }
2149
- buffer += character;
2150
- break;
2151
2445
  }
2152
2446
  }
2153
2447