@acemir/cssom 0.9.23 → 0.9.24

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
@@ -9,6 +9,8 @@ var CSSOM = {};
9
9
  * @param {string} token - The CSS string to parse.
10
10
  * @param {object} [opts] - Optional parsing options.
11
11
  * @param {object} [opts.globalObject] - An optional global object to attach to the stylesheet. Useful on jsdom webplatform tests.
12
+ * @param {CSSOM.CSSStyleSheet} [opts.styleSheet] - Reuse a style sheet instead of creating a new one (e.g. as `parentStyleSheet`)
13
+ * @param {CSSOM.CSSRuleList} [opts.cssRules] - Prepare all rules in this list instead of mutating the style sheet continually
12
14
  * @param {function|boolean} [errorHandler] - Optional error handler function or `true` to use `console.error`.
13
15
  * @returns {CSSOM.CSSStyleSheet} The parsed CSSStyleSheet object.
14
16
  */
@@ -55,14 +57,26 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
55
57
  "pageBlock": true
56
58
  };
57
59
 
58
- var styleSheet = new CSSOM.CSSStyleSheet();
60
+ var styleSheet;
61
+ if (opts && opts.styleSheet) {
62
+ styleSheet = opts.styleSheet;
63
+ } else {
64
+ styleSheet = new CSSOM.CSSStyleSheet()
65
+ }
66
+
67
+ var topScope;
68
+ if (opts && opts.cssRules) {
69
+ topScope = { cssRules: opts.cssRules };
70
+ } else {
71
+ topScope = styleSheet;
72
+ }
59
73
 
60
74
  if (opts && opts.globalObject) {
61
75
  styleSheet.__globalObject = opts.globalObject;
62
76
  }
63
77
 
64
78
  // @type CSSStyleSheet|CSSMediaRule|CSSContainerRule|CSSSupportsRule|CSSFontFaceRule|CSSKeyframesRule|CSSDocumentRule
65
- var currentScope = styleSheet;
79
+ var currentScope = topScope;
66
80
 
67
81
  // @type CSSMediaRule|CSSContainerRule|CSSSupportsRule|CSSKeyframesRule|CSSDocumentRule
68
82
  var parentRule;
@@ -391,7 +405,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
391
405
  var ruleRegExp = new RegExp(atRuleKey + sourceRuleRegExp.source, sourceRuleRegExp.flags);
392
406
  var ruleSlice = token.slice(i);
393
407
  // Not all rules can be nested, if the rule cannot be nested and is in the root scope, do not perform the check
394
- var shouldPerformCheck = cannotBeNested && currentScope !== styleSheet ? false : true;
408
+ var shouldPerformCheck = cannotBeNested && currentScope !== topScope ? false : true;
395
409
  // First, check if there is no invalid characters just after the at-rule
