single-file-core 1.6.5 → 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.
@@ -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.5",
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) {