@acemir/cssom 0.9.22 → 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.
640
948
  */
641
- var globalPseudoClassRegExp = /:([a-zA-Z-]+)\(((?:[^()"]+|"[^"]*"|'[^']*'|\((?:[^()"]+|"[^"]*"|'[^']*')*\))*?)\)/g;
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]
962
+ */
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;
@@ -906,10 +1301,47 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
906
1301
  case "importRule":
907
1302
  case "namespaceRule":
908
1303
  case "layerBlock":
909
- token += ";"
1304
+ if (character !== ";") {
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;
1332
+ }
910
1333
  }
911
1334
  }
912
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
+
913
1345
  switch (character) {
914
1346
 
915
1347
  case " ":
@@ -1007,6 +1439,15 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1007
1439
 
1008
1440
  // At-rule
1009
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
+ }
1010
1451
  if (token.indexOf("@-moz-document", i) === i) {
1011
1452
  validateAtRule("@-moz-document", function(){
1012
1453
  state = "documentRule-begin";
@@ -1141,7 +1582,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1141
1582
  break;
1142
1583
 
1143
1584
  case "{":
1144
- if (currentScope === styleSheet) {
1585
+ if (currentScope === topScope) {
1145
1586
  nestedSelectorRule = null;
1146
1587
  }
1147
1588
  if (state === 'before-selector') {
@@ -1163,7 +1604,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1163
1604
 
1164
1605
  if (parentRule) {
1165
1606
  styleRule.__parentRule = parentRule;
1166
- ancestorRules.push(parentRule);
1607
+ pushToAncestorRules(parentRule);
1167
1608
  }
1168
1609
 
1169
1610
  currentScope = parentRule = styleRule;
@@ -1177,7 +1618,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1177
1618
 
1178
1619
  if (parentRule) {
1179
1620
  mediaRule.__parentRule = parentRule;
1180
- ancestorRules.push(parentRule);
1621
+ pushToAncestorRules(parentRule);
1181
1622
  }
1182
1623
 
1183
1624
  currentScope = parentRule = mediaRule;
@@ -1189,7 +1630,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1189
1630
 
1190
1631
  if (parentRule) {
1191
1632
  containerRule.__parentRule = parentRule;
1192
- ancestorRules.push(parentRule);
1633
+ pushToAncestorRules(parentRule);
1193
1634
  }
1194
1635
  currentScope = parentRule = containerRule;
1195
1636
  containerRule.__parentStyleSheet = styleSheet;
@@ -1206,7 +1647,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1206
1647
 
1207
1648
  if (parentRule) {
1208
1649
  supportsRule.__parentRule = parentRule;
1209
- ancestorRules.push(parentRule);
1650
+ pushToAncestorRules(parentRule);
1210
1651
  }
1211
1652
 
1212
1653
  currentScope = parentRule = supportsRule;
@@ -1228,7 +1669,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1228
1669
 
1229
1670
  if (parentRule) {
1230
1671
  scopeRule.__parentRule = parentRule;
1231
- ancestorRules.push(parentRule);
1672
+ pushToAncestorRules(parentRule);
1232
1673
  }
1233
1674
  currentScope = parentRule = scopeRule;
1234
1675
  scopeRule.__parentStyleSheet = styleSheet;
@@ -1242,7 +1683,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1242
1683
  if (isValidName) {
1243
1684
  if (parentRule) {
1244
1685
  layerBlockRule.__parentRule = parentRule;
1245
- ancestorRules.push(parentRule);
1686
+ pushToAncestorRules(parentRule);
1246
1687
  }
1247
1688
 
1248
1689
  currentScope = parentRule = layerBlockRule;
@@ -1255,7 +1696,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1255
1696
 
1256
1697
  if (parentRule) {
1257
1698
  pageRule.__parentRule = parentRule;
1258
- ancestorRules.push(parentRule);
1699
+ pushToAncestorRules(parentRule);
1259
1700
  }
1260
1701
 
1261
1702
  currentScope = parentRule = pageRule;
@@ -1265,7 +1706,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1265
1706
  state = "before-name";
1266
1707
  } else if (state === "hostRule-begin") {
1267
1708
  if (parentRule) {
1268
- ancestorRules.push(parentRule);
1709
+ pushToAncestorRules(parentRule);
1269
1710
  }
1270
1711
 
1271
1712
  currentScope = parentRule = hostRule;
@@ -1275,7 +1716,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1275
1716
  } else if (state === "startingStyleRule-begin") {
1276
1717
  if (parentRule) {
1277
1718
  startingStyleRule.__parentRule = parentRule;
1278
- ancestorRules.push(parentRule);
1719
+ pushToAncestorRules(parentRule);
1279
1720
  }
1280
1721
 
1281
1722
  currentScope = parentRule = startingStyleRule;
@@ -1294,7 +1735,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1294
1735
  } else if (state === "keyframesRule-begin") {
1295
1736
  keyframesRule.name = buffer.trim();
1296
1737
  if (parentRule) {
1297
- ancestorRules.push(parentRule);
1738
+ pushToAncestorRules(parentRule);
1298
1739
  keyframesRule.__parentRule = parentRule;
1299
1740
  }
1300
1741
  keyframesRule.__parentStyleSheet = styleSheet;
@@ -1311,7 +1752,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1311
1752
  // FIXME: what if this '{' is in the url text of the match function?
1312
1753
  documentRule.matcher.matcherText = buffer.trim();
1313
1754
  if (parentRule) {
1314
- ancestorRules.push(parentRule);
1755
+ pushToAncestorRules(parentRule);
1315
1756
  documentRule.__parentRule = parentRule;
1316
1757
  }
1317
1758
  currentScope = parentRule = documentRule;
@@ -1324,21 +1765,21 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1324
1765
  parentRule.cssRules.push(styleRule);
1325
1766
  styleRule.__parentRule = parentRule;
1326
1767
  styleRule.__parentStyleSheet = styleSheet;
1327
- ancestorRules.push(parentRule);
1768
+ pushToAncestorRules(parentRule);
1328
1769
  } else {
1329
1770
  // If the styleRule is empty, we can assume that it's a nested selector
1330
- ancestorRules.push(parentRule);
1771
+ pushToAncestorRules(parentRule);
1331
1772
  }
1332
1773
  } else {
1333
1774
  currentScope = parentRule = styleRule;
1334
- ancestorRules.push(parentRule);
1775
+ pushToAncestorRules(parentRule);
1335
1776
  styleRule.__parentStyleSheet = styleSheet;
1336
1777
  }
1337
1778
 
1338
1779
  styleRule = new CSSOM.CSSStyleRule();
1339
1780
  var processedSelectorText = processSelectorText(buffer.trim());
1340
1781
  // In a nested selector, ensure each selector contains '&' at the beginning, except for selectors that already have '&' somewhere
1341
- if (parentRule.constructor.name !== "CSSStyleRule" && parentRule.parentRule === null) {
1782
+ if (parentRule.constructor.name === "CSSScopeRule" || (parentRule.constructor.name !== "CSSStyleRule" && parentRule.parentRule === null)) {
1342
1783
  styleRule.selectorText = processedSelectorText;
1343
1784
  } else {
1344
1785
  styleRule.selectorText = parseAndSplitNestedSelectors(processedSelectorText).map(function(sel) {
@@ -1437,20 +1878,20 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1437
1878
  state = "before-selector";
1438
1879
  break;
1439
1880
  case "importRule":
1440
- var isValid = styleSheet.cssRules.length === 0 || styleSheet.cssRules.some(function (rule) {
1881
+ var isValid = topScope.cssRules.length === 0 || topScope.cssRules.some(function (rule) {
1441
1882
  return ['CSSImportRule', 'CSSLayerStatementRule'].indexOf(rule.constructor.name) !== -1
1442
1883
  });
1443
1884
  if (isValid) {
1444
1885
  importRule = new CSSOM.CSSImportRule();
1445
1886
  importRule.__parentStyleSheet = importRule.styleSheet.__parentStyleSheet = styleSheet;
1446
1887
  importRule.cssText = buffer + character;
1447
- styleSheet.cssRules.push(importRule);
1888
+ topScope.cssRules.push(importRule);
1448
1889
  }
1449
1890
  buffer = "";
1450
1891
  state = "before-selector";
1451
1892
  break;
1452
1893
  case "namespaceRule":
1453
- var isValid = styleSheet.cssRules.length === 0 || styleSheet.cssRules.every(function (rule) {
1894
+ var isValid = topScope.cssRules.length === 0 || topScope.cssRules.every(function (rule) {
1454
1895
  return ['CSSImportRule','CSSLayerStatementRule','CSSNamespaceRule'].indexOf(rule.constructor.name) !== -1
1455
1896
  });
1456
1897
  if (isValid) {
@@ -1461,7 +1902,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1461
1902
 
1462
1903
  namespaceRule = testNamespaceRule;
1463
1904
  namespaceRule.__parentStyleSheet = styleSheet;
1464
- styleSheet.cssRules.push(namespaceRule);
1905
+ topScope.cssRules.push(namespaceRule);
1465
1906
 
1466
1907
  // Track the namespace prefix for validation
1467
1908
  if (namespaceRule.prefix) {
@@ -1488,7 +1929,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1488
1929
  layerStatementRule.__starts = layerBlockRule.__starts;
1489
1930
  layerStatementRule.__ends = i;
1490
1931
  layerStatementRule.nameList = nameListStr;
1491
- styleSheet.cssRules.push(layerStatementRule);
1932
+ topScope.cssRules.push(layerStatementRule);
1492
1933
  }
1493
1934
  buffer = "";
1494
1935
  state = "before-selector";
@@ -1529,7 +1970,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1529
1970
  styleRule.__parentStyleSheet = styleSheet;
1530
1971
 
1531
1972
  if (currentScope === styleRule) {
1532
- currentScope = parentRule || styleSheet;
1973
+ currentScope = parentRule || topScope;
1533
1974
  }
1534
1975
 
1535
1976
  if (styleRule.constructor.name === "CSSStyleRule" && !isValidSelectorText(styleRule.selectorText)) {
@@ -1538,7 +1979,11 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1538
1979
  }
1539
1980
  parseError('Invalid CSSStyleRule (selectorText = "' + styleRule.selectorText + '")', styleRule.parentRule !== null);
1540
1981
  } else {
1541
- currentScope.cssRules.push(styleRule);
1982
+ if (styleRule.parentRule) {
1983
+ styleRule.parentRule.cssRules.push(styleRule);
1984
+ } else {
1985
+ currentScope.cssRules.push(styleRule);
1986
+ }
1542
1987
  }
1543
1988
  buffer = "";
1544
1989
  if (currentScope.constructor === CSSOM.CSSKeyframesRule) {
@@ -1548,7 +1993,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1548
1993
  }
1549
1994
 
1550
1995
  if (styleRule.constructor.name === "CSSNestedDeclarations") {
1551
- if (currentScope !== styleSheet) {
1996
+ if (currentScope !== topScope) {
1552
1997
  nestedSelectorRule = currentScope;
1553
1998
  }
1554
1999
  styleRule = null;
@@ -1571,7 +2016,6 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1571
2016
  break;
1572
2017
  }
1573
2018
 
1574
-
1575
2019
  while (ancestorRules.length > 0) {
1576
2020
  parentRule = ancestorRules.pop();
1577
2021
 
@@ -1597,8 +2041,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1597
2041
  }
1598
2042
  } else {
1599
2043
  prevScope = currentScope;
1600
- currentScope = parentRule;
1601
- currentScope !== prevScope && currentScope.cssRules.push(prevScope);
2044
+ parentRule !== prevScope && parentRule.cssRules.push(prevScope);
1602
2045
  break;
1603
2046
  }
1604
2047
  }
@@ -1606,12 +2049,12 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1606
2049
 
1607
2050
  if (currentScope.parentRule == null) {
1608
2051
  currentScope.__ends = i + 1;
1609
- if (currentScope !== styleSheet && styleSheet.cssRules.findIndex(function (rule) {
2052
+ if (currentScope !== topScope && topScope.cssRules.findIndex(function (rule) {
1610
2053
  return rule === currentScope
1611
2054
  }) === -1) {
1612
- styleSheet.cssRules.push(currentScope);
2055
+ topScope.cssRules.push(currentScope);
1613
2056
  }
1614
- currentScope = styleSheet;
2057
+ currentScope = topScope;
1615
2058
  if (nestedSelectorRule === parentRule) {
1616
2059
  // Check if this selector is really starting inside another selector
1617
2060
  var nestedSelectorTokenToCurrentSelectorToken = token.slice(nestedSelectorRule.__starts, i + 1);
@@ -1630,6 +2073,8 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1630
2073
  parentRule = null;
1631
2074
 
1632
2075
  }
2076
+ } else {
2077
+ currentScope = parentRule;
1633
2078
  }
1634
2079
 
1635
2080
  buffer = "";
@@ -1642,7 +2087,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1642
2087
  switch (state) {
1643
2088
  case "before-selector":
1644
2089
  state = "selector";
1645
- if (styleRule && parentRule) {
2090
+ if ((styleRule || scopeRule) && parentRule) {
1646
2091
  // Assuming it's a declaration inside Nested Selector OR a Nested Declaration
1647
2092
  // If Declaration inside Nested Selector let's keep the same styleRule
1648
2093
  if (