@acemir/cssom 0.9.18 → 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
@@ -301,6 +301,27 @@ CSSOM.CSSRule = function CSSRule() {
301
301
  this.__parentStyleSheet = null;
302
302
  };
303
303
 
304
+ CSSOM.CSSRule.UNKNOWN_RULE = 0; // obsolete
305
+ CSSOM.CSSRule.STYLE_RULE = 1;
306
+ CSSOM.CSSRule.CHARSET_RULE = 2; // obsolete
307
+ CSSOM.CSSRule.IMPORT_RULE = 3;
308
+ CSSOM.CSSRule.MEDIA_RULE = 4;
309
+ CSSOM.CSSRule.FONT_FACE_RULE = 5;
310
+ CSSOM.CSSRule.PAGE_RULE = 6;
311
+ CSSOM.CSSRule.KEYFRAMES_RULE = 7;
312
+ CSSOM.CSSRule.KEYFRAME_RULE = 8;
313
+ CSSOM.CSSRule.MARGIN_RULE = 9;
314
+ CSSOM.CSSRule.NAMESPACE_RULE = 10;
315
+ CSSOM.CSSRule.COUNTER_STYLE_RULE = 11;
316
+ CSSOM.CSSRule.SUPPORTS_RULE = 12;
317
+ CSSOM.CSSRule.DOCUMENT_RULE = 13;
318
+ CSSOM.CSSRule.FONT_FEATURE_VALUES_RULE = 14;
319
+ CSSOM.CSSRule.VIEWPORT_RULE = 15;
320
+ CSSOM.CSSRule.REGION_STYLE_RULE = 16;
321
+ CSSOM.CSSRule.CONTAINER_RULE = 17;
322
+ CSSOM.CSSRule.LAYER_BLOCK_RULE = 18;
323
+ CSSOM.CSSRule.STARTING_STYLE_RULE = 1002;
324
+
304
325
  Object.defineProperties(CSSOM.CSSRule.prototype, {
305
326
 
306
327
  constructor: { value: CSSOM.CSSRule },
@@ -337,7 +358,29 @@ Object.defineProperties(CSSOM.CSSRule.prototype, {
337
358
  CONTAINER_RULE: { value: 17, enumerable: true },
338
359
  LAYER_BLOCK_RULE: { value: 18, enumerable: true },
339
360
  STARTING_STYLE_RULE: { value: 1002, enumerable: true },
340
- })
361
+ });
362
+
363
+
364
+
365
+
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
+
341
384
 
342
385
 
343
386
 
