single-file-core 1.5.80 → 1.5.82

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.
@@ -24,12 +24,14 @@
24
24
  import * as cssTree from "./../vendor/css-tree.js";
25
25
  import { computeMaxSpecificity } from "./css-specificity.js";
26
26
  import { parsePrelude } from "./css-scope-prelude-parser.js";
27
- import { sanitizeSelector, DYNAMIC_STATE_PSEUDO_CLASSES } from "./css-selector-sanitizer.js";
27
+ import { sanitizeSelector, matchUnqueryablePseudoClass } from "./css-selector-sanitizer.js";
28
28
 
29
29
  const DEBUG = false;
30
30
 
31
- const CANONICAL_PSEUDO_ELEMENT_NAMES = new Set(["after", "before", "first-letter", "first-line", "placeholder", "selection", "part", "marker"]);
32
- const CONDITIONAL_AT_RULE_NAMES = new Set(["media", "supports", "container"]);
31
+ const PSEUDO_ELEMENT_SYNONYMS = new Set(["after", "before", "first-letter", "first-line"]);
32
+ const MEDIA_AT_RULE_NAME = "media";
33
+ const SUPPORTS_AT_RULE_NAME = "supports";
34
+ const CONDITIONAL_AT_RULE_NAMES = new Set([MEDIA_AT_RULE_NAME, SUPPORTS_AT_RULE_NAME, "container"]);
33
35
  const RULE_TYPE = "Rule";
34
36
  const AT_RULE_TYPE = "Atrule";
35
37
  const NESTING_SELECTOR_TYPE = "NestingSelector";
@@ -51,7 +53,6 @@ const SELECTOR_CONTEXT = "selector";
51
53
  const DECLARATION_LIST_CONTEXT = "declarationList";
52
54
  const PARSE_CSS_ERROR_MESSAGE = "Failed to parse CSS";
53
55
  const QSA_ERROR_MESSAGE = "Failed to match selector";
54
- const ROOT_PSEUDO_CLASS = ":root";
55
56
  const PRELUDE_SEPARATOR = ",";
56
57
  const NESTING_SELECTOR = "&";
57
58
  const VENDOR_PREFIX = "-";
@@ -84,9 +85,8 @@ function process(doc, stylesheets) {
84
85
  selectorData: new Map(),
85
86
  selectorTexts: new Map(),
86
87
  preludeTexts: new Map(),
87
- scopeRoots: new Map(),
88
- scopeSpecificities: new Map(),
89
88
  rulesCounter: 0,
89
+ scopeIdCounter: 0
90
90
  };
91
91
  collectLayerOrder(stylesheets, docContext);
92
92
  buildEffectiveLayerOrder(docContext);