396
410
  if (shouldPerformCheck && ruleSlice.search(ruleRegExp) === 0) {
397
411
  // Find the closest allowed character before the at-rule (a opening or closing brace, a semicolon or a comment ending)
@@ -523,7 +537,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
523
537
  var ruleStatementMatch = atRulesStatemenRegExpES5Alternative(ruleSlice);
524
538
 
525
539
  // If it's a statement inside a nested rule, ignore only the statement
526
- if (ruleStatementMatch && currentScope !== styleSheet) {
540
+ if (ruleStatementMatch && currentScope !== topScope) {
527
541
  var ignoreEnd = ruleStatementMatch[0].indexOf(";");
528
542
  i += ruleStatementMatch.index + ignoreEnd;
529
543
  return;
@@ -548,6 +562,255 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
548
562
  }
549
563
  }
550
564
 
565
+ // Helper functions for looseSelectorValidator
566
+ // Defined outside to avoid recreation on every validation call
567
+
568
+ /**
569
+ * Check if character is a valid identifier start
570
+ * @param {string} c - Character to check
571
+ * @returns {boolean}
572
+ */
573
+ function isIdentStart(c) {
574
+ return /[a-zA-Z_\u00A0-\uFFFF]/.test(c);
575
+ }
576
+
577
+ /**
578
+ * Check if character is a valid identifier character
579
+ * @param {string} c - Character to check
580
+ * @returns {boolean}
581
+ */
582
+ function isIdentChar(c) {
583
+ return /[a-zA-Z0-9_\u00A0-\uFFFF\-]/.test(c);
584
+ }
585
+
586
+ /**
587
+ * Helper function to validate CSS selector syntax without regex backtracking.
588
+ * Iteratively parses the selector string to identify valid components.
589
+ *
590
+ * Supports:
591
+ * - Escaped special characters (e.g., .class\!, #id\@name)
592
+ * - Namespace selectors (ns|element, *|element, |element)
593
+ * - All standard CSS selectors (class, ID, type, attribute, pseudo, etc.)
594
+ * - Combinators (>, +, ~, whitespace)
595
+ * - Nesting selector (&)
596
+ *
597
+ * This approach eliminates exponential backtracking by using explicit character-by-character
598
+ * parsing instead of nested quantifiers in regex.
599
+ *
600
+ * @param {string} selector - The selector to validate
601
+ * @returns {boolean} - True if valid selector syntax
602
+ */
603
+ function looseSelectorValidator(selector) {
604
+ if (!selector || selector.length === 0) {
605
+ return false;
606
+ }
607
+
608
+ var i = 0;
609
+ var len = selector.length;
610
+ var hasMatchedComponent = false;
611
+
612
+ // Helper: Skip escaped character (backslash + any char)
613
+ function skipEscape() {
614
+ if (i < len && selector[i] === '\\') {
615
+ i += 2; // Skip backslash and next character
616
+ return true;
617
+ }
618
+ return false;
619
+ }
620
+
621
+ // Helper: Parse identifier (with possible escapes)
622
+ function parseIdentifier() {
623
+ var start = i;
624
+ while (i < len) {
625
+ if (skipEscape()) {
626
+ continue;
627
+ } else if (isIdentChar(selector[i])) {
628
+ i++;
629
+ } else {
630
+ break;
631
+ }
632
+ }
633
+ return i > start;
634
+ }
635
+
636
+ // Helper: Parse namespace prefix (optional)
637
+ function parseNamespace() {
638
+ var start = i;
639
+
640
+ // Match: *| or identifier| or |
641
+ if (i < len && selector[i] === '*') {
642
+ i++;
643
+ } else if (i < len && (isIdentStart(selector[i]) || selector[i] === '\\')) {
644
+ parseIdentifier();
645
+ }
646
+
647
+ if (i < len && selector[i] === '|') {
648
+ i++;
649
+ return true;
650
+ }
651
+
652
+ // Rollback if no pipe found
653
+ i = start;
654
+ return false;
655
+ }
656
+
657
+ // Helper: Parse pseudo-class/element arguments (with balanced parens)
658
+ function parsePseudoArgs() {
659
+ if (i >= len || selector[i] !== '(') {
660
+ return false;
661
+ }
662
+
663
+ i++; // Skip opening paren
664
+ var depth = 1;
665
+ var inString = false;
666
+ var stringChar = '';
667
+
668
+ while (i < len && depth > 0) {
669
+ var c = selector[i];
670
+
671
+ if (c === '\\' && i + 1 < len) {
672
+ i += 2; // Skip escaped character
673
+ } else if (!inString && (c === '"' || c === '\'')) {
674
+ inString = true;
675
+ stringChar = c;
676
+ i++;
677
+ } else if (inString && c === stringChar) {
678
+ inString = false;
679
+ i++;
680
+ } else if (!inString && c === '(') {
681
+ depth++;
682
+ i++;
683
+ } else if (!inString && c === ')') {
684
+ depth--;
685
+ i++;
686
+ } else {
687
+ i++;
688
+ }
689
+ }
690
+
691
+ return depth === 0;
692
+ }
693
+
694
+ // Main parsing loop
695
+ while (i < len) {
696
+ var matched = false;
697
+ var start = i;
698
+
699
+ // Skip whitespace
700
+ while (i < len && /\s/.test(selector[i])) {
701
+ i++;
702
+ }
703
+ if (i > start) {
704
+ hasMatchedComponent = true;
705
+ continue;
706
+ }
707
+
708
+ // Match combinators: >, +, ~
709
+ if (i < len && /[>+~]/.test(selector[i])) {
710
+ i++;
711
+ hasMatchedComponent = true;
712
+ // Skip trailing whitespace
713
+ while (i < len && /\s/.test(selector[i])) {
714
+ i++;
715
+ }
716
+ continue;
717
+ }
718
+
719
+ // Match nesting selector: &
720
+ if (i < len && selector[i] === '&') {
721
+ i++;
722
+ hasMatchedComponent = true;
723
+ matched = true;
724
+ }
725
+ // Match class selector: .identifier
726
+ else if (i < len && selector[i] === '.') {
727
+ i++;
728
+ if (parseIdentifier()) {
729
+ hasMatchedComponent = true;
730
+ matched = true;
731
+ }
732
+ }
733
+ // Match ID selector: #identifier
734
+ else if (i < len && selector[i] === '#') {
735
+ i++;
736
+ if (parseIdentifier()) {
737
+ hasMatchedComponent = true;
738
+ matched = true;
739
+ }
740
+ }
741
+ // Match pseudo-class/element: :identifier or ::identifier
742
+ else if (i < len && selector[i] === ':') {
743
+ i++;
744
+ if (i < len && selector[i] === ':') {
745
+ i++; // Pseudo-element
746
+ }
747
+ if (parseIdentifier()) {
748
+ parsePseudoArgs(); // Optional arguments
749
+ hasMatchedComponent = true;
750
+ matched = true;
751
+ }
752
+ }
753
+ // Match attribute selector: [...]
754
+ else if (i < len && selector[i] === '[') {
755
+ i++;
756
+ var depth = 1;
757
+ while (i < len && depth > 0) {
758
+ if (selector[i] === '\\') {
759
+ i += 2;
760
+ } else if (selector[i] === '\'') {
761
+ i++;
762
+ while (i < len && selector[i] !== '\'') {
763
+ if (selector[i] === '\\') i += 2;
764
+ else i++;
765
+ }
766
+ if (i < len) i++; // Skip closing quote
767
+ } else if (selector[i] === '"') {
768
+ i++;
769
+ while (i < len && selector[i] !== '"') {
770
+ if (selector[i] === '\\') i += 2;
771
+ else i++;
772
+ }
773
+ if (i < len) i++; // Skip closing quote
774
+ } else if (selector[i] === '[') {
775
+ depth++;
776
+ i++;
777
+ } else if (selector[i] === ']') {
778
+ depth--;
779
+ i++;
780
+ } else {
781
+ i++;
782
+ }
783
+ }
784
+ if (depth === 0) {
785
+ hasMatchedComponent = true;
786
+ matched = true;
787
+ }
788
+ }
789
+ // Match type selector with optional namespace: [namespace|]identifier
790
+ else if (i < len && (isIdentStart(selector[i]) || selector[i] === '\\' || selector[i] === '*' || selector[i] === '|')) {
791
+ parseNamespace(); // Optional namespace prefix
792
+
793
+ if (i < len && selector[i] === '*') {
794
+ i++; // Universal selector
795
+ hasMatchedComponent = true;
796
+ matched = true;
797
+ } else if (i < len && (isIdentStart(selector[i]) || selector[i] === '\\')) {
798
+ if (parseIdentifier()) {
799
+ hasMatchedComponent = true;
800
+ matched = true;
801
+ }
802
+ }
803
+ }
804
+
805
+ // If no match found, invalid selector
806
+ if (!matched && i === start) {
807
+ return false;
808
+ }
809
+ }
810
+
811
+ return hasMatchedComponent;
812
+ }
813
+
551
814
  /**
552
815
  * Validates a basic CSS selector, allowing for deeply nested balanced parentheses in pseudo-classes.
553
816
  * This function replaces the previous basicSelectorRegExp.
@@ -572,6 +835,12 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
572
835
  * @returns {boolean}
573
836
  */
574
837
  function basicSelectorValidator(selector) {
838
+ // Guard against extremely long selectors to prevent potential regex performance issues
839
+ // Reasonable selectors are typically under 1000 characters
840
+ if (selector.length > 10000) {
841
+ return false;
842
+ }
843
+
575
844
  // Validate balanced syntax with attribute tracking and stack-based parentheses matching
576
845
  if (!validateBalancedSyntax(selector, true, true)) {
577
846
  return false;
@@ -594,31 +863,69 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
594
863
 
595
864
  // Check for invalid pseudo-class usage with quoted strings
596
865
  // Pseudo-classes like :lang(), :dir(), :nth-*() should not accept quoted strings
597
- var pseudoPattern = /::?([a-zA-Z][\w-]*)\(([^)]+)\)/g;
598
- var pseudoMatch;
599
- while ((pseudoMatch = pseudoPattern.exec(selector)) !== null) {
600
- var pseudoName = pseudoMatch[1];
601
- var pseudoContent = pseudoMatch[2];
602
-
603
- // List of pseudo-classes that should not accept quoted strings
604
- // :lang() - accepts language codes: en, fr-CA
605
- // :dir() - accepts direction: ltr, rtl
606
- // :nth-*() - accepts An+B notation: 2n+1, odd, even
607
- var noQuotesPseudos = ['lang', 'dir', 'nth-child', 'nth-last-child', 'nth-of-type', 'nth-last-of-type'];
608
-
609
- for (var i = 0; i < noQuotesPseudos.length; i++) {
610
- if (pseudoName === noQuotesPseudos[i] && /['"]/.test(pseudoContent)) {
611
- return false;
866
+ // Using iterative parsing instead of regex to avoid exponential backtracking
867
+ var noQuotesPseudos = ['lang', 'dir', 'nth-child', 'nth-last-child', 'nth-of-type', 'nth-last-of-type'];
868
+
869
+ for (var idx = 0; idx < selector.length; idx++) {
870
+ // Look for pseudo-class/element start
871
+ if (selector[idx] === ':') {
872
+ var pseudoStart = idx;
873
+ idx++;
874
+
875
+ // Skip second colon for pseudo-elements
876
+ if (idx < selector.length && selector[idx] === ':') {
877
+ idx++;
878
+ }
879
+
880
+ // Extract pseudo name
881
+ var nameStart = idx;
882
+ while (idx < selector.length && /[a-zA-Z0-9\-]/.test(selector[idx])) {
883
+ idx++;
884
+ }
885
+
886
+ if (idx === nameStart) {
887
+ continue; // No name found
888
+ }
889
+
890
+ var pseudoName = selector.substring(nameStart, idx).toLowerCase();
891
+
892
+ // Check if this pseudo has arguments
893
+ if (idx < selector.length && selector[idx] === '(') {
894
+ idx++;
895
+ var contentStart = idx;
896
+ var depth = 1;
897
+
898
+ // Find matching closing paren (handle nesting)
899
+ while (idx < selector.length && depth > 0) {
900
+ if (selector[idx] === '\\') {
901
+ idx += 2; // Skip escaped character
902
+ } else if (selector[idx] === '(') {
903
+ depth++;
904
+ idx++;
905
+ } else if (selector[idx] === ')') {
906
+ depth--;
907
+ idx++;
908
+ } else {
909
+ idx++;
910
+ }
911
+ }
912
+
913
+ if (depth === 0) {
914
+ var pseudoContent = selector.substring(contentStart, idx - 1);
915
+
916
+ // Check if this pseudo should not have quoted strings
917
+ for (var j = 0; j < noQuotesPseudos.length; j++) {
918
+ if (pseudoName === noQuotesPseudos[j] && /['"]/.test(pseudoContent)) {
919
+ return false;
920
+ }
921
+ }
922
+ }
612
923
  }
613
924
  }
614
925
  }
615
926
 
616
- // Fallback to a loose regexp for the overall selector structure (without deep paren matching)
617
- // This is similar to the original, but without nested paren limitations
618
- // Modified to support namespace selectors: *|element, prefix|element, |element
619
- // Fixed attribute selector regex to properly handle |=, ~=, ^=, $=, *= operators
620
- var looseSelectorRegExp = /^((?:(?:\*|[a-zA-Z_\u00A0-\uFFFF\\][a-zA-Z0-9_\u00A0-\uFFFF\-\\]*|)\|)?[a-zA-Z_\u00A0-\uFFFF\\][a-zA-Z0-9_\u00A0-\uFFFF\-\\]*|(?:(?:\*|[a-zA-Z_\u00A0-\uFFFF\\][a-zA-Z0-9_\u00A0-\uFFFF\-\\]*|)\|)?\*|#[a-zA-Z0-9_\u00A0-\uFFFF\-\\]+|\.[a-zA-Z0-9_\u00A0-\uFFFF\-\\]+|\[(?:[^\[\]'"]|'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*")*(?:\s+[iI])?\]|::?[a-zA-Z0-9_\u00A0-\uFFFF\-\\]+(?:\((.*)\))?|&|\s*[>+~]\s*|\s+)+$/;
621
- return looseSelectorRegExp.test(selector);
927
+ // Use the iterative validator to avoid regex backtracking issues
928
+ return looseSelectorValidator(selector);
622
929
  }
623
930
 
624
931
  /**
@@ -636,9 +943,96 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
636
943
  * - :nth-child(2n+1)
637
944
  * - :has(.sel:nth-child(3n))
638
945
  * - :not(".foo, .bar")
639
- * @type {RegExp}
946
+ *
947
+ * REPLACED WITH FUNCTION to avoid exponential backtracking.
948
+ */
949
+
950
+ /**
951
+ * Extract pseudo-classes with arguments from a selector using iterative parsing.
952
+ * Replaces the previous globalPseudoClassRegExp to avoid exponential backtracking.
953
+ *
954
+ * Handles:
955
+ * - Regular content without parentheses or quotes
956
+ * - Single-quoted strings
957
+ * - Double-quoted strings
958
+ * - Nested parentheses (arbitrary depth)
959
+ *
960
+ * @param {string} selector - The CSS selector to parse
961
+ * @returns {Array} Array of matches, each with: [fullMatch, pseudoName, pseudoArgs, startIndex]
640
962
  */
641
- var globalPseudoClassRegExp = /:([a-zA-Z-]+)\(((?:[^()"]+|"[^"]*"|'[^']*'|\((?:[^()"]+|"[^"]*"|'[^']*')*\))*?)\)/g;
963
+ function extractPseudoClasses(selector) {
964
+ var matches = [];
965
+
966
+ for (var i = 0; i < selector.length; i++) {
967
+ // Look for pseudo-class start (single or double colon)
968
+ if (selector[i] === ':') {
969
+ var pseudoStart = i;
970
+ i++;
971
+
972
+ // Skip second colon for pseudo-elements (::)
973
+ if (i < selector.length && selector[i] === ':') {
974
+ i++;
975
+ }
976
+
977
+ // Extract pseudo name
978
+ var nameStart = i;
979
+ while (i < selector.length && /[a-zA-Z\-]/.test(selector[i])) {
980
+ i++;
981
+ }
982
+
983
+ if (i === nameStart) {
984
+ continue; // No name found
985
+ }
986
+
987
+ var pseudoName = selector.substring(nameStart, i);
988
+
989
+ // Check if this pseudo has arguments
990
+ if (i < selector.length && selector[i] === '(') {
991
+ i++;
992
+ var argsStart = i;
993
+ var depth = 1;
994
+ var inSingleQuote = false;
995
+ var inDoubleQuote = false;
996
+
997
+ // Find matching closing paren (handle nesting and strings)
998
+ while (i < selector.length && depth > 0) {
999
+ var ch = selector[i];
1000
+
1001
+ if (ch === '\\') {
1002
+ i += 2; // Skip escaped character
1003
+ } else if (ch === "'" && !inDoubleQuote) {
1004
+ inSingleQuote = !inSingleQuote;
1005
+ i++;
1006
+ } else if (ch === '"' && !inSingleQuote) {
1007
+ inDoubleQuote = !inDoubleQuote;
1008
+ i++;
1009
+ } else if (ch === '(' && !inSingleQuote && !inDoubleQuote) {
1010
+ depth++;
1011
+ i++;
1012
+ } else if (ch === ')' && !inSingleQuote && !inDoubleQuote) {
1013
+ depth--;
1014
+ i++;
1015
+ } else {
1016
+ i++;
1017
+ }
1018
+ }
1019
+
1020
+ if (depth === 0) {
1021
+ var pseudoArgs = selector.substring(argsStart, i - 1);
1022
+ var fullMatch = selector.substring(pseudoStart, i);
1023
+
1024
+ // Store match in same format as regex: [fullMatch, pseudoName, pseudoArgs, startIndex]
1025
+ matches.push([fullMatch, pseudoName, pseudoArgs, pseudoStart]);
1026
+ }
1027
+
1028
+ // Move back one since loop will increment
1029
+ i--;
1030
+ }
1031
+ }
1032
+ }
1033
+
1034
+ return matches;
1035
+ }
642
1036
 
643
1037
  /**
644
1038
  * Parses a CSS selector string and splits it into parts, handling nested parentheses.
@@ -733,13 +1127,8 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
733
1127
  return validatedSelectorsCache[selector];
734
1128
  }
735
1129
 
736
- // Use a non-global regex to find all pseudo-classes with arguments
737
- var pseudoClassMatches = [];
738
- var pseudoClassRegExp = new RegExp(globalPseudoClassRegExp.source, globalPseudoClassRegExp.flags);
739
- var match;
740
- while ((match = pseudoClassRegExp.exec(selector)) !== null) {
741
- pseudoClassMatches.push(match);
742
- }
1130
+ // Use function-based parsing to extract pseudo-classes (avoids backtracking)
1131
+ var pseudoClassMatches = extractPseudoClasses(selector);
743
1132
 
744
1133
  for (var j = 0; j < pseudoClassMatches.length; j++) {
745
1134
  var pseudoClass = pseudoClassMatches[j][1];
@@ -880,6 +1269,12 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
880
1269
  return true;
881
1270
  }
882
1271
 
1272
+ function pushToAncestorRules(rule) {
1273
+ if (ancestorRules.indexOf(rule) === -1) {
1274
+ ancestorRules.push(rule);
1275
+ }
1276
+ }
1277
+
883
1278
  function parseError(message, isNested) {
884
1279
  var lines = token.substring(0, i).split('\n');
885
1280
  var lineCount = lines.length;
@@ -907,11 +1302,46 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
907
1302
  case "namespaceRule":
908
1303
  case "layerBlock":
909
1304
  if (character !== ";") {
910
- token += ";"
1305
+ token += ";";
1306
+ break;
1307
+ }
1308
+ case "value":
1309
+ if (character !== "}") {
1310
+ if (character === ";") {
1311
+ token += "}"
1312
+ } else {
1313
+ token += ";";
1314
+ }
1315
+ endingIndex += 1;
1316
+ break;
1317
+ }
1318
+ case "name":
1319
+ case "before-name":
1320
+ if (character === "}") {
1321
+ token += " "
1322
+ } else {
1323
+ token += "}"
1324
+ }
1325
+ endingIndex += 1
1326
+ break;
1327
+ case "before-selector":
1328
+ if (character !== "}" && currentScope !== styleSheet) {
1329
+ token += "}"
1330
+ endingIndex += 1
1331
+ break;
911
1332
  }
912
1333
  }
913
1334
  }
914
1335
 
1336
+ // Handle escape sequences before processing special characters
1337
+ // If we encounter a backslash, add both the backslash and the next character to buffer
1338
+ // and skip the next iteration to prevent the escaped character from being interpreted
1339
+ if (character === '\\' && i + 1 < token.length) {
1340
+ buffer += character + token.charAt(i + 1);
1341
+ i++; // Skip the next character
1342
+ continue;
1343
+ }
1344
+
915
1345
  switch (character) {
916
1346
 
917
1347
  case " ":
@@ -1009,6 +1439,15 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1009
1439
 
1010
1440
  // At-rule
1011
1441
  case "@":
1442
+ if (nestedSelectorRule) {
1443
+ if (styleRule && styleRule.constructor.name === "CSSNestedDeclarations") {
1444
+ currentScope.cssRules.push(styleRule);
1445
+ }
1446
+ if (nestedSelectorRule.parentRule.constructor.name === "CSSStyleRule") {
1447
+ styleRule = nestedSelectorRule.parentRule;
1448
+ }
1449
+ nestedSelectorRule = null;
1450
+ }
1012
1451
  if (token.indexOf("@-moz-document", i) === i) {
1013
1452
  validateAtRule("@-moz-document", function(){
1014
1453
  state = "documentRule-begin";
@@ -1143,7 +1582,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1143
1582
  break;
1144
1583
 
1145
1584
  case "{":
1146
- if (currentScope === styleSheet) {
1585
+ if (currentScope === topScope) {
1147
1586
  nestedSelectorRule = null;
1148
1587
  }
1149
1588
  if (state === 'before-selector') {
@@ -1165,7 +1604,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1165
1604
 
1166
1605
  if (parentRule) {
1167
1606
  styleRule.__parentRule = parentRule;
1168
- ancestorRules.push(parentRule);
1607
+ pushToAncestorRules(parentRule);
1169
1608
  }
1170
1609
 
1171
1610
  currentScope = parentRule = styleRule;
@@ -1179,7 +1618,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1179
1618
 
1180
1619
  if (parentRule) {
1181
1620
  mediaRule.__parentRule = parentRule;
1182
- ancestorRules.push(parentRule);
1621
+ pushToAncestorRules(parentRule);
1183
1622
  }
1184
1623
 
1185
1624
  currentScope = parentRule = mediaRule;
@@ -1191,7 +1630,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1191
1630
 
1192
1631
  if (parentRule) {
1193
1632
  containerRule.__parentRule = parentRule;
1194
- ancestorRules.push(parentRule);
1633
+ pushToAncestorRules(parentRule);
1195
1634
  }
1196
1635
  currentScope = parentRule = containerRule;
1197
1636
  containerRule.__parentStyleSheet = styleSheet;
@@ -1208,7 +1647,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1208
1647
 
1209
1648
  if (parentRule) {
1210
1649
  supportsRule.__parentRule = parentRule;
1211
- ancestorRules.push(parentRule);
1650
+ pushToAncestorRules(parentRule);
1212
1651
  }
1213
1652
 
1214
1653
  currentScope = parentRule = supportsRule;
@@ -1230,7 +1669,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1230
1669
 
1231
1670
  if (parentRule) {
1232
1671
  scopeRule.__parentRule = parentRule;
1233
- ancestorRules.push(parentRule);
1672
+ pushToAncestorRules(parentRule);
1234
1673
  }
1235
1674
  currentScope = parentRule = scopeRule;
1236
1675
  scopeRule.__parentStyleSheet = styleSheet;
@@ -1244,7 +1683,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1244
1683
  if (isValidName) {
1245
1684
  if (parentRule) {
1246
1685
  layerBlockRule.__parentRule = parentRule;
1247
- ancestorRules.push(parentRule);
1686
+ pushToAncestorRules(parentRule);
1248
1687
  }
1249
1688
 
1250
1689
  currentScope = parentRule = layerBlockRule;
@@ -1257,7 +1696,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1257
1696
 
1258
1697
  if (parentRule) {
1259
1698
  pageRule.__parentRule = parentRule;
1260
- ancestorRules.push(parentRule);
1699
+ pushToAncestorRules(parentRule);
1261
1700
  }
1262
1701
 
1263
1702
  currentScope = parentRule = pageRule;
@@ -1267,7 +1706,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1267
1706
  state = "before-name";
1268
1707
  } else if (state === "hostRule-begin") {
1269
1708
  if (parentRule) {
1270
- ancestorRules.push(parentRule);
1709
+ pushToAncestorRules(parentRule);
1271
1710
  }
1272
1711
 
1273
1712
  currentScope = parentRule = hostRule;
@@ -1277,7 +1716,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1277
1716
  } else if (state === "startingStyleRule-begin") {
1278
1717
  if (parentRule) {
1279
1718
  startingStyleRule.__parentRule = parentRule;
1280
- ancestorRules.push(parentRule);
1719
+ pushToAncestorRules(parentRule);
1281
1720
  }
1282
1721
 
1283
1722
  currentScope = parentRule = startingStyleRule;
@@ -1296,7 +1735,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1296
1735
  } else if (state === "keyframesRule-begin") {
1297
1736
  keyframesRule.name = buffer.trim();
1298
1737
  if (parentRule) {
1299
- ancestorRules.push(parentRule);
1738
+ pushToAncestorRules(parentRule);
1300
1739
  keyframesRule.__parentRule = parentRule;
1301
1740
  }
1302
1741
  keyframesRule.__parentStyleSheet = styleSheet;
@@ -1313,7 +1752,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1313
1752
  // FIXME: what if this '{' is in the url text of the match function?
1314
1753
  documentRule.matcher.matcherText = buffer.trim();
1315
1754
  if (parentRule) {
1316
- ancestorRules.push(parentRule);
1755
+ pushToAncestorRules(parentRule);
1317
1756
  documentRule.__parentRule = parentRule;
1318
1757
  }
1319
1758
  currentScope = parentRule = documentRule;
@@ -1326,21 +1765,21 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1326
1765
  parentRule.cssRules.push(styleRule);
1327
1766
  styleRule.__parentRule = parentRule;
1328
1767
  styleRule.__parentStyleSheet = styleSheet;
1329
- ancestorRules.push(parentRule);
1768
+ pushToAncestorRules(parentRule);
1330
1769
  } else {
1331
1770
  // If the styleRule is empty, we can assume that it's a nested selector
1332
- ancestorRules.push(parentRule);
1771
+ pushToAncestorRules(parentRule);
1333
1772
  }
1334
1773
  } else {
1335
1774
  currentScope = parentRule = styleRule;
1336
- ancestorRules.push(parentRule);
1775
+ pushToAncestorRules(parentRule);
1337
1776
  styleRule.__parentStyleSheet = styleSheet;
1338
1777
  }
1339
1778
 
1340
1779
  styleRule = new CSSOM.CSSStyleRule();
1341
1780
  var processedSelectorText = processSelectorText(buffer.trim());
1342
1781
  // In a nested selector, ensure each selector contains '&' at the beginning, except for selectors that already have '&' somewhere
1343
- if (parentRule.constructor.name !== "CSSStyleRule" && parentRule.parentRule === null) {
1782
+ if (parentRule.constructor.name === "CSSScopeRule" || (parentRule.constructor.name !== "CSSStyleRule" && parentRule.parentRule === null)) {
1344
1783
  styleRule.selectorText = processedSelectorText;
1345
1784
  } else {
1346
1785
  styleRule.selectorText = parseAndSplitNestedSelectors(processedSelectorText).map(function(sel) {
@@ -1439,20 +1878,20 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1439
1878
  state = "before-selector";
1440
1879
  break;
1441
1880
  case "importRule":
1442
- var isValid = styleSheet.cssRules.length === 0 || styleSheet.cssRules.some(function (rule) {
1881
+ var isValid = topScope.cssRules.length === 0 || topScope.cssRules.some(function (rule) {
1443
1882
  return ['CSSImportRule', 'CSSLayerStatementRule'].indexOf(rule.constructor.name) !== -1
1444
1883
  });
1445
1884
  if (isValid) {
1446
1885
  importRule = new CSSOM.CSSImportRule();
1447
1886
  importRule.__parentStyleSheet = importRule.styleSheet.__parentStyleSheet = styleSheet;
1448
1887
  importRule.cssText = buffer + character;
1449
- styleSheet.cssRules.push(importRule);
1888
+ topScope.cssRules.push(importRule);
1450
1889
  }
1451
1890
  buffer = "";
1452
1891
  state = "before-selector";
1453
1892
  break;
1454
1893
  case "namespaceRule":
1455
- var isValid = styleSheet.cssRules.length === 0 || styleSheet.cssRules.every(function (rule) {
1894
+ var isValid = topScope.cssRules.length === 0 || topScope.cssRules.every(function (rule) {
1456
1895
  return ['CSSImportRule','CSSLayerStatementRule','CSSNamespaceRule'].indexOf(rule.constructor.name) !== -1
1457
1896
  });
1458
1897
  if (isValid) {
@@ -1463,7 +1902,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1463
1902
 
1464
1903
  namespaceRule = testNamespaceRule;
1465
1904
  namespaceRule.__parentStyleSheet = styleSheet;
1466
- styleSheet.cssRules.push(namespaceRule);
1905
+ topScope.cssRules.push(namespaceRule);
1467
1906
 
1468
1907
  // Track the namespace prefix for validation
1469
1908
  if (namespaceRule.prefix) {
@@ -1490,7 +1929,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1490
1929
  layerStatementRule.__starts = layerBlockRule.__starts;
1491
1930
  layerStatementRule.__ends = i;
1492
1931
  layerStatementRule.nameList = nameListStr;
1493
- styleSheet.cssRules.push(layerStatementRule);
1932
+ topScope.cssRules.push(layerStatementRule);
1494
1933
  }
1495
1934
  buffer = "";
1496
1935
  state = "before-selector";
@@ -1531,7 +1970,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1531
1970
  styleRule.__parentStyleSheet = styleSheet;
1532
1971
 
1533
1972
  if (currentScope === styleRule) {
1534
- currentScope = parentRule || styleSheet;
1973
+ currentScope = parentRule || topScope;
1535
1974
  }
1536
1975
 
1537
1976
  if (styleRule.constructor.name === "CSSStyleRule" && !isValidSelectorText(styleRule.selectorText)) {
@@ -1540,7 +1979,11 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1540
1979
  }
1541
1980
  parseError('Invalid CSSStyleRule (selectorText = "' + styleRule.selectorText + '")', styleRule.parentRule !== null);
1542
1981
  } else {
1543
- currentScope.cssRules.push(styleRule);
1982
+ if (styleRule.parentRule) {
1983
+ styleRule.parentRule.cssRules.push(styleRule);
1984
+ } else {
1985
+ currentScope.cssRules.push(styleRule);
1986
+ }
1544
1987
  }
1545
1988
  buffer = "";
1546
1989
  if (currentScope.constructor === CSSOM.CSSKeyframesRule) {
@@ -1550,7 +1993,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1550
1993
  }
1551
1994
 
1552
1995
  if (styleRule.constructor.name === "CSSNestedDeclarations") {
1553
- if (currentScope !== styleSheet) {
1996
+ if (currentScope !== topScope) {
1554
1997
  nestedSelectorRule = currentScope;
1555
1998
  }
1556
1999
  styleRule = null;
@@ -1573,7 +2016,6 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1573
2016
  break;
1574
2017
  }
1575
2018
 
1576
-
1577
2019
  while (ancestorRules.length > 0) {
1578
2020
  parentRule = ancestorRules.pop();
1579
2021
 
@@ -1599,8 +2041,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1599
2041
  }
1600
2042
  } else {
1601
2043
  prevScope = currentScope;
1602
- currentScope = parentRule;
1603
- currentScope !== prevScope && currentScope.cssRules.push(prevScope);
2044
+ parentRule !== prevScope && parentRule.cssRules.push(prevScope);
1604
2045
  break;
1605
2046
  }
1606
2047
  }
@@ -1608,12 +2049,12 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1608
2049
 
1609
2050
  if (currentScope.parentRule == null) {
1610
2051
  currentScope.__ends = i + 1;
1611
- if (currentScope !== styleSheet && styleSheet.cssRules.findIndex(function (rule) {
2052
+ if (currentScope !== topScope && topScope.cssRules.findIndex(function (rule) {
1612
2053
  return rule === currentScope
1613
2054
  }) === -1) {
1614
- styleSheet.cssRules.push(currentScope);
2055
+ topScope.cssRules.push(currentScope);
1615
2056
  }
1616
- currentScope = styleSheet;
2057
+ currentScope = topScope;
1617
2058
  if (nestedSelectorRule === parentRule) {
1618
2059
  // Check if this selector is really starting inside another selector
1619
2060
  var nestedSelectorTokenToCurrentSelectorToken = token.slice(nestedSelectorRule.__starts, i + 1);
@@ -1632,6 +2073,8 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1632
2073
  parentRule = null;
1633
2074
 
1634
2075
  }
2076
+ } else {
2077
+ currentScope = parentRule;
1635
2078
  }
1636
2079
 
1637
2080
  buffer = "";
@@ -1644,7 +2087,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1644
2087
  switch (state) {
1645
2088
  case "before-selector":
1646
2089
  state = "selector";
1647
- if (styleRule && parentRule) {
2090
+ if ((styleRule || scopeRule) && parentRule) {
1648
2091
  // Assuming it's a declaration inside Nested Selector OR a Nested Declaration
1649
2092
  // If Declaration inside Nested Selector let's keep the same styleRule
1650
2093
  if (