@acemir/cssom 0.9.19 → 0.9.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/parse.js CHANGED
@@ -50,7 +50,9 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
50
50
  "conditionBlock": true,
51
51
  "counterStyleBlock": true,
52
52
  'documentRule-begin': true,
53
- "layerBlock": true
53
+ "scopeBlock": true,
54
+ "layerBlock": true,
55
+ "pageBlock": true
54
56
  };
55
57
 
56
58
  var styleSheet = new CSSOM.CSSStyleSheet();
@@ -68,7 +70,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
68
70
  var ancestorRules = [];
69
71
  var prevScope;
70
72
 
71
- var name, priority="", styleRule, mediaRule, containerRule, counterStyleRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule, startingStyleRule, layerBlockRule, layerStatementRule, nestedSelectorRule, namespaceRule;
73
+ var name, priority="", styleRule, mediaRule, containerRule, counterStyleRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule, startingStyleRule, scopeRule, pageRule, layerBlockRule, layerStatementRule, nestedSelectorRule, namespaceRule;
72
74
 
73
75
  // Track defined namespace prefixes for validation
74
76
  var definedNamespacePrefixes = {};
@@ -78,11 +80,13 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
78
80
  // var atRulesStatemenRegExp = /(?<!{.*)[;}]\s*/; // Match a statement by verifying it finds a semicolon or closing brace not followed by another semicolon or closing brace
79
81
  var beforeRulePortionRegExp = /{(?!.*{)|}(?!.*})|;(?!.*;)|\*\/(?!.*\*\/)/g; // Match the closest allowed character (a opening or closing brace, a semicolon or a comment ending) before the rule
80
82
  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
