single-file-core 1.6.4 → 1.6.6

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.
@@ -48,6 +48,8 @@ const REGEXP_CUSTOM_PROPERTY = /var\(\s*(--[^\s,)]+)\s*(?:,[^)]*)?\)/g;
48
48
  const REGEXP_CUSTOM_PROPERTY_FAMILY = /^var\(\s*(--[^\s,)]+)\s*(?:,(.*))?\)$/;
49
49
  const REGEXP_CUSTOM_PROPERTY_NAME = /^--/;
50
50
  const VALID_FONT_STYLES = [/^normal$/, /^italic$/, /^oblique$/, /^oblique\s+/];
51
+ const NON_GLYPH_CHAR_CODES = [9, 10, 12, 13];
52
+ const MAX_NON_GLYPH_CHAR_CODE = 13;
51
53
  // a family name kept when the "font" shorthand cannot be read: it resolves to nothing, so it
52
54
  // survives the substitution below and marks the fonts as undetermined instead of unused
53
55
  const UNRESOLVED_CUSTOM_PROPERTY_FAMILY = "var(--)";
@@ -125,11 +127,12 @@ function process(doc, stylesheets, styles, options) {
125
127
  unusedFonts = fontsInfo.declared.filter(fontInfo => !filteredUsedFonts.has(fontInfo.fontFamily));
126
128
  }
127
129
  const docChars = Array.from(new Set(docContent)).map(char => char.charCodeAt(0)).sort((value1, value2) => value1 - value2);
130
+ const usedFontsCharacters = getUsedFontsCharacters(options);
128
131
  stylesheets.forEach(stylesheetInfo => {
129
132
  if (stylesheetInfo.stylesheet) {
130
133
  const cssRules = stylesheetInfo.stylesheet.children;
131
134
  if (cssRules) {
132
- filterUnusedFonts(cssRules, fontsInfo.declared, unusedFonts, filteredUsedFonts, docChars);
135
+ filterUnusedFonts(cssRules, fontsInfo.declared, unusedFonts, filteredUsedFonts, docChars, usedFontsCharacters);
133
136
  stats.rules.discarded -= cssRules.size;
134
137
  }
135
138
  }
@@ -164,8 +167,9 @@ function getFontsInfo(cssRules, fontsInfo, options) {
164
167
  const fontWeight = getDeclarationValue(ruleData.block.children, "font-weight") || "400";
165
168
  const fontStyle = getDeclarationValue(ruleData.block.children, "font-style") || "normal";
166
169
  const fontVariant = getDeclarationValue(ruleData.block.children, "font-variant") || "normal";
170
+ const unicodeRange = getDeclarationValue(ruleData.block.children, "unicode-range");
167
171
  fontWeight.split(",").forEach(weightValue =>
168
- fontsInfo.declared.push({ fontFamily, fontWeight: helper.getFontWeight(helper.removeQuotes(weightValue)), fontStyle, fontVariant }));
172
+ fontsInfo.declared.push({ fontFamily, fontWeight: helper.getFontWeight(helper.removeQuotes(weightValue)), fontStyle, fontVariant, unicodeRange, ruleData }));
169
173
  }
170
174
  }
171
175
  }
@@ -292,19 +296,24 @@ function splitValues(value) {
292
296
  return values;
293
297
  }
294
298
 