@@ -387,7 +430,7 @@ Object.defineProperty(CSSOM.CSSNestedDeclarations.prototype, "cssText", {
387
430
  */
388
431
  CSSOM.CSSGroupingRule = function CSSGroupingRule() {
389
432
  CSSOM.CSSRule.call(this);
390
- this.cssRules = [];
433
+ this.cssRules = new CSSOM.CSSRuleList();
391
434
  };
392
435
 
393
436
  CSSOM.CSSGroupingRule.prototype = new CSSOM.CSSRule();
@@ -504,7 +547,6 @@ CSSOM.CSSCounterStyleRule.prototype.type = 11;
504
547
  */
505
548
  CSSOM.CSSConditionRule = function CSSConditionRule() {
506
549
  CSSOM.CSSGroupingRule.call(this);
507
- this.cssRules = [];
508
550
  };
509
551
 
510
552
  CSSOM.CSSConditionRule.prototype = new CSSOM.CSSGroupingRule();
@@ -1198,7 +1240,7 @@ Object.defineProperty(CSSOM.CSSFontFaceRule.prototype, "cssText", {
1198
1240
  */
1199
1241
  CSSOM.CSSHostRule = function CSSHostRule() {
1200
1242
  CSSOM.CSSRule.call(this);
1201
- this.cssRules = [];
1243
+ this.cssRules = new CSSOM.CSSRuleList();
1202
1244
  };
1203
1245
 
1204
1246
  CSSOM.CSSHostRule.prototype = new CSSOM.CSSRule();
@@ -1228,11 +1270,10 @@ Object.defineProperty(CSSOM.CSSHostRule.prototype, "cssText", {
1228
1270
  * @see http://www.w3.org/TR/shadow-dom/#host-at-rule
1229
1271
  */
1230
1272
  CSSOM.CSSStartingStyleRule = function CSSStartingStyleRule() {
1231
- CSSOM.CSSRule.call(this);
1232
- this.cssRules = [];
1273
+ CSSOM.CSSGroupingRule.call(this);
1233
1274
  };
1234
1275
 
1235
- CSSOM.CSSStartingStyleRule.prototype = new CSSOM.CSSRule();
1276
+ CSSOM.CSSStartingStyleRule.prototype = new CSSOM.CSSGroupingRule();
1236
1277
  CSSOM.CSSStartingStyleRule.prototype.constructor = CSSOM.CSSStartingStyleRule;
1237
1278
  CSSOM.CSSStartingStyleRule.prototype.type = 1002;
1238
1279
  //FIXME
@@ -1280,13 +1321,18 @@ Object.defineProperties(CSSOM.StyleSheet.prototype, {
1280
1321
  */
1281
1322
  CSSOM.CSSStyleSheet = function CSSStyleSheet() {
1282
1323
  CSSOM.StyleSheet.call(this);
1283
- this.cssRules = [];
1324
+ this.cssRules = new CSSOM.CSSRuleList();
1284
1325
  };
1285
1326
 
1286
1327
 
1287
1328
  CSSOM.CSSStyleSheet.prototype = new CSSOM.StyleSheet();
1288
1329
  CSSOM.CSSStyleSheet.prototype.constructor = CSSOM.CSSStyleSheet;
1289
1330
 
1331
+ Object.defineProperty(CSSOM.CSSStyleSheet.prototype, "rules", {
1332
+ get: function() {
1333
+ return this.cssRules;
1334
+ }
1335
+ });
1290
1336
 
1291
1337
  /**
1292
1338
  * Used to insert a new rule into the style sheet. The new rule now becomes part of the cascade.
@@ -1417,6 +1463,13 @@ CSSOM.CSSStyleSheet.prototype.insertRule = function(rule, index) {
1417
1463
  return index;
1418
1464
  };
1419
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
+ };
1420
1473
 
1421
1474
  /**
1422
1475
  * Used to delete a rule from the style sheet.
@@ -1453,6 +1506,9 @@ CSSOM.CSSStyleSheet.prototype.deleteRule = function(index) {
1453
1506
  this.cssRules.splice(index, 1);
1454
1507
  };
1455
1508
 
1509
+ CSSOM.CSSStyleSheet.prototype.removeRule = function(index) {
1510
+ this.deleteRule(index);
1511
+ };
1456
1512
 
1457
1513
  /**
1458
1514
  * NON-STANDARD
@@ -1479,7 +1535,7 @@ CSSOM.CSSStyleSheet.prototype.toString = function() {
1479
1535
  CSSOM.CSSKeyframesRule = function CSSKeyframesRule() {
1480
1536
  CSSOM.CSSRule.call(this);
1481
1537
  this.name = '';
1482
- this.cssRules = [];
1538
+ this.cssRules = new CSSOM.CSSRuleList();
1483
1539
 
1484
1540
  // Set up initial indexed access
1485
1541
  this._setupIndexedAccess();
@@ -1805,7 +1861,7 @@ CSSOM.MatcherList.prototype = {
1805
1861
  CSSOM.CSSDocumentRule = function CSSDocumentRule() {
1806
1862
  CSSOM.CSSRule.call(this);
1807
1863
  this.matcher = new CSSOM.MatcherList();
1808
- this.cssRules = [];
1864
+ this.cssRules = new CSSOM.CSSRuleList();
1809
1865
  };
1810
1866
 
1811
1867
  CSSOM.CSSDocumentRule.prototype = new CSSOM.CSSRule();
@@ -2206,6 +2262,51 @@ CSSOM.CSSValueExpression.prototype._findMatchedIdx = function(token, idx, sep) {
2206
2262
 
2207
2263
 
2208
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
+
2209
2310
  /**
2210
2311
  * @constructor
2211
2312
  * @see https://drafts.csswg.org/css-cascade-5/#csslayerblockrule
@@ -2213,7 +2314,6 @@ CSSOM.CSSValueExpression.prototype._findMatchedIdx = function(token, idx, sep) {
2213
2314
  CSSOM.CSSLayerBlockRule = function CSSLayerBlockRule() {
2214
2315
  CSSOM.CSSGroupingRule.call(this);
2215
2316
  this.name = "";
2216
- this.cssRules = [];
2217
2317
  };
2218
2318
 
2219
2319
  CSSOM.CSSLayerBlockRule.prototype = new CSSOM.CSSGroupingRule();
@@ -2311,6 +2411,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2311
2411
  "conditionBlock": true,
2312
2412
  "counterStyleBlock": true,
2313
2413
  'documentRule-begin': true,
2414
+ "scopeBlock": true,
2314
2415
  "layerBlock": true
2315
2416
  };
2316
2417
 
@@ -2329,7 +2430,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2329
2430
  var ancestorRules = [];
2330
2431
  var prevScope;
2331
2432
 
2332
- 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;
2333
2434
 
2334
2435
  // Track defined namespace prefixes for validation
2335
2436
  var definedNamespacePrefixes = {};
@@ -2339,11 +2440,12 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2339
2440
  // var atRulesStatemenRegExp = /(?<!{.*)[;}]\s*/; // Match a statement by verifying it finds a semicolon or closing brace not followed by another semicolon or closing brace
2340
2441
  var beforeRulePortionRegExp = /{(?!.*{)|}(?!.*})|;(?!.*;)|\*\/(?!.*\*\/)/g; // Match the closest allowed character (a opening or closing brace, a semicolon or a comment ending) before the rule
2341
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
2342
- 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
2343
2444
  var forwardImportRuleValidationRegExp = /(?:\s|\/\*|'|")/; // Match that the rule is followed by any whitespace, an opening comment, a single quote or double quote
2344
2445
  var forwardRuleClosingBraceRegExp = /{[^{}]*}|}/; // Finds the next closing brace of a rule block
2345
2446
  var forwardRuleSemicolonAndOpeningBraceRegExp = /^.*?({|;)/; // Finds the next semicolon or opening brace after the at-rule
2346
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 (>, +, ~)
2347
2449
 
2348
2450
  /**
2349
2451
  * Searches for the first occurrence of a CSS at-rule statement terminator (`;` or `}`)
@@ -2435,24 +2537,214 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2435
2537
  return i;
2436
2538
  }
2437
2539
 
2438
- var parseError = function(message) {
2439
- var lines = token.substring(0, i).split('\n');
2440
- var lineCount = lines.length;
2441
- var charCount = lines.pop().length + 1;
2442
- var error = new Error(message + ' (line ' + lineCount + ', char ' + charCount + ')');
2443
- error.line = lineCount;
2444
- /* jshint sub : true */
2445
- error['char'] = charCount;
2446
- error.styleSheet = styleSheet;
2447
- // Print the error but continue parsing the sheet
2448
- try {
2449
- throw error;
2450
- } catch(e) {
2451
- 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
+ }
2452
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;
2678
+ }
2679
+ };
2680
+
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);
2453
2688
  };
2454
2689
 
2455
- var validateAtRule = function(atRuleKey, validCallback, cannotBeNested) {
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) {
2456
2748
  var isValid = false;
2457
2749
  var sourceRuleRegExp = atRuleKey === "@import" ? forwardImportRuleValidationRegExp : forwardRuleValidationRegExp;
2458
2750
  var ruleRegExp = new RegExp(atRuleKey + sourceRuleRegExp.source, sourceRuleRegExp.flags);
@@ -2473,6 +2765,56 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2473
2765
  isValid = true;
2474
2766
  }
2475
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
+
2476
2818
  if (!isValid) {
2477
2819
  // If it's invalid the browser will simply ignore the entire invalid block
2478
2820
  // Use regex to find the closing brace of the invalid rule
@@ -2531,52 +2873,23 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2531
2873
  * @returns {boolean}
2532
2874
  */
2533
2875
  function basicSelectorValidator(selector) {
2534
- var length = selector.length;
2535
- var i = 0;
2536
- var stack = [];
2537
- var inAttr = false;
2538
- var inSingleQuote = false;
2539
- 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
+ }
2540
2880
 
2541
- while (i < length) {
2542
- var char = selector[i];
2881
+ // Check for invalid combinator patterns
2882
+ if (hasInvalidCombinators(selector)) {
2883
+ return false;
2884
+ }
2543
2885
 
2544
- if (inSingleQuote) {
2545
- if (char === "'" && selector[i - 1] !== "\\") {
2546
- inSingleQuote = false;
2547
- }
2548
- } else if (inDoubleQuote) {
2549
- if (char === '"' && selector[i - 1] !== "\\") {
2550
- inDoubleQuote = false;
2551
- }
2552
- } else if (inAttr) {
2553
- if (char === "]") {
2554
- inAttr = false;
2555
- } else if (char === "'") {
2556
- inSingleQuote = true;
2557
- } else if (char === '"') {
2558
- inDoubleQuote = true;
2559
- }
2560
- } else {
2561
- if (char === "[") {
2562
- inAttr = true;
2563
- } else if (char === "'") {
2564
- inSingleQuote = true;
2565
- } else if (char === '"') {
2566
- inDoubleQuote = true;
2567
- } else if (char === "(") {
2568
- stack.push("(");
2569
- } else if (char === ")") {
2570
- if (!stack.length || stack.pop() !== "(") {
2571
- return false;
2572
- }
2573
- }
2574
- }
2575
- i++;
2886
+ // Check for invalid pseudo-like syntax
2887
+ if (hasInvalidPseudoSyntax(selector)) {
2888
+ return false;
2576
2889
  }
2577
2890
 
2578
- // If any stack or quote/attr context remains, it's invalid
2579
- if (stack.length || inAttr || inSingleQuote || inDoubleQuote) {
2891
+ // Check for invalid nesting selector (&) usage
2892
+ if (hasInvalidNestingSelector(selector)) {
2580
2893
  return false;
2581
2894
  }
2582
2895
 
@@ -2587,7 +2900,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2587
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+)+$/;
2588
2901
  return looseSelectorRegExp.test(selector);
2589
2902
  }
2590
-
2903
+
2591
2904
  /**
2592
2905
  * Regular expression to match CSS pseudo-classes with arguments.
2593
2906
  *
@@ -2618,48 +2931,56 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2618
2931
  * @returns {string[]} An array of selector parts, split by top-level commas, with whitespace trimmed.
2619
2932
  */
2620
2933
  function parseAndSplitNestedSelectors(selector) {
2621
- var depth = 0;
2622
- var buffer = "";
2623
- var parts = [];
2624
- var inSingleQuote = false;
2625
- 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
2626
2939
  var i, char;
2627
2940
 
2628
2941
  for (i = 0; i < selector.length; i++) {
2629
2942
  char = selector.charAt(i);
2630
2943
 
2944
+ // Handle single quote strings
2631
2945
  if (char === "'" && !inDoubleQuote) {
2632
2946
  inSingleQuote = !inSingleQuote;
2633
2947
  buffer += char;
2634
- } else if (char === '"' && !inSingleQuote) {
2948
+ }
2949
+ // Handle double quote strings
2950
+ else if (char === '"' && !inSingleQuote) {
2635
2951
  inDoubleQuote = !inDoubleQuote;
2636
2952
  buffer += char;
2637
- } else if (!inSingleQuote && !inDoubleQuote) {
2953
+ }
2954
+ // Process characters outside of quoted strings
2955
+ else if (!inSingleQuote && !inDoubleQuote) {
2638
2956
  if (char === '(') {
2957
+ // Entering a nested level (e.g., :is(...))
2639
2958
  depth++;
2640
2959
  buffer += char;
2641
2960
  } else if (char === ')') {
2961
+ // Exiting a nested level
2642
2962
  depth--;
2643
2963
  buffer += char;
2644
- if (depth === 0) {
2645
- parts.push(buffer.replace(/^\s+|\s+$/g, ""));
2646
- buffer = "";
2647
- }
2648
2964
  } else if (char === ',' && depth === 0) {
2649
- if (buffer.replace(/^\s+|\s+$/g, "")) {
2650
- 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());
2651
2968
  }
2652
2969
  buffer = "";
2653
2970
  } else {
2971
+ // Regular character - add to buffer
2654
2972
  buffer += char;
2655
2973
  }
2656
- } else {
2974
+ }
2975
+ // Characters inside quoted strings - add to buffer
2976
+ else {
2657
2977
  buffer += char;
2658
2978
  }
2659
2979
  }
2660
2980
 
2661
- if (buffer.replace(/^\s+|\s+$/g, "")) {
2662
- 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());
2663
2984
  }
2664
2985
 
2665
2986
  return parts;
@@ -2676,8 +2997,8 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2676
2997
  * @returns {boolean} Returns `true` if the selector is valid, otherwise `false`.
2677
2998
  */
2678
2999
 
2679
- // Cache to store validated selectors
2680
- var validatedSelectorsCache = new Map();
3000
+ // Cache to store validated selectors (ES5-compliant object)
3001
+ var validatedSelectorsCache = {};
2681
3002
 
2682
3003
  // Only pseudo-classes that accept selector lists should recurse
2683
3004
  var selectorListPseudoClasses = {
@@ -2688,8 +3009,8 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2688
3009
  };
2689
3010
 
2690
3011
  function validateSelector(selector) {
2691
- if (validatedSelectorsCache.has(selector)) {
2692
- return validatedSelectorsCache.get(selector);
3012
+ if (validatedSelectorsCache.hasOwnProperty(selector)) {
3013
+ return validatedSelectorsCache[selector];
2693
3014
  }
2694
3015
 
2695
3016
  // Use a non-global regex to find all pseudo-classes with arguments
@@ -2706,15 +3027,15 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2706
3027
  var nestedSelectors = parseAndSplitNestedSelectors(pseudoClassMatches[j][2]);
2707
3028
  for (var i = 0; i < nestedSelectors.length; i++) {
2708
3029
  var nestedSelector = nestedSelectors[i];
2709
- if (!validatedSelectorsCache.has(nestedSelector)) {
3030
+ if (!validatedSelectorsCache.hasOwnProperty(nestedSelector)) {
2710
3031
  var nestedSelectorValidation = validateSelector(nestedSelector);
2711
- validatedSelectorsCache.set(nestedSelector, nestedSelectorValidation);
3032
+ validatedSelectorsCache[nestedSelector] = nestedSelectorValidation;
2712
3033
  if (!nestedSelectorValidation) {
2713
- validatedSelectorsCache.set(selector, false);
3034
+ validatedSelectorsCache[selector] = false;
2714
3035
  return false;
2715
3036
  }
2716
- } else if (!validatedSelectorsCache.get(nestedSelector)) {
2717
- validatedSelectorsCache.set(selector, false);
3037
+ } else if (!validatedSelectorsCache[nestedSelector]) {
3038
+ validatedSelectorsCache[selector] = false;
2718
3039
  return false;
2719
3040
  }
2720
3041
  }
@@ -2722,7 +3043,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2722
3043
  }
2723
3044
 
2724
3045
  var basicSelectorValidation = basicSelectorValidator(selector);
2725
- validatedSelectorsCache.set(selector, basicSelectorValidation);
3046
+ validatedSelectorsCache[selector] = basicSelectorValidation;
2726
3047
 
2727
3048
  return basicSelectorValidation;
2728
3049
  }
@@ -2814,6 +3135,23 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2814
3135
  return true;
2815
3136
  }
2816
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
+
2817
3155
  var endingIndex = token.length - 1;
2818
3156
 
2819
3157
  for (var character; (character = token.charAt(i)); i++) {
@@ -2958,6 +3296,15 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2958
3296
  }, true);
2959
3297
  buffer = "";
2960
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;
2961
3308
  } else if (token.indexOf("@layer", i) === i) {
2962
3309
  validateAtRule("@layer", function(){
2963
3310
  state = "layerBlock"
@@ -3113,6 +3460,27 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3113
3460
  supportsRule.__parentStyleSheet = styleSheet;
3114
3461
  buffer = "";
3115
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";
3116
3484
  } else if (state === "layerBlock") {
3117
3485
  layerBlockRule.name = buffer.trim();
3118
3486
 
@@ -3211,8 +3579,9 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3211
3579
  styleRule.selectorText = processedSelectorText;
3212
3580
  } else {
3213
3581
  styleRule.selectorText = parseAndSplitNestedSelectors(processedSelectorText).map(function(sel) {
3214
- return sel.indexOf('&') === -1 ? '& ' + sel : sel;
3215
- }).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(', ');
3216
3585
  }
3217
3586
  styleRule.style.__starts = i - buffer.length;
3218
3587
  styleRule.__parentRule = parentRule;
@@ -3448,6 +3817,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3448
3817
  || parentRule.constructor.name === "CSSMediaRule"
3449
3818
  || parentRule.constructor.name === "CSSSupportsRule"
3450
3819
  || parentRule.constructor.name === "CSSContainerRule"
3820
+ || parentRule.constructor.name === "CSSScopeRule"
3451
3821
  || parentRule.constructor.name === "CSSLayerBlockRule"
3452
3822
  || parentRule.constructor.name === "CSSStartingStyleRule"
3453
3823
  ) {
@@ -3517,6 +3887,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3517
3887
  || parentRule.constructor.name === "CSSMediaRule"
3518
3888
  || parentRule.constructor.name === "CSSSupportsRule"
3519
3889
  || parentRule.constructor.name === "CSSContainerRule"
3890
+ || parentRule.constructor.name === "CSSScopeRule"
3520
3891
  || parentRule.constructor.name === "CSSLayerBlockRule"
3521
3892
  || parentRule.constructor.name === "CSSStartingStyleRule"
3522
3893
  ) {