single-file-core 1.5.115 → 1.5.117
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 +34 -13
- package/core/index.js +19 -5
- package/core/lib/processor-helper-common.js +42 -18
- package/core/lib/processor-helper-inline.js +15 -10
- package/core/lib/processor-helper.js +44 -19
- package/doc/singlefile-archive.md +123 -26
- package/modules/css-fonts-minifier.js +191 -33
- package/modules/css-rules-minifier.js +9 -2
- package/package.json +2 -2
- package/processors/compression/compression-display.js +23 -2
- package/processors/compression/compression-packager.js +2 -11
- package/processors/compression/compression.js +33 -9
- package/processors/hooks/content/content-hooks-frames-web.js +27 -4
- package/test/sfz-harness/README.md +5 -2
- package/test/sfz-harness/adopted-stylesheets-hook.js +240 -0
- package/test/sfz-harness/css-fonts-minifier.js +235 -0
- package/test/sfz-harness/css-property-filter.js +85 -0
- package/test/sfz-harness/format-rules.js +31 -1
- package/test/sfz-harness/inlined-functions.js +82 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// The CSS-removal option runs every declaration through css-tree's lexer and drops the ones it
|
|
2
|
+
// judges invalid. css-tree reports two different failures the same way — a real value mismatch
|
|
3
|
+
// (SyntaxMatchError) and a property it simply has no entry for (SyntaxReferenceError) — so the
|
|
4
|
+
// original `!matchProperty(...).matched` deleted valid declarations whose property is missing from
|
|
5
|
+
// the vendored dictionary. That silently flattened every CSS-defined SVG gradient: stop-opacity was
|
|
6
|
+
// dropped, both stops became opaque, and the gradient rendered as a solid block. It hid for a long
|
|
7
|
+
// time because stop-color survived alongside it — a var() value takes a different branch and is
|
|
8
|
+
// never handed to the lexer at all, so the colour was right and only the fade was gone.
|
|
9
|
+
//
|
|
10
|
+
// The rule this pins: unknown must fail open. A dropped valid declaration breaks rendering; a kept
|
|
11
|
+
// invalid one is ignored by the browser.
|
|
12
|
+
import * as cssTree from "../../vendor/css-tree.js";
|
|
13
|
+
import { isUnsupportedPropertyValue } from "../../modules/css-rules-minifier.js";
|
|
14
|
+
|
|
15
|
+
// valid declarations whose property the vendored css-tree does not know. Every one of these was
|
|
16
|
+
// deleted before the fix. The SVG paint-server and filter properties are the ones that matter in
|
|
17
|
+
// practice; the last two are here as the moving target — any property newer than the vendored
|
|
18
|
+
// dictionary lands in this bucket, and the list will keep growing
|
|
19
|
+
const MUST_KEEP = [
|
|
20
|
+
["stop-color", "#2a78d6"],
|
|
21
|
+
["stop-opacity", ".20"],
|
|
22
|
+
["stop-opacity", "0"],
|
|
23
|
+
["flood-color", "red"],
|
|
24
|
+
["flood-opacity", "0.5"],
|
|
25
|
+
["lighting-color", "white"],
|
|
26
|
+
["text-box-trim", "trim-both"],
|
|
27
|
+
["corner-shape", "squircle"]
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// declarations css-tree knows and correctly rejects. These keep the filter honest: the fix must not
|
|
31
|
+
// turn the check off, only stop it firing on properties css-tree has never heard of
|
|
32
|
+
const MUST_DROP = [
|
|
33
|
+
["margin-trim", "block"],
|
|
34
|
+
["opacity", "not-a-number"],
|
|
35
|
+
["color", "12px"]
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// properties close enough to the broken ones to be worth stating outright, because they are the
|
|
39
|
+
// reason the bug looked like an SVG quirk rather than a dictionary gap: css-tree does know these,
|
|
40
|
+
// so gradients that used them were unaffected and the failure looked arbitrary
|
|
41
|
+
const KNOWN_GOOD = [
|
|
42
|
+
["fill-opacity", "0.5"],
|
|
43
|
+
["stroke-opacity", "0.5"],
|
|
44
|
+
["fill", "red"],
|
|
45
|
+
["vector-effect", "non-scaling-stroke"]
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
let failed = false;
|
|
49
|
+
|
|
50
|
+
function check(label, actual, expected) {
|
|
51
|
+
const ok = actual === expected;
|
|
52
|
+
console.log(`${ok ? "PASS" : "FAIL"} ${label}: ${actual}${ok ? "" : " (expected " + expected + ")"}`);
|
|
53
|
+
failed ||= !ok;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isDropped(property, declaration) {
|
|
57
|
+
return isUnsupportedPropertyValue(property, cssTree.parse(declaration, { context: "value" }));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const [property, declaration] of MUST_KEEP) {
|
|
61
|
+
check(`kept: ${property}: ${declaration}`, isDropped(property, declaration), false);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const [property, declaration] of MUST_DROP) {
|
|
65
|
+
check(`dropped: ${property}: ${declaration}`, isDropped(property, declaration), true);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const [property, declaration] of KNOWN_GOOD) {
|
|
69
|
+
check(`kept: ${property}: ${declaration}`, isDropped(property, declaration), false);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// the distinction the fix rests on. If a css-tree upgrade ever collapses these two error types into
|
|
73
|
+
// one, the predicate above cannot tell "unknown" from "wrong" any more and needs rewriting rather
|
|
74
|
+
// than adjusting
|
|
75
|
+
const unknownProperty = cssTree.lexer.matchProperty("stop-opacity", cssTree.parse("0", { context: "value" }));
|
|
76
|
+
const wrongValue = cssTree.lexer.matchProperty("margin-trim", cssTree.parse("block", { context: "value" }));
|
|
77
|
+
check("unknown property reports SyntaxReferenceError", unknownProperty.error && unknownProperty.error.name, "SyntaxReferenceError");
|
|
78
|
+
check("wrong value reports SyntaxMatchError", wrongValue.error && wrongValue.error.name, "SyntaxMatchError");
|
|
79
|
+
|
|
80
|
+
if (failed) {
|
|
81
|
+
console.log("FAILED");
|
|
82
|
+
Deno.exit(1);
|
|
83
|
+
} else {
|
|
84
|
+
console.log("PASSED");
|
|
85
|
+
}
|
|
@@ -2,7 +2,9 @@ import "./dom-stub.js";
|
|
|
2
2
|
import { makePageData, makeOptions, runProcess, mulberry32 } from "./common.js";
|
|
3
3
|
import { ZipReader, BlobReader } from "../../vendor/zip/zip.js";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// the quote is there because the escaper the title shares with the table of contents encodes
|
|
6
|
+
// it for an attribute value, where it matters, and a title has to round-trip through that too
|
|
7
|
+
const TITLE = "日本語 — café & <b> \"quoted\"";
|
|
6
8
|
const PDF = new TextEncoder().encode("%PDF-1.4\n1 0 obj\n<<>>\nendobj\ntrailer\n<<>>\n%%EOF\n");
|
|
7
9
|
|
|
8
10
|
let failed = false;
|
|
@@ -207,6 +209,34 @@ function countIdentifiers(text) {
|
|
|
207
209
|
check("the cdata rung leaves the appended data placement alone", options.preventAppendedData !== true, true);
|
|
208
210
|
}
|
|
209
211
|
|
|
212
|
+
// the rung a caller names is resolved by its start tag, not by the identity of the array holding
|
|
213
|
+
// it. The option is internal and is normally set from the module's own EXTRA_DATA_TAGS, so an
|
|
214
|
+
// identity match worked by accident; a caller passing an equal pair of its own indexed the regexp
|
|
215
|
+
// table with -1 and threw a bare TypeError out of library internals
|
|
216
|
+
{
|
|
217
|
+
const options = makeOptions({ extractDataFromPageTags: ["<noframes>", "</noframes>"] });
|
|
218
|
+
const pageData = makePageData(31, 4 * 1024);
|
|
219
|
+
let error;
|
|
220
|
+
try {
|
|
221
|
+
await runProcess(pageData, options);
|
|
222
|
+
} catch (caught) {
|
|
223
|
+
error = caught;
|
|
224
|
+
}
|
|
225
|
+
check("a rung named by an equal pair is resolved", error, undefined);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
{
|
|
229
|
+
const options = makeOptions({ extractDataFromPageTags: ["<marquee>", "</marquee>"] });
|
|
230
|
+
const pageData = makePageData(32, 4 * 1024);
|
|
231
|
+
let message;
|
|
232
|
+
try {
|
|
233
|
+
await runProcess(pageData, options);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
message = error.message;
|
|
236
|
+
}
|
|
237
|
+
check("a rung that is not in the ladder names itself", message, "Unknown data tags: <marquee>");
|
|
238
|
+
}
|
|
239
|
+
|
|
210
240
|
// the terminator is the whole test for this rung, so a payload holding it must step past
|
|
211
241
|
{
|
|
212
242
|
const content = new Uint8Array(4096).fill(0x21);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2010-2026 Gildas Lormeau
|
|
3
|
+
* contact : gildas.lormeau <at> gmail.com
|
|
4
|
+
*
|
|
5
|
+
* This file is part of SingleFile.
|
|
6
|
+
*
|
|
7
|
+
* The code in this file is free software: you can redistribute it and/or
|
|
8
|
+
* modify it under the terms of the GNU Affero General Public License
|
|
9
|
+
* (GNU AGPL) as published by the Free Software Foundation, either version 3
|
|
10
|
+
* of the License, or (at your option) any later version.
|
|
11
|
+
*
|
|
12
|
+
* The code in this file is distributed in the hope that it will be useful,
|
|
13
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
|
|
15
|
+
* General Public License for more details.
|
|
16
|
+
*
|
|
17
|
+
* As additional permission under GNU AGPL version 3 section 7, you may
|
|
18
|
+
* distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU
|
|
19
|
+
* AGPL normally required by section 4, provided you include this license
|
|
20
|
+
* notice and a URL through which recipients can access the Corresponding
|
|
21
|
+
* Source.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// `createArchive` builds the script of a self-extracting archive by serializing functions with
|
|
25
|
+
// toString() and pasting the text into the generated page. That page has no module scope, so
|
|
26
|
+
// anything such a function names has to be declared inside it. An import is the trap: it survives
|
|
27
|
+
// bundling, the call still reads correctly in the source, and the archive then fails at runtime
|
|
28
|
+
// with a bare ReferenceError while the page stays blank.
|
|
29
|
+
//
|
|
30
|
+
// It is worse than it looks, because a minifier can hide it. `display` called an imported
|
|
31
|
+
// `getDoctypeString`, and terser inlined that helper into the release bundle, so released
|
|
32
|
+
// archives worked while every unminified build produced archives that threw
|
|
33
|
+
// `ReferenceError: getDoctypeString is not defined` and rendered nothing.
|
|
34
|
+
//
|
|
35
|
+
// The invariant below is what makes that impossible to reintroduce: a module holding an inlined
|
|
36
|
+
// function declares that function and nothing else, so there is no module scope to close over.
|
|
37
|
+
|
|
38
|
+
const MODULES = [
|
|
39
|
+
{ path: "../../processors/compression/compression-display.js", functions: ["display"] },
|
|
40
|
+
{ path: "../../processors/compression/compression-extract.js", functions: ["extract"] },
|
|
41
|
+
{ path: "../../processors/compression/compression-router.js", functions: ["router"] }
|
|
42
|
+
];
|
|
43
|
+
const TOP_LEVEL_DECLARATION = /^(?:export\s+)?(?:async\s+)?(?:function|const|let|var|class)\s+([A-Za-z_$][\w$]*)/;
|
|
44
|
+
|
|
45
|
+
let failures = 0;
|
|
46
|
+
|
|
47
|
+
for (const { path, functions } of MODULES) {
|
|
48
|
+
const source = await Deno.readTextFile(new URL(path, import.meta.url));
|
|
49
|
+
const name = path.split("/").pop();
|
|
50
|
+
const lines = source.split("\n");
|
|
51
|
+
const imports = lines.filter(line => /^import[\s{]/.test(line) || /^\s*import\s*\(/.test(line));
|
|
52
|
+
check(name + " imports nothing", imports.length === 0, imports.join(" | "));
|
|
53
|
+
const declared = [];
|
|
54
|
+
lines.forEach(line => {
|
|
55
|
+
if (line === line.trimStart()) {
|
|
56
|
+
const match = line.match(TOP_LEVEL_DECLARATION);
|
|
57
|
+
if (match) {
|
|
58
|
+
declared.push(match[1]);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
const unexpected = declared.filter(identifier => !functions.includes(identifier));
|
|
63
|
+
check(name + " declares nothing at module scope but " + functions.join(", "),
|
|
64
|
+
unexpected.length === 0, unexpected.join(", "));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// the one inlined function that does live beside module scope, in compression.js, states the rule
|
|
68
|
+
// in a comment and binds every global it uses itself; assert that binding is still there
|
|
69
|
+
const compression = await Deno.readTextFile(new URL("../../processors/compression/compression.js", import.meta.url));
|
|
70
|
+
const getContentBody = compression.slice(compression.indexOf("async function getContent()"));
|
|
71
|
+
check("getContent binds its globals locally",
|
|
72
|
+
/const \{[^}]*\} = globalThis;/.test(getContentBody.slice(0, 2000)));
|
|
73
|
+
|
|
74
|
+
console.log(failures ? `\n${failures} check(s) FAILED` : "\nall checks passed");
|
|
75
|
+
Deno.exit(failures ? 1 : 0);
|
|
76
|
+
|
|
77
|
+
function check(label, condition, detail) {
|
|
78
|
+
if (!condition) {
|
|
79
|
+
failures++;
|
|
80
|
+
}
|
|
81
|
+
console.log((condition ? "PASS" : "FAIL") + " " + label + (condition || !detail ? "" : ": " + detail));
|
|
82
|
+
}
|