single-file-core 1.5.81 → 1.5.83

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/core/index.js CHANGED
@@ -678,6 +678,7 @@ class Processor {
678
678
  replaceEmojisInFilename: this.options.replaceEmojisInFilename,
679
679
  compressContent: this.options.compressContent,
680
680
  selfExtractingArchive: this.options.selfExtractingArchive,
681
+ disableCompression: this.options.disableCompression,
681
682
  extractDataFromPage: this.options.extractDataFromPage,
682
683
  referrer: this.options.referrer,
683
684
  title: this.options.title,
@@ -98,8 +98,8 @@ class ProcessorHelperCommon {
98
98
  ["embed[src*=\".svg\"]", "src"],
99
99
  ["video[poster]", "poster"],
100
100
  ["*[background]", "background"],
101
- ["image", "xlink:href"],
102
- ["image", "href"]
101
+ ["image, feImage", "xlink:href"],
102
+ ["image, feImage", "href"]
103
103
  ];
104
104
  if (options.blockImages) {
105
105
  doc.querySelectorAll("svg").forEach(element => element.remove());
@@ -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,44 @@ 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 { property, value } = declaration.data;
486
+ const isRawValue = value.type === RAW_TYPE;
487
+ const hasValueChildNodes = hasChildNodes(value) || value.type === RAW_TYPE;
488
+ let isInvalidValue;
489
+ if (value.type === VALUE_TYPE &&
490
+ hasValueChildNodes &&
491
+ value.children.size == 1) {
492
+ if (value.children.head.data.name) {
493
+ isInvalidValue = value.children.head.data.name.startsWith(VENDOR_PREFIX) || INVALID_CSS_ESCAPE_TEST.test(value.children.head.data.name);
494
+ } if (!property.startsWith(VENDOR_PREFIX) && value.children.head.data.value) {
495
+ try {
496
+ isInvalidValue = !cssTree.lexer.matchProperty(property, value).matched;
497
+ } catch {
498
+ // ignored
499
+ }
500
+ }
501
+ }
502
+ if (hasValueChildNodes && !isRawValue && !isInvalidValue) {
503
+ allDeclarations.push({
504
+ declaration,
505
+ selector,
506
+ specificity,
507
+ isInline
508
+ });
509
+ }
510
+ }
446
511
  }
447
512
 
448
513
  function getConditionalStackForSelector(selector, docContext) {
@@ -456,132 +521,169 @@ function getConditionalStackForSelector(selector, docContext) {
456
521
  return conditionalStack;
457
522
  }
458
523
 
459
- function matchElements(selector, ancestorsSelectors, docContext) {
524
+ function matchElements(selector, ancestorsSelectors, scopeStack, docContext) {
460
525
  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;
526
+ const cacheKey = createScopeCacheKey(selectorText, scopeStack);
527
+ const cachedNodes = docContext.matchedSelectors.get(cacheKey);
528
+ if (cachedNodes) {
529
+ return cachedNodes;
530
+ }
531
+ let nodes;
532
+ if (scopeStack && scopeStack.length) {
533
+ nodes = matchElementsInScope(selectorText, scopeStack);
534
+ nodes = filterElementsByScopes(nodes, scopeStack);
467
535
  } 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
- }
536
+ nodes = querySelectorAll(docContext.doc, selectorText);
475
537
  }
538
+ docContext.matchedSelectors.set(cacheKey, nodes);
539
+ return nodes;
476
540
  }
