@acemir/cssom 0.9.19 → 0.9.20

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 CHANGED
@@ -364,6 +364,28 @@ Object.defineProperties(CSSOM.CSSRule.prototype, {
364
364
 
365
365
 
366
366
 
367
+ /**
368
+ * @constructor
369
+ * @see https://drafts.csswg.org/cssom/#the-cssrulelist-interface
370
+ */
371
+ CSSOM.CSSRuleList = function CSSRuleList(){
372
+ const arr = new Array();
373
+ Object.setPrototypeOf(arr, CSSOM.CSSRuleList.prototype);
374
+ return arr;
375
+ };
376
+
377
+ CSSOM.CSSRuleList.prototype = Object.create(Array.prototype);
378
+ CSSOM.CSSRuleList.prototype.constructor = CSSOM.CSSRuleList;
379
+
380
+ CSSOM.CSSRuleList.prototype.item = function(index) {
381
+ return this[index] || null;
382
+ };
383
+
384
+
385
+
386
+
387
+
388
+
367
389
  /**
368
390
  * @constructor
369
391
  * @see https://drafts.csswg.org/css-nesting-1/
@@ -408,7 +430,7 @@ Object.defineProperty(CSSOM.CSSNestedDeclarations.prototype, "cssText", {
408
430
  */
409
431
  CSSOM.CSSGroupingRule = function CSSGroupingRule() {
410
432
  CSSOM.CSSRule.call(this);
411
- this.cssRules = [];
433
+ this.cssRules = new CSSOM.CSSRuleList();
412
434
  };
413
435
 
414
436
  CSSOM.CSSGroupingRule.prototype = new CSSOM.CSSRule();
@@ -525,7 +547,6 @@ CSSOM.CSSCounterStyleRule.prototype.type = 11;
525
547
  */
526
548
  CSSOM.CSSConditionRule = function CSSConditionRule() {
527
549
  CSSOM.CSSGroupingRule.call(this);
528
- this.cssRules = [];
529
550
  };
530
551
 
531
552
  CSSOM.CSSConditionRule.prototype = new CSSOM.CSSGroupingRule();
@@ -1219,7 +1240,7 @@ Object.defineProperty(CSSOM.CSSFontFaceRule.prototype, "cssText", {
1219
1240
  */
1220
1241
  CSSOM.CSSHostRule = function CSSHostRule() {
1221
1242
  CSSOM.CSSRule.call(this);
1222
- this.cssRules = [];
1243
+ this.cssRules = new CSSOM.CSSRuleList();
1223
1244
  };
1224
1245
 
1225
1246
  CSSOM.CSSHostRule.prototype = new CSSOM.CSSRule();
@@ -1249,11 +1270,10 @@ Object.defineProperty(CSSOM.CSSHostRule.prototype, "cssText", {
1249
1270
  * @see http://www.w3.org/TR/shadow-dom/#host-at-rule
1250
1271
  */
1251
1272
  CSSOM.CSSStartingStyleRule = function CSSStartingStyleRule() {
1252
- CSSOM.CSSRule.call(this);
1253
- this.cssRules = [];
1273
+ CSSOM.CSSGroupingRule.call(this);
1254
1274
  };
1255
1275
 
1256
- CSSOM.CSSStartingStyleRule.prototype = new CSSOM.CSSRule();
1276
+ CSSOM.CSSStartingStyleRule.prototype = new CSSOM.CSSGroupingRule();
1257
1277
  CSSOM.CSSStartingStyleRule.prototype.constructor = CSSOM.CSSStartingStyleRule;
1258
1278
  CSSOM.CSSStartingStyleRule.prototype.type = 1002;
1259
1279
  //FIXME
@@ -1301,13 +1321,18 @@ Object.defineProperties(CSSOM.StyleSheet.prototype, {
1301
1321
  */
1302
1322
  CSSOM.CSSStyleSheet = function CSSStyleSheet() {
1303
1323
  CSSOM.StyleSheet.call(this);
1304
- this.cssRules = [];
1324
+ this.cssRules = new CSSOM.CSSRuleList();
1305
1325
  };
1306
1326
 
1307
1327
 
1308
1328
  CSSOM.CSSStyleSheet.prototype = new CSSOM.StyleSheet();
1309
1329
  CSSOM.CSSStyleSheet.prototype.constructor = CSSOM.CSSStyleSheet;
1310
1330
 
1331
+ Object.defineProperty(CSSOM.CSSStyleSheet.prototype, "rules", {
1332
+ get: function() {
1333
+ return this.cssRules;
1334
+ }
1335
+ });
1311
1336
 
1312
1337
  /**
1313
1338
  * Used to insert a new rule into the style sheet. The new rule now becomes part of the cascade.
@@ -1438,6 +1463,13 @@ CSSOM.CSSStyleSheet.prototype.insertRule = function(rule, index) {
1438
1463
  return index;
1439
1464
  };
1440
1465
 
1466
+ CSSOM.CSSStyleSheet.prototype.addRule = function(selector, styleBlock, index) {
1467
+ if (index === void 0) {
1468
+ index = this.cssRules.length;
1469
+ }
1470
+ this.insertRule(selector + "{" + styleBlock + "}", index);
1471
+ return -1;
1472
+ };
1441
1473
 
1442
1474
  /**
1443
1475
  * Used to delete a rule from the style sheet.
@@ -1474,6 +1506,9 @@ CSSOM.CSSStyleSheet.prototype.deleteRule = function(index) {
1474
1506
  this.cssRules.splice(index, 1);
1475
1507
  };
1476
1508
 
1509
+ CSSOM.CSSStyleSheet.prototype.removeRule = function(index) {
1510
+ this.deleteRule(index);
1511
+ };
1477
1512
 
1478
1513
  /**
1479
1514
  * NON-STANDARD
@@ -1500,7 +1535,7 @@ CSSOM.CSSStyleSheet.prototype.toString = function() {
1500
1535
  CSSOM.CSSKeyframesRule = function CSSKeyframesRule() {
1501
1536
  CSSOM.CSSRule.call(this);
1502
1537
  this.name = '';
1503
- this.cssRules = [];
1538
+ this.cssRules = new CSSOM.CSSRuleList();
1504
1539
 
1505
1540
  // Set up initial indexed access
1506
1541
  this._setupIndexedAccess();
@@ -1826,7 +1861,7 @@ CSSOM.MatcherList.prototype = {
1826
1861
  CSSOM.CSSDocumentRule = function CSSDocumentRule() {
1827
1862
  CSSOM.CSSRule.call(this);
1828
1863
  this.matcher = new CSSOM.MatcherList();
1829
- this.cssRules = [];
1864
+ this.cssRules = new CSSOM.CSSRuleList();
1830
1865
  };
1831
1866
 
1832
1867
  CSSOM.CSSDocumentRule.prototype = new CSSOM.CSSRule();
@@ -2227,6 +2262,51 @@ CSSOM.CSSValueExpression.prototype._findMatchedIdx = function(token, idx, sep) {
2227
2262
 
2228
2263
 
2229
2264
 
2265
+ /**
2266
+ * @constructor
2267
+ * @see https://drafts.csswg.org/css-cascade-6/#cssscoperule
2268
+ */
2269
+ CSSOM.CSSScopeRule = function CSSScopeRule() {
2270
+ CSSOM.CSSGroupingRule.call(this);
2271
+ this.__start = null;
2272
+ this.__end = null;
2273
+ };
2274
+
2275
+ CSSOM.CSSScopeRule.prototype = new CSSOM.CSSGroupingRule();
2276
+ CSSOM.CSSScopeRule.prototype.constructor = CSSOM.CSSScopeRule;
2277
+
2278
+
2279
+ Object.defineProperties(CSSOM.CSSScopeRule.prototype, {
2280
+ type: {
2281
+ value: 0,
2282
+ writable: false,
2283
+ },
2284
+ cssText: {
2285
+ get: function () {
2286
+ var cssTexts = [];
2287
+ for (var i = 0, length = this.cssRules.length; i < length; i++) {
2288
+ cssTexts.push(this.cssRules[i].cssText);
2289
+ }
2290
+ return "@scope " + (this.start ? "(" + this.start + ") " : "") + (this.end ? "to (" + this.end + ") " : "") + "{" + (cssTexts.length ? "\n " + cssTexts.join("\n ") : "") + "\n}";
2291
+ },
2292
+ configurable: true,
2293
+ enumerable: true,
2294
+ },
2295
+ start: {
2296
+ get: function () {
2297
+ return this.__start;
2298
+ }
2299
+ },
2300
+ end: {
2301
+ get: function () {
2302
+ return this.__end;
2303
+ }
2304
+ }
2305
+ });
2306
+
2307
+
2308
+
2309
+
2230
2310
  /**
2231
2311
  * @constructor
2232
2312
  * @see https://drafts.csswg.org/css-cascade-5/#csslayerblockrule
@@ -2234,7 +2314,6 @@ CSSOM.CSSValueExpression.prototype._findMatchedIdx = function(token, idx, sep) {
2234
2314
  CSSOM.CSSLayerBlockRule = function CSSLayerBlockRule() {
2235
2315
  CSSOM.CSSGroupingRule.call(this);
2236
2316
  this.name = "";
2237
- this.cssRules = [];
2238
2317
  };
2239
2318
 
2240
2319
  CSSOM.CSSLayerBlockRule.prototype = new CSSOM.CSSGroupingRule();
@@ -2332,6 +2411,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2332
2411
  "conditionBlock": true,
2333
2412
  "counterStyleBlock": true,
2334
2413
  'documentRule-begin': true,
2414
+ "scopeBlock": true,
2335
2415
  "layerBlock": true
2336
2416
  };
2337
2417
 
@@ -2350,7 +2430,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2350
2430
  var ancestorRules = [];
2351
2431
  var prevScope;
2352
2432
 
2353
- var name, priority="", styleRule, mediaRule, containerRule, counterStyleRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule, startingStyleRule, layerBlockRule, layerStatementRule, nestedSelectorRule, namespaceRule;
2433
+ var name, priority="", styleRule, mediaRule, containerRule, counterStyleRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule, startingStyleRule, scopeRule, layerBlockRule, layerStatementRule, nestedSelectorRule, namespaceRule;
2354
2434
 
2355
2435
  // Track defined namespace prefixes for validation
2356
2436
  var definedNamespacePrefixes = {};
@@ -2360,11 +2440,12 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2360
2440
  // var atRulesStatemenRegExp = /(?<!{.*)[;}]\s*/; // Match a statement by verifying it finds a semicolon or closing brace not followed by another semicolon or closing brace
2361
2441
  var beforeRulePortionRegExp = /{(?!.*{)|}(?!.*})|;(?!.*;)|\*\/(?!.*\*\/)/g; // Match the closest allowed character (a opening or closing brace, a semicolon or a comment ending) before the rule
2362
2442
  var beforeRuleValidationRegExp = /^[\s{};]*(\*\/\s*)?$/; // Match that the portion before the rule is empty or contains only whitespace, semicolons, opening/closing braces, and optionally a comment ending (*/) followed by whitespace
2363
- var forwardRuleValidationRegExp = /(?:\(|\s|\/\*)/; // Match that the rule is followed by any whitespace, a opening comment or a condition opening parenthesis
2443
+ var forwardRuleValidationRegExp = /(?:\s|\/\*|\{|\()/; // Match that the rule is followed by any whitespace, a opening comment, a condition opening parenthesis or a opening brace
2364
2444
  var forwardImportRuleValidationRegExp = /(?:\s|\/\*|'|")/; // Match that the rule is followed by any whitespace, an opening comment, a single quote or double quote
2365
2445
  var forwardRuleClosingBraceRegExp = /{[^{}]*}|}/; // Finds the next closing brace of a rule block
2366
2446
  var forwardRuleSemicolonAndOpeningBraceRegExp = /^.*?({|;)/; // Finds the next semicolon or opening brace after the at-rule
2367
2447
  var layerRuleNameRegExp = /^(-?[_a-zA-Z]+(\.[_a-zA-Z]+)*[_a-zA-Z0-9-]*)$/; // Validates a single @layer name
2448
+ var startsWithCombinatorRegExp = /^\s*[>+~]/; // Checks if a selector starts with a CSS combinator (>, +, ~)
2368
2449
 
2369
2450
  /**
2370
2451
  * Searches for the first occurrence of a CSS at-rule statement terminator (`;` or `}`)
@@ -2456,24 +2537,214 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2456
2537
  return i;
2457
2538
  }
2458
2539
 
2459
- var parseError = function(message) {
2460
- var lines = token.substring(0, i).split('\n');
2461
- var lineCount = lines.length;
2462
- var charCount = lines.pop().length + 1;
2463
- var error = new Error(message + ' (line ' + lineCount + ', char ' + charCount + ')');
2464
- error.line = lineCount;
2465
- /* jshint sub : true */
2466
- error['char'] = charCount;
2467
- error.styleSheet = styleSheet;
2468
- // Print the error but continue parsing the sheet
2469
- try {
2470
- throw error;
2471
- } catch(e) {
2472
- errorHandler && errorHandler(e);
2540
+ /**
2541
+ * Parses the scope prelude and extracts start and end selectors.
2542
+ * @param {string} preludeContent - The scope prelude content (without @scope keyword)
2543
+ * @returns {object} Object with startSelector and endSelector properties
2544
+ */
2545
+ function parseScopePrelude(preludeContent) {
2546
+ var parts = preludeContent.split(/\s*\)\s*to\s+\(/);
2547
+
2548
+ // Restore the parentheses that were consumed by the split
2549
+ if (parts.length === 2) {
2550
+ parts[0] = parts[0] + ')';
2551
+ parts[1] = '(' + parts[1];
2552
+ }
2553
+
2554
+ var hasStart = parts[0] &&
2555
+ parts[0].charAt(0) === '(' &&
2556
+ parts[0].charAt(parts[0].length - 1) === ')';
2557
+ var hasEnd = parts[1] &&
2558
+ parts[1].charAt(0) === '(' &&
2559
+ parts[1].charAt(parts[1].length - 1) === ')';
2560
+
2561
+ // Handle case: @scope to (<end>)
2562
+ var hasOnlyEnd = !hasStart &&
2563
+ !hasEnd &&
2564
+ parts[0].indexOf('to (') === 0 &&
2565
+ parts[0].charAt(parts[0].length - 1) === ')';
2566
+
2567
+ var startSelector = '';
2568
+ var endSelector = '';
2569
+
2570
+ if (hasStart) {
2571
+ startSelector = parts[0].slice(1, -1).trim();
2572
+ }
2573
+ if (hasEnd) {
2574
+ endSelector = parts[1].slice(1, -1).trim();
2575
+ }
2576
+ if (hasOnlyEnd) {
2577
+ endSelector = parts[0].slice(4, -1).trim();
2578
+ }
2579
+
2580
+ return {
2581
+ startSelector: startSelector,
2582
+ endSelector: endSelector,
2583
+ hasStart: hasStart,
2584
+ hasEnd: hasEnd,
2585
+ hasOnlyEnd: hasOnlyEnd
2586
+ };
2587
+ };
2588
+
2589
+ /**
2590
+ * Checks if a selector contains pseudo-elements.
2591
+ * @param {string} selector - The CSS selector to check
2592
+ * @returns {boolean} True if the selector contains pseudo-elements
2593
+ */
2594
+ function hasPseudoElement(selector) {
2595
+ // Match only double-colon (::) pseudo-elements
2596
+ // Also match legacy single-colon pseudo-elements: :before, :after, :first-line, :first-letter
2597
+ // These must NOT be followed by alphanumeric characters (to avoid matching :before-x or similar)
2598
+ var pseudoElementRegex = /::[a-zA-Z][\w-]*|:(before|after|first-line|first-letter)(?![a-zA-Z0-9_-])/;
2599
+ return pseudoElementRegex.test(selector);
2600
+ };
2601
+
2602
+ /**
2603
+ * Validates balanced parentheses, brackets, and quotes in a selector.
2604
+ *
2605
+ * @param {string} selector - The CSS selector to validate
2606
+ * @param {boolean} trackAttributes - Whether to track attribute selector context
2607
+ * @param {boolean} useStack - Whether to use a stack for parentheses (needed for nested validation)
2608
+ * @returns {boolean} True if the syntax is valid (all brackets, parentheses, and quotes are balanced)
2609
+ */
2610
+ function validateBalancedSyntax(selector, trackAttributes, useStack) {
2611
+ var parenDepth = 0;
2612
+ var bracketDepth = 0;
2613
+ var inSingleQuote = false;
2614
+ var inDoubleQuote = false;
2615
+ var inAttr = false;
2616
+ var stack = useStack ? [] : null;
2617
+
2618
+ for (var i = 0; i < selector.length; i++) {
2619
+ var char = selector[i];
2620
+ var prevChar = i > 0 ? selector[i - 1] : '';
2621
+
2622
+ if (inSingleQuote) {
2623
+ if (char === "'" && prevChar !== "\\") {
2624
+ inSingleQuote = false;
2625
+ }
2626
+ } else if (inDoubleQuote) {
2627
+ if (char === '"' && prevChar !== "\\") {
2628
+ inDoubleQuote = false;
2629
+ }
2630
+ } else if (trackAttributes && inAttr) {
2631
+ if (char === "]") {
2632
+ inAttr = false;
2633
+ } else if (char === "'") {
2634
+ inSingleQuote = true;
2635
+ } else if (char === '"') {
2636
+ inDoubleQuote = true;
2637
+ }
2638
+ } else {
2639
+ if (trackAttributes && char === "[") {
2640
+ inAttr = true;
2641
+ } else if (char === "'") {
2642
+ inSingleQuote = true;
2643
+ } else if (char === '"') {
2644
+ inDoubleQuote = true;
2645
+ } else if (char === '(') {
2646
+ if (useStack) {
2647
+ stack.push("(");
2648
+ } else {
2649
+ parenDepth++;
2650
+ }
2651
+ } else if (char === ')') {
2652
+ if (useStack) {
2653
+ if (!stack.length || stack.pop() !== "(") {
2654
+ return false;
2655
+ }
2656
+ } else {
2657
+ parenDepth--;
2658
+ if (parenDepth < 0) {
2659
+ return false;
2660
+ }
2661
+ }
2662
+ } else if (char === '[') {
2663
+ bracketDepth++;
2664
+ } else if (char === ']') {
2665
+ bracketDepth--;
2666
+ if (bracketDepth < 0) {
2667
+ return false;
2668
+ }
2669
+ }
2670
+ }
2671
+ }
2672
+
2673
+ // Check if everything is balanced
2674
+ if (useStack) {
2675
+ return stack.length === 0 && bracketDepth === 0 && !inSingleQuote && !inDoubleQuote && !inAttr;
2676
+ } else {
2677
+ return parenDepth === 0 && bracketDepth === 0 && !inSingleQuote && !inDoubleQuote;
2473
2678
  }
2474
2679
  };
2475
2680
 
2476
- var validateAtRule = function(atRuleKey, validCallback, cannotBeNested) {
2681
+ /**
2682
+ * Checks for basic syntax errors in selectors (mismatched parentheses, brackets, quotes).
2683
+ * @param {string} selector - The CSS selector to check
2684
+ * @returns {boolean} True if there are syntax errors
2685
+ */
2686
+ function hasBasicSyntaxError(selector) {
2687
+ return !validateBalancedSyntax(selector, false, false);
2688
+ };
2689
+
2690
+ /**
2691
+ * Checks for invalid combinator patterns in selectors.
2692
+ * @param {string} selector - The CSS selector to check
2693
+ * @returns {boolean} True if the selector contains invalid combinators
2694
+ */
2695
+ function hasInvalidCombinators(selector) {
2696
+ // Check for invalid combinator patterns:
2697
+ // - <> (not a valid combinator)
2698
+ // - >> (deep descendant combinator, deprecated and invalid)
2699
+ // - Multiple consecutive combinators like >>, >~, etc.
2700
+ if (/<>/.test(selector)) return true;
2701
+ if (/>>/.test(selector)) return true;
2702
+ // Check for other invalid consecutive combinator patterns
2703
+ if (/[>+~]\s*[>+~]/.test(selector)) return true;
2704
+ return false;
2705
+ };
2706
+
2707
+ /**
2708
+ * Checks for invalid pseudo-like syntax (function calls without proper pseudo prefix).
2709
+ * @param {string} selector - The CSS selector to check
2710
+ * @returns {boolean} True if the selector contains invalid pseudo-like syntax
2711
+ */
2712
+ function hasInvalidPseudoSyntax(selector) {
2713
+ // Check for specific known pseudo-elements used without : or :: prefix
2714
+ // Examples: slotted(div), part(name), cue(selector)
2715
+ // These are ONLY valid as ::slotted(), ::part(), ::cue()
2716
+ var invalidPatterns = [
2717
+ /(?:^|[\s>+~,\[])slotted\s*\(/i,
2718
+ /(?:^|[\s>+~,\[])part\s*\(/i,
2719
+ /(?:^|[\s>+~,\[])cue\s*\(/i,
2720
+ /(?:^|[\s>+~,\[])cue-region\s*\(/i
2721
+ ];
2722
+
2723
+ for (var i = 0; i < invalidPatterns.length; i++) {
2724
+ if (invalidPatterns[i].test(selector)) {
2725
+ return true;
2726
+ }
2727
+ }
2728
+ return false;
2729
+ };
2730
+
2731
+ /**
2732
+ * Checks for invalid nesting selector (&) usage.
2733
+ * The & selector cannot be directly followed by a type selector without a delimiter.
2734
+ * Valid: &.class, &#id, &[attr], &:hover, &::before, & div, &>div
2735
+ * Invalid: &div, &span
2736
+ * @param {string} selector - The CSS selector to check
2737
+ * @returns {boolean} True if the selector contains invalid & usage
2738
+ */
2739
+ function hasInvalidNestingSelector(selector) {
2740
+ // Check for & followed directly by a letter (type selector) without any delimiter
2741
+ // This regex matches & followed by a letter (start of type selector) that's not preceded by an escape
2742
+ // We need to exclude valid cases like &.class, &#id, &[attr], &:pseudo, &::pseudo, & (with space), &>
2743
+ var invalidNestingPattern = /&(?![.\#\[:>\+~\s])[a-zA-Z]/;
2744
+ return invalidNestingPattern.test(selector);
2745
+ };
2746
+
2747
+ function validateAtRule(atRuleKey, validCallback, cannotBeNested) {
2477
2748
  var isValid = false;
2478
2749
  var sourceRuleRegExp = atRuleKey === "@import" ? forwardImportRuleValidationRegExp : forwardRuleValidationRegExp;
2479
2750
  var ruleRegExp = new RegExp(atRuleKey + sourceRuleRegExp.source, sourceRuleRegExp.flags);
@@ -2494,6 +2765,56 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2494
2765
  isValid = true;
2495
2766
  }
2496
2767
  }
2768
+
2769
+ // Additional validation for @scope rule
2770
+ if (isValid && atRuleKey === "@scope") {
2771
+ var openBraceIndex = ruleSlice.indexOf('{');
2772
+ if (openBraceIndex !== -1) {
2773
+ // Extract the scope prelude (everything between @scope and {)
2774
+ var scopePrelude = ruleSlice.slice(0, openBraceIndex).trim();
2775
+
2776
+ // Skip past '@scope' keyword and whitespace
2777
+ var preludeContent = scopePrelude.slice(6).trim();
2778
+
2779
+ if (preludeContent.length > 0) {
2780
+ // Parse the scope prelude
2781
+ var parsedScopePrelude = parseScopePrelude(preludeContent);
2782
+ var startSelector = parsedScopePrelude.startSelector;
2783
+ var endSelector = parsedScopePrelude.endSelector;
2784
+ var hasStart = parsedScopePrelude.hasStart;
2785
+ var hasEnd = parsedScopePrelude.hasEnd;
2786
+ var hasOnlyEnd = parsedScopePrelude.hasOnlyEnd;
2787
+
2788
+ // Validation rules for @scope:
2789
+ // 1. Empty selectors in parentheses are invalid: @scope () {} or @scope (.a) to () {}
2790
+ if ((hasStart && startSelector === '') || (hasEnd && endSelector === '') || (hasOnlyEnd && endSelector === '')) {
2791
+ isValid = false;
2792
+ }
2793
+ // 2. Pseudo-elements are invalid in scope selectors
2794
+ else if ((startSelector && hasPseudoElement(startSelector)) || (endSelector && hasPseudoElement(endSelector))) {
2795
+ isValid = false;
2796
+ }
2797
+ // 3. Basic syntax errors (mismatched parens, brackets, quotes)
2798
+ else if ((startSelector && hasBasicSyntaxError(startSelector)) || (endSelector && hasBasicSyntaxError(endSelector))) {
2799
+ isValid = false;
2800
+ }
2801
+ // 4. Invalid combinator patterns
2802
+ else if ((startSelector && hasInvalidCombinators(startSelector)) || (endSelector && hasInvalidCombinators(endSelector))) {
2803
+ isValid = false;
2804
+ }
2805
+ // 5. Invalid pseudo-like syntax (function without : or :: prefix)
2806
+ else if ((startSelector && hasInvalidPseudoSyntax(startSelector)) || (endSelector && hasInvalidPseudoSyntax(endSelector))) {
2807
+ isValid = false;
2808
+ }
2809
+ // 6. Invalid structure (no proper parentheses found when prelude is not empty)
2810
+ else if (!hasStart && !hasOnlyEnd) {
2811
+ isValid = false;
2812
+ }
2813
+ }
2814
+ // Empty prelude (@scope {}) is valid
2815
+ }
2816
+ }
2817
+
2497
2818
  if (!isValid) {
2498
2819
  // If it's invalid the browser will simply ignore the entire invalid block
2499
2820
  // Use regex to find the closing brace of the invalid rule
@@ -2552,52 +2873,23 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2552
2873
  * @returns {boolean}
2553
2874
  */
2554
2875
  function basicSelectorValidator(selector) {
2555
- var length = selector.length;
2556
- var i = 0;
2557
- var stack = [];
2558
- var inAttr = false;
2559
- var inSingleQuote = false;
2560
- var inDoubleQuote = false;
2876
+ // Validate balanced syntax with attribute tracking and stack-based parentheses matching
2877
+ if (!validateBalancedSyntax(selector, true, true)) {
2878
+ return false;
2879
+ }
2561
2880
 
2562
- while (i < length) {
2563
- var char = selector[i];
2881
+ // Check for invalid combinator patterns
2882
+ if (hasInvalidCombinators(selector)) {
2883
+ return false;
2884
+ }
2564
2885
 
2565
- if (inSingleQuote) {
2566
- if (char === "'" && selector[i - 1] !== "\\") {
2567
- inSingleQuote = false;
2568
- }
2569
- } else if (inDoubleQuote) {
2570
- if (char === '"' && selector[i - 1] !== "\\") {
2571
- inDoubleQuote = false;
2572
- }
2573
- } else if (inAttr) {
2574
- if (char === "]") {
2575
- inAttr = false;
2576
- } else if (char === "'") {
2577
- inSingleQuote = true;
2578
- } else if (char === '"') {
2579
- inDoubleQuote = true;
2580
- }
2581
- } else {
2582
- if (char === "[") {
2583
- inAttr = true;
2584
- } else if (char === "'") {
2585
- inSingleQuote = true;
2586
- } else if (char === '"') {
2587
- inDoubleQuote = true;
2588
- } else if (char === "(") {
2589
- stack.push("(");
2590
- } else if (char === ")") {
2591
- if (!stack.length || stack.pop() !== "(") {
2592
- return false;
2593
- }
2594
- }
2595
- }
2596
- i++;
2886
+ // Check for invalid pseudo-like syntax
2887
+ if (hasInvalidPseudoSyntax(selector)) {
2888
+ return false;
2597
2889
  }
2598
2890
 
2599
- // If any stack or quote/attr context remains, it's invalid
2600
- if (stack.length || inAttr || inSingleQuote || inDoubleQuote) {
2891
+ // Check for invalid nesting selector (&) usage
2892
+ if (hasInvalidNestingSelector(selector)) {
2601
2893
  return false;
2602
2894
  }
2603
2895
 
@@ -2608,7 +2900,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2608
2900
  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+)+$/;
2609
2901
  return looseSelectorRegExp.test(selector);
2610
2902
  }
2611
-
2903
+
2612
2904
  /**
2613
2905
  * Regular expression to match CSS pseudo-classes with arguments.
2614
2906
  *
@@ -2639,48 +2931,56 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2639
2931
  * @returns {string[]} An array of selector parts, split by top-level commas, with whitespace trimmed.
2640
2932
  */
2641
2933
  function parseAndSplitNestedSelectors(selector) {
2642
- var depth = 0;
2643
- var buffer = "";
2644
- var parts = [];
2645
- var inSingleQuote = false;
2646
- var inDoubleQuote = false;
2934
+ var depth = 0; // Track parenthesis nesting depth
2935
+ var buffer = ""; // Accumulate characters for current selector part
2936
+ var parts = []; // Array of split selector parts
2937
+ var inSingleQuote = false; // Track if we're inside single quotes
2938
+ var inDoubleQuote = false; // Track if we're inside double quotes
2647
2939
  var i, char;
2648
2940
 
2649
2941
  for (i = 0; i < selector.length; i++) {
2650
2942
  char = selector.charAt(i);
2651
2943
 
2944
+ // Handle single quote strings
2652
2945
  if (char === "'" && !inDoubleQuote) {
2653
2946
  inSingleQuote = !inSingleQuote;
2654
2947
  buffer += char;
2655
- } else if (char === '"' && !inSingleQuote) {
2948
+ }
2949
+ // Handle double quote strings
2950
+ else if (char === '"' && !inSingleQuote) {
2656
2951
  inDoubleQuote = !inDoubleQuote;
2657
2952
  buffer += char;
2658
- } else if (!inSingleQuote && !inDoubleQuote) {
2953
+ }
2954
+ // Process characters outside of quoted strings
2955
+ else if (!inSingleQuote && !inDoubleQuote) {
2659
2956
  if (char === '(') {
2957
+ // Entering a nested level (e.g., :is(...))
2660
2958
  depth++;
2661
2959
  buffer += char;
2662
2960
  } else if (char === ')') {
2961
+ // Exiting a nested level
2663
2962
  depth--;
2664
2963
  buffer += char;
2665
- if (depth === 0) {
2666
- parts.push(buffer.replace(/^\s+|\s+$/g, ""));
2667
- buffer = "";
2668
- }
2669
2964
  } else if (char === ',' && depth === 0) {
2670
- if (buffer.replace(/^\s+|\s+$/g, "")) {
2671
- parts.push(buffer.replace(/^\s+|\s+$/g, ""));
2965
+ // Found a top-level comma separator - split here
2966
+ if (buffer.trim()) {
2967
+ parts.push(buffer.trim());
2672
2968
  }
2673
2969
  buffer = "";
2674
2970
  } else {
2971
+ // Regular character - add to buffer
2675
2972
  buffer += char;
2676
2973
  }
2677
- } else {
2974
+ }
2975
+ // Characters inside quoted strings - add to buffer
2976
+ else {
2678
2977
  buffer += char;
2679
2978
  }
2680
2979
  }
2681
2980
 
2682
- if (buffer.replace(/^\s+|\s+$/g, "")) {
2683
- parts.push(buffer.replace(/^\s+|\s+$/g, ""));
2981
+ // Add any remaining content in buffer as the last part
2982
+ if (buffer.trim()) {
2983
+ parts.push(buffer.trim());
2684
2984
  }
2685
2985
 
2686
2986
  return parts;
@@ -2697,8 +2997,8 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2697
2997
  * @returns {boolean} Returns `true` if the selector is valid, otherwise `false`.
2698
2998
  */
2699
2999
 
2700
- // Cache to store validated selectors
2701
- var validatedSelectorsCache = new Map();
3000
+ // Cache to store validated selectors (ES5-compliant object)
3001
+ var validatedSelectorsCache = {};
2702
3002
 
2703
3003
  // Only pseudo-classes that accept selector lists should recurse
2704
3004
  var selectorListPseudoClasses = {
@@ -2709,8 +3009,8 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2709
3009
  };
2710
3010
 
2711
3011
  function validateSelector(selector) {
2712
- if (validatedSelectorsCache.has(selector)) {
2713
- return validatedSelectorsCache.get(selector);
3012
+ if (validatedSelectorsCache.hasOwnProperty(selector)) {
3013
+ return validatedSelectorsCache[selector];
2714
3014
  }
2715
3015
 
2716
3016
  // Use a non-global regex to find all pseudo-classes with arguments
@@ -2727,15 +3027,15 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2727
3027
  var nestedSelectors = parseAndSplitNestedSelectors(pseudoClassMatches[j][2]);
2728
3028
  for (var i = 0; i < nestedSelectors.length; i++) {
2729
3029
  var nestedSelector = nestedSelectors[i];
2730
- if (!validatedSelectorsCache.has(nestedSelector)) {
3030
+ if (!validatedSelectorsCache.hasOwnProperty(nestedSelector)) {
2731
3031
  var nestedSelectorValidation = validateSelector(nestedSelector);
2732
- validatedSelectorsCache.set(nestedSelector, nestedSelectorValidation);
3032
+ validatedSelectorsCache[nestedSelector] = nestedSelectorValidation;
2733
3033
  if (!nestedSelectorValidation) {
2734
- validatedSelectorsCache.set(selector, false);
3034
+ validatedSelectorsCache[selector] = false;
2735
3035
  return false;
2736
3036
  }
2737
- } else if (!validatedSelectorsCache.get(nestedSelector)) {
2738
- validatedSelectorsCache.set(selector, false);
3037
+ } else if (!validatedSelectorsCache[nestedSelector]) {
3038
+ validatedSelectorsCache[selector] = false;
2739
3039
  return false;
2740
3040
  }
2741
3041
  }
@@ -2743,7 +3043,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2743
3043
  }
2744
3044
 
2745
3045
  var basicSelectorValidation = basicSelectorValidator(selector);
2746
- validatedSelectorsCache.set(selector, basicSelectorValidation);
3046
+ validatedSelectorsCache[selector] = basicSelectorValidation;
2747
3047
 
2748
3048
  return basicSelectorValidation;
2749
3049
  }
@@ -2835,6 +3135,23 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2835
3135
  return true;
2836
3136
  }
2837
3137
 
3138
+ function parseError(message) {
3139
+ var lines = token.substring(0, i).split('\n');
3140
+ var lineCount = lines.length;
3141
+ var charCount = lines.pop().length + 1;
3142
+ var error = new Error(message + ' (line ' + lineCount + ', char ' + charCount + ')');
3143
+ error.line = lineCount;
3144
+ /* jshint sub : true */
3145
+ error['char'] = charCount;
3146
+ error.styleSheet = styleSheet;
3147
+ // Print the error but continue parsing the sheet
3148
+ try {
3149
+ throw error;
3150
+ } catch(e) {
3151
+ errorHandler && errorHandler(e);
3152
+ }
3153
+ };
3154
+
2838
3155
  var endingIndex = token.length - 1;
2839
3156
 
2840
3157
  for (var character; (character = token.charAt(i)); i++) {
@@ -2979,6 +3296,15 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2979
3296
  }, true);
2980
3297
  buffer = "";
2981
3298
  break;
3299
+ } else if (token.indexOf("@scope", i) === i) {
3300
+ validateAtRule("@scope", function(){
3301
+ state = "scopeBlock";
3302
+ scopeRule = new CSSOM.CSSScopeRule();
3303
+ scopeRule.__starts = i;
3304
+ i += "scope".length;
3305
+ });
3306
+ buffer = "";
3307
+ break;
2982
3308
  } else if (token.indexOf("@layer", i) === i) {
2983
3309
  validateAtRule("@layer", function(){
2984
3310
  state = "layerBlock"
@@ -3134,6 +3460,27 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3134
3460
  supportsRule.__parentStyleSheet = styleSheet;
3135
3461
  buffer = "";
3136
3462
  state = "before-selector";
3463
+ } else if (state === "scopeBlock") {
3464
+ var parsedScopePrelude = parseScopePrelude(buffer.trim());
3465
+
3466
+ if (parsedScopePrelude.hasStart) {
3467
+ scopeRule.__start = parsedScopePrelude.startSelector;
3468
+ }
3469
+ if (parsedScopePrelude.hasEnd) {
3470
+ scopeRule.__end = parsedScopePrelude.endSelector;
3471
+ }
3472
+ if (parsedScopePrelude.hasOnlyEnd) {
3473
+ scopeRule.__end = parsedScopePrelude.endSelector;
3474
+ }
3475
+
3476
+ if (parentRule) {
3477
+ scopeRule.__parentRule = parentRule;
3478
+ ancestorRules.push(parentRule);
3479
+ }
3480
+ currentScope = parentRule = scopeRule;
3481
+ scopeRule.__parentStyleSheet = styleSheet;
3482
+ buffer = "";
3483
+ state = "before-selector";
3137
3484
  } else if (state === "layerBlock") {
3138
3485
  layerBlockRule.name = buffer.trim();
3139
3486
 
@@ -3232,8 +3579,9 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3232
3579
  styleRule.selectorText = processedSelectorText;
3233
3580
  } else {
3234
3581
  styleRule.selectorText = parseAndSplitNestedSelectors(processedSelectorText).map(function(sel) {
3235
- return sel.indexOf('&') === -1 ? '& ' + sel : sel;
3236
- }).join(', ');
3582
+ // Add & at the beginning if there's no & in the selector, or if it starts with a combinator
3583
+ return (sel.indexOf('&') === -1 || startsWithCombinatorRegExp.test(sel)) ? '& ' + sel : sel;
3584
+ }).join(', ');
3237
3585
  }
3238
3586
  styleRule.style.__starts = i - buffer.length;
3239
3587
  styleRule.__parentRule = parentRule;
@@ -3469,6 +3817,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3469
3817
  || parentRule.constructor.name === "CSSMediaRule"
3470
3818
  || parentRule.constructor.name === "CSSSupportsRule"
3471
3819
  || parentRule.constructor.name === "CSSContainerRule"
3820
+ || parentRule.constructor.name === "CSSScopeRule"
3472
3821
  || parentRule.constructor.name === "CSSLayerBlockRule"
3473
3822
  || parentRule.constructor.name === "CSSStartingStyleRule"
3474
3823
  ) {
@@ -3538,6 +3887,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3538
3887
  || parentRule.constructor.name === "CSSMediaRule"
3539
3888
  || parentRule.constructor.name === "CSSSupportsRule"
3540
3889
  || parentRule.constructor.name === "CSSContainerRule"
3890
+ || parentRule.constructor.name === "CSSScopeRule"
3541
3891
  || parentRule.constructor.name === "CSSLayerBlockRule"
3542
3892
  || parentRule.constructor.name === "CSSStartingStyleRule"
3543
3893
  ) {