@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/lib/parse.js CHANGED
@@ -50,6 +50,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
50
50
  "conditionBlock": true,
51
51
  "counterStyleBlock": true,
52
52
  'documentRule-begin': true,
53
+ "scopeBlock": true,
53
54
  "layerBlock": true
54
55
  };
55
56
 
@@ -68,7 +69,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
68
69
  var ancestorRules = [];
69
70
  var prevScope;
70
71
 
71
- var name, priority="", styleRule, mediaRule, containerRule, counterStyleRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule, startingStyleRule, layerBlockRule, layerStatementRule, nestedSelectorRule, namespaceRule;
72
+ var name, priority="", styleRule, mediaRule, containerRule, counterStyleRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule, startingStyleRule, scopeRule, layerBlockRule, layerStatementRule, nestedSelectorRule, namespaceRule;
72
73
 
73
74
  // Track defined namespace prefixes for validation
74
75
  var definedNamespacePrefixes = {};
@@ -78,11 +79,12 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
78
79
  // var atRulesStatemenRegExp = /(?<!{.*)[;}]\s*/; // Match a statement by verifying it finds a semicolon or closing brace not followed by another semicolon or closing brace
79
80
  var beforeRulePortionRegExp = /{(?!.*{)|}(?!.*})|;(?!.*;)|\*\/(?!.*\*\/)/g; // Match the closest allowed character (a opening or closing brace, a semicolon or a comment ending) before the rule
80
81
  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