477
541
 
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);
542
+ function createScopeCacheKey(selectorText, scopeStack) {
543
+ if (!scopeStack || !scopeStack.length) {
544
+ return selectorText;
484
545
  }
485
- if (!selectorText) {
486
- selectorText = sanitizeSelector(selector, ancestorsSelectors, docContext);
546
+ const signature = scopeStack.map(scope => scope.id).join(CONTEXT_KEY_SEPARATOR);
547
+ return `${selectorText}${CONTEXT_KEY_SEPARATOR}${signature}`;
548
+ }
549
+
550
+ function matchElementsInScope(selectorText, scopeStack) {
551
+ const currentScope = scopeStack[scopeStack.length - 1];
552
+ const roots = Array.from(currentScope.rootElements);
553
+ if (!roots.length) {
554
+ return [];
487
555
  }
488
- return selectorText;
556
+ const matchedNodes = new Set();
557
+ roots.forEach(root => {
558
+ matchSelectorWithinRoot(root, selectorText).forEach(node => matchedNodes.add(node));
559
+ });
560
+ return Array.from(matchedNodes);
489
561
  }
490
562
 
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;
563
+ function matchSelectorWithinRoot(root, selectorText) {
564
+ const matchedNodes = new Set();
565
+ if (!root || root.nodeType !== 1) {
566
+ return matchedNodes;
567
+ }
568
+ if (matches(root, selectorText)) {
569
+ matchedNodes.add(root);
570
+ }
571
+ let nodes;
572
+ try {
573
+ nodes = root.querySelectorAll(selectorText);
574
+ } catch {
575
+ if (DEBUG) {
576
+ // eslint-disable-next-line no-console
577
+ console.error(QSA_ERROR_MESSAGE, { root, selectorText });
578
+ }
579
+ nodes = matchByTraversal([root], selectorText, true);
580
+ }
581
+ for (const node of nodes) {
582
+ matchedNodes.add(node);
504
583
  }
584
+ return Array.from(matchedNodes);
505
585
  }
506
586
 
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);
587
+ function matches(element, selectorText) {
588
+ if (!element || element.nodeType !== 1) {
589
+ return false;
590
+ }
591
+ try {
592
+ return element.matches(selectorText);
593
+ } catch {
594
+ return false;
515
595
  }
516
- const matchedElements = Array.from(matchedSet);
517
- docContext.matchedSelectors.set(cacheKey, matchedElements);
518
- return matchedElements;
519
596
  }
520
597
 
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
- }
598
+ function matchByTraversal(roots, selectorText, skipFirst) {
599
+ const results = [];
600
+ const visited = new Set();
601
+ const stack = [];
602
+ roots.forEach(root => {
603
+ if (root && root.nodeType === 1) {
604
+ stack.push({ node: root, include: !skipFirst });
605
+ }
606
+ });
607
+ while (stack.length) {
608
+ const { node, include } = stack.pop();
609
+ if (!node || visited.has(node)) {
610
+ continue;
611
+ }
612
+ visited.add(node);
613
+ if (include && matches(node, selectorText)) {
614
+ results.push(node);
615
+ }
616
+ for (let child = node.firstElementChild; child; child = child.nextElementSibling) {
617
+ stack.push({ node: child, include: true });
532
618
  }
533
619
  }
620
+ return results;
534
621
  }
535
622
 
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);
623
+ function getTraversalRoots(root) {
624
+ if (!root) {
625
+ return [];
626
+ }
627
+ if (root.nodeType === 9 || root.nodeType === 11) {
628
+ const roots = [];
629
+ if (root.documentElement) {
630
+ roots.push(root.documentElement);
631
+ }
632
+ if (root.body && (!roots.length || root.body !== roots[0])) {
633
+ roots.push(root.body);
541
634
  }
635
+ if (!roots.length && root.children) {
636
+ roots.push(...Array.from(root.children).filter(node => node.nodeType === 1));
637
+ }
638
+ return roots;
542
639
  }
543
- return nodes;
640
+ return [root];
544
641
  }
545
642
 
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
- }
643
+ function filterElementsByScopes(elements, scopeStack) {
644
+ if (!scopeStack || !scopeStack.length) {
645
+ return elements;
646
+ }
647
+ return elements.filter(element => isElementWithinScopes(element, scopeStack));
648
+ }
649
+
650
+ function isElementWithinScopes(element, scopeStack) {
651
+ if (!element) {
652
+ return false;
653
+ }
654
+ for (let index = 0; index < scopeStack.length; index++) {
655
+ if (!isElementWithinScope(element, scopeStack[index])) {
656
+ return false;
565
657
  }
566
658
  }
567
- return cssTree.generate(selectorData);
659
+ return true;
568
660
  }
569
661
 
570
- function getScopeRoots(selector, docContext) {
571
- let roots = docContext.scopeRoots.get(selector);
572
- if (!roots) {
573
- roots = querySelectorAll(docContext.doc, selector, docContext.scopeRoots);
662
+ function isElementWithinScope(element, scopeContext) {
663
+ let current = element;
664
+ while (current && current.nodeType === 1) {
665
+ if (scopeContext.stopElements && scopeContext.stopElements.has(current)) {
666
+ return false;
667
+ }
668
+ if (scopeContext.rootElements.has(current)) {
669
+ return true;
670
+ }
671
+ current = current.parentElement;
574
672
  }
575
- return roots;
673
+ return false;
576
674
  }
577
675
 
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));
676
+ function createSelectorText(selector, ancestorsSelectors, docContext) {
677
+ let selectorText;
678
+ if (ancestorsSelectors && ancestorsSelectors.length) {
679
+ selectorText = combineSelectorWithAncestors(selector.data, ancestorsSelectors, docContext);
680
+ const combinedAst = parseCss(selectorText, SELECTOR_LIST_CONTEXT);
681
+ selectorText = sanitizeSelector({ data: combinedAst }, ancestorsSelectors, docContext);
682
+ }
683
+ if (!selectorText) {
684
+ selectorText = sanitizeSelector(selector, ancestorsSelectors, docContext);
583
685
  }
584
- return roots.filter(node => !Array.from(excludeRoots).some(excludedRoot => excludedRoot.contains(node)));
686
+ return selectorText;
585
687
  }