@@ -124,10 +124,11 @@ function minifyRules(stylesheets, docContext) {
124
124
  stylesheets.forEach((stylesheetInfo, key) => {
125
125
  if (!stylesheetInfo.scoped && stylesheetInfo.stylesheet && !key.urlNode) {
126
126
  if (hasChildNodes(stylesheetInfo.stylesheet)) {
127
- const topConditionalStack = stylesheetInfo.mediaText ? [{ name: "media", prelude: stylesheetInfo.mediaText }] : [];
127
+ const topConditionalStack = stylesheetInfo.mediaText ? [{ name: MEDIA_AT_RULE_NAME, prelude: stylesheetInfo.mediaText }] : [];
128
128
  minifyStylesheetRules(stylesheetInfo.stylesheet.children, stylesheets, {
129
129
  ancestorsSelectors: [],
130
130
  layerStack: [],
131
+ scopeStack: [],
131
132
  conditionalStack: topConditionalStack
132
133
  }, docContext);
133
134
  }
@@ -218,12 +219,12 @@ function minifyRule(ruleData, cssRule, stylesheets, processingContext, removedRu
218
219
 
219
220
  function minifyImportRule(ruleData, _cssRule, stylesheets, processingContext, _removedRules, docContext) {
220
221
  const urlNode = ruleData.prelude.children.head.data;
221
- const topConditionalStack = urlNode.importedMediaText ? [{ name: "media", prelude: urlNode.importedMediaText }] : [];
222
+ const topConditionalStack = urlNode.importedMediaText ? [{ name: MEDIA_AT_RULE_NAME, prelude: urlNode.importedMediaText }] : [];
222
223
  if (urlNode.importedLayerName !== undefined) {
223
- topConditionalStack.push({ name: "layer", prelude: urlNode.importedLayerName });
224
+ topConditionalStack.push({ name: LAYER_NAME, prelude: urlNode.importedLayerName });
224
225
  }
225
226
  if (urlNode.importedSupportsCondition !== undefined) {
226
- topConditionalStack.push({ name: "supports", prelude: urlNode.importedSupportsCondition });
227
+ topConditionalStack.push({ name: SUPPORTS_AT_RULE_NAME, prelude: urlNode.importedSupportsCondition });
227
228
  }
228
229
  minifyStylesheetRules(urlNode.importedChildren, stylesheets, {
229
230
  ...processingContext,
@@ -243,16 +244,24 @@ function minifyLayerRule(ruleData, cssRule, stylesheets, processingContext, remo
243
244
  }
244
245
 
245
246
  function minifyScopeRule(ruleData, cssRule, stylesheets, processingContext, removedRules, docContext) {
246
- const parsedPrelude = parsePrelude(ruleData.prelude);
247
- const includeLists = parsedPrelude.include.map(item => item.text);
248
- const excludeLists = parsedPrelude.exclude.map(item => item.text);
249
- const newConditionalStack = buildConditionalStack(processingContext.conditionalStack, ruleData, docContext);
247
+ let scopeContext;
248
+ try {
249
+ const parsedPrelude = parsePrelude(ruleData.prelude);
250
+ scopeContext = buildScopeContext(parsedPrelude, processingContext, docContext);
251
+ } catch (error) {
252
+ if (DEBUG) {
253
+ // eslint-disable-next-line no-console
254
+ console.error(PARSE_CSS_ERROR_MESSAGE, { ruleData, error });
255
+ }
256
+ }
257
+ if (!scopeContext) {
258
+ docContext.stats.discarded++;
259
+ removedRules.add(cssRule);
260
+ return;
261
+ }
250
262
  const newProcessingContext = {
251
263
  ...processingContext,
252
- conditionalStack: newConditionalStack,
253
- scopeIncludeLists: [...(processingContext.scopeIncludeLists || []), includeLists],
254
- scopeExclusionLists: [...(processingContext.scopeExclusionLists || []), excludeLists],
255
- scopeNestingLevel: (processingContext.scopeNestingLevel || 0) + 1
264
+ scopeStack: [...(processingContext.scopeStack || []), scopeContext]
256
265
  };
257
266
  expandRawCssRules(ruleData);
258
267
  minifyStylesheetRules(ruleData.block.children, stylesheets, newProcessingContext, docContext);
@@ -262,6 +271,67 @@ function minifyScopeRule(ruleData, cssRule, stylesheets, processingContext, remo
262
271
  }
263
272
  }
264
273
 
274
+ function buildScopeContext(parsedPrelude, processingContext, docContext) {
275
+ const scopeStack = processingContext.scopeStack || [];
276
+ const includeSelectors = parsedPrelude && parsedPrelude.include ? parsedPrelude.include : [];
277
+ let rootElements = [];
278
+ if (includeSelectors.length) {
279
+ rootElements = collectScopeRootElements(includeSelectors, scopeStack, docContext);
280
+ } else if (scopeStack.length) {
281
+ rootElements = Array.from(scopeStack[scopeStack.length - 1].rootElements);
282
+ } else {
283
+ rootElements = getDefaultScopeRoots(docContext);
284
+ }
285
+ const uniqueRoots = Array.from(new Set(rootElements.filter(Boolean)));
286
+ if (!uniqueRoots.length) {
287
+ return null;
288
+ }
289
+ const boundaryElements = collectScopeBoundaryElements(parsedPrelude.exclude || [], uniqueRoots, docContext);
290
+ return {
291
+ id: docContext.scopeIdCounter++,
292
+ rootElements: new Set(uniqueRoots),
293
+ stopElements: boundaryElements,
294
+ };
295
+ }
296
+
297
+ function collectScopeRootElements(includeSelectors, scopeStack, docContext) {
298
+ const roots = new Set();
299
+ includeSelectors.forEach(selectorInfo => {
300
+ const selectorText = sanitizeSelector(selectorInfo, null, docContext);
301
+ const matchedNodes = querySelectorAll(docContext.doc, selectorText);
302
+ filterElementsByScopes(matchedNodes, scopeStack).forEach(match => roots.add(match));
303
+ });
304
+ return Array.from(roots);
305
+ }
306
+
307
+ function collectScopeBoundaryElements(excludeSelectors, rootElements, docContext) {
308
+ const boundaries = new Set();
309
+ if (!excludeSelectors.length || !rootElements.length) {
310
+ return boundaries;
311
+ }
312
+ excludeSelectors.forEach(selectorInfo => {
313
+ const selectorText = sanitizeSelector(selectorInfo, null, docContext);
314
+ rootElements.forEach(root => {
315
+ matchSelectorWithinRoot(root, selectorText).forEach(node => boundaries.add(node));
316
+ });
317
+ });
318
+ return boundaries;
319
+ }
320
+
321
+ function getDefaultScopeRoots(docContext) {
322
+ const roots = [];
323
+ if (docContext.doc && docContext.doc.documentElement) {
324
+ roots.push(docContext.doc.documentElement);
325
+ }
326
+ if (!roots.length && docContext.doc && docContext.doc.body) {
327
+ roots.push(docContext.doc.body);
328
+ }
329
+ if (!roots.length && docContext.doc && docContext.doc.children) {
330
+ roots.push(...Array.from(docContext.doc.children).filter(node => node.nodeType === 1));
331
+ }
332
+ return roots;
333
+ }
334
+
265
335
  function minifyAtRule(ruleData, cssRule, stylesheets, processingContext, removedRules, docContext) {
266
336
  const newConditionalStack = buildConditionalStack(processingContext.conditionalStack, ruleData, docContext);
267
337
  const newProcessingContext = { ...processingContext, conditionalStack: newConditionalStack };
@@ -288,16 +358,17 @@ function processSelectors(ruleData, processingContext, docContext) {
288
358
  for (let selector = ruleData.prelude.children.head, selectorIndex = 0; selector; selector = selector.next, selectorIndex++) {
289
359
  const {
290
360
  startsWithCombinator,
291
- hasCanonicalPseudoElement,
292
- hasDynamicStatePseudoClass,
293
- scopeRelative
361
+ hasUnqueryableSelector
294
362
  } = analyzeSelector(selector.data);
295
- registerSelector(selector, ruleData, scopeRelative, processingContext, docContext);
296
- if (!startsWithCombinator || !ancestorsSelectors || !ancestorsSelectors.length) {
297
- const matchedElements = matchElements(selector, ancestorsSelectors, docContext);
298
- if (matchedElements.length && !(hasCanonicalPseudoElement || hasDynamicStatePseudoClass)) {
363
+ if (hasUnqueryableSelector) {
364
+ ruleData.hasUnqueryableSelector = true;
365
+ }
366
+ registerSelector(selector, ruleData, processingContext, docContext);
367
+ if (!hasUnqueryableSelector && (!startsWithCombinator || !ancestorsSelectors || !ancestorsSelectors.length)) {
368
+ const matchedElements = matchElements(selector, ancestorsSelectors, processingContext.scopeStack, docContext);
369
+ if (matchedElements.length) {
299
370
  updateMatchingSelectors(matchedElements, selector, docContext);
300
- } else if (!matchedElements.length) {
371
+ } else {
301
372
  removedSelectors.push(selector);
302
373
  }
303
374
  }
@@ -306,31 +377,22 @@ function processSelectors(ruleData, processingContext, docContext) {
306
377
  }
307
378
 
308
379
  function analyzeSelector(selector) {
309
- let hasCanonicalPseudoElement = false;
310
- let hasDynamicStatePseudoClass = false;
311
- let hasNestingOrScope = false;
380
+ let hasUnqueryableSelector = false;
312
381
  let startsWithCombinator = false;
313
382
  cssTree.walk(selector, {
314
383
  enter(node) {
315
384
  if (node.type === PSEUDO_ELEMENT_SELECTOR_TYPE) {
316
- hasCanonicalPseudoElement = true;
385
+ hasUnqueryableSelector = true;
317
386
  } else if (node.type === PSEUDO_CLASS_SELECTOR_TYPE) {
318
- if (CANONICAL_PSEUDO_ELEMENT_NAMES.has(node.name)) {
319
- hasCanonicalPseudoElement = true;
320
- } else if (DYNAMIC_STATE_PSEUDO_CLASSES.includes(node.name)) {
321
- hasDynamicStatePseudoClass = true;
322
- } else if (node.name === SCOPE_NAME) {
323
- hasNestingOrScope = true;
387
+ if (PSEUDO_ELEMENT_SYNONYMS.has(node.name) || matchUnqueryablePseudoClass(node)) {
388
+ hasUnqueryableSelector = true;
324
389
  }
325
- } else if (node.type === NESTING_SELECTOR_TYPE) {
326
- hasNestingOrScope = true;
327
390
  }
328
391
  }
329
392
  });
330
393
  const firstChild = selector.children.head.data;
331
394
  startsWithCombinator = firstChild && firstChild.type === COMBINATOR_NAME;
332
- const scopeRelative = !startsWithCombinator && !hasNestingOrScope;
333
- return { hasCanonicalPseudoElement, hasDynamicStatePseudoClass, startsWithCombinator, scopeRelative };
395
+ return { hasUnqueryableSelector, startsWithCombinator };
334
396
  }
335
397
 
336
398
  function updateMatchingSelectors(matchedElements, selector, docContext) {
@@ -352,24 +414,18 @@ function processNestedRules(ruleData, stylesheets, processingContext, docContext
352
414
  minifyStylesheetRules(ruleData.block.children, stylesheets, newProcessingContext, docContext);
353
415
  }
354
416
 
355
- function registerSelector(selector, ruleData, scopeRelative, processingContext, docContext) {
417
+ function registerSelector(selector, ruleData, processingContext, docContext) {
356
418
  const {
357
419
  ancestorsSelectors,
358
420
  layerStack,
359
- conditionalStack,
360
- scopeIncludeLists,
361
- scopeExclusionLists,
362
- scopeNestingLevel
421
+ scopeStack,
422
+ conditionalStack
363
423
  } = processingContext;
364
424
  docContext.selectorData.set(selector, {
365
- specificity: computeMaxSpecificity(selector.data, ancestorsSelectors),
425
+ specificity: computeMaxSpecificity(selector.data, ancestorsSelectors, scopeStack),
366
426
  rule: ruleData,
367
427
  layerStack,
368
- conditionalStack,
369
- scopeIncludeLists,
370
- scopeExclusionLists,
371
- scopeNestingLevel,
372
- scopeRelative
428
+ conditionalStack
373
429
  });
374
430
  }
375
431
 
@@ -414,35 +470,36 @@ function collectDeclarationItemsForElement(element, docContext) {
414
470
  for (let declaration = declarations.head; declaration; declaration = declaration.next) {
415
471
  const { type, value } = declaration.data;
416
472
  if (type === DECLARATION_TYPE && value) {
417
- const isRawValue = value.type === RAW_TYPE;
418
- const isSingleValue = value.type === VALUE_TYPE &&
419
- hasChildNodes(value) &&
420
- value.children.length == 1 &&
421
- value.children.head.data.name;
422
- const isVendorValue = isSingleValue && value.children.head.data.name.startsWith(VENDOR_PREFIX);
423
- const isInvalidValue = isSingleValue && INVALID_CSS_ESCAPE_TEST.test(value.children.head.data.name);
424
- if (!isRawValue && !isVendorValue && !isInvalidValue) {
425
- allDeclarations.push({
426
- declaration,
427
- selector,
428
- effectiveSpecificity: computeEffectiveSpecificity(
429
- docContext.selectorData.get(selector), element, docContext),
430
- isInline: false
431
- });
432
- }
473
+ addDeclaration(declaration, docContext.selectorData.get(selector).specificity, false, selector);
433
474
  }
434
475
  }
435
476
  }
436
477
  });
437
478
  const inlineDeclarations = getInlineStyleDeclarations(element);
438
479
  for (const declaration of inlineDeclarations) {
439
- allDeclarations.push({
440
- declaration: declaration.declaration,
441
- effectiveSpecificity: declaration.effectiveSpecificity,
442
- isInline: true
443
- });
480
+ addDeclaration(declaration.declaration, declaration.specificity, true);
444
481
  }
445
482
  return allDeclarations;
483
+
484
+ function addDeclaration(declaration, specificity, isInline, selector) {
485
+ const { value } = declaration.data;
486
+ const isRawValue = value.type === RAW_TYPE;
487
+ const hasValueChildNodes = hasChildNodes(value) || value.type === RAW_TYPE;
488
+ const isSingleValue = value.type === VALUE_TYPE &&
489
+ hasValueChildNodes &&
490
+ value.children.length == 1 &&
491
+ value.children.head.data.name;
492
+ const isVendorValue = isSingleValue && value.children.head.data.name.startsWith(VENDOR_PREFIX);
493
+ const isInvalidValue = isSingleValue && INVALID_CSS_ESCAPE_TEST.test(value.children.head.data.name);
494
+ if (hasValueChildNodes && !isRawValue && !isVendorValue && !isInvalidValue) {
495
+ allDeclarations.push({
496
+ declaration,
497
+ selector,
498
+ specificity,
499
+ isInline
500
+ });
501
+ }
502
+ }
446
503
  }
447
504
 
448
505
  function getConditionalStackForSelector(selector, docContext) {
@@ -456,132 +513,169 @@ function getConditionalStackForSelector(selector, docContext) {
456
513
  return conditionalStack;
457
514
  }
458
515
 
459
- function matchElements(selector, ancestorsSelectors, docContext) {
516
+ function matchElements(selector, ancestorsSelectors, scopeStack, docContext) {
460
517
  const selectorText = createSelectorText(selector, ancestorsSelectors, docContext);
461
- const selectorData = docContext.selectorData.get(selector);
462
- const hasScope = selectorData && ((selectorData.scopeIncludeLists && selectorData.scopeIncludeLists.length) || selectorData.scopeNestingLevel > 0);
463
- const cacheKey = createMatchCacheKey(hasScope, selectorData, selectorText);
464
- const cached = docContext.matchedSelectors.get(cacheKey);
465
- if (cached) {
466
- return cached;
518
+ const cacheKey = createScopeCacheKey(selectorText, scopeStack);
519
+ const cachedNodes = docContext.matchedSelectors.get(cacheKey);
520
+ if (cachedNodes) {
521
+ return cachedNodes;
522
+ }
523
+ let nodes;
524
+ if (scopeStack && scopeStack.length) {
525
+ nodes = matchElementsInScope(selectorText, scopeStack);
526
+ nodes = filterElementsByScopes(nodes, scopeStack);
467
527
  } else {
468
- if (hasScope) {
469
- return collectScopedMatches(cacheKey, selector, docContext);
470
- } else {
471
- const nodes = querySelectorAll(docContext.doc, selectorText, docContext.scopeRoots);
472
- docContext.matchedSelectors.set(cacheKey, nodes);
473
- return nodes;
474
- }
528
+ nodes = querySelectorAll(docContext.doc, selectorText);
475
529
  }
530
+ docContext.matchedSelectors.set(cacheKey, nodes);
531
+ return nodes;
476
532
  }
477
533
 
478
- function createSelectorText(selector, ancestorsSelectors, docContext) {
479
- let selectorText;
480
- if (ancestorsSelectors && ancestorsSelectors.length) {
481
- selectorText = combineSelectorWithAncestors(selector.data, ancestorsSelectors, docContext);
482
- const combinedAst = parseCss(selectorText, SELECTOR_LIST_CONTEXT);
483
- selectorText = sanitizeSelector({ data: combinedAst }, ancestorsSelectors, docContext);
534
+ function createScopeCacheKey(selectorText, scopeStack) {
535
+ if (!scopeStack || !scopeStack.length) {
536
+ return selectorText;
484
537
  }
485
- if (!selectorText) {
486
- selectorText = sanitizeSelector(selector, ancestorsSelectors, docContext);
538
+ const signature = scopeStack.map(scope => scope.id).join(CONTEXT_KEY_SEPARATOR);
539
+ return `${selectorText}${CONTEXT_KEY_SEPARATOR}${signature}`;
540
+ }
541
+
542
+ function matchElementsInScope(selectorText, scopeStack) {
543
+ const currentScope = scopeStack[scopeStack.length - 1];
544
+ const roots = Array.from(currentScope.rootElements);
545
+ if (!roots.length) {
546
+ return [];
487
547
  }
488
- return selectorText;
548
+ const matchedNodes = new Set();
549
+ roots.forEach(root => {
550
+ matchSelectorWithinRoot(root, selectorText).forEach(node => matchedNodes.add(node));
551
+ });
552
+ return Array.from(matchedNodes);
489
553
  }
490
554
 
491
- function createMatchCacheKey(hasScope, selectorData, selectorText) {
492
- if (hasScope) {
493
- const include = selectorData.scopeIncludeLists || [];
494
- const exclude = selectorData.scopeExclusionLists || [];
495
- const relative = selectorData.scopeRelative ? 1 : 0;
496
- const nesting = selectorData.scopeNestingLevel || 0;
497
- return [
498
- selectorText,
499
- JSON.stringify(include),
500
- JSON.stringify(exclude), String(relative), String(nesting)
501
- ].join(CONTEXT_KEY_SEPARATOR);
502
- } else {
503
- return selectorText;
555
+ function matchSelectorWithinRoot(root, selectorText) {
556
+ const matchedNodes = new Set();
557
+ if (!root || root.nodeType !== 1) {
558
+ return matchedNodes;
559
+ }
560
+ if (matches(root, selectorText)) {
561
+ matchedNodes.add(root);
562
+ }
563
+ let nodes;
564
+ try {
565
+ nodes = root.querySelectorAll(selectorText);
566
+ } catch {
567
+ if (DEBUG) {
568
+ // eslint-disable-next-line no-console
569
+ console.error(QSA_ERROR_MESSAGE, { root, selectorText });
570
+ }
571
+ nodes = matchByTraversal([root], selectorText, true);
504
572
  }
573
+ for (const node of nodes) {
574
+ matchedNodes.add(node);
575
+ }
576
+ return Array.from(matchedNodes);
505
577
  }
506
578
 
507
- function collectScopedMatches(cacheKey, selector, docContext) {
508
- const selectorData = docContext.selectorData.get(selector);
509
- const includeLists = selectorData.scopeIncludeLists && selectorData.scopeIncludeLists.length ? selectorData.scopeIncludeLists[selectorData.scopeIncludeLists.length - 1] : [];
510
- const excludeLists = selectorData.scopeExclusionLists && selectorData.scopeExclusionLists.length ? selectorData.scopeExclusionLists[selectorData.scopeExclusionLists.length - 1] : [];
511
- const matchedSet = new Set();
512
- const includes = includeLists.length ? includeLists : [ROOT_PSEUDO_CLASS];
513
- for (const includeSelector of includes) {
514
- collectMatchesForInclude(includeSelector, selector, excludeLists, docContext, matchedSet);
579
+ function matches(element, selectorText) {
580
+ if (!element || element.nodeType !== 1) {
581
+ return false;
582
+ }
583
+ try {
584
+ return element.matches(selectorText);
585
+ } catch {
586
+ return false;
515
587
  }
516
- const matchedElements = Array.from(matchedSet);
517
- docContext.matchedSelectors.set(cacheKey, matchedElements);
518
- return matchedElements;
519
588
  }
520
589
 
521
- function collectMatchesForInclude(includeSelector, selector, excludeLists, docContext, matchedSet) {
522
- const rootsForInclude = getScopeRoots(includeSelector, docContext);
523
- for (const rootForInclude of rootsForInclude) {
524
- const roots = querySelectorForRoot(rootForInclude, normalizeForRoot(selector), docContext.scopeRoots);
525
- if (roots.length) {
526
- if (excludeLists && excludeLists.length) {
527
- const filteredRoots = filterExcludedRoots(roots, excludeLists, docContext);
528
- filteredRoots.forEach(root => matchedSet.add(root));
529
- } else {
530
- roots.forEach(root => matchedSet.add(root));
531
- }
590
+ function matchByTraversal(roots, selectorText, skipFirst) {
591
+ const results = [];
592
+ const visited = new Set();
593
+ const stack = [];
594
+ roots.forEach(root => {
595
+ if (root && root.nodeType === 1) {
596
+ stack.push({ node: root, include: !skipFirst });
597
+ }
598
+ });
599
+ while (stack.length) {
600
+ const { node, include } = stack.pop();
601
+ if (!node || visited.has(node)) {
602
+ continue;
603
+ }
604
+ visited.add(node);
605
+ if (include && matches(node, selectorText)) {
606
+ results.push(node);
607
+ }
608
+ for (let child = node.firstElementChild; child; child = child.nextElementSibling) {
609
+ stack.push({ node: child, include: true });
532
610
  }
533
611
  }
612
+ return results;
534
613
  }
535
614
 
536
- function querySelectorForRoot(root, selector, cache) {
537
- const nodes = querySelectorAll(root, selector, cache);
538
- if (root.matches && root.matches(selector)) {
539
- if (nodes.indexOf(root) === -1) {
540
- nodes.unshift(root);
615
+ function getTraversalRoots(root) {
616
+ if (!root) {
617
+ return [];
618
+ }
619
+ if (root.nodeType === 9 || root.nodeType === 11) {
620
+ const roots = [];
621
+ if (root.documentElement) {
622
+ roots.push(root.documentElement);
623
+ }
624
+ if (root.body && (!roots.length || root.body !== roots[0])) {
625
+ roots.push(root.body);
541
626
  }
627
+ if (!roots.length && root.children) {
628
+ roots.push(...Array.from(root.children).filter(node => node.nodeType === 1));
629
+ }
630
+ return roots;
542
631
  }
543
- return nodes;
632
+ return [root];
544
633
  }
545
634
 
546
- function normalizeForRoot(selector) {
547
- const selectorData = cssTree.clone(selector.data);
548
- cssTree.walk(selectorData, {
549
- visit: NESTING_SELECTOR_TYPE,
550
- enter(_node, item, list) {
551
- const scope = { type: PSEUDO_CLASS_SELECTOR_TYPE, name: SCOPE_NAME };
552
- list.insertData(scope, item);
553
- list.remove(item);
554
- }
555
- });
556
- for (let selectorChild = selectorData.children.head; selectorChild; selectorChild = selectorChild.next) {
557
- const childData = selectorChild.data;
558
- if (hasChildNodes(childData)) {
559
- const head = childData.children.head;
560
- const headData = head.data;
561
- if (headData && headData.type === COMBINATOR_NAME) {
562
- const scope = { type: PSEUDO_CLASS_SELECTOR_TYPE, name: SCOPE_NAME };
563
- childData.children.insertData(scope, head);
564
- }
635
+ function filterElementsByScopes(elements, scopeStack) {
636
+ if (!scopeStack || !scopeStack.length) {
637
+ return elements;
638
+ }
639
+ return elements.filter(element => isElementWithinScopes(element, scopeStack));
640
+ }
641
+
642
+ function isElementWithinScopes(element, scopeStack) {
643
+ if (!element) {
644
+ return false;
645
+ }
646
+ for (let index = 0; index < scopeStack.length; index++) {
647
+ if (!isElementWithinScope(element, scopeStack[index])) {
648
+ return false;
565
649
  }
566
650
  }
567
- return cssTree.generate(selectorData);
651
+ return true;
568
652
  }
569
653
 
570
- function getScopeRoots(selector, docContext) {
571
- let roots = docContext.scopeRoots.get(selector);
572
- if (!roots) {
573
- roots = querySelectorAll(docContext.doc, selector, docContext.scopeRoots);
654
+ function isElementWithinScope(element, scopeContext) {
655
+ let current = element;
656
+ while (current && current.nodeType === 1) {
657
+ if (scopeContext.stopElements && scopeContext.stopElements.has(current)) {
658
+ return false;
659
+ }
660
+ if (scopeContext.rootElements.has(current)) {
661
+ return true;
662
+ }
663
+ current = current.parentElement;
574
664
  }
575
- return roots;
665
+ return false;
576
666
  }
577
667
 
578
- function filterExcludedRoots(roots, excludeLists, docContext) {
579
- const excludeRoots = new Set();
580
- for (const excludeSelector of excludeLists) {
581
- const rootsForExclude = getScopeRoots(excludeSelector, docContext);
582
- rootsForExclude.forEach(root => excludeRoots.add(root));
668
+ function createSelectorText(selector, ancestorsSelectors, docContext) {
669
+ let selectorText;
670
+ if (ancestorsSelectors && ancestorsSelectors.length) {
671
+ selectorText = combineSelectorWithAncestors(selector.data, ancestorsSelectors, docContext);
672
+ const combinedAst = parseCss(selectorText, SELECTOR_LIST_CONTEXT);
673
+ selectorText = sanitizeSelector({ data: combinedAst }, ancestorsSelectors, docContext);
674
+ }
675
+ if (!selectorText) {
676
+ selectorText = sanitizeSelector(selector, ancestorsSelectors, docContext);
583
677
  }
584
- return roots.filter(node => !Array.from(excludeRoots).some(excludedRoot => excludedRoot.contains(node)));
678
+ return selectorText;
585
679
  }
586
680
 
587
681
  function compareDeclarations(declarationA, declarationB, docContext) {
@@ -599,8 +693,8 @@ function compareDeclarations(declarationA, declarationB, docContext) {
599
693
  if (layerComparison !== 0) {
600
694
  return importantA ? -layerComparison : layerComparison;
601
695
  }
602
- const specificityA = declarationA.effectiveSpecificity;
603
- const specificityB = declarationB.effectiveSpecificity;
696
+ const specificityA = declarationA.specificity;
697
+ const specificityB = declarationB.specificity;
604
698
  if (specificityA.a !== specificityB.a) {
605
699
  return specificityA.a - specificityB.a;
606
700
  }
@@ -615,8 +709,8 @@ function compareDeclarations(declarationA, declarationB, docContext) {
615
709
  }
616
710
  return 0;
617
711
  } else {
618
- const specificityA = declarationA.effectiveSpecificity;
619
- const specificityB = declarationB.effectiveSpecificity;
712
+ const specificityA = declarationA.specificity;
713
+ const specificityB = declarationB.specificity;
620
714
  if (specificityA.a !== specificityB.a) {
621
715
  return specificityA.a - specificityB.a;
622
716
  }
@@ -718,7 +812,7 @@ function removeLosingDeclarations(winningDeclarations, docContext) {
718
812
  if (declaration.data.type === DECLARATION_TYPE) {
719
813
  allDeclarations.set(declaration, declarations);
720
814
  const { property, value } = declaration.data;
721
- if (property && property.startsWith(CUSTOM_PROPERTY_PREFIX) || (value && value.type === RAW_TYPE)) {
815
+ if (property && property.startsWith(CUSTOM_PROPERTY_PREFIX) || (value && value.type === RAW_TYPE) || cssRule.hasUnqueryableSelector) {
722
816
  protectedDeclarations.add(declaration);
723
817
  }
724
818
  }
@@ -822,36 +916,6 @@ function combineSelectors(parentSelectorText, childSelectorText) {
822
916
  return cssTree.generate(combinedSelector);
823
917
  }
824
918
 
825
- function computeEffectiveSpecificity(selectorData, element, docContext) {
826
- const baseSpecificity = selectorData.specificity;
827
- let effectiveSpecificity = { a: baseSpecificity.a, b: baseSpecificity.b, c: baseSpecificity.c };
828
- const includeLists = selectorData && selectorData.scopeIncludeLists && selectorData.scopeIncludeLists.length ? selectorData.scopeIncludeLists[selectorData.scopeIncludeLists.length - 1] : [];
829
- if (includeLists && includeLists.length) {
830
- for (const includeSelector of includeLists) {
831
- const roots = getScopeRoots(includeSelector, docContext);
832
- if (roots.some(root => root.contains(element))) {
833
- const includeSpecificity = getIncludeSpecificity(includeSelector, docContext);
834
- effectiveSpecificity = {
835
- a: effectiveSpecificity.a + includeSpecificity.a,
836
- b: effectiveSpecificity.b + includeSpecificity.b,
837
- c: effectiveSpecificity.c + includeSpecificity.c
838
- };
839
- }
840
- }
841
- }
842
- return effectiveSpecificity;
843
- }
844
-
845
- function getIncludeSpecificity(includeSelector, docContext) {
846
- let specificity = docContext.scopeSpecificities.get(includeSelector);
847
- if (!specificity) {
848
- const selector = parseCss(includeSelector);
849
- specificity = computeMaxSpecificity(selector, []);
850
- docContext.scopeSpecificities.set(includeSelector, specificity);
851
- }
852
- return specificity;
853
- }
854
-
855
919
  function hasChildNodes(node) {
856
920
  return Boolean(node && node.children && node.children.head);
857
921
  }
@@ -883,42 +947,39 @@ function parseCss(text, context = SELECTOR_CONTEXT) {
883
947
  return cssTree.parse(text, options);
884
948
  }
885
949
 
886
- function querySelectorAll(root, selector, cache) {
887
- if (cache && cache !== root) {
888
- let rootCache = cache.get(root);
889
- if (!rootCache) {
890
- rootCache = new Map();
891
- cache.set(root, rootCache);
892
- }
893
- if (rootCache.has(selector)) {
894
- return rootCache.get(selector);
895
- } else {
896
- try {
897
- const nodes = Array.from(root.querySelectorAll(selector));
898
- rootCache.set(selector, nodes);
899
- return nodes;
900
- } catch {
901
- if (DEBUG) {
902
- // eslint-disable-next-line no-console
903
- console.warn(QSA_ERROR_MESSAGE, selector, root.tagName ? root.tagName : EMPTY_STRING);
904
- }
905
- rootCache.set(selector, []);
906
- return [];
907
- }
908
- }
909
- } else {
910
- try {
911
- return Array.from(root.querySelectorAll(selector));
912
- } catch {
913
- if (DEBUG) {
914
- // eslint-disable-next-line no-console
915
- console.warn(QSA_ERROR_MESSAGE, selector);
916
- }
917
- return [];
950
+ function querySelectorAll(root, selectorText) {
951
+ if (!root) {
952
+ return [];
953
+ }
954
+ const isDocumentNode = root.nodeType === 9 || root.nodeType === 11;
955
+ const hasScopePseudo = Boolean(selectorText && selectorText.indexOf(":scope") !== -1);
956
+ if (isDocumentNode && hasScopePseudo) {
957
+ return matchDocumentScopeSelector(root, selectorText);
958
+ }
959
+ try {
960
+ return Array.from(root.querySelectorAll(selectorText));
961
+ } catch {
962
+ if (DEBUG) {
963
+ // eslint-disable-next-line no-console
964
+ console.warn(QSA_ERROR_MESSAGE, selectorText, root.nodeType === 1 && root.tagName ? root.tagName : EMPTY_STRING);
918
965
  }
966
+ const traversalRoots = getTraversalRoots(root);
967
+ return matchByTraversal(traversalRoots, selectorText, false);
919
968
  }
920
969
  }
921
970
 
971
+ function matchDocumentScopeSelector(root, selectorText) {
972
+ const traversalRoots = getTraversalRoots(root);
973
+ if (!traversalRoots.length) {
974
+ return [];
975
+ }
976
+ const matchedNodes = new Set();
977
+ traversalRoots.forEach(scopeRoot => {
978
+ matchSelectorWithinRoot(scopeRoot, selectorText).forEach(node => matchedNodes.add(node));
979
+ });
980
+ return Array.from(matchedNodes);
981
+ }
982
+
922
983
  function getInlineStyleDeclarations(element) {
923
984
  const style = element.getAttribute(STYLE_ATTRIBUTE_NAME);
924
985
  if (style) {
@@ -933,7 +994,7 @@ function getInlineStyleDeclarations(element) {
933
994
  if (node.data.type === DECLARATION_TYPE) {
934
995
  declarations.push({
935
996
  declaration: node,
936
- effectiveSpecificity: { a: 1, b: 0, c: 0 },
997
+ specificity: { a: 1, b: 0, c: 0 },
937
998
  isInline: true
938
999
  });
939
1000
  }
@@ -20,8 +20,7 @@
20
20
  * notice and a URL through which recipients can access the Corresponding
21
21
  * Source.
22
22
  */
23
-
24
- import * as cssTree from "./../vendor/css-tree.js";
23
+ import * as cssTree from "../vendor/css-tree.js";
25
24
 
26
25
  const CANONICAL_PSEUDO_ELEMENT_NAMES = new Set(["after", "before", "first-letter", "first-line", "placeholder", "selection", "part", "marker"]);
27
26
 
@@ -34,52 +33,13 @@ function parsePrelude(prelude) {
34
33
  return { include: [], exclude: [] };
35
34
  }
36
35
 
37
- // Normalize prelude to a string then split on a top-level `to` keyword.
38
- // Using generated string is pragmatic: `to` as an at-rule keyword is expected
39
- // to appear at top-level with surrounding whitespace. We split on whitespace+to+whitespace.
40
- const preludeText = cssTree.generate(prelude).trim();
41
- if (!preludeText) return { include: [], exclude: [] };
42
-
43
- // Split on top-level ' to ' (case-insensitive) — join remaining parts if multiple 'to' appear
44
- const parts = preludeText.split(/\s+to\s+/i);
45
- const includeText = parts[0].trim();
46
- const excludeText = parts.length > 1 ? parts.slice(1).join(" to ").trim() : "";
47
-
48
- function parseSelectorList(text) {
49
- if (!text) return [];
50
- // Strip balanced outer parentheses that css-tree may produce in generated preludes
51
- function stripOuterParens(s) {
52
- let str = s.trim();
53
- while (str.length >= 2 && str[0] === "(" && str[str.length - 1] === ")") {
54
- // ensure they are balanced pairs for the whole string
55
- let depth = 0;
56
- let balanced = true;
57
- for (let i = 0; i < str.length; i++) {
58
- if (str[i] === "(") depth++;
59
- else if (str[i] === ")") depth--;
60
- if (depth === 0 && i < str.length - 1) { balanced = false; break; }
61
- }
62
- if (!balanced) break;
63
- str = str.substring(1, str.length - 1).trim();
64
- }
65
- return str;
66
- }
67
-
68
- const cleaned = stripOuterParens(text);
69
- // css-tree expects a selectorList context
70
- const ast = cssTree.parse(cleaned, { context: "selectorList" });
71
- const selectors = [];
72
- if (ast && ast.children) {
73
- for (let node = ast.children.head; node; node = node.next) {
74
- const sel = node.data;
75
- selectors.push({ ast: sel, text: cssTree.generate(sel) });
76
- }
77
- }
78
- return selectors;
36
+ const scopeNode = findScopeNode(prelude);
37
+ if (!scopeNode) {
38
+ return { include: [], exclude: [] };
79
39
  }
80
40
 
81
- const include = parseSelectorList(includeText);
82
- const exclude = parseSelectorList(excludeText);
41
+ const include = extractSelectorList(scopeNode.root);
42
+ const exclude = extractSelectorList(scopeNode.limit);
83
43
 
84
44
  // Validate: pseudo-elements are not allowed in scope start/end selectors
85
45
  function containsPseudoElement(selectorAst) {
@@ -105,15 +65,39 @@ function parsePrelude(prelude) {
105
65
  }
106
66
 
107
67
  for (const s of include) {
108
- if (containsPseudoElement(s.ast)) {
68
+ if (containsPseudoElement(s.data)) {
109
69
  throw new Error("Pseudo-elements are not allowed in @scope prelude (scope-start)");
110
70
  }
111
71
  }
112
72
  for (const s of exclude) {
113
- if (containsPseudoElement(s.ast)) {
73
+ if (containsPseudoElement(s.data)) {
114
74
  throw new Error("Pseudo-elements are not allowed in @scope prelude (scope-end)");
115
75
  }
116
76
  }
117
77
 
118
78
  return { include, exclude };
119
79
  }
80
+
81
+ function findScopeNode(prelude) {
82
+ if (!prelude || !prelude.children) {
83
+ return null;
84
+ }
85
+ for (let node = prelude.children.head; node; node = node.next) {
86
+ if (node.data && node.data.type === "Scope") {
87
+ return node.data;
88
+ }
89
+ }
90
+ return null;
91
+ }
92
+
93
+ function extractSelectorList(selectorList) {
94
+ if (!selectorList || !selectorList.children) {
95
+ return [];
96
+ }
97
+ const selectors = [];
98
+ for (let node = selectorList.children.head; node; node = node.next) {
99
+ const selector = node.data;
100
+ selectors.push({ data: selector, text: cssTree.generate(selector) });
101
+ }
102
+ return selectors;
103
+ }
@@ -23,70 +23,35 @@
23
23
  */
24
24
 
25
25
  import * as cssTree from "./../vendor/css-tree.js";
26
- const DYNAMIC_STATE_PSEUDO_CLASSES = [
27
- "active-view-transition",
28
- "active-view-transition-type",
29
- "blank",
30
- "buffering",
31
- "current",
32
- "first",
33
- "future",
34
- "has-slotted",
35
- "host-context",
26
+
27
+ const TREE_STRUCTURAL_PSEUDO_CLASSES = [
28
+ "root",
29
+ "empty",
30
+ "first-child",
31
+ "last-child",
32
+ "only-child",
33
+ "first-of-type",
34
+ "last-of-type",
35
+ "only-of-type"
36
+ ];
37
+ const TREE_STRUCTURAL_FUNCTIONAL_PSEUDO_CLASSES = [
38
+ "nth-child",
39
+ "nth-last-child",
36
40
  "heading",
37
- "left",
38
- "muted",
39
- "open",
40
- "past",
41
- "paused",
42
- "picture-in-picture",
43
- "playing",
44
- "right",
45
- "seeking",
46
- "stalled",
47
- "volume-locked",
48
- "after",
49
- "before",
50
- "visited",
51
- "link",
52
- "any-link",
53
- "local-link",
54
- "target",
55
- "scope",
56
- "hover",
57
- "active",
58
- "focus",
59
- "focus-within",
60
- "focus-visible",
61
- "target-current",
62
- "enabled",
63
- "disabled",
64
- "read-only",
65
- "read-write",
66
- "placeholder-shown",
67
- "autofill",
68
- "default",
69
- "checked",
70
- "indeterminate",
71
- "blank",
72
- "valid",
73
- "invalid",
74
- "in-range",
75
- "out-of-range",
76
- "required",
77
- "optional",
78
- "user-valid",
79
- "user-invalid"
41
+ "nth-of-type",
42
+ "nth-last-of-type"
43
+ ];
44
+ const FUNCTIONAL_PSEUDO_CLASSES = [
45
+ "not",
46
+ "is",
47
+ "where",
48
+ "has"
80
49
  ];
81
-
82
50
  export {
83
- DYNAMIC_STATE_PSEUDO_CLASSES,
51
+ matchUnqueryablePseudoClass,
84
52
  sanitizeSelector,
85
53
  };
86
- /**
87
- * Sanitize a selector AST into a QSA-safe selector string.
88
- * Optional `ancestors` array may be provided to expand nesting selectors (`&`).
89
- */
54
+
90
55
  function sanitizeSelector(selector, ancestors, docContext) {
91
56
  if (!docContext.normalizedSelectorText) {
92
57
  docContext.normalizedSelectorText = new WeakMap();
@@ -94,7 +59,7 @@ function sanitizeSelector(selector, ancestors, docContext) {
94
59
  if (docContext.normalizedSelectorText.has(selector)) {
95
60
  return docContext.normalizedSelectorText.get(selector);
96
61
  }
97
- const ast = cssTree.clone(selector.data);
62
+ const ast = cssTree.parse(cssTree.generate(selector.data), { context: "selectorList" });
98
63
  normalizeSelectorNode(ast, ancestors);
99
64
  let normalized = cssTree.generate(ast);
100
65
  if (!normalized || !normalized.trim()) {
@@ -103,6 +68,7 @@ function sanitizeSelector(selector, ancestors, docContext) {
103
68
  docContext.normalizedSelectorText.set(selector, normalized);
104
69
  return normalized;
105
70
  }
71
+
106
72
  function normalizeSelectorNode(selector, ancestors) {
107
73
  let current = selector.children.head;
108
74
  while (current) {
@@ -128,11 +94,28 @@ function normalizeSelectorNode(selector, ancestors) {
128
94
  } else if (childNode.type === "PseudoElementSelector") {
129
95
  selector.children.remove(current);
130
96
  } else if (childNode.type === "PseudoClassSelector") {
131
- const pseudoName = (childNode.name || "").toLowerCase();
132
- if (DYNAMIC_STATE_PSEUDO_CLASSES.includes(pseudoName)) {
133
- selector.children.remove(current);
97
+ if (matchUnqueryablePseudoClass(childNode)) {
98
+ removeNode(selector.children, current);
134
99
  }
100
+ } else if (childNode.type === "Selector") {
101
+ normalizeSelectorNode(childNode, ancestors);
135
102
  }
136
103
  current = next;
137
104
  }
138
105
  }
106
+
107
+ function removeNode(list, item) {
108
+ if (item.prev == null || item.prev.data.type == "Combinator" || item.prev.data.type == "WhiteSpace") {
109
+ list.replace(item, cssTree.parse("*", { context: "selector" }).children.head);
110
+ } else {
111
+ list.remove(item);
112
+ }
113
+ }
114
+
115
+ function matchUnqueryablePseudoClass(pseudoClass) {
116
+ const name = pseudoClass.name.toLowerCase();
117
+ return pseudoClass.children ? (
118
+ !TREE_STRUCTURAL_FUNCTIONAL_PSEUDO_CLASSES.includes(name) &&
119
+ !FUNCTIONAL_PSEUDO_CLASSES.includes(name)
120
+ ) : !TREE_STRUCTURAL_PSEUDO_CLASSES.includes(name);
121
+ }
@@ -139,7 +139,7 @@ function getMaxSpecificityFromList(selectorList) {
139
139
  return maxSpec;
140
140
  }
141
141
 
142
- function computeMaxSpecificity(selector, ancestorsSelectors) {
142
+ function computeMaxSpecificity(selector, ancestorsSelectors, scopeStack) {
143
143
  // If no ancestors provided, keep existing behavior
144
144
  if (!ancestorsSelectors || !ancestorsSelectors.length) {
145
145
  let maxSpecificity = { a: 0, b: 0, c: 0 };
@@ -162,6 +162,9 @@ function computeMaxSpecificity(selector, ancestorsSelectors) {
162
162
  stack.pop();
163
163
  }
164
164
  });
165
+ if (scopeStack && scopeStack.length) {
166
+ maxSpecificity.b++;
167
+ }
165
168
  return maxSpecificity;
166
169
  }
167
170
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "single-file-core",
3
- "version": "1.5.80",
3
+ "version": "1.5.82",
4
4
  "description": "SingleFile Core",
5
5
  "author": "Gildas Lormeau",
6
6
  "license": "AGPL-3.0-or-later",
@@ -29,17 +29,21 @@ import { appendInfobar, refreshInfobarInfo, extractInfobarData } from "./core/in
29
29
 
30
30
  const browser = globalThis.browser;
31
31
  const MutationObserver = globalThis.MutationObserver;
32
+ let mutationObserver;
32
33
  init();
33
34
 
34
35
  function init() {
35
36
  if (globalThis.window == globalThis.top) {
36
- if (document.readyState == "loading") {
37
- document.addEventListener("DOMContentLoaded", displayIcon, false);
37
+ document.addEventListener("single-file-display-infobar", displayIcon, false);
38
+ if (document.documentElement.getAttribute("data-sfz") == "" && !mutationObserver) {
39
+ mutationObserver = new MutationObserver(init).observe(document, { childList: true });
38
40
  } else {
39
- displayIcon();
41
+ if (document.readyState == "loading") {
42
+ document.addEventListener("DOMContentLoaded", displayIcon, false);
43
+ } else {
44
+ displayIcon();
45
+ }
40
46
  }
41
- document.addEventListener("single-file-display-infobar", displayIcon, false);
42
- new MutationObserver(init).observe(document, { childList: true });
43
47
  }
44
48
  if (globalThis.singlefile) {
45
49
  globalThis.singlefile.infobar = {