@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/build/CSSOM.js +716 -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 +511 -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.
|
|
640
948
|
*/
|
|
641
|
-
|
|
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
|
|
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
|
-
|
|
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 ===
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1768
|
+
pushToAncestorRules(parentRule);
|
|
1328
1769
|
} else {
|
|
1329
1770
|
// If the styleRule is empty, we can assume that it's a nested selector
|
|
1330
|
-
|
|
1771
|
+
pushToAncestorRules(parentRule);
|
|
1331
1772
|
}
|
|
1332
1773
|
} else {
|
|
1333
1774
|
currentScope = parentRule = styleRule;
|
|
1334
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
|
|
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 !==
|
|
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
|
-
|
|
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 !==
|
|
2052
|
+
if (currentScope !== topScope && topScope.cssRules.findIndex(function (rule) {
|
|
1610
2053
|
return rule === currentScope
|
|
1611
2054
|
}) === -1) {
|
|
1612
|
-
|
|
2055
|
+
topScope.cssRules.push(currentScope);
|
|
1613
2056
|
}
|
|
1614
|
-
currentScope =
|
|
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 (
|