586
688
 
587
689
  function compareDeclarations(declarationA, declarationB, docContext) {
@@ -599,8 +701,8 @@ function compareDeclarations(declarationA, declarationB, docContext) {
599
701
  if (layerComparison !== 0) {
600
702
  return importantA ? -layerComparison : layerComparison;
601
703
  }
602
- const specificityA = declarationA.effectiveSpecificity;
603
- const specificityB = declarationB.effectiveSpecificity;
704
+ const specificityA = declarationA.specificity;
705
+ const specificityB = declarationB.specificity;
604
706
  if (specificityA.a !== specificityB.a) {
605
707
  return specificityA.a - specificityB.a;
606
708
  }
@@ -615,8 +717,8 @@ function compareDeclarations(declarationA, declarationB, docContext) {
615
717
  }
616
718
  return 0;
617
719
  } else {
618
- const specificityA = declarationA.effectiveSpecificity;
619
- const specificityB = declarationB.effectiveSpecificity;
720
+ const specificityA = declarationA.specificity;
721
+ const specificityB = declarationB.specificity;
620
722
  if (specificityA.a !== specificityB.a) {
621
723
  return specificityA.a - specificityB.a;
622
724
  }
@@ -718,7 +820,7 @@ function removeLosingDeclarations(winningDeclarations, docContext) {
718
820
  if (declaration.data.type === DECLARATION_TYPE) {
719
821
  allDeclarations.set(declaration, declarations);
720
822
  const { property, value } = declaration.data;
721
- if (property && property.startsWith(CUSTOM_PROPERTY_PREFIX) || (value && value.type === RAW_TYPE)) {
823
+ if (property && property.startsWith(CUSTOM_PROPERTY_PREFIX) || (value && value.type === RAW_TYPE) || cssRule.hasUnqueryableSelector) {
722
824
  protectedDeclarations.add(declaration);
723
825
  }
724
826
  }
@@ -822,36 +924,6 @@ function combineSelectors(parentSelectorText, childSelectorText) {
822
924
  return cssTree.generate(combinedSelector);
823
925
  }
824
926
 
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
927
  function hasChildNodes(node) {
856
928
  return Boolean(node && node.children && node.children.head);
857
929
  }
@@ -883,42 +955,39 @@ function parseCss(text, context = SELECTOR_CONTEXT) {
883
955
  return cssTree.parse(text, options);
884
956
  }
885
957
 
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 [];
958
+ function querySelectorAll(root, selectorText) {
959
+ if (!root) {
960
+ return [];
961
+ }
962
+ const isDocumentNode = root.nodeType === 9 || root.nodeType === 11;
963
+ const hasScopePseudo = Boolean(selectorText && selectorText.indexOf(":scope") !== -1);
964
+ if (isDocumentNode && hasScopePseudo) {
965
+ return matchDocumentScopeSelector(root, selectorText);
966
+ }
967
+ try {
968
+ return Array.from(root.querySelectorAll(selectorText));
969
+ } catch {
970
+ if (DEBUG) {
971
+ // eslint-disable-next-line no-console
972
+ console.warn(QSA_ERROR_MESSAGE, selectorText, root.nodeType === 1 && root.tagName ? root.tagName : EMPTY_STRING);
918
973
  }
974
+ const traversalRoots = getTraversalRoots(root);
975
+ return matchByTraversal(traversalRoots, selectorText, false);
919
976
  }
920
977
  }
921
978
 
979
+ function matchDocumentScopeSelector(root, selectorText) {
980
+ const traversalRoots = getTraversalRoots(root);
981
+ if (!traversalRoots.length) {
982
+ return [];
983
+ }
984
+ const matchedNodes = new Set();
985
+ traversalRoots.forEach(scopeRoot => {
986
+ matchSelectorWithinRoot(scopeRoot, selectorText).forEach(node => matchedNodes.add(node));
987
+ });
988
+ return Array.from(matchedNodes);
989
+ }
990
+
922
991
  function getInlineStyleDeclarations(element) {
923
992
  const style = element.getAttribute(STYLE_ATTRIBUTE_NAME);
924
993
  if (style) {
@@ -933,7 +1002,7 @@ function getInlineStyleDeclarations(element) {
933
1002
  if (node.data.type === DECLARATION_TYPE) {
934
1003
  declarations.push({
935
1004
  declaration: node,
936
- effectiveSpecificity: { a: 1, b: 0, c: 0 },
1005
+ specificity: { a: 1, b: 0, c: 0 },
937
1006
  isInline: true
938
1007
  });
939
1008
  }
@@ -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();
@@ -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,9 +94,8 @@ 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
  }
135
100
  } else if (childNode.type === "Selector") {
136
101
  normalizeSelectorNode(childNode, ancestors);
@@ -138,3 +103,19 @@ function normalizeSelectorNode(selector, ancestors) {
138
103
  current = next;
139
104
  }
140
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.81",
3
+ "version": "1.5.83",
4
4
  "description": "SingleFile Core",
5
5
  "author": "Gildas Lormeau",
6
6
  "license": "AGPL-3.0-or-later",
@@ -133,7 +133,7 @@ async function process(pageData, options, lastModDate = new Date()) {
133
133
  const startOffset = zipDataWriter.offset;
134
134
  pageData.url = options.url;
135
135
  pageData.archiveTime = (new Date()).toISOString();
136
- await addPageResources(zipWriter, pageData, { password: options.password }, options.createRootDirectory ? String(Date.now()) + "_" + (options.tabId || 0) + "/" : "", options.url);
136
+ await addPageResources(zipWriter, pageData, { password: options.password, disableCompression: options.disableCompression }, options.createRootDirectory ? String(Date.now()) + "_" + (options.tabId || 0) + "/" : "", options.url);
137
137
  const data = await zipWriter.close(null, { preventClose: true });
138
138
  if (options.selfExtractingArchive) {
139
139
  const insertionsCRLF = [];
@@ -393,25 +393,25 @@ async function addPageResources(zipWriter, pageData, options, prefixName, url) {
393
393
  }, null, 2);
394
394
  await Promise.all([
395
395
  Promise.all([
396
- addFile(zipWriter, prefixName, { name: "index.html", extension: ".html", content: pageData.content, url, password: options.password }),
397
- addFile(zipWriter, prefixName, { name: "manifest.json", extension: ".json", content: jsonContent, password: options.password })
396
+ addFile(zipWriter, prefixName, { name: "index.html", extension: ".html", content: pageData.content, url, password: options.password }, options.disableCompression),
397
+ addFile(zipWriter, prefixName, { name: "manifest.json", extension: ".json", content: jsonContent, password: options.password }, options.disableCompression)
398
398
  ]),
399
399
  Promise.all(Object.keys(pageData.resources).map(async resourceType =>
400
400
  Promise.all(pageData.resources[resourceType].map(data => {
401
401
  if (resourceType == "frames") {
402
402
  return addPageResources(zipWriter, data, options, prefixName + data.name, data.url);
403
403
  } else {
404
- return addFile(zipWriter, prefixName, data, true);
404
+ return addFile(zipWriter, prefixName, data, options.disableCompression);
405
405
  }
406
406
  }))
407
407
  ))
408
408
  ]);
409
409
  }
410
410
 
411
- async function addFile(zipWriter, prefixName, data) {
411
+ async function addFile(zipWriter, prefixName, data, disableCompresson) {
412
412
  const dataReader = typeof data.content == "string" ? new TextReader(data.content) : new BlobReader(new Blob([new Uint8Array(data.content)]));
413
413
  const options = { comment: data.url && data.url.startsWith("data:") ? "data:" : data.url, password: data.password, bufferedWrite: true };
414
- if (NO_COMPRESSION_EXTENSIONS.includes(data.extension)) {
414
+ if (NO_COMPRESSION_EXTENSIONS.includes(data.extension) || disableCompresson) {
415
415
  options.level = 0;
416
416
  }
417
417
  await zipWriter.add(prefixName + data.name, dataReader, options);
@@ -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 = {
package/single-file.js CHANGED
@@ -116,6 +116,7 @@ async function getPageData(options = {}, initOptions, doc, win) {
116
116
  url: options.url,
117
117
  createRootDirectory: options.createRootDirectory,
118
118
  selfExtractingArchive: options.selfExtractingArchive,
119
+ disableCompression: options.disableCompression,
119
120
  extractDataFromPage: options.extractDataFromPage,
120
121
  preventAppendedData: options.preventAppendedData,
121
122
  insertCanonicalLink: options.insertCanonicalLink,