81
- var forwardRuleValidationRegExp = /(?:\(|\s|\/\*)/; // Match that the rule is followed by any whitespace, a opening comment or a condition opening parenthesis
83
+ var forwardRuleValidationRegExp = /(?:\s|\/\*|\{|\()/; // Match that the rule is followed by any whitespace, a opening comment, a condition opening parenthesis or a opening brace
82
84
  var forwardImportRuleValidationRegExp = /(?:\s|\/\*|'|")/; // Match that the rule is followed by any whitespace, an opening comment, a single quote or double quote
83
85
  var forwardRuleClosingBraceRegExp = /{[^{}]*}|}/; // Finds the next closing brace of a rule block
84
86
  var forwardRuleSemicolonAndOpeningBraceRegExp = /^.*?({|;)/; // Finds the next semicolon or opening brace after the at-rule
85
- var layerRuleNameRegExp = /^(-?[_a-zA-Z]+(\.[_a-zA-Z]+)*[_a-zA-Z0-9-]*)$/; // Validates a single @layer name
87
+ var cssCustomIdentifierRegExp = /^(-?[_a-zA-Z]+(\.[_a-zA-Z]+)*[_a-zA-Z0-9-]*)$/; // Validates a css custom identifier
88
+ var startsWithCombinatorRegExp = /^\s*[>+~]/; // Checks if a selector starts with a CSS combinator (>, +, ~)
89
+ var atPageRuleSelectorRegExp = /^([^\s:]+)?((?::\w+)*)$/;
86
90
 
87
91
  /**
88
92
  * Searches for the first occurrence of a CSS at-rule statement terminator (`;` or `}`)
@@ -174,24 +178,214 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
174
178
  return i;
175
179
  }
176
180
 
177
- var parseError = function(message) {
178
- var lines = token.substring(0, i).split('\n');
179
- var lineCount = lines.length;
180
- var charCount = lines.pop().length + 1;
181
- var error = new Error(message + ' (line ' + lineCount + ', char ' + charCount + ')');
182
- error.line = lineCount;
183
- /* jshint sub : true */
184
- error['char'] = charCount;
185
- error.styleSheet = styleSheet;
186
- // Print the error but continue parsing the sheet
187
- try {
188
- throw error;
189
- } catch(e) {
190
- errorHandler && errorHandler(e);
181
+ /**
182
+ * Parses the scope prelude and extracts start and end selectors.
183
+ * @param {string} preludeContent - The scope prelude content (without @scope keyword)
184
+ * @returns {object} Object with startSelector and endSelector properties
185
+ */
186
+ function parseScopePrelude(preludeContent) {
187
+ var parts = preludeContent.split(/\s*\)\s*to\s+\(/);
188
+
189
+ // Restore the parentheses that were consumed by the split
190
+ if (parts.length === 2) {
191
+ parts[0] = parts[0] + ')';
192
+ parts[1] = '(' + parts[1];
193
+ }
194
+
195
+ var hasStart = parts[0] &&
196
+ parts[0].charAt(0) === '(' &&
197
+ parts[0].charAt(parts[0].length - 1) === ')';
198
+ var hasEnd = parts[1] &&
199
+ parts[1].charAt(0) === '(' &&
200
+ parts[1].charAt(parts[1].length - 1) === ')';
201
+
202
+ // Handle case: @scope to (<end>)
203
+ var hasOnlyEnd = !hasStart &&
204
+ !hasEnd &&
205
+ parts[0].indexOf('to (') === 0 &&
206
+ parts[0].charAt(parts[0].length - 1) === ')';
207
+
208
+ var startSelector = '';
209
+ var endSelector = '';
210
+
211
+ if (hasStart) {
212
+ startSelector = parts[0].slice(1, -1).trim();
213
+ }
214
+ if (hasEnd) {
215
+ endSelector = parts[1].slice(1, -1).trim();
216
+ }
217
+ if (hasOnlyEnd) {
218
+ endSelector = parts[0].slice(4, -1).trim();
219
+ }
220
+
221
+ return {
222
+ startSelector: startSelector,
223
+ endSelector: endSelector,
224
+ hasStart: hasStart,
225
+ hasEnd: hasEnd,
226
+ hasOnlyEnd: hasOnlyEnd
227
+ };
228
+ };
229
+
230
+ /**
231
+ * Checks if a selector contains pseudo-elements.
232
+ * @param {string} selector - The CSS selector to check
233
+ * @returns {boolean} True if the selector contains pseudo-elements
234
+ */
235
+ function hasPseudoElement(selector) {
236
+ // Match only double-colon (::) pseudo-elements
237
+ // Also match legacy single-colon pseudo-elements: :before, :after, :first-line, :first-letter
238
+ // These must NOT be followed by alphanumeric characters (to avoid matching :before-x or similar)
239
+ var pseudoElementRegex = /::[a-zA-Z][\w-]*|:(before|after|first-line|first-letter)(?![a-zA-Z0-9_-])/;
240
+ return pseudoElementRegex.test(selector);
241
+ };
242
+
243
+ /**
244
+ * Validates balanced parentheses, brackets, and quotes in a selector.
245
+ *
246
+ * @param {string} selector - The CSS selector to validate
247
+ * @param {boolean} trackAttributes - Whether to track attribute selector context
248
+ * @param {boolean} useStack - Whether to use a stack for parentheses (needed for nested validation)
249
+ * @returns {boolean} True if the syntax is valid (all brackets, parentheses, and quotes are balanced)
250
+ */
251
+ function validateBalancedSyntax(selector, trackAttributes, useStack) {
252
+ var parenDepth = 0;
253
+ var bracketDepth = 0;
254
+ var inSingleQuote = false;
255
+ var inDoubleQuote = false;
256
+ var inAttr = false;
257
+ var stack = useStack ? [] : null;
258
+
259
+ for (var i = 0; i < selector.length; i++) {
260
+ var char = selector[i];
261
+ var prevChar = i > 0 ? selector[i - 1] : '';
262
+
263
+ if (inSingleQuote) {
264
+ if (char === "'" && prevChar !== "\\") {
265
+ inSingleQuote = false;
266
+ }
267
+ } else if (inDoubleQuote) {
268
+ if (char === '"' && prevChar !== "\\") {
269
+ inDoubleQuote = false;
270
+ }
271
+ } else if (trackAttributes && inAttr) {
272
+ if (char === "]") {
273
+ inAttr = false;
274
+ } else if (char === "'") {
275
+ inSingleQuote = true;
276
+ } else if (char === '"') {
277
+ inDoubleQuote = true;
278
+ }
279
+ } else {
280
+ if (trackAttributes && char === "[") {
281
+ inAttr = true;
282
+ } else if (char === "'") {
283
+ inSingleQuote = true;
284
+ } else if (char === '"') {
285
+ inDoubleQuote = true;
286
+ } else if (char === '(') {
287
+ if (useStack) {
288
+ stack.push("(");
289
+ } else {
290
+ parenDepth++;
291
+ }
292
+ } else if (char === ')') {
293
+ if (useStack) {
294
+ if (!stack.length || stack.pop() !== "(") {
295
+ return false;
296
+ }
297
+ } else {
298
+ parenDepth--;
299
+ if (parenDepth < 0) {
300
+ return false;
301
+ }
302
+ }
303
+ } else if (char === '[') {
304
+ bracketDepth++;
305
+ } else if (char === ']') {
306
+ bracketDepth--;
307
+ if (bracketDepth < 0) {
308
+ return false;
309
+ }
310
+ }
311
+ }
191
312
  }
313
+
314
+ // Check if everything is balanced
315
+ if (useStack) {
316
+ return stack.length === 0 && bracketDepth === 0 && !inSingleQuote && !inDoubleQuote && !inAttr;
317
+ } else {
318
+ return parenDepth === 0 && bracketDepth === 0 && !inSingleQuote && !inDoubleQuote;
319
+ }
320
+ };
321
+
322
+ /**
323
+ * Checks for basic syntax errors in selectors (mismatched parentheses, brackets, quotes).
324
+ * @param {string} selector - The CSS selector to check
325
+ * @returns {boolean} True if there are syntax errors
326
+ */
327
+ function hasBasicSyntaxError(selector) {
328
+ return !validateBalancedSyntax(selector, false, false);
192
329
  };
193
330
 
194
- var validateAtRule = function(atRuleKey, validCallback, cannotBeNested) {
331
+ /**
332
+ * Checks for invalid combinator patterns in selectors.
333
+ * @param {string} selector - The CSS selector to check
334
+ * @returns {boolean} True if the selector contains invalid combinators
335
+ */
336
+ function hasInvalidCombinators(selector) {
337
+ // Check for invalid combinator patterns:
338
+ // - <> (not a valid combinator)
339
+ // - >> (deep descendant combinator, deprecated and invalid)
340
+ // - Multiple consecutive combinators like >>, >~, etc.
341
+ if (/<>/.test(selector)) return true;
342
+ if (/>>/.test(selector)) return true;
343
+ // Check for other invalid consecutive combinator patterns
344
+ if (/[>+~]\s*[>+~]/.test(selector)) return true;
345
+ return false;
346
+ };
347
+
348
+ /**
349
+ * Checks for invalid pseudo-like syntax (function calls without proper pseudo prefix).
350
+ * @param {string} selector - The CSS selector to check
351
+ * @returns {boolean} True if the selector contains invalid pseudo-like syntax
352
+ */
353
+ function hasInvalidPseudoSyntax(selector) {
354
+ // Check for specific known pseudo-elements used without : or :: prefix
355
+ // Examples: slotted(div), part(name), cue(selector)
356
+ // These are ONLY valid as ::slotted(), ::part(), ::cue()
357
+ var invalidPatterns = [
358
+ /(?:^|[\s>+~,\[])slotted\s*\(/i,
359
+ /(?:^|[\s>+~,\[])part\s*\(/i,
360
+ /(?:^|[\s>+~,\[])cue\s*\(/i,
361
+ /(?:^|[\s>+~,\[])cue-region\s*\(/i
362
+ ];
363
+
364
+ for (var i = 0; i < invalidPatterns.length; i++) {
365
+ if (invalidPatterns[i].test(selector)) {
366
+ return true;
367
+ }
368
+ }
369
+ return false;
370
+ };
371
+
372
+ /**
373
+ * Checks for invalid nesting selector (&) usage.
374
+ * The & selector cannot be directly followed by a type selector without a delimiter.
375
+ * Valid: &.class, &#id, &[attr], &:hover, &::before, & div, &>div
376
+ * Invalid: &div, &span
377
+ * @param {string} selector - The CSS selector to check
378
+ * @returns {boolean} True if the selector contains invalid & usage
379
+ */
380
+ function hasInvalidNestingSelector(selector) {
381
+ // Check for & followed directly by a letter (type selector) without any delimiter
382
+ // This regex matches & followed by a letter (start of type selector) that's not preceded by an escape
383
+ // We need to exclude valid cases like &.class, &#id, &[attr], &:pseudo, &::pseudo, & (with space), &>
384
+ var invalidNestingPattern = /&(?![.\#\[:>\+~\s])[a-zA-Z]/;
385
+ return invalidNestingPattern.test(selector);
386
+ };
387
+
388
+ function validateAtRule(atRuleKey, validCallback, cannotBeNested) {
195
389
  var isValid = false;
196
390
  var sourceRuleRegExp = atRuleKey === "@import" ? forwardImportRuleValidationRegExp : forwardRuleValidationRegExp;
197
391
  var ruleRegExp = new RegExp(atRuleKey + sourceRuleRegExp.source, sourceRuleRegExp.flags);
@@ -212,6 +406,114 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
212
406
  isValid = true;
213
407
  }
214
408
  }
409
+
410
+ // Additional validation for @scope rule
411
+ if (isValid && atRuleKey === "@scope") {
412
+ var openBraceIndex = ruleSlice.indexOf('{');
413
+ if (openBraceIndex !== -1) {
414
+ // Extract the rule prelude (everything between the at-rule and {)
415
+ var rulePrelude = ruleSlice.slice(0, openBraceIndex).trim();
416
+
417
+ // Skip past at-rule keyword and whitespace
418
+ var preludeContent = rulePrelude.slice("@scope".length).trim();
419
+
420
+ if (preludeContent.length > 0) {
421
+ // Parse the scope prelude
422
+ var parsedScopePrelude = parseScopePrelude(preludeContent);
423
+ var startSelector = parsedScopePrelude.startSelector;
424
+ var endSelector = parsedScopePrelude.endSelector;
425
+ var hasStart = parsedScopePrelude.hasStart;
426
+ var hasEnd = parsedScopePrelude.hasEnd;
427
+ var hasOnlyEnd = parsedScopePrelude.hasOnlyEnd;
428
+
429
+ // Validation rules for @scope:
430
+ // 1. Empty selectors in parentheses are invalid: @scope () {} or @scope (.a) to () {}
431
+ if ((hasStart && startSelector === '') || (hasEnd && endSelector === '') || (hasOnlyEnd && endSelector === '')) {
432
+ isValid = false;
433
+ }
434
+ // 2. Pseudo-elements are invalid in scope selectors
435
+ else if ((startSelector && hasPseudoElement(startSelector)) || (endSelector && hasPseudoElement(endSelector))) {
436
+ isValid = false;
437
+ }
438
+ // 3. Basic syntax errors (mismatched parens, brackets, quotes)
439
+ else if ((startSelector && hasBasicSyntaxError(startSelector)) || (endSelector && hasBasicSyntaxError(endSelector))) {
440
+ isValid = false;
441
+ }
442
+ // 4. Invalid combinator patterns
443
+ else if ((startSelector && hasInvalidCombinators(startSelector)) || (endSelector && hasInvalidCombinators(endSelector))) {
444
+ isValid = false;
445
+ }
446
+ // 5. Invalid pseudo-like syntax (function without : or :: prefix)
447
+ else if ((startSelector && hasInvalidPseudoSyntax(startSelector)) || (endSelector && hasInvalidPseudoSyntax(endSelector))) {
448
+ isValid = false;
449
+ }
450
+ // 6. Invalid structure (no proper parentheses found when prelude is not empty)
451
+ else if (!hasStart && !hasOnlyEnd) {
452
+ isValid = false;
453
+ }
454
+ }
455
+ // Empty prelude (@scope {}) is valid
456
+ }
457
+ }
458
+
459
+ if (isValid && atRuleKey === "@page") {
460
+ var openBraceIndex = ruleSlice.indexOf('{');
461
+ if (openBraceIndex !== -1) {
462
+ // Extract the rule prelude (everything between the at-rule and {)
463
+ var rulePrelude = ruleSlice.slice(0, openBraceIndex).trim();
464
+
465
+ // Skip past at-rule keyword and whitespace
466
+ var preludeContent = rulePrelude.slice("@page".length).trim();
467
+
468
+ if (preludeContent.length > 0) {
469
+ var trimmedValue = preludeContent.trim();
470
+
471
+ // Empty selector is valid for @page
472
+ if (trimmedValue !== '') {
473
+ // Parse @page selectorText for page name and pseudo-pages
474
+ // Valid formats:
475
+ // - (empty - no name, no pseudo-page)
476
+ // - :left, :right, :first, :blank (pseudo-page only)
477
+ // - named (named page only)
478
+ // - named:first (named page with single pseudo-page)
479
+ // - named:first:left (named page with multiple pseudo-pages)
480
+ var match = trimmedValue.match(atPageRuleSelectorRegExp);
481
+ if (match) {
482
+ var pageName = match[1] || '';
483
+ var pseudoPages = match[2] || '';
484
+
485
+ // Validate page name if present
486
+ if (pageName) {
487
+ if (!cssCustomIdentifierRegExp.test(pageName)) {
488
+ isValid = false;
489
+ }
490
+ }
491
+
492
+ // Validate pseudo-pages if present
493
+ if (pseudoPages) {
494
+ var pseudos = pseudoPages.split(':').filter(function(p) { return p; });
495
+ var validPseudos = ['left', 'right', 'first', 'blank'];
496
+ var allValid = true;
497
+ for (var j = 0; j < pseudos.length; j++) {
498
+ if (validPseudos.indexOf(pseudos[j].toLowerCase()) === -1) {
499
+ allValid = false;
500
+ break;
501
+ }
502
+ }
503
+
504
+ if (!allValid) {
505
+ isValid = false;
506
+ }
507
+ }
508
+ } else {
509
+ isValid = false;
510
+ }
511
+ }
512
+
513
+ }
514
+ }
515
+ }
516
+
215
517
  if (!isValid) {
216
518
  // If it's invalid the browser will simply ignore the entire invalid block
217
519
  // Use regex to find the closing brace of the invalid rule
@@ -270,55 +572,47 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
270
572
  * @returns {boolean}
271
573
  */
272
574
  function basicSelectorValidator(selector) {
273
- var length = selector.length;
274
- var i = 0;
275
- var stack = [];
276
- var inAttr = false;
277
- var inSingleQuote = false;
278
- var inDoubleQuote = false;
575
+ // Validate balanced syntax with attribute tracking and stack-based parentheses matching
576
+ if (!validateBalancedSyntax(selector, true, true)) {
577
+ return false;
578
+ }
279
579
 
280
- while (i < length) {
281
- var char = selector[i];
580
+ // Check for invalid combinator patterns
581
+ if (hasInvalidCombinators(selector)) {
582
+ return false;
583
+ }
282
584
 
283
- if (inSingleQuote) {
284
- if (char === "'" && selector[i - 1] !== "\\") {
285
- inSingleQuote = false;
286
- }
287
- } else if (inDoubleQuote) {
288
- if (char === '"' && selector[i - 1] !== "\\") {
289
- inDoubleQuote = false;
290
- }
291
- } else if (inAttr) {
292
- if (char === "]") {
293
- inAttr = false;
294
- } else if (char === "'") {
295
- inSingleQuote = true;
296
- } else if (char === '"') {
297
- inDoubleQuote = true;
298
- }
299
- } else {
300
- if (char === "[") {
301
- inAttr = true;
302
- } else if (char === "'") {
303
- inSingleQuote = true;
304
- } else if (char === '"') {
305
- inDoubleQuote = true;
306
- } else if (char === "(") {
307
- stack.push("(");
308
- } else if (char === ")") {
309
- if (!stack.length || stack.pop() !== "(") {
310
- return false;
311
- }
312
- }
313
- }
314
- i++;
585
+ // Check for invalid pseudo-like syntax
586
+ if (hasInvalidPseudoSyntax(selector)) {
587
+ return false;
315
588
  }
316
589
 
317
- // If any stack or quote/attr context remains, it's invalid
318
- if (stack.length || inAttr || inSingleQuote || inDoubleQuote) {
590
+ // Check for invalid nesting selector (&) usage
591
+ if (hasInvalidNestingSelector(selector)) {
319
592
  return false;
320
593
  }
321
594
 
595
+ // Check for invalid pseudo-class usage with quoted strings
596
+ // Pseudo-classes like :lang(), :dir(), :nth-*() should not accept quoted strings
597
+ var pseudoPattern = /::?([a-zA-Z][\w-]*)\(([^)]+)\)/g;
598
+ var pseudoMatch;
599
+ while ((pseudoMatch = pseudoPattern.exec(selector)) !== null) {
600
+ var pseudoName = pseudoMatch[1];
601
+ var pseudoContent = pseudoMatch[2];
602
+
603
+ // List of pseudo-classes that should not accept quoted strings
604
+ // :lang() - accepts language codes: en, fr-CA
605
+ // :dir() - accepts direction: ltr, rtl
606
+ // :nth-*() - accepts An+B notation: 2n+1, odd, even
607
+ var noQuotesPseudos = ['lang', 'dir', 'nth-child', 'nth-last-child', 'nth-of-type', 'nth-last-of-type'];
608
+
609
+ for (var i = 0; i < noQuotesPseudos.length; i++) {
610
+ if (pseudoName === noQuotesPseudos[i] && /['"]/.test(pseudoContent)) {
611
+ return false;
612
+ }
613
+ }
614
+ }
615
+
322
616
  // Fallback to a loose regexp for the overall selector structure (without deep paren matching)
323
617
  // This is similar to the original, but without nested paren limitations
324
618
  // Modified to support namespace selectors: *|element, prefix|element, |element
@@ -326,7 +620,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
326
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+)+$/;
327
621
  return looseSelectorRegExp.test(selector);
328
622
  }
329
-
623
+
330
624
  /**
331
625
  * Regular expression to match CSS pseudo-classes with arguments.
332
626
  *
@@ -357,48 +651,56 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
357
651
  * @returns {string[]} An array of selector parts, split by top-level commas, with whitespace trimmed.
358
652
  */
359
653
  function parseAndSplitNestedSelectors(selector) {
360
- var depth = 0;
361
- var buffer = "";
362
- var parts = [];
363
- var inSingleQuote = false;
364
- var inDoubleQuote = false;
654
+ var depth = 0; // Track parenthesis nesting depth
655
+ var buffer = ""; // Accumulate characters for current selector part
656
+ var parts = []; // Array of split selector parts
657
+ var inSingleQuote = false; // Track if we're inside single quotes
658
+ var inDoubleQuote = false; // Track if we're inside double quotes
365
659
  var i, char;
366
660
 
367
661
  for (i = 0; i < selector.length; i++) {
368
662
  char = selector.charAt(i);
369
663
 
664
+ // Handle single quote strings
370
665
  if (char === "'" && !inDoubleQuote) {
371
666
  inSingleQuote = !inSingleQuote;
372
667
  buffer += char;
373
- } else if (char === '"' && !inSingleQuote) {
668
+ }
669
+ // Handle double quote strings
670
+ else if (char === '"' && !inSingleQuote) {
374
671
  inDoubleQuote = !inDoubleQuote;
375
672
  buffer += char;
376
- } else if (!inSingleQuote && !inDoubleQuote) {
673
+ }
674
+ // Process characters outside of quoted strings
675
+ else if (!inSingleQuote && !inDoubleQuote) {
377
676
  if (char === '(') {
677
+ // Entering a nested level (e.g., :is(...))
378
678
  depth++;
379
679
  buffer += char;
380
680
  } else if (char === ')') {
681
+ // Exiting a nested level
381
682
  depth--;
382
683
  buffer += char;
383
- if (depth === 0) {
384
- parts.push(buffer.replace(/^\s+|\s+$/g, ""));
385
- buffer = "";
386
- }
387
684
  } else if (char === ',' && depth === 0) {
388
- if (buffer.replace(/^\s+|\s+$/g, "")) {
389
- parts.push(buffer.replace(/^\s+|\s+$/g, ""));
685
+ // Found a top-level comma separator - split here
686
+ if (buffer.trim()) {
687
+ parts.push(buffer.trim());
390
688
  }
391
689
  buffer = "";
392
690
  } else {
691
+ // Regular character - add to buffer
393
692
  buffer += char;
394
693
  }
395
- } else {
694
+ }
695
+ // Characters inside quoted strings - add to buffer
696
+ else {
396
697
  buffer += char;
397
698
  }
398
699
  }
399
700
 
400
- if (buffer.replace(/^\s+|\s+$/g, "")) {
401
- parts.push(buffer.replace(/^\s+|\s+$/g, ""));
701
+ // Add any remaining content in buffer as the last part
702
+ if (buffer.trim()) {
703
+ parts.push(buffer.trim());
402
704
  }
403
705
 
404
706
  return parts;
@@ -415,8 +717,8 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
415
717
  * @returns {boolean} Returns `true` if the selector is valid, otherwise `false`.
416
718
  */
417
719
 
418
- // Cache to store validated selectors
419
- var validatedSelectorsCache = new Map();
720
+ // Cache to store validated selectors (previously a ES6 Map, now an ES5-compliant object)
721
+ var validatedSelectorsCache = {};
420
722
 
421
723
  // Only pseudo-classes that accept selector lists should recurse
422
724
  var selectorListPseudoClasses = {
@@ -427,8 +729,8 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
427
729
  };
428
730
 
429
731
  function validateSelector(selector) {
430
- if (validatedSelectorsCache.has(selector)) {
431
- return validatedSelectorsCache.get(selector);
732
+ if (validatedSelectorsCache.hasOwnProperty(selector)) {
733
+ return validatedSelectorsCache[selector];
432
734
  }
433
735
 
434
736
  // Use a non-global regex to find all pseudo-classes with arguments
@@ -445,15 +747,15 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
445
747
  var nestedSelectors = parseAndSplitNestedSelectors(pseudoClassMatches[j][2]);
446
748
  for (var i = 0; i < nestedSelectors.length; i++) {
447
749
  var nestedSelector = nestedSelectors[i];
448
- if (!validatedSelectorsCache.has(nestedSelector)) {
750
+ if (!validatedSelectorsCache.hasOwnProperty(nestedSelector)) {
449
751
  var nestedSelectorValidation = validateSelector(nestedSelector);
450
- validatedSelectorsCache.set(nestedSelector, nestedSelectorValidation);
752
+ validatedSelectorsCache[nestedSelector] = nestedSelectorValidation;
451
753
  if (!nestedSelectorValidation) {
452
- validatedSelectorsCache.set(selector, false);
754
+ validatedSelectorsCache[selector] = false;
453
755
  return false;
454
756
  }
455
- } else if (!validatedSelectorsCache.get(nestedSelector)) {
456
- validatedSelectorsCache.set(selector, false);
757
+ } else if (!validatedSelectorsCache[nestedSelector]) {
758
+ validatedSelectorsCache[selector] = false;
457
759
  return false;
458
760
  }
459
761
  }
@@ -461,7 +763,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
461
763
  }
462
764
 
463
765
  var basicSelectorValidation = basicSelectorValidator(selector);
464
- validatedSelectorsCache.set(selector, basicSelectorValidation);
766
+ validatedSelectorsCache[selector] = basicSelectorValidation;
465
767
 
466
768
  return basicSelectorValidation;
467
769
  }
@@ -525,6 +827,28 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
525
827
  return definedNamespacePrefixes.hasOwnProperty(namespacePrefix);
526
828
  }
527
829
 
830
+ /**
831
+ * Processes a CSS selector text
832
+ *
833
+ * @param {string} selectorText - The CSS selector text to process
834
+ * @returns {string} The processed selector text with normalized whitespace
835
+ */
836
+ function processSelectorText(selectorText) {
837
+ // TODO: Remove invalid selectors that appears inside pseudo classes
838
+ // TODO: The same processing here needs to be reused in CSSStyleRule.selectorText setter
839
+ // TODO: Move these validation logic to a shared function to be reused in CSSStyleRule.selectorText setter
840
+
841
+ /**
842
+ * Normalizes whitespace and preserving quoted strings.
843
+ * Replaces all newline characters (CRLF, CR, or LF) with spaces while keeping quoted
844
+ * strings (single or double quotes) intact, including any escaped characters within them.
845
+ */
846
+ return selectorText.replace(/(['"])(?:\\.|[^\\])*?\1|(\r\n|\r|\n)/g, function(match, _, newline) {
847
+ if (newline) return " ";
848
+ return match;
849
+ });
850
+ }
851
+
528
852
  /**
529
853
  * Checks if a given CSS selector text is valid by splitting it by commas
530
854
  * and validating each individual selector using the `validateSelector` function.
@@ -533,6 +857,9 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
533
857
  * @returns {boolean} Returns true if all selectors are valid, otherwise false.
534
858
  */
535
859
  function isValidSelectorText(selectorText) {
860
+ // TODO: The same validations here needs to be reused in CSSStyleRule.selectorText setter
861
+ // TODO: Move these validation logic to a shared function to be reused in CSSStyleRule.selectorText setter
862
+
536
863
  // Check for newlines inside single or double quotes using regex
537
864
  // This matches any quoted string (single or double) containing a newline
538
865
  var quotedNewlineRegExp = /(['"])(?:\\.|[^\\])*?\1/g;
@@ -553,6 +880,23 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
553
880
  return true;
554
881
  }
555
882
 
883
+ function parseError(message) {
884
+ var lines = token.substring(0, i).split('\n');
885
+ var lineCount = lines.length;
886
+ var charCount = lines.pop().length + 1;
887
+ var error = new Error(message + ' (line ' + lineCount + ', char ' + charCount + ')');
888
+ error.line = lineCount;
889
+ /* jshint sub : true */
890
+ error['char'] = charCount;
891
+ error.styleSheet = styleSheet;
892
+ // Print the error but continue parsing the sheet
893
+ try {
894
+ throw error;
895
+ } catch(e) {
896
+ errorHandler && errorHandler(e);
897
+ }
898
+ };
899
+
556
900
  var endingIndex = token.length - 1;
557
901
 
558
902
  for (var character; (character = token.charAt(i)); i++) {
@@ -642,7 +986,8 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
642
986
  i += 2;
643
987
  index = token.indexOf("*/", i);
644
988
  if (index === -1) {
645
- parseError("Missing */");
989
+ i = token.length - 1;
990
+ buffer = "";
646
991
  } else {
647
992
  i = index + 1;
648
993
  }
@@ -697,6 +1042,15 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
697
1042
  }, true);
698
1043
  buffer = "";
699
1044
  break;
1045
+ } else if (token.indexOf("@scope", i) === i) {
1046
+ validateAtRule("@scope", function(){
1047
+ state = "scopeBlock";
1048
+ scopeRule = new CSSOM.CSSScopeRule();
1049
+ scopeRule.__starts = i;
1050
+ i += "scope".length;
1051
+ });
1052
+ buffer = "";
1053
+ break;
700
1054
  } else if (token.indexOf("@layer", i) === i) {
701
1055
  validateAtRule("@layer", function(){
702
1056
  state = "layerBlock"
@@ -706,6 +1060,15 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
706
1060
  });
707
1061
  buffer = "";
708
1062
  break;
1063
+ } else if (token.indexOf("@page", i) === i) {
1064
+ validateAtRule("@page", function(){
1065
+ state = "pageBlock"
1066
+ pageRule = new CSSOM.CSSPageRule();
1067
+ pageRule.__starts = i;
1068
+ i += "page".length;
1069
+ });
1070
+ buffer = "";
1071
+ break;
709
1072
  } else if (token.indexOf("@supports", i) === i) {
710
1073
  validateAtRule("@supports", function(){
711
1074
  state = "conditionBlock";
@@ -803,10 +1166,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
803
1166
  }
804
1167
 
805
1168
  currentScope = parentRule = styleRule;
806
- styleRule.selectorText = buffer.trim().replace(/(['"])(?:\\.|[^\\])*?\1|(\r\n|\r|\n)/g, function(match, _, newline) {
807
- if (newline) return " ";
808
- return match;
809
- });
1169
+ styleRule.selectorText = processSelectorText(buffer.trim());
810
1170
  styleRule.style.__starts = i;
811
1171
  styleRule.__parentStyleSheet = styleSheet;
812
1172
  buffer = "";
@@ -824,7 +1184,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
824
1184
  buffer = "";
825
1185
  state = "before-selector";
826
1186
  } else if (state === "containerBlock") {
827
- containerRule.containerText = buffer.trim();
1187
+ containerRule.__conditionText = buffer.trim();
828
1188
 
829
1189
  if (parentRule) {
830
1190
  containerRule.__parentRule = parentRule;
@@ -841,7 +1201,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
841
1201
  counterStyleRule.__parentStyleSheet = styleSheet;
842
1202
  buffer = "";
843
1203
  } else if (state === "conditionBlock") {
844
- supportsRule.conditionText = buffer.trim();
1204
+ supportsRule.__conditionText = buffer.trim();
845
1205
 
846
1206
  if (parentRule) {
847
1207
  supportsRule.__parentRule = parentRule;
@@ -852,10 +1212,31 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
852
1212
  supportsRule.__parentStyleSheet = styleSheet;
853
1213
  buffer = "";
854
1214
  state = "before-selector";
1215
+ } else if (state === "scopeBlock") {
1216
+ var parsedScopePrelude = parseScopePrelude(buffer.trim());
1217
+
1218
+ if (parsedScopePrelude.hasStart) {
1219
+ scopeRule.__start = parsedScopePrelude.startSelector;
1220
+ }
1221
+ if (parsedScopePrelude.hasEnd) {
1222
+ scopeRule.__end = parsedScopePrelude.endSelector;
1223
+ }
1224
+ if (parsedScopePrelude.hasOnlyEnd) {
1225
+ scopeRule.__end = parsedScopePrelude.endSelector;
1226
+ }
1227
+
1228
+ if (parentRule) {
1229
+ scopeRule.__parentRule = parentRule;
1230
+ ancestorRules.push(parentRule);
1231
+ }
1232
+ currentScope = parentRule = scopeRule;
1233
+ scopeRule.__parentStyleSheet = styleSheet;
1234
+ buffer = "";
1235
+ state = "before-selector";
855
1236
  } else if (state === "layerBlock") {
856
1237
  layerBlockRule.name = buffer.trim();
857
1238
 
858
- var isValidName = layerBlockRule.name.length === 0 || layerBlockRule.name.match(layerRuleNameRegExp) !== null;
1239
+ var isValidName = layerBlockRule.name.length === 0 || layerBlockRule.name.match(cssCustomIdentifierRegExp) !== null;
859
1240
 
860
1241
  if (isValidName) {
861
1242
  if (parentRule) {
@@ -868,6 +1249,19 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
868
1249
  }
869
1250
  buffer = "";
870
1251
  state = "before-selector";
1252
+ } else if (state === "pageBlock") {
1253
+ pageRule.selectorText = buffer.trim();
1254
+
1255
+ if (parentRule) {
1256
+ pageRule.__parentRule = parentRule;
1257
+ ancestorRules.push(parentRule);
1258
+ }
1259
+
1260
+ currentScope = parentRule = pageRule;
1261
+ pageRule.__parentStyleSheet = styleSheet;
1262
+ styleRule = pageRule;
1263
+ buffer = "";
1264
+ state = "before-name";
871
1265
  } else if (state === "hostRule-begin") {
872
1266
  if (parentRule) {
873
1267
  ancestorRules.push(parentRule);
@@ -941,17 +1335,15 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
941
1335
  }
942
1336
 
943
1337
  styleRule = new CSSOM.CSSStyleRule();
944
- var processedSelectorText = buffer.trim().replace(/(['"])(?:\\.|[^\\])*?\1|(\r\n|\r|\n)/g, function(match, _, newline) {
945
- if (newline) return " ";
946
- return match;
947
- });
1338
+ var processedSelectorText = processSelectorText(buffer.trim());
948
1339
  // In a nested selector, ensure each selector contains '&' at the beginning, except for selectors that already have '&' somewhere
949
1340
  if (parentRule.constructor.name !== "CSSStyleRule" && parentRule.parentRule === null) {
950
1341
  styleRule.selectorText = processedSelectorText;
951
1342
  } else {
952
1343
  styleRule.selectorText = parseAndSplitNestedSelectors(processedSelectorText).map(function(sel) {
953
- return sel.indexOf('&') === -1 ? '& ' + sel : sel;
954
- }).join(', ');
1344
+ // Add & at the beginning if there's no & in the selector, or if it starts with a combinator
1345
+ return (sel.indexOf('&') === -1 || startsWithCombinatorRegExp.test(sel)) ? '& ' + sel : sel;
1346
+ }).join(', ');
955
1347
  }
956
1348
  styleRule.style.__starts = i - buffer.length;
957
1349
  styleRule.__parentRule = parentRule;
@@ -1067,7 +1459,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1067
1459
  testNamespaceRule.cssText = buffer + character;
1068
1460
 
1069
1461
  namespaceRule = testNamespaceRule;
1070
- namespaceRule.__parentStyleSheet = namespaceRule.styleSheet.__parentStyleSheet = styleSheet;
1462
+ namespaceRule.__parentStyleSheet = styleSheet;
1071
1463
  styleSheet.cssRules.push(namespaceRule);
1072
1464
 
1073
1465
  // Track the namespace prefix for validation
@@ -1086,7 +1478,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1086
1478
  return name.trim();
1087
1479
  });
1088
1480
  var isInvalid = parentRule !== undefined || nameListStr.some(function (name) {
1089
- return name.trim().match(layerRuleNameRegExp) === null;
1481
+ return name.trim().match(cssCustomIdentifierRegExp) === null;
1090
1482
  });
1091
1483
 
1092
1484
  if (!isInvalid) {
@@ -1187,6 +1579,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1187
1579
  || parentRule.constructor.name === "CSSMediaRule"
1188
1580
  || parentRule.constructor.name === "CSSSupportsRule"
1189
1581
  || parentRule.constructor.name === "CSSContainerRule"
1582
+ || parentRule.constructor.name === "CSSScopeRule"
1190
1583
  || parentRule.constructor.name === "CSSLayerBlockRule"
1191
1584
  || parentRule.constructor.name === "CSSStartingStyleRule"
1192
1585
  ) {
@@ -1256,6 +1649,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1256
1649
  || parentRule.constructor.name === "CSSMediaRule"
1257
1650
  || parentRule.constructor.name === "CSSSupportsRule"
1258
1651
  || parentRule.constructor.name === "CSSContainerRule"
1652
+ || parentRule.constructor.name === "CSSScopeRule"
1259
1653
  || parentRule.constructor.name === "CSSLayerBlockRule"
1260
1654
  || parentRule.constructor.name === "CSSStartingStyleRule"
1261
1655
  ) {
@@ -1311,6 +1705,10 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1311
1705
  }
1312
1706
  }
1313
1707
 
1708
+ if (buffer.trim() !== "") {
1709
+ parseError("Unexpected end of input");
1710
+ }
1711
+
1314
1712
  return styleSheet;
1315
1713
  };
1316
1714
 
@@ -1337,8 +1735,10 @@ CSSOM.CSSKeyframeRule = require('./CSSKeyframeRule').CSSKeyframeRule;
1337
1735
  CSSOM.CSSKeyframesRule = require('./CSSKeyframesRule').CSSKeyframesRule;
1338
1736
  CSSOM.CSSValueExpression = require('./CSSValueExpression').CSSValueExpression;
1339
1737
  CSSOM.CSSDocumentRule = require('./CSSDocumentRule').CSSDocumentRule;
1738
+ CSSOM.CSSScopeRule = require('./CSSScopeRule').CSSScopeRule;
1340
1739
  CSSOM.CSSLayerBlockRule = require("./CSSLayerBlockRule").CSSLayerBlockRule;
1341
1740
  CSSOM.CSSLayerStatementRule = require("./CSSLayerStatementRule").CSSLayerStatementRule;
1741
+ CSSOM.CSSPageRule = require("./CSSPageRule").CSSPageRule;
1342
1742
  // Use cssstyle if available
1343
1743
  try {
1344
1744
  CSSOM.CSSStyleDeclaration = require("cssstyle").CSSStyleDeclaration;