single-file-core 1.5.118 → 1.5.120
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/helper.js +23 -22
- package/core/index.js +5 -6
- package/core/lib/doctype.js +0 -3
- package/core/lib/processor-helper-common.js +0 -19
- package/core/lib/processor-helper-inline.js +0 -6
- package/core/lib/processor-helper.js +7 -20
- package/doc/singlefile-archive.md +323 -135
- package/eslint.config.mjs +6 -0
- package/modules/template-formatter.js +4 -2
- package/package.json +2 -2
- package/processors/compression/compression-display.js +0 -11
- package/processors/compression/compression-extract.js +0 -4
- package/processors/compression/compression-packager.js +0 -12
- package/processors/compression/compression-router.js +0 -35
- package/processors/compression/compression.js +80 -173
- package/processors/hooks/content/content-hooks-frames-web.js +2 -1
- package/processors/lazy/content/content-lazy-loader.js +21 -18
- package/test/sfz-harness/README.md +10 -1
- package/test/sfz-harness/byte-map.js +137 -0
- package/test/sfz-harness/entry-compression.js +77 -0
- package/test/sfz-harness/filename-characters.js +55 -0
- package/test/sfz-harness/filename-max-length.js +69 -0
- package/test/sfz-harness/format-rules.js +132 -2
- package/test/sfz-harness/option-wiring.js +0 -3
- package/test/sfz-harness/zip64.js +77 -0
- package/vendor/zip/zip.min.js +1 -1
- package/zip-build/rollup.config.js +7 -0
package/core/helper.js
CHANGED
|
@@ -493,6 +493,15 @@ function getStylesheetsContent(styleSheets, adoptedStyleSheetsCache = new Map())
|
|
|
493
493
|
}
|
|
494
494
|
}
|
|
495
495
|
|
|
496
|
+
// an untouched canvas is fully transparent, so it encodes exactly like a blank one of the same
|
|
497
|
+
// size: comparing the two is cheaper than reading the pixels back and needs no drawing context
|
|
498
|
+
function isBlankCanvas(doc, element, dataURI) {
|
|
499
|
+
const blankElement = doc.createElement("canvas");
|
|
500
|
+
blankElement.width = element.width;
|
|
501
|
+
blankElement.height = element.height;
|
|
502
|
+
return blankElement.toDataURL("image/png") == dataURI;
|
|
503
|
+
}
|
|
504
|
+
|
|
496
505
|
function getResourcesInfo(win, doc, element, options, data, elementHidden, computedStyle) {
|
|
497
506
|
const tagName = element.tagName && element.tagName.toUpperCase();
|
|
498
507
|
if (tagName == "CANVAS") {
|
|
@@ -501,13 +510,19 @@ function getResourcesInfo(win, doc, element, options, data, elementHidden, compu
|
|
|
501
510
|
backgroundColor: canvasComputedStyle && canvasComputedStyle.getPropertyValue("background-color")
|
|
502
511
|
};
|
|
503
512
|
try {
|
|
504
|
-
|
|
513
|
+
const dataURI = element.toDataURL("image/png");
|
|
514
|
+
// a canvas in a page SingleFile has already saved is empty, no script ran to draw into
|
|
515
|
+
// it, and the picture it displays is the background image the previous save left
|
|
516
|
+
// behind. An empty bitmap must not overwrite it
|
|
517
|
+
const backgroundImage = canvasComputedStyle ? canvasComputedStyle.getPropertyValue("background-image") : element.style.getPropertyValue("background-image");
|
|
518
|
+
if (backgroundImage && backgroundImage != "none" && isBlankCanvas(doc, element, dataURI)) {
|
|
519
|
+
canvasData.blank = true;
|
|
520
|
+
} else {
|
|
521
|
+
canvasData.dataURI = dataURI;
|
|
522
|
+
}
|
|
505
523
|
// eslint-disable-next-line no-unused-vars
|
|
506
524
|
} catch (error) {
|
|
507
|
-
//
|
|
508
|
-
// whole element, its own drawing included. The entry is pushed and the element marked
|
|
509
|
-
// anyway: the loss is then counted and reported instead of being silent, and an
|
|
510
|
-
// out-of-page fallback re-rendering the element has something to find it by
|
|
525
|
+
// ignored
|
|
511
526
|
}
|
|
512
527
|
data.canvases.push(canvasData);
|
|
513
528
|
element.setAttribute(CANVAS_ATTRIBUTE_NAME, data.canvases.length - 1);
|
|
@@ -611,27 +626,11 @@ function getResourcesInfo(win, doc, element, options, data, elementHidden, compu
|
|
|
611
626
|
}
|
|
612
627
|
}
|
|
613
628
|
|
|
614
|
-
// Every family in the computed font-family is recorded, and the set of faces the document actually
|
|
615
|
-
// resolved is deliberately NOT consulted. Narrowing the list to the loaded faces is the obvious
|
|
616
|
-
// idea — a fallback stack names every family the page might need and only some are used — and it
|
|
617
|
-
// was written in 2019, wired to an option nothing ever set, and dead until it was reconnected here.
|
|
618
|
-
// Reconnected, it saved nothing measurable on nine real pages and cost fidelity twice: a face
|
|
619
|
-
// matched on its declared style is not found when the browser SYNTHESIZED that style, so a family
|
|
620
|
-
// drawn in an italic it declares no face for was dropped from the page that draws it; and a webfont
|
|
621
|
-
// that merely failed to load on the capturing machine would be dropped from the archive for good.
|
|
622
|
-
// It belongs with the unicode-range work, which needs the same information and can weigh a measured
|
|
623
|
-
// benefit against those, not here.
|
|
624
629
|
function getUsedFont(computedStyle, usedFonts) {
|
|
625
630
|
if (computedStyle) {
|
|
626
631
|
const fontStyle = computedStyle.getPropertyValue("font-style") || "normal";
|
|
627
632
|
computedStyle.getPropertyValue("font-family").split(",").forEach(fontFamilyName => {
|
|
628
633
|
fontFamilyName = normalizeFontFamily(fontFamilyName);
|
|
629
|
-
// an element with no computed font-family at all is one whose styles could not be read,
|
|
630
|
-
// not one drawn in a family with no name. It happens to every element of a frame that
|
|
631
|
-
// was re-parsed from its srcdoc, because the computed style is asked of the parent
|
|
632
|
-
// window and the document it belongs to is not the one being rendered. Recorded, it
|
|
633
|
-
// turns "nothing is known here" into a list of length one, which reads downstream as a
|
|
634
|
-
// complete answer and prunes every face the frame declares
|
|
635
634
|
if (fontFamilyName) {
|
|
636
635
|
const fontWeight = getFontWeight(computedStyle.getPropertyValue("font-weight"));
|
|
637
636
|
const fontVariant = computedStyle.getPropertyValue("font-variant") || "normal";
|
|
@@ -899,7 +898,9 @@ function getComputedStyle(win, element, pseudoElement) {
|
|
|
899
898
|
function getValidFilename(filename, replacedCharacters = DEFAULT_REPLACED_CHARACTERS, replacementCharacter = DEFAULT_REPLACEMENT_CHARACTER, replacementCharacters = DEFAULT_REPLACEMENT_CHARACTERS) {
|
|
900
899
|
replacementCharacters.forEach((indexReplacementCharacter, index) => {
|
|
901
900
|
if (replacedCharacters[index] !== undefined && indexReplacementCharacter != replacedCharacters[index]) {
|
|
902
|
-
|
|
901
|
+
// no "+" here, unlike the fallback below: a lookalike replaces its character one for
|
|
902
|
+
// one, so collapsing a run would drop characters the name needs ("C++" -> "C+")
|
|
903
|
+
filename = filename.replace(new RegExp("[" + getCharacterClassContent(replacedCharacters[index]) + "]", "g"), indexReplacementCharacter);
|
|
903
904
|
}
|
|
904
905
|
});
|
|
905
906
|
replacedCharacters.forEach((replacedCharacter, index) => {
|
package/core/index.js
CHANGED
|
@@ -530,6 +530,10 @@ class Processor {
|
|
|
530
530
|
" \n url: " + infobarURL +
|
|
531
531
|
(this.options.removeSavedDate ? " " : " \n saved date: " + infobarSaveDate) +
|
|
532
532
|
(infobarContent ? " \n info: " + infobarContent : "") + "\n";
|
|
533
|
+
// the infobar template resolves {page-title} and its kind against the captured page, so
|
|
534
|
+
// this text can carry "-->" and close the comment early, turning the rest into markup.
|
|
535
|
+
// A comment cannot escape its own delimiters, so the run is broken with a space
|
|
536
|
+
commentText = commentText.replace(/--(!?)>/g, "--$1 >");
|
|
533
537
|
const commentNode = this.doc.createComment(commentText);
|
|
534
538
|
this.doc.documentElement.insertBefore(commentNode, this.doc.documentElement.firstChild);
|
|
535
539
|
}
|
|
@@ -826,11 +830,6 @@ class Processor {
|
|
|
826
830
|
});
|
|
827
831
|
}
|
|
828
832
|
|
|
829
|
-
// a media element left without any source can never start, and the attribute
|
|
830
|
-
// would keep it announcing a playback that never happens. The sources are
|
|
831
|
-
// dropped whenever they could not be stored, so this is not specific to
|
|
832
|
-
// blocked videos, and it runs on the final state to leave alone an element
|
|
833
|
-
// that kept one of several sources
|
|
834
833
|
removeEmptyMediaAutoplay() {
|
|
835
834
|
this.doc.querySelectorAll("video[autoplay], audio[autoplay]").forEach(element => {
|
|
836
835
|
const sourceElements = Array.from(element.querySelectorAll("source"));
|
|
@@ -1069,7 +1068,7 @@ class Processor {
|
|
|
1069
1068
|
}
|
|
1070
1069
|
this.processorHelper.setBackgroundImage(canvasElement, "url(" + canvasData.dataURI + ")", backgroundStyle);
|
|
1071
1070
|
this.stats.add("processed", "canvas", 1);
|
|
1072
|
-
} else {
|
|
1071
|
+
} else if (!canvasData.blank) {
|
|
1073
1072
|
discardedCount++;
|
|
1074
1073
|
this.stats.add("discarded", "canvas", 1);
|
|
1075
1074
|
}
|
package/core/lib/doctype.js
CHANGED
|
@@ -102,10 +102,6 @@ class ProcessorHelperCommon {
|
|
|
102
102
|
["image, feImage", "xlink:href"],
|
|
103
103
|
["image, feImage", "href"]
|
|
104
104
|
];
|
|
105
|
-
// an inline svg is document content, not a fetched resource: blocking images empties the
|
|
106
|
-
// references it makes outwards (image, feImage and use are all processed below) and leaves
|
|
107
|
-
// the markup itself alone. Removing it deleted JS-drawn charts, inline icons, and the
|
|
108
|
-
// <defs> holding gradients and filters that CSS applies to ordinary HTML elements
|
|
109
105
|
let resourcePromises = processAttributeArgs.map(([selector, attributeName, removeElementIfMissing, processDuplicates]) =>
|
|
110
106
|
this.processAttribute(doc, doc.querySelectorAll(selector), attributeName, baseURI, options, "image", resources, removeElementIfMissing, batchRequest, styles, processDuplicates)
|
|
111
107
|
);
|
|
@@ -137,9 +133,6 @@ class ProcessorHelperCommon {
|
|
|
137
133
|
resourceElement.setAttribute("data-sf-original-href", originalResourceURL);
|
|
138
134
|
}
|
|
139
135
|
let resourceURL = normalizeURL(originalResourceURL);
|
|
140
|
-
// a reference into the page itself costs no request, so blocking images must not empty
|
|
141
|
-
// it: normalizeURL drops the fragment, which leaves a bare "#symbol" as the empty path
|
|
142
|
-
// below and a same-document URL matching options.url
|
|
143
136
|
if (testValidPath(resourceURL) && !testIgnoredPath(resourceURL)) {
|
|
144
137
|
resourceElement.setAttribute(attributeName, util.EMPTY_RESOURCE);
|
|
145
138
|
if (!options.blockImages) {
|
|
@@ -257,9 +250,6 @@ class ProcessorHelperCommon {
|
|
|
257
250
|
}));
|
|
258
251
|
}
|
|
259
252
|
|
|
260
|
-
// a video or an audio element gets the attribute REMOVED rather than emptied: util.EMPTY_RESOURCE
|
|
261
|
-
// is inert on an image but not on a media element, which would attempt the load, fail, and sit
|
|
262
|
-
// in an error state where removing the attribute leaves the poster showing cleanly
|
|
263
253
|
setAttributeEmpty(resourceElement, attributeName, expectedType) {
|
|
264
254
|
if (expectedType == "video" || expectedType == "audio") {
|
|
265
255
|
resourceElement.removeAttribute(attributeName);
|
|
@@ -454,11 +444,6 @@ class ProcessorHelperCommon {
|
|
|
454
444
|
const key = this.getFontKey(ruleData);
|
|
455
445
|
const fontInfo = fontsDetails.fonts.get(key);
|
|
456
446
|
if (fontInfo) {
|
|
457
|
-
// a face declaring the same descriptors and the same sources as one already kept
|
|
458
|
-
// cannot change what that one did, and its sources are embedded a second time. A
|
|
459
|
-
// stylesheet reached from two <link> elements, or imported by two sheets, repeats
|
|
460
|
-
// every face it carries. The test is made before the await: the sources are read
|
|
461
|
-
// as they stand for both rules, and processing them is what would interleave
|
|
462
447
|
const ruleKey = key + " " + this.getPropertyValue(ruleData, "src");
|
|
463
448
|
if (fontsDetails.emittedFonts.has(ruleKey)) {
|
|
464
449
|
removedRules.push(cssRule);
|
|
@@ -524,10 +509,6 @@ class ProcessorHelperCommon {
|
|
|
524
509
|
medias: new Map(),
|
|
525
510
|
supports: new Map(),
|
|
526
511
|
layers: new Map(),
|
|
527
|
-
// the faces already emitted in this cascade context, so a second declaration of one of
|
|
528
|
-
// them can be dropped instead of embedding the same bytes again. Each media, supports
|
|
529
|
-
// and layer block gets its own info, so only rules that apply under the same conditions
|
|
530
|
-
// are ever compared
|
|
531
512
|
emittedFonts: new Set()
|
|
532
513
|
};
|
|
533
514
|
}
|
|
@@ -44,8 +44,6 @@ const EMPTY_URL_SOURCE = /^url\(["']?data:[^,]*,?["']?\)/;
|
|
|
44
44
|
const LOCAL_SOURCE = "local(";
|
|
45
45
|
const FONT_MAX_LOAD_DELAY = 5000;
|
|
46
46
|
const DUPLICATE_STYLESHEET_ATTRIBUTE_NAME = "data-sf-duplicate-stylesheet-ref";
|
|
47
|
-
// the attributes that say how the link fetched its stylesheet, which the style element now holding
|
|
48
|
-
// that stylesheet has no use for. Everything else identified the element in the page
|
|
49
47
|
const LINK_FETCH_ATTRIBUTE_NAMES = ["rel", "href", "type", "media", "as", "crossorigin", "integrity",
|
|
50
48
|
"referrerpolicy", "hreflang", "sizes", "imagesrcset", "imagesizes", "fetchpriority"];
|
|
51
49
|
|
|
@@ -166,10 +164,6 @@ function getProcessorHelperClass(utilInstance) {
|
|
|
166
164
|
if (stylesheetInfo) {
|
|
167
165
|
stylesheets.delete(linkElement);
|
|
168
166
|
const styleElement = doc.createElement("style");
|
|
169
|
-
// the element is replaced rather than rewritten, so whatever identified it in
|
|
170
|
-
// the page has to be carried over: an id a script still looks up, a class a
|
|
171
|
-
// selector still matches. The attributes left out are the ones that describe
|
|
172
|
-
// the fetch the style element no longer performs
|
|
173
167
|
Array.from(linkElement.attributes).forEach(({ name, value }) => {
|
|
174
168
|
if (!LINK_FETCH_ATTRIBUTE_NAMES.includes(name.toLowerCase())) {
|
|
175
169
|
styleElement.setAttribute(name, value);
|
|
@@ -36,6 +36,7 @@ const CANVAS_TAG_FOUND = /<canvas/gi;
|
|
|
36
36
|
const EMPTY_URL_SOURCE = /^url\(["']?data:[^,]*,?["']?\)/;
|
|
37
37
|
const LOCAL_SOURCE = "local(";
|
|
38
38
|
const FONT_MAX_LOAD_DELAY = 5000;
|
|
39
|
+
const SCRIPT_EXTENSION = ".js";
|
|
39
40
|
const LINK_OWN_ATTRIBUTE_NAMES = ["rel", "type", "href", "media"];
|
|
40
41
|
|
|
41
42
|
let util;
|
|
@@ -108,10 +109,6 @@ function getProcessorHelperClass(utilInstance) {
|
|
|
108
109
|
linkElement.setAttribute("type", "text/css");
|
|
109
110
|
const name = "stylesheet_" + resources.stylesheets.size + ".css";
|
|
110
111
|
linkElement.setAttribute("href", name);
|
|
111
|
-
// the shared copy is generated from the stylesheet of the element the duplicates were
|
|
112
|
-
// folded into, not from the text that was captured: that text still names the resources
|
|
113
|
-
// by their addresses in the original page, which resolve to nothing once the page is
|
|
114
|
-
// inside the archive
|
|
115
112
|
const { styleElement, content } = options.inlineStylesheets.get(stylesheetRefIndex);
|
|
116
113
|
const sharedEntry = entries.find(([key]) => key.element == styleElement);
|
|
117
114
|
const stylesheet = sharedEntry
|
|
@@ -119,10 +116,6 @@ function getProcessorHelperClass(utilInstance) {
|
|
|
119
116
|
: cssTree.parse(content, { context: "stylesheet", parseCustomProperty: true });
|
|
120
117
|
resources.stylesheets.set(resources.stylesheets.size, { name, content: this.generateStylesheetContent(stylesheet, options) });
|
|
121
118
|
linkElements.set(stylesheetRefIndex, linkElement);
|
|
122
|
-
// the element the duplicates were folded into is not itself a duplicate, so it is
|
|
123
|
-
// absent from inlineStylesheetsRefs and would keep its content inline: the archive
|
|
124
|
-
// would then hold the same stylesheet twice, once as the entry the links point at
|
|
125
|
-
// and once in the page
|
|
126
119
|
sharedStyleElements.set(styleElement, stylesheetRefIndex);
|
|
127
120
|
});
|
|
128
121
|
for (const [key, stylesheetInfo] of entries) {
|
|
@@ -149,10 +142,6 @@ function getProcessorHelperClass(utilInstance) {
|
|
|
149
142
|
styleElement.textContent = this.generateStylesheetContent(stylesheetInfo.stylesheet, options);
|
|
150
143
|
} else {
|
|
151
144
|
const linkElement = linkElements.get(stylesheetRefIndex).cloneNode(true);
|
|
152
|
-
// the element is replaced rather than rewritten, so whatever identified it in
|
|
153
|
-
// the page has to be carried over: an id a script still looks up, a class a
|
|
154
|
-
// selector still matches. The attributes left out are the ones that describe
|
|
155
|
-
// the link itself, which the code around here sets
|
|
156
145
|
Array.from(styleElement.attributes).forEach(({ name, value }) => {
|
|
157
146
|
if (!LINK_OWN_ATTRIBUTE_NAMES.includes(name.toLowerCase())) {
|
|
158
147
|
linkElement.setAttribute(name, value);
|
|
@@ -191,9 +180,6 @@ function getProcessorHelperClass(utilInstance) {
|
|
|
191
180
|
} catch (error) {
|
|
192
181
|
// ignored
|
|
193
182
|
}
|
|
194
|
-
// a sheet already open higher in this import chain must not be entered again: the
|
|
195
|
-
// ancestors are carried down the branch, not accumulated across the whole document,
|
|
196
|
-
// so two sibling imports of one sheet are still both resolved
|
|
197
183
|
if (testValidURL(resourceURL) && !importedStyleSheets.has(resourceURL)) {
|
|
198
184
|
const mediaQueryListNode = cssTree.find(node, node => node.type == "MediaQueryList");
|
|
199
185
|
let mediaText, layerName, supportsCondition;
|
|
@@ -231,9 +217,6 @@ function getProcessorHelperClass(utilInstance) {
|
|
|
231
217
|
stylesheetInfo.stylesheet = cssTree.parse(content.data, { context: "stylesheet", parseCustomProperty: true });
|
|
232
218
|
stylesheet = stylesheetInfo.stylesheet;
|
|
233
219
|
const ancestorStyleSheets = new Set(importedStyleSheets);
|
|
234
|
-
// both identities of the sheet are remembered: a redirect makes the URL that was
|
|
235
|
-
// requested and the URL that answered differ, and an import of either one is the
|
|
236
|
-
// same cycle
|
|
237
220
|
ancestorStyleSheets.add(requestedURL);
|
|
238
221
|
ancestorStyleSheets.add(resourceURL);
|
|
239
222
|
await this.resolveImportURLs(stylesheetInfo, resourceURL, options, workStylesheet, resources, stylesheets, ancestorStyleSheets);
|
|
@@ -472,7 +455,11 @@ function getProcessorHelperClass(utilInstance) {
|
|
|
472
455
|
networkTimeout: options.networkTimeout
|
|
473
456
|
});
|
|
474
457
|
content = getUpdatedResourceContent(resourceURL, options) || content;
|
|
475
|
-
|
|
458
|
+
// the extension is taken from the URL when the content type is not in the map, and a
|
|
459
|
+
// script URL says nothing about the content: a module served as text/javascript from a
|
|
460
|
+
// ".ts" URL was named ".ts", stored uncompressed and served back as video/mp2t, which
|
|
461
|
+
// the browser refuses to execute. Stylesheets have always been named this way
|
|
462
|
+
const name = "scripts/" + indexResource + SCRIPT_EXTENSION;
|
|
476
463
|
element.setAttribute("src", name);
|
|
477
464
|
resources.scripts.set(indexResource, { name, content, extension, contentType, url: resourceURL });
|
|
478
465
|
}
|
|
@@ -491,7 +478,7 @@ function getProcessorHelperClass(utilInstance) {
|
|
|
491
478
|
acceptHeaders: options.acceptHeaders,
|
|
492
479
|
networkTimeout: options.networkTimeout
|
|
493
480
|
});
|
|
494
|
-
const name = "scripts/" + indexResource +
|
|
481
|
+
const name = "scripts/" + indexResource + SCRIPT_EXTENSION;
|
|
495
482
|
if (workletOptions) {
|
|
496
483
|
scriptElement.textContent += ` CSS.paintWorklet.addModule("${name}", ${JSON.stringify(workletOptions)});\n`;
|
|
497
484
|
} else {
|