@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/build/CSSOM.js +714 -111
- package/lib/CSSContainerRule.js +11 -4
- package/lib/CSSDocumentRule.js +1 -0
- package/lib/CSSHostRule.js +13 -4
- package/lib/CSSKeyframesRule.js +12 -5
- package/lib/CSSLayerBlockRule.js +11 -4
- package/lib/CSSMediaRule.js +11 -4
- package/lib/CSSPageRule.js +8 -3
- package/lib/CSSScopeRule.js +11 -4
- package/lib/CSSStartingStyleRule.js +11 -4
- package/lib/CSSStyleRule.js +8 -3
- package/lib/CSSStyleSheet.js +79 -0
- package/lib/CSSSupportsRule.js +11 -6
- package/lib/MediaList.js +3 -1
- package/lib/errorUtils.js +15 -3
- package/lib/parse.js +509 -66
- package/package.json +1 -1
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
|
|
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 =
|
|
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 !==
|
|
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 !==
|
|
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
|
-
|
|
598
|
-
var
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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
|
-
//
|
|
617
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
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 ===
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1768
|
+
pushToAncestorRules(parentRule);
|
|
1330
1769
|
} else {
|
|
1331
1770
|
// If the styleRule is empty, we can assume that it's a nested selector
|
|
1332
|
-
|
|
1771
|
+
pushToAncestorRules(parentRule);
|
|
1333
1772
|
}
|
|
1334
1773
|
} else {
|
|
1335
1774
|
currentScope = parentRule = styleRule;
|
|
1336
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
|
|
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 !==
|
|
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
|
-
|
|
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 !==
|
|
2052
|
+
if (currentScope !== topScope && topScope.cssRules.findIndex(function (rule) {
|
|
1612
2053
|
return rule === currentScope
|
|
1613
2054
|
}) === -1) {
|
|
1614
|
-
|
|
2055
|
+
topScope.cssRules.push(currentScope);
|
|
1615
2056
|
}
|
|
1616
|
-
currentScope =
|
|
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 (
|