82
+ var forwardRuleValidationRegExp = /(?:\s|\/\*|\{|\()/; // Match that the rule is followed by any whitespace, a opening comment, a condition opening parenthesis or a opening brace
82
83
  var forwardImportRuleValidationRegExp = /(?:\s|\/\*|'|")/; // Match that the rule is followed by any whitespace, an opening comment, a single quote or double quote
83
84
  var forwardRuleClosingBraceRegExp = /{[^{}]*}|}/; // Finds the next closing brace of a rule block
84
85
  var forwardRuleSemicolonAndOpeningBraceRegExp = /^.*?({|;)/; // Finds the next semicolon or opening brace after the at-rule
85
86
  var layerRuleNameRegExp = /^(-?[_a-zA-Z]+(\.[_a-zA-Z]+)*[_a-zA-Z0-9-]*)$/; // Validates a single @layer name
87
+ var startsWithCombinatorRegExp = /^\s*[>+~]/; // Checks if a selector starts with a CSS combinator (>, +, ~)
86
88
 
87
89
  /**
88
90
  * Searches for the first occurrence of a CSS at-rule statement terminator (`;` or `}`)
@@ -174,24 +176,214 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
174
176
  return i;
175
177
  }
176
178
 
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);
179
+ /**
180
+ * Parses the scope prelude and extracts start and end selectors.
181
+ * @param {string} preludeContent - The scope prelude content (without @scope keyword)
182
+ * @returns {object} Object with startSelector and endSelector properties
183
+ */
184
+ function parseScopePrelude(preludeContent) {
185
+ var parts = preludeContent.split(/\s*\)\s*to\s+\(/);
186
+
187
+ // Restore the parentheses that were consumed by the split
188
+ if (parts.length === 2) {
189
+ parts[0] = parts[0] + ')';
190
+ parts[1] = '(' + parts[1];
191
+ }
192
+
193
+ var hasStart = parts[0] &&
194
+ parts[0].charAt(0) === '(' &&
195
+ parts[0].charAt(parts[0].length - 1) === ')';
196
+ var hasEnd = parts[1] &&
197
+ parts[1].charAt(0) === '(' &&
198
+ parts[1].charAt(parts[1].length - 1) === ')';
199
+
200
+ // Handle case: @scope to (<end>)
201
+ var hasOnlyEnd = !hasStart &&
202
+ !hasEnd &&
203
+ parts[0].indexOf('to (') === 0 &&
204
+ parts[0].charAt(parts[0].length - 1) === ')';
205
+
206
+ var startSelector = '';
207
+ var endSelector = '';
208
+
209
+ if (hasStart) {
210
+ startSelector = parts[0].slice(1, -1).trim();
191
211
  }
212
+ if (hasEnd) {
213
+ endSelector = parts[1].slice(1, -1).trim();
214
+ }
215
+ if (hasOnlyEnd) {
216
+ endSelector = parts[0].slice(4, -1).trim();
217
+ }
218
+
219
+ return {
220
+ startSelector: startSelector,
221
+ endSelector: endSelector,
222
+ hasStart: hasStart,
223
+ hasEnd: hasEnd,
224
+ hasOnlyEnd: hasOnlyEnd
225
+ };
226
+ };
227
+
228
+ /**
229
+ * Checks if a selector contains pseudo-elements.
230
+ * @param {string} selector - The CSS selector to check
231
+ * @returns {boolean} True if the selector contains pseudo-elements
232
+ */
233
+ function hasPseudoElement(selector) {
234
+ // Match only double-colon (::) pseudo-elements
235
+ // Also match legacy single-colon pseudo-elements: :before, :after, :first-line, :first-letter
236
+ // These must NOT be followed by alphanumeric characters (to avoid matching :before-x or similar)
237
+ var pseudoElementRegex = /::[a-zA-Z][\w-]*|:(before|after|first-line|first-letter)(?![a-zA-Z0-9_-])/;
238
+ return pseudoElementRegex.test(selector);
239
+ };
240
+
241
+ /**
242
+ * Validates balanced parentheses, brackets, and quotes in a selector.
243
+ *
244
+ * @param {string} selector - The CSS selector to validate
245
+ * @param {boolean} trackAttributes - Whether to track attribute selector context
246
+ * @param {boolean} useStack - Whether to use a stack for parentheses (needed for nested validation)
247
+ * @returns {boolean} True if the syntax is valid (all brackets, parentheses, and quotes are balanced)
248
+ */
249
+ function validateBalancedSyntax(selector, trackAttributes, useStack) {
250
+ var parenDepth = 0;
251
+ var bracketDepth = 0;
252
+ var inSingleQuote = false;
253
+ var inDoubleQuote = false;
254
+ var inAttr = false;
255
+ var stack = useStack ? [] : null;
256
+
257
+ for (var i = 0; i < selector.length; i++) {
258
+ var char = selector[i];
259
+ var prevChar = i > 0 ? selector[i - 1] : '';
260
+
261
+ if (inSingleQuote) {
262
+ if (char === "'" && prevChar !== "\\") {
263
+ inSingleQuote = false;
264
+ }
265
+ } else if (inDoubleQuote) {
266
+ if (char === '"' && prevChar !== "\\") {
267
+ inDoubleQuote = false;
268
+ }
269
+ } else if (trackAttributes && inAttr) {
270
+ if (char === "]") {
271
+ inAttr = false;
272
+ } else if (char === "'") {
273
+ inSingleQuote = true;
274
+ } else if (char === '"') {
275
+ inDoubleQuote = true;
276
+ }
277
+ } else {
278
+ if (trackAttributes && char === "[") {
279
+ inAttr = true;
280
+ } else if (char === "'") {
281
+ inSingleQuote = true;
282
+ } else if (char === '"') {
283
+ inDoubleQuote = true;
284
+ } else if (char === '(') {
285
+ if (useStack) {
286
+ stack.push("(");
287
+ } else {
288
+ parenDepth++;
289
+ }
290
+ } else if (char === ')') {
291
+ if (useStack) {
292
+ if (!stack.length || stack.pop() !== "(") {
293
+ return false;
294
+ }
295
+ } else {
296
+ parenDepth--;
297
+ if (parenDepth < 0) {
298
+ return false;
299
+ }
300
+ }
301
+ } else if (char === '[') {
302
+ bracketDepth++;
303
+ } else if (char === ']') {
304
+ bracketDepth--;
305
+ if (bracketDepth < 0) {
306
+ return false;
307
+ }
308
+ }
309
+ }
310
+ }
311
+
312
+ // Check if everything is balanced
313
+ if (useStack) {
314
+ return stack.length === 0 && bracketDepth === 0 && !inSingleQuote && !inDoubleQuote && !inAttr;
315
+ } else {
316
+ return parenDepth === 0 && bracketDepth === 0 && !inSingleQuote && !inDoubleQuote;
317
+ }
318
+ };
319
+
320
+ /**
321
+ * Checks for basic syntax errors in selectors (mismatched parentheses, brackets, quotes).
322
+ * @param {string} selector - The CSS selector to check
323
+ * @returns {boolean} True if there are syntax errors
324
+ */
325
+ function hasBasicSyntaxError(selector) {
326
+ return !validateBalancedSyntax(selector, false, false);
327
+ };
328
+
329
+ /**
330
+ * Checks for invalid combinator patterns in selectors.
331
+ * @param {string} selector - The CSS selector to check
332
+ * @returns {boolean} True if the selector contains invalid combinators
333
+ */
334
+ function hasInvalidCombinators(selector) {
335
+ // Check for invalid combinator patterns:
336
+ // - <> (not a valid combinator)
337
+ // - >> (deep descendant combinator, deprecated and invalid)
338
+ // - Multiple consecutive combinators like >>, >~, etc.
339
+ if (/<>/.test(selector)) return true;
340
+ if (/>>/.test(selector)) return true;
341
+ // Check for other invalid consecutive combinator patterns
342
+ if (/[>+~]\s*[>+~]/.test(selector)) return true;
343
+ return false;
344
+ };
345
+
346
+ /**
347
+ * Checks for invalid pseudo-like syntax (function calls without proper pseudo prefix).
348
+ * @param {string} selector - The CSS selector to check
349
+ * @returns {boolean} True if the selector contains invalid pseudo-like syntax
350
+ */
351
+ function hasInvalidPseudoSyntax(selector) {
352
+ // Check for specific known pseudo-elements used without : or :: prefix
353
+ // Examples: slotted(div), part(name), cue(selector)
354
+ // These are ONLY valid as ::slotted(), ::part(), ::cue()
355
+ var invalidPatterns = [
356
+ /(?:^|[\s>+~,\[])slotted\s*\(/i,
357
+ /(?:^|[\s>+~,\[])part\s*\(/i,
358
+ /(?:^|[\s>+~,\[])cue\s*\(/i,
359
+ /(?:^|[\s>+~,\[])cue-region\s*\(/i
360
+ ];
361
+
362
+ for (var i = 0; i < invalidPatterns.length; i++) {
363
+ if (invalidPatterns[i].test(selector)) {
364
+ return true;
365
+ }
366
+ }
367
+ return false;
368
+ };
369
+
370
+ /**
371
+ * Checks for invalid nesting selector (&) usage.
372
+ * The & selector cannot be directly followed by a type selector without a delimiter.
373
+ * Valid: &.class, &#id, &[attr], &:hover, &::before, & div, &>div
374
+ * Invalid: &div, &span
375
+ * @param {string} selector - The CSS selector to check
376
+ * @returns {boolean} True if the selector contains invalid & usage
377
+ */
378
+ function hasInvalidNestingSelector(selector) {
379
+ // Check for & followed directly by a letter (type selector) without any delimiter
380
+ // This regex matches & followed by a letter (start of type selector) that's not preceded by an escape
381
+ // We need to exclude valid cases like &.class, &#id, &[attr], &:pseudo, &::pseudo, & (with space), &>
382
+ var invalidNestingPattern = /&(?![.\#\[:>\+~\s])[a-zA-Z]/;
383
+ return invalidNestingPattern.test(selector);
192
384
  };
193
385
 
194
- var validateAtRule = function(atRuleKey, validCallback, cannotBeNested) {
386
+ function validateAtRule(atRuleKey, validCallback, cannotBeNested) {
195
387
  var isValid = false;
196
388
  var sourceRuleRegExp = atRuleKey === "@import" ? forwardImportRuleValidationRegExp : forwardRuleValidationRegExp;
197
389
  var ruleRegExp = new RegExp(atRuleKey + sourceRuleRegExp.source, sourceRuleRegExp.flags);
@@ -212,6 +404,56 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
212
404
  isValid = true;
213
405
  }
214
406
  }
407
+
408
+ // Additional validation for @scope rule
409
+ if (isValid && atRuleKey === "@scope") {
410
+ var openBraceIndex = ruleSlice.indexOf('{');
411
+ if (openBraceIndex !== -1) {
412
+ // Extract the scope prelude (everything between @scope and {)
413
+ var scopePrelude = ruleSlice.slice(0, openBraceIndex).trim();
414
+
415
+ // Skip past '@scope' keyword and whitespace
416
+ var preludeContent = scopePrelude.slice(6).trim();
417
+
418
+ if (preludeContent.length > 0) {
419
+ // Parse the scope prelude
420
+ var parsedScopePrelude = parseScopePrelude(preludeContent);
421
+ var startSelector = parsedScopePrelude.startSelector;
422
+ var endSelector = parsedScopePrelude.endSelector;
423
+ var hasStart = parsedScopePrelude.hasStart;
424
+ var hasEnd = parsedScopePrelude.hasEnd;
425
+ var hasOnlyEnd = parsedScopePrelude.hasOnlyEnd;
426
+
427
+ // Validation rules for @scope:
428
+ // 1. Empty selectors in parentheses are invalid: @scope () {} or @scope (.a) to () {}
429
+ if ((hasStart && startSelector === '') || (hasEnd && endSelector === '') || (hasOnlyEnd && endSelector === '')) {
430
+ isValid = false;
431
+ }
432
+ // 2. Pseudo-elements are invalid in scope selectors
433
+ else if ((startSelector && hasPseudoElement(startSelector)) || (endSelector && hasPseudoElement(endSelector))) {
434
+ isValid = false;
435
+ }
436
+ // 3. Basic syntax errors (mismatched parens, brackets, quotes)
437
+ else if ((startSelector && hasBasicSyntaxError(startSelector)) || (endSelector && hasBasicSyntaxError(endSelector))) {
438
+ isValid = false;
439
+ }
440
+ // 4. Invalid combinator patterns
441
+ else if ((startSelector && hasInvalidCombinators(startSelector)) || (endSelector && hasInvalidCombinators(endSelector))) {
442
+ isValid = false;
443
+ }
444
+ // 5. Invalid pseudo-like syntax (function without : or :: prefix)
445
+ else if ((startSelector && hasInvalidPseudoSyntax(startSelector)) || (endSelector && hasInvalidPseudoSyntax(endSelector))) {
446
+ isValid = false;
447
+ }
448
+ // 6. Invalid structure (no proper parentheses found when prelude is not empty)
449
+ else if (!hasStart && !hasOnlyEnd) {
450
+ isValid = false;
451
+ }
452
+ }
453
+ // Empty prelude (@scope {}) is valid
454
+ }
455
+ }
456
+
215
457
  if (!isValid) {
216
458
  // If it's invalid the browser will simply ignore the entire invalid block
217
459
  // Use regex to find the closing brace of the invalid rule
@@ -270,52 +512,23 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
270
512
  * @returns {boolean}
271
513
  */
272
514
  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;
515
+ // Validate balanced syntax with attribute tracking and stack-based parentheses matching
516
+ if (!validateBalancedSyntax(selector, true, true)) {
517
+ return false;
518
+ }
279
519
 
280
- while (i < length) {
281
- var char = selector[i];
520
+ // Check for invalid combinator patterns
521
+ if (hasInvalidCombinators(selector)) {
522
+ return false;
523
+ }
282
524
 
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++;
525
+ // Check for invalid pseudo-like syntax
526
+ if (hasInvalidPseudoSyntax(selector)) {
527
+ return false;
315
528
  }
316
529
 
317
- // If any stack or quote/attr context remains, it's invalid
318
- if (stack.length || inAttr || inSingleQuote || inDoubleQuote) {
530
+ // Check for invalid nesting selector (&) usage
531
+ if (hasInvalidNestingSelector(selector)) {
319
532
  return false;
320
533
  }
321
534
 
@@ -326,7 +539,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
326
539
  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
540
  return looseSelectorRegExp.test(selector);
328
541
  }
329
-
542
+
330
543
  /**
331
544
  * Regular expression to match CSS pseudo-classes with arguments.
332
545
  *
@@ -357,48 +570,56 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
357
570
  * @returns {string[]} An array of selector parts, split by top-level commas, with whitespace trimmed.
358
571
  */
359
572
  function parseAndSplitNestedSelectors(selector) {
360
- var depth = 0;
361
- var buffer = "";
362
- var parts = [];
363
- var inSingleQuote = false;
364
- var inDoubleQuote = false;
573
+ var depth = 0; // Track parenthesis nesting depth
574
+ var buffer = ""; // Accumulate characters for current selector part
575
+ var parts = []; // Array of split selector parts
576
+ var inSingleQuote = false; // Track if we're inside single quotes
577
+ var inDoubleQuote = false; // Track if we're inside double quotes
365
578
  var i, char;
366
579
 
367
580
  for (i = 0; i < selector.length; i++) {
368
581
  char = selector.charAt(i);
369
582
 
583
+ // Handle single quote strings
370
584
  if (char === "'" && !inDoubleQuote) {
371
585
  inSingleQuote = !inSingleQuote;
372
586
  buffer += char;
373
- } else if (char === '"' && !inSingleQuote) {
587
+ }
588
+ // Handle double quote strings
589
+ else if (char === '"' && !inSingleQuote) {
374
590
  inDoubleQuote = !inDoubleQuote;
375
591
  buffer += char;
376
- } else if (!inSingleQuote && !inDoubleQuote) {
592
+ }
593
+ // Process characters outside of quoted strings
594
+ else if (!inSingleQuote && !inDoubleQuote) {
377
595
  if (char === '(') {
596
+ // Entering a nested level (e.g., :is(...))
378
597
  depth++;
379
598
  buffer += char;
380
599
  } else if (char === ')') {
600
+ // Exiting a nested level
381
601
  depth--;
382
602
  buffer += char;
383
- if (depth === 0) {
384
- parts.push(buffer.replace(/^\s+|\s+$/g, ""));
385
- buffer = "";
386
- }
387
603
  } else if (char === ',' && depth === 0) {
388
- if (buffer.replace(/^\s+|\s+$/g, "")) {
389
- parts.push(buffer.replace(/^\s+|\s+$/g, ""));
604
+ // Found a top-level comma separator - split here
605
+ if (buffer.trim()) {
606
+ parts.push(buffer.trim());
390
607
  }
391
608
  buffer = "";
392
609
  } else {
610
+ // Regular character - add to buffer
393
611
  buffer += char;
394
612
  }
395
- } else {
613
+ }
614
+ // Characters inside quoted strings - add to buffer
615
+ else {
396
616
  buffer += char;
397
617
  }
398
618
  }
399
619
 
400
- if (buffer.replace(/^\s+|\s+$/g, "")) {
401
- parts.push(buffer.replace(/^\s+|\s+$/g, ""));
620
+ // Add any remaining content in buffer as the last part
621
+ if (buffer.trim()) {
622
+ parts.push(buffer.trim());
402
623
  }
403
624
 
404
625
  return parts;
@@ -415,8 +636,8 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
415
636
  * @returns {boolean} Returns `true` if the selector is valid, otherwise `false`.
416
637
  */
417
638
 
418
- // Cache to store validated selectors
419
- var validatedSelectorsCache = new Map();
639
+ // Cache to store validated selectors (ES5-compliant object)
640
+ var validatedSelectorsCache = {};
420
641
 
421
642
  // Only pseudo-classes that accept selector lists should recurse
422
643
  var selectorListPseudoClasses = {
@@ -427,8 +648,8 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
427
648
  };
428
649
 
429
650
  function validateSelector(selector) {
430
- if (validatedSelectorsCache.has(selector)) {
431
- return validatedSelectorsCache.get(selector);
651
+ if (validatedSelectorsCache.hasOwnProperty(selector)) {
652
+ return validatedSelectorsCache[selector];
432
653
  }
433
654
 
434
655
  // Use a non-global regex to find all pseudo-classes with arguments
@@ -445,15 +666,15 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
445
666
  var nestedSelectors = parseAndSplitNestedSelectors(pseudoClassMatches[j][2]);
446
667
  for (var i = 0; i < nestedSelectors.length; i++) {
447
668
  var nestedSelector = nestedSelectors[i];
448
- if (!validatedSelectorsCache.has(nestedSelector)) {
669
+ if (!validatedSelectorsCache.hasOwnProperty(nestedSelector)) {
449
670
  var nestedSelectorValidation = validateSelector(nestedSelector);
450
- validatedSelectorsCache.set(nestedSelector, nestedSelectorValidation);
671
+ validatedSelectorsCache[nestedSelector] = nestedSelectorValidation;
451
672
  if (!nestedSelectorValidation) {
452
- validatedSelectorsCache.set(selector, false);
673
+ validatedSelectorsCache[selector] = false;
453
674
  return false;
454
675
  }
455
- } else if (!validatedSelectorsCache.get(nestedSelector)) {
456
- validatedSelectorsCache.set(selector, false);
676
+ } else if (!validatedSelectorsCache[nestedSelector]) {
677
+ validatedSelectorsCache[selector] = false;
457
678
  return false;
458
679
  }
459
680
  }
@@ -461,7 +682,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
461
682
  }
462
683
 
463
684
  var basicSelectorValidation = basicSelectorValidator(selector);
464
- validatedSelectorsCache.set(selector, basicSelectorValidation);
685
+ validatedSelectorsCache[selector] = basicSelectorValidation;
465
686
 
466
687
  return basicSelectorValidation;
467
688
  }
@@ -553,6 +774,23 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
553
774
  return true;
554
775
  }
555
776
 
777
+ function parseError(message) {
778
+ var lines = token.substring(0, i).split('\n');
779
+ var lineCount = lines.length;
780
+ var charCount = lines.pop().length + 1;
781
+ var error = new Error(message + ' (line ' + lineCount + ', char ' + charCount + ')');
782
+ error.line = lineCount;
783
+ /* jshint sub : true */
784
+ error['char'] = charCount;
785
+ error.styleSheet = styleSheet;
786
+ // Print the error but continue parsing the sheet
787
+ try {
788
+ throw error;
789
+ } catch(e) {
790
+ errorHandler && errorHandler(e);
791
+ }
792
+ };
793
+
556
794
  var endingIndex = token.length - 1;
557
795
 
558
796
  for (var character; (character = token.charAt(i)); i++) {
@@ -697,6 +935,15 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
697
935
  }, true);
698
936
  buffer = "";
699
937
  break;
938
+ } else if (token.indexOf("@scope", i) === i) {
939
+ validateAtRule("@scope", function(){
940
+ state = "scopeBlock";
941
+ scopeRule = new CSSOM.CSSScopeRule();
942
+ scopeRule.__starts = i;
943
+ i += "scope".length;
944
+ });
945
+ buffer = "";
946
+ break;
700
947
  } else if (token.indexOf("@layer", i) === i) {
701
948
  validateAtRule("@layer", function(){
702
949
  state = "layerBlock"
@@ -852,6 +1099,27 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
852
1099
  supportsRule.__parentStyleSheet = styleSheet;
853
1100
  buffer = "";
854
1101
  state = "before-selector";
1102
+ } else if (state === "scopeBlock") {
1103
+ var parsedScopePrelude = parseScopePrelude(buffer.trim());
1104
+
1105
+ if (parsedScopePrelude.hasStart) {
1106
+ scopeRule.__start = parsedScopePrelude.startSelector;
1107
+ }
1108
+ if (parsedScopePrelude.hasEnd) {
1109
+ scopeRule.__end = parsedScopePrelude.endSelector;
1110
+ }
1111
+ if (parsedScopePrelude.hasOnlyEnd) {
1112
+ scopeRule.__end = parsedScopePrelude.endSelector;
1113
+ }
1114
+
1115
+ if (parentRule) {
1116
+ scopeRule.__parentRule = parentRule;
1117
+ ancestorRules.push(parentRule);
1118
+ }
1119
+ currentScope = parentRule = scopeRule;
1120
+ scopeRule.__parentStyleSheet = styleSheet;
1121
+ buffer = "";
1122
+ state = "before-selector";
855
1123
  } else if (state === "layerBlock") {
856
1124
  layerBlockRule.name = buffer.trim();
857
1125
 
@@ -950,8 +1218,9 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
950
1218
  styleRule.selectorText = processedSelectorText;
951
1219
  } else {
952
1220
  styleRule.selectorText = parseAndSplitNestedSelectors(processedSelectorText).map(function(sel) {
953
- return sel.indexOf('&') === -1 ? '& ' + sel : sel;
954
- }).join(', ');
1221
+ // Add & at the beginning if there's no & in the selector, or if it starts with a combinator
1222
+ return (sel.indexOf('&') === -1 || startsWithCombinatorRegExp.test(sel)) ? '& ' + sel : sel;
1223
+ }).join(', ');
955
1224
  }
956
1225
  styleRule.style.__starts = i - buffer.length;
957
1226
  styleRule.__parentRule = parentRule;
@@ -1187,6 +1456,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1187
1456
  || parentRule.constructor.name === "CSSMediaRule"
1188
1457
  || parentRule.constructor.name === "CSSSupportsRule"
1189
1458
  || parentRule.constructor.name === "CSSContainerRule"
1459
+ || parentRule.constructor.name === "CSSScopeRule"
1190
1460
  || parentRule.constructor.name === "CSSLayerBlockRule"
1191
1461
  || parentRule.constructor.name === "CSSStartingStyleRule"
1192
1462
  ) {
@@ -1256,6 +1526,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1256
1526
  || parentRule.constructor.name === "CSSMediaRule"
1257
1527
  || parentRule.constructor.name === "CSSSupportsRule"
1258
1528
  || parentRule.constructor.name === "CSSContainerRule"
1529
+ || parentRule.constructor.name === "CSSScopeRule"
1259
1530
  || parentRule.constructor.name === "CSSLayerBlockRule"
1260
1531
  || parentRule.constructor.name === "CSSStartingStyleRule"
1261
1532
  ) {
@@ -1337,6 +1608,7 @@ CSSOM.CSSKeyframeRule = require('./CSSKeyframeRule').CSSKeyframeRule;
1337
1608
  CSSOM.CSSKeyframesRule = require('./CSSKeyframesRule').CSSKeyframesRule;
1338
1609
  CSSOM.CSSValueExpression = require('./CSSValueExpression').CSSValueExpression;
1339
1610
  CSSOM.CSSDocumentRule = require('./CSSDocumentRule').CSSDocumentRule;
1611
+ CSSOM.CSSScopeRule = require('./CSSScopeRule').CSSScopeRule;
1340
1612
  CSSOM.CSSLayerBlockRule = require("./CSSLayerBlockRule").CSSLayerBlockRule;
1341
1613
  CSSOM.CSSLayerStatementRule = require("./CSSLayerStatementRule").CSSLayerStatementRule;
1342
1614
  // Use cssstyle if available
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "parser",
8
8
  "styleSheet"
9
9
  ],
10
- "version": "0.9.19",
10
+ "version": "0.9.20",
11
11
  "author": "Nikita Vasilyev <me@elv1s.ru>",
12
12
  "contributors": [
13
13
  "Acemir Sousa Mendes <acemirsm@gmail.com>"