295
- function filterUnusedFonts(cssRules, declaredFonts, unusedFonts, filteredUsedFonts, docChars) {
299
+ function filterUnusedFonts(cssRules, declaredFonts, unusedFonts, filteredUsedFonts, docChars, usedFontsCharacters) {
296
300
  const removedRules = [];
297
301
  for (let cssRule = cssRules.head; cssRule; cssRule = cssRule.next) {
298
302
  const ruleData = cssRule.data;
299
303
  if (ruleData.type == "Atrule" && ruleData.name == "import" && ruleData.prelude && ruleData.prelude.children && ruleData.prelude.children.head.data.importedChildren) {
300
- filterUnusedFonts(ruleData.prelude.children.head.data.importedChildren, declaredFonts, unusedFonts, filteredUsedFonts, docChars);
304
+ filterUnusedFonts(ruleData.prelude.children.head.data.importedChildren, declaredFonts, unusedFonts, filteredUsedFonts, docChars, usedFontsCharacters);
301
305
  } else if (ruleData.type == "Atrule" && (ruleData.name == "media" || ruleData.name == "supports" || ruleData.name == "layer" || ruleData.name == "container") && ruleData.block && ruleData.block.children) {
302
- filterUnusedFonts(ruleData.block.children, declaredFonts, unusedFonts, filteredUsedFonts, docChars);
306
+ filterUnusedFonts(ruleData.block.children, declaredFonts, unusedFonts, filteredUsedFonts, docChars, usedFontsCharacters);
303
307
  } else if (ruleData.type == "Atrule" && ruleData.name == "font-face") {
304
308
  const fontFamily = helper.normalizeFontFamily(getDeclarationValue(ruleData.block.children, "font-family"));
305
309
  if (fontFamily) {
306
310
  const unicodeRange = getDeclarationValue(ruleData.block.children, "unicode-range");
307
- if (unusedFonts.find(fontInfo => fontInfo.fontFamily == fontFamily) || !testUnicodeRange(docChars, unicodeRange) || !testUsedFont(ruleData, fontFamily, declaredFonts, filteredUsedFonts)) {
311
+ const laterUnicodeRanges = getLaterUnicodeRanges(declaredFonts, ruleData);
312
+ if (unusedFonts.find(fontInfo => fontInfo.fontFamily == fontFamily) ||
313
+ !testUnicodeRange(docChars, unicodeRange) ||
314
+ !testReachableUnicodeRange(docChars, unicodeRange, laterUnicodeRanges) ||
315
+ !testDrawnUnicodeRange(ruleData, fontFamily, unicodeRange, usedFontsCharacters) ||
316
+ !testUsedFont(ruleData, fontFamily, declaredFonts, filteredUsedFonts)) {
308
317
  removedRules.push(cssRule);
309
318
  }
310
319
  }
@@ -322,6 +331,71 @@ function filterUnusedFonts(cssRules, declaredFonts, unusedFonts, filteredUsedFon
322
331
  removedRules.forEach(cssRule => cssRules.remove(cssRule));
323
332
  }
324
333
 
334
+ function getUsedFontsCharacters(options) {
335
+ const usedFontsCharacters = new Map();
336
+ if (options.usedFontsCharacters && options.usedFontsCharacters.length) {
337
+ options.usedFontsCharacters.forEach(([fontFamily, fontStyle, ranges, unknown]) => {
338
+ let buckets = usedFontsCharacters.get(fontFamily);
339
+ if (!buckets) {
340
+ buckets = [];
341
+ usedFontsCharacters.set(fontFamily, buckets);
342
+ }
343
+ buckets.push({ fontStyle, ranges: ranges || [], unknown: Boolean(unknown) });
344
+ });
345
+ }
346
+ return usedFontsCharacters;
347
+ }
348
+
349
+ function testDrawnUnicodeRange(ruleData, familyName, unicodeRange, usedFontsCharacters) {
350
+ if (!unicodeRange || !usedFontsCharacters || !usedFontsCharacters.size) {
351
+ return true;
352
+ }
353
+ const buckets = usedFontsCharacters.get(familyName);
354
+ if (!buckets || !buckets.length) {
355
+ return true;
356
+ }
357
+ const fontStyle = getDeclarationValue(ruleData.block.children, "font-style") || "normal";
358
+ if (!VALID_FONT_STYLES.find(rule => fontStyle.trim().match(rule))) {
359
+ return true;
360
+ }
361
+ const matchedBuckets = buckets.filter(bucket => testFontStyle(bucket.fontStyle, fontStyle));
362
+ if (!matchedBuckets.length || matchedBuckets.find(bucket => bucket.unknown)) {
363
+ return true;
364
+ }
365
+ if (!matchedBuckets.find(bucket => bucket.ranges.length)) {
366
+ return true;
367
+ }
368
+ const ranges = parseUnicodeRanges(unicodeRange);
369
+ if (!ranges.length) {
370
+ return true;
371
+ }
372
+ return Boolean(matchedBuckets.find(bucket =>
373
+ bucket.ranges.find(drawnRange =>
374
+ ranges.find(range => testDrawnGlyph(drawnRange, range)))));
375
+ }
376
+
377
+ // A tab or a newline is recorded as drawn like any other character, but neither can ever select a
378
+ // glyph, so a face whose unicode-range meets the page in nothing else is dead however the page is
379
+ // laid out. Google Fonts symbol subsets are exactly that shape: measured on a capture of
380
+ // techcrunch.com, two Roboto faces worth 23,505 bytes matched the document in U+0009 and U+000A and
381
+ // nothing more.
382
+ function testDrawnGlyph(drawnRange, range) {
383
+ const fromCharCode = Math.max(drawnRange[0], range[0]);
384
+ const toCharCode = Math.min(drawnRange[1], range[1]);
385
+ if (fromCharCode > toCharCode) {
386
+ return false;
387
+ }
388
+ if (fromCharCode > MAX_NON_GLYPH_CHAR_CODE || toCharCode > MAX_NON_GLYPH_CHAR_CODE) {
389
+ return true;
390
+ }
391
+ for (let charCode = fromCharCode; charCode <= toCharCode; charCode++) {
392
+ if (!NON_GLYPH_CHAR_CODES.includes(charCode)) {
393
+ return true;
394
+ }
395
+ }
396
+ return false;
397
+ }
398
+
325
399
  function testUsedFont(ruleData, familyName, declaredFonts, filteredUsedFonts) {
326
400
  let test;
327
401
  const optionalUsedFonts = filteredUsedFonts && filteredUsedFonts.get(familyName);
@@ -389,7 +463,7 @@ function testFontweight(fontWeight, usedFontWeights) {
389
463
  for (const fontWeightValue of fontWeight.split(",")) {
390
464
  let { min: fontWeightMin, max: fontWeightMax } = parseFontWeight(fontWeightValue);
391
465
  if (!fontWeightMax) {
392
- fontWeightMax = 900;
466
+ fontWeightMax = fontWeightMin;
393
467
  }
394
468
  test = test || usedFontWeights.find(usedFontWeight => {
395
469
  let { min: usedFontWeightMin, max: usedFontWeightMax } = parseFontWeight(usedFontWeight);
@@ -631,6 +705,64 @@ function testUnicodeRange(docCharCodes, unicodeRange) {
631
705
  return true;
632
706
  }
633
707
 
708
+ function getLaterUnicodeRanges(declaredFonts, ruleData) {
709
+ const index = declaredFonts.findIndex(fontInfo => fontInfo.ruleData == ruleData);
710
+ if (index == -1) {
711
+ return [];
712
+ }
713
+ const { fontFamily, fontWeight, fontStyle } = declaredFonts[index];
714
+ return declaredFonts
715
+ .slice(index + 1)
716
+ .filter(fontInfo => fontInfo.ruleData != ruleData &&
717
+ fontInfo.fontFamily == fontFamily &&
718
+ fontInfo.fontWeight == fontWeight &&
719
+ fontInfo.fontStyle == fontStyle)
720
+ .map(fontInfo => fontInfo.unicodeRange);
721
+ }
722
+
723
+ function testReachableUnicodeRange(docCharCodes, unicodeRange, laterUnicodeRanges) {
724
+ if (!laterUnicodeRanges.length) {
725
+ return true;
726
+ }
727
+ const laterRanges = helper.flatten(laterUnicodeRanges.map(laterUnicodeRange => parseUnicodeRanges(laterUnicodeRange)));
728
+ if (!laterRanges.length) {
729
+ return true;
730
+ }
731
+ const ranges = parseUnicodeRanges(unicodeRange);
732
+ return Boolean(docCharCodes.find(charCode =>
733
+ (!ranges.length || ranges.find(range => testCharCodeInRange(charCode, range))) &&
734
+ !laterRanges.find(range => testCharCodeInRange(charCode, range))));
735
+ }
736
+
737
+ function testCharCodeInRange(charCode, range) {
738
+ return charCode >= range[0] && charCode <= range[1];
739
+ }
740
+
741
+ function parseUnicodeRanges(unicodeRange) {
742
+ const ranges = [];
743
+ if (unicodeRange) {
744
+ unicodeRange.split(REGEXP_COMMA).forEach(rangeValue => {
745
+ const range = rangeValue.split(REGEXP_DASH);
746
+ let min, max;
747
+ if (range.length == 2) {
748
+ min = transformRange(range[0]);
749
+ max = transformRange(range[1]);
750
+ } else if (range.length == 1 && range[0]) {
751
+ if (range[0].includes("?")) {
752
+ min = transformRange(range[0].replace(REGEXP_QUESTION_MARK, "0"));
753
+ max = transformRange(range[0].replace(REGEXP_QUESTION_MARK, "F"));
754
+ } else {
755
+ min = max = transformRange(range[0]);
756
+ }
757
+ }
758
+ if (Number.isInteger(min) && Number.isInteger(max)) {
759
+ ranges.push([min, max]);
760
+ }
761
+ });
762
+ }
763
+ return ranges;
764
+ }
765
+
634
766
  function transformRange(range) {
635
767
  range = range.replace(REGEXP_STARTS_U_PLUS, "");
636
768
  return parseInt(range, 16);
@@ -24,11 +24,12 @@
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, matchUnqueryablePseudoClass } from "./css-selector-sanitizer.js";
27
+ import { sanitizeSelector, matchUnqueryableAttributeSelector, matchUnqueryablePseudoClass } from "./css-selector-sanitizer.js";
28
28
 
29
29
  const DEBUG = false;
30
30
 
31
31
  const PSEUDO_ELEMENT_SYNONYMS = new Set(["after", "before", "first-letter", "first-line"]);
32
+ const FUNCTIONAL_PSEUDO_CLASS_NAMES = new Set(["not", "is", "where", "has"]);
32
33
  const MEDIA_AT_RULE_NAME = "media";
33
34
  const SUPPORTS_AT_RULE_NAME = "supports";
34
35
  const CONDITIONAL_AT_RULE_NAMES = new Set([MEDIA_AT_RULE_NAME, SUPPORTS_AT_RULE_NAME, "container"]);
@@ -36,6 +37,7 @@ const RULE_TYPE = "Rule";
36
37
  const AT_RULE_TYPE = "Atrule";
37
38
  const NESTING_SELECTOR_TYPE = "NestingSelector";
38
39
  const PSEUDO_CLASS_SELECTOR_TYPE = "PseudoClassSelector";
40
+ const ATTRIBUTE_SELECTOR_TYPE = "AttributeSelector";
39
41
  const DECLARATION_TYPE = "Declaration";
40
42
  const RAW_TYPE = "Raw";
41
43
  const VALUE_TYPE = "Value";
@@ -390,17 +392,20 @@ function processSelectors(ruleData, processingContext, docContext) {
390
392
  for (let selector = ruleData.prelude.children.head, selectorIndex = 0; selector; selector = selector.next, selectorIndex++) {
391
393
  const {
392
394
  startsWithCombinator,
393
- hasUnqueryableSelector
395
+ hasUnqueryableSelector,
396
+ hasNestedUnqueryablePseudoClass
394
397
  } = analyzeSelector(selector.data);
395
398
  if (hasUnqueryableSelector) {
396
399
  ruleData.hasUnqueryableSelector = true;
397
400
  }
398
401
  registerSelector(selector, ruleData, processingContext, docContext);
399
- if (!hasUnqueryableSelector && (!startsWithCombinator || !ancestorsSelectors || !ancestorsSelectors.length)) {
402
+ if (!startsWithCombinator || !ancestorsSelectors || !ancestorsSelectors.length) {
400
403
  const matchedElements = matchElements(selector, ancestorsSelectors, processingContext.scopeStack, docContext);
401
404
  if (matchedElements.length) {
402
- updateMatchingSelectors(matchedElements, selector, docContext);
403
- } else {
405
+ if (!hasUnqueryableSelector) {
406
+ updateMatchingSelectors(matchedElements, selector, docContext);
407
+ }
408
+ } else if (!hasNestedUnqueryablePseudoClass) {
404
409
  removedSelectors.push(selector);
405
410
  }
406
411
  }
@@ -410,21 +415,42 @@ function processSelectors(ruleData, processingContext, docContext) {
410
415
 
411
416
  function analyzeSelector(selector) {
412
417
  let hasUnqueryableSelector = false;
418
+ let hasNestedUnqueryablePseudoClass = false;
413
419
  let startsWithCombinator = false;
420
+ let functionalPseudoClassDepth = 0;
414
421
  cssTree.walk(selector, {
415
422
  enter(node) {
416
423
  if (node.type === PSEUDO_ELEMENT_SELECTOR_TYPE) {
417
424
  hasUnqueryableSelector = true;
425
+ if (functionalPseudoClassDepth) {
426
+ hasNestedUnqueryablePseudoClass = true;
427
+ }
418
428
  } else if (node.type === PSEUDO_CLASS_SELECTOR_TYPE) {
419
429
  if (PSEUDO_ELEMENT_SYNONYMS.has(node.name) || matchUnqueryablePseudoClass(node)) {
420
430
  hasUnqueryableSelector = true;
431
+ if (functionalPseudoClassDepth) {
432
+ hasNestedUnqueryablePseudoClass = true;
433
+ }
421
434
  }
435
+ if (FUNCTIONAL_PSEUDO_CLASS_NAMES.has(node.name.toLowerCase())) {
436
+ functionalPseudoClassDepth++;
437
+ }
438
+ } else if (node.type === ATTRIBUTE_SELECTOR_TYPE && matchUnqueryableAttributeSelector(node)) {
439
+ hasUnqueryableSelector = true;
440
+ if (functionalPseudoClassDepth) {
441
+ hasNestedUnqueryablePseudoClass = true;
442
+ }
443
+ }
444
+ },
445
+ leave(node) {
446
+ if (node.type === PSEUDO_CLASS_SELECTOR_TYPE && FUNCTIONAL_PSEUDO_CLASS_NAMES.has(node.name.toLowerCase())) {
447
+ functionalPseudoClassDepth--;
422
448
  }
423
449
  }
424
450
  });
425
451
  const firstChild = selector.children.head.data;
426
452
  startsWithCombinator = firstChild && firstChild.type === COMBINATOR_NAME;
427
- return { hasUnqueryableSelector, startsWithCombinator };
453
+ return { hasUnqueryableSelector, hasNestedUnqueryablePseudoClass, startsWithCombinator };
428
454
  }
429
455
 
430
456
  function updateMatchingSelectors(matchedElements, selector, docContext) {
@@ -47,7 +47,11 @@ const FUNCTIONAL_PSEUDO_CLASSES = [
47
47
  "where",
48
48
  "has"
49
49
  ];
50
+ const STATE_ATTRIBUTE_NAMES = [
51
+ "open"
52
+ ];
50
53
  export {
54
+ matchUnqueryableAttributeSelector,
51
55
  matchUnqueryablePseudoClass,
52
56
  sanitizeSelector,
53
57
  };
@@ -97,6 +101,10 @@ function normalizeSelectorNode(selector, ancestors) {
97
101
  if (matchUnqueryablePseudoClass(childNode)) {
98
102
  removeNode(selector.children, current);
99
103
  }
104
+ } else if (childNode.type === "AttributeSelector") {
105
+ if (matchUnqueryableAttributeSelector(childNode)) {
106
+ removeNode(selector.children, current);
107
+ }
100
108
  } else if (childNode.type === "Selector") {
101
109
  normalizeSelectorNode(childNode, ancestors);
102
110
  }
@@ -112,6 +120,10 @@ function removeNode(list, item) {
112
120
  }
113
121
  }
114
122
 
123
+ function matchUnqueryableAttributeSelector(attributeSelector) {
124
+ return Boolean(attributeSelector.name) && STATE_ATTRIBUTE_NAMES.includes(attributeSelector.name.name.toLowerCase());
125
+ }
126
+
115
127
  function matchUnqueryablePseudoClass(pseudoClass) {
116
128
  const name = pseudoClass.name.toLowerCase();
117
129
  return pseudoClass.children ? (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "single-file-core",
3
- "version": "1.6.4",
3
+ "version": "1.6.6",
4
4
  "description": "SingleFile Core",
5
5
  "author": "Gildas Lormeau",
6
6
  "license": "AGPL-3.0-or-later",
@@ -26,6 +26,7 @@
26
26
  import {
27
27
  configure,
28
28
  TextReader,
29
+ TextWriter,
29
30
  Uint8ArrayReader,
30
31
  Uint8ArrayWriter,
31
32
  ZipReader
@@ -40,6 +41,7 @@ const browser = globalThis.browser;
40
41
 
41
42
  const PAGES_PREFIX = "pages/";
42
43
  const PAGES_FILENAME = "sfz-pages.json";
44
+ const MANIFEST_FILENAME = "manifest.json";
43
45
  const TOC_FILENAME = "sfz-toc.html";
44
46
  const TOC_TITLE = "Table of contents";
45
47
  const TOC_STYLE = "body{font-family:system-ui,sans-serif;margin:2em auto;max-width:40em;padding:0 1em;background-color:#fff;color:#000}" +
@@ -102,6 +104,12 @@ async function createPagesArchive(pages, options) {
102
104
  for (const entry of await zipReader.getEntries()) {
103
105
  const filename = pagePath + entry.filename;
104
106
  const rawData = entry.directory ? undefined : await entry.getData(new Uint8ArrayWriter(), { passThrough: true, checkCrc32: false });
107
+ if (entry.filename == MANIFEST_FILENAME && !entry.encrypted) {
108
+ const { archiveTime } = JSON.parse(await entry.getData(new TextWriter()));
109
+ if (archiveTime) {
110
+ manifest.pages[pageIndex].archiveTime = archiveTime;
111
+ }
112
+ }
105
113
  const canonicalFilename = writtenEntries && findDuplicate(writtenEntries, filename, entry, rawData);
106
114
  if (canonicalFilename === undefined) {
107
115
  await zipWriter.add(filename, entry.directory ? null : new Uint8ArrayReader(rawData), {
@@ -95,6 +95,7 @@ const MAX_ZIP_COMMENT_LENGTH = 65535;
95
95
  const PDF_ENTRY_FILENAME = "page.pdf";
96
96
  const PRESCAN_WINDOW_LENGTH = 1024;
97
97
  const PNG_TEXT_CHUNK_HEADER_LENGTH = 12;
98
+ const PNG_LENGTH_PADDING_LENGTH = 1;
98
99
  const PNG_ZIP_CHUNK_TYPE_KEYWORD = new Uint8Array([0x74, 0x45, 0x58, 0x74, 0x5a, 0x49, 0x50, 0]);
99
100
  const MAX_HIDDEN_PNG_CHUNK_LENGTH = 0x2D000000;
100
101
  const WRAPPER_PATTERN_WINDOW_LENGTH = 12;
@@ -277,7 +278,7 @@ async function buildArchive(pageData, options, script, entriesData, zipWriterOpt
277
278
  const payloadView = new DataView(payload.buffer);
278
279
  words.forEach((word, indexWord) => payloadView.setUint32(indexWord * 4, word, true));
279
280
  extraData = "<sfz-extra-data>" + base64Encode(deflateRaw(payload)) + "</sfz-extra-data>";
280
- if (options.preventAppendedData || extraData.length > getMaxAppendedDataLength(options) - pageContent.length - endTags.length - (options.embeddedImage ? PNG_IEND_LENGTH + PNG_CHUNK_CRC_LENGTH : 0)) {
281
+ if (options.preventAppendedData || extraData.length > getMaxAppendedDataLength(options) - pageContent.length - endTags.length - (options.embeddedImage ? PNG_IEND_LENGTH + PNG_CHUNK_CRC_LENGTH + PNG_LENGTH_PADDING_LENGTH : 0)) {
281
282
  if (!options.extraDataSize) {
282
283
  options.preventAppendedData = true;
283
284
  options.extraDataSize = getReservationSize(extraData.length);
@@ -288,7 +289,11 @@ async function buildArchive(pageData, options, script, entriesData, zipWriterOpt
288
289
  }
289
290
  }
290
291
  pageContent += endTags;
291
- await writeData(zipDataWriter.writable, (new TextEncoder()).encode(pageContent));
292
+ let pageContentData = new TextEncoder().encode(pageContent);
293
+ if (options.embeddedImage && !isChunkLengthHidden(data, embeddedImageDataOffset, zipDataWriter.offset + pageContentData.length - embeddedImageDataOffset - 4, imageChunk.tagIndex)) {
294
+ pageContentData = concatArrays(pageContentData, new Uint8Array(PNG_LENGTH_PADDING_LENGTH).fill(0x20));
295
+ }
296
+ await writeData(zipDataWriter.writable, pageContentData);
292
297
  }
293
298
  await zipDataWriter.writable.close();
294
299
  const pageContent = await zipDataWriter.getData();
@@ -337,6 +342,15 @@ function isDeclaredLengthHidden(pageContent, zipDataEnd, appendedDataLength, opt
337
342
  return !containsDataPattern(tail, EMBEDDED_DATA_PATTERNS[tagIndex]);
338
343
  }
339
344
 
345
+ function isChunkLengthHidden(data, embeddedImageDataOffset, chunkLength, tagIndex) {
346
+ const lengthOffset = embeddedImageDataOffset - 4;
347
+ const window = concatArrays(
348
+ data.subarray(Math.max(0, lengthOffset - WRAPPER_PATTERN_WINDOW_LENGTH), lengthOffset),
349
+ getLength(chunkLength),
350
+ data.subarray(embeddedImageDataOffset, embeddedImageDataOffset + PNG_ZIP_CHUNK_TYPE_KEYWORD.length));
351
+ return !containsDataPattern(window, EMBEDDED_DATA_PATTERNS[tagIndex]);
352
+ }
353
+
340
354
  function getCRC32(data, indexData = 0) {
341
355
  const crcArray = new Uint8Array(4);
342
356
  setUint32(crcArray, getCRC32Value(data, indexData));
@@ -723,7 +737,7 @@ function getImageHTMLChunk(pageData, options, lastModDate) {
723
737
  findEmbeddedDataTagIndex(wrappedData, tagIndex) != tagIndex) {
724
738
  tagIndex = findEmbeddedDataTagIndex(embeddedImageData, tagIndex + 1);
725
739
  } else {
726
- return { endTag, startHTMLData, htmlData, htmlDataCRC };
740
+ return { tagIndex, endTag, startHTMLData, htmlData, htmlDataCRC };
727
741
  }
728
742
  }
729
743
  }
@@ -798,6 +812,7 @@ function isCompressibleContentType(contentType) {
798
812
  async function getContent() {
799
813
  const BASE64_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
800
814
  const DATA_IDENTIFIER = "sfz-data";
815
+ const WRAPPER_TAG_NAMES = ["script", "style", "noframes", "noembed", "iframe", "xmp", "svg", "plaintext"];
801
816
  const { Blob, XMLHttpRequest, NodeFilter, document, zip, location } = globalThis;
802
817
  const characterMap = new Map([
803
818
  [65533, 0], [8364, 128], [8218, 130], [402, 131], [8222, 132], [8230, 133], [8224, 134], [8225, 135], [710, 136], [8240, 137],
@@ -902,15 +917,22 @@ async function getContent() {
902
917
  if (zipDataElement) {
903
918
  const inflatedPayload = zip.inflateRaw(base64Decode(zipDataElement.textContent));
904
919
  const payload = new DataView(inflatedPayload.buffer, inflatedPayload.byteOffset, inflatedPayload.length & -4);
905
- const dataElement = document.getElementById(DATA_IDENTIFIER);
906
- if (dataElement) {
907
- return decodeZipData(dataElement, payload, 0);
908
- }
909
- const walker = document.createTreeWalker(document, NodeFilter.SHOW_COMMENT);
910
- while (walker.nextNode()) {
911
- if (walker.currentNode.data.startsWith(DATA_IDENTIFIER)) {
912
- return decodeZipData(walker.currentNode, payload, DATA_IDENTIFIER.length);
920
+ const candidates = Array.from(document.querySelectorAll("[id=" + DATA_IDENTIFIER + "]")).filter(element => WRAPPER_TAG_NAMES.includes(element.localName));
921
+ let startIndex = 0;
922
+ if (!candidates.length) {
923
+ const walker = document.createTreeWalker(document, NodeFilter.SHOW_COMMENT);
924
+ while (walker.nextNode()) {
925
+ if (walker.currentNode.data.startsWith(DATA_IDENTIFIER)) {
926
+ candidates.push(walker.currentNode);
927
+ }
913
928
  }
929
+ startIndex = DATA_IDENTIFIER.length;
930
+ }
931
+ if (candidates.length > 1) {
932
+ throw new Error("Multiple zip data candidates found");
933
+ }
934
+ if (candidates.length) {
935
+ return decodeZipData(candidates[0], payload, startIndex);
914
936
  }
915
937
  }
916
938
  throw new Error("Extra zip data not found");
@@ -231,6 +231,7 @@ function initResponse(message) {
231
231
  frameData.posters = messageFrameData.posters;
232
232
  frameData.videos = messageFrameData.videos;
233
233
  frameData.usedFonts = messageFrameData.usedFonts;
234
+ frameData.usedFontsCharacters = messageFrameData.usedFontsCharacters;
234
235
  frameData.shadowRoots = messageFrameData.shadowRoots;
235
236
  frameData.processed = messageFrameData.processed;
236
237
  frameData.scrollPosition = messageFrameData.scrollPosition;
@@ -453,6 +454,7 @@ function getFrameData(document, win, windowId, options, scrolling) {
453
454
  posters: docData.posters,
454
455
  videos: docData.videos,
455
456
  usedFonts: docData.usedFonts,
457
+ usedFontsCharacters: docData.usedFontsCharacters,
456
458
  shadowRoots: docData.shadowRoots,
457
459
  scrollPosition: docData.scrollPosition,
458
460
  scrolling,
@@ -586,7 +586,9 @@
586
586
  if (adoptedStylesheetsData.has(stylesheet)) {
587
587
  return adoptedStylesheetsData.get(stylesheet);
588
588
  } else {
589
- return Array.from(stylesheet.cssRules).map(cssRule => cssRule.cssText).join("\n");
589
+ const text = Array.from(stylesheet.cssRules).map(cssRule => cssRule.cssText).join("\n");
590
+ adoptedStylesheetsData.set(stylesheet, text);
591
+ return text;
590
592
  }
591
593
  });
592
594
  if (adoptedStyleSheets.length) {
@@ -54,7 +54,7 @@ import { appendInfobar, refreshInfobarInfo, extractInfobarData } from "./core/in
54
54
  }
55
55
 
56
56
  async function displayIcon() {
57
- let options = { displayInfobar: true };
57
+ let options = { displayInfobar: true, animateInfobar: true };
58
58
  const infoData = extractInfobarData(document);
59
59
  if (infoData && infoData.saveUrl) {
60
60
  if (browser && browser.runtime && browser.runtime.sendMessage) {
@@ -67,6 +67,7 @@ import { appendInfobar, refreshInfobarInfo, extractInfobarData } from "./core/in
67
67
  }
68
68
  if (options.displayInfobar) {
69
69
  infoData.openInfobar = options.openInfobar;
70
+ infoData.animateInfobar = options.animateInfobar;
70
71
  infoData.infobarPositionAbsolute = options.infobarPositionAbsolute;
71
72
  infoData.infobarPositionTop = options.infobarPositionTop;
72
73
  infoData.infobarPositionRight = options.infobarPositionRight;