single-file-cli 2.6.2 → 2.6.3
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/build.sh +1 -1
- package/deno.json +1 -1
- package/lib/deno-polyfill.js +6 -4
- package/lib/single-file-archive.js +5 -5
- package/lib/single-file-bundle.js +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
- package/test/e2e/fidelity.test.js +214 -0
- package/test/e2e/saved-page-opens.test.js +79 -0
- package/test/fidelity/README.md +111 -0
- package/test/fidelity/browser.js +162 -0
- package/test/fidelity/make-font.js +294 -0
- package/test/fidelity/pages/duplicate-stylesheet/index.html +126 -0
- package/test/fidelity/pages/fonts/band.ttf +0 -0
- package/test/fidelity/pages/fonts/bar.ttf +0 -0
- package/test/fidelity/pages/fonts/block.ttf +0 -0
- package/test/fidelity/pages/frame-fonts/index.html +53 -0
- package/test/fidelity/pages/linked-stylesheet/index.html +30 -0
- package/test/fidelity/pages/linked-stylesheet/theme.css +38 -0
- package/test/fidelity/pages/synthetic-italic/index.html +59 -0
- package/test/fidelity/pages/unresolved-font-property/index.html +75 -0
- package/test/fidelity/pages/used-fonts/index.html +96 -0
- package/test/fidelity/server.js +70 -0
- package/test/target.js +49 -4
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// Writes a TrueType font in which every printable ASCII character is the same filled rectangle.
|
|
2
|
+
// Text set in it is a solid bar, so a page that loses the face does not merely reflow slightly —
|
|
3
|
+
// it changes from a black block to readable words, which no pixel comparison can miss.
|
|
4
|
+
//
|
|
5
|
+
// It is generated rather than borrowed for two reasons. A real font carries a licence and a few
|
|
6
|
+
// hundred kilobytes into a fixture directory, and, more importantly, the fonts already lying around
|
|
7
|
+
// for probing turned out to map no letters at all: text set in them silently fell back to a system
|
|
8
|
+
// family, which made a working fix look broken and cost an afternoon. A font built here maps
|
|
9
|
+
// exactly what it claims to map, and the shapes differ per variant so that "the right family was
|
|
10
|
+
// kept" is a visible statement and not only "some family was kept".
|
|
11
|
+
//
|
|
12
|
+
// Not a test. Run it to regenerate the committed .ttf files:
|
|
13
|
+
//
|
|
14
|
+
// node test/fidelity/make-font.js
|
|
15
|
+
//
|
|
16
|
+
import { writeFile } from "node:fs/promises";
|
|
17
|
+
import { join, dirname } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import process from "node:process";
|
|
20
|
+
|
|
21
|
+
const UNITS_PER_EM = 1000;
|
|
22
|
+
const FIRST_CHARACTER_CODE = 0x20;
|
|
23
|
+
const LAST_CHARACTER_CODE = 0x7e;
|
|
24
|
+
const ASCENT = 800;
|
|
25
|
+
const DESCENT = -200;
|
|
26
|
+
const ADVANCE_WIDTH = 600;
|
|
27
|
+
const MAGIC_NUMBER = 0x5f0f3cf5;
|
|
28
|
+
const CHECKSUM_MAGIC = 0xb1b0afba;
|
|
29
|
+
const TABLE_RECORD_SIZE = 16;
|
|
30
|
+
const HEAD_CHECKSUM_ADJUSTMENT_OFFSET = 8;
|
|
31
|
+
|
|
32
|
+
// each fixture family is a different shape, so a screenshot says which face was used and not just
|
|
33
|
+
// that one was: a full block, a band across the middle, a bar sitting on the baseline
|
|
34
|
+
const VARIANTS = {
|
|
35
|
+
"block": { bottom: 0, top: 760, left: 60, right: 540 },
|
|
36
|
+
"band": { bottom: 260, top: 500, left: 40, right: 560 },
|
|
37
|
+
"bar": { bottom: 0, top: 160, left: 40, right: 560 }
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export { createFont, VARIANTS };
|
|
41
|
+
|
|
42
|
+
if (import.meta.url === ("file://" + process.argv[1]) || import.meta.filename === process.argv[1]) {
|
|
43
|
+
const directory = join(dirname(fileURLToPath(import.meta.url)), "pages", "fonts");
|
|
44
|
+
await Promise.all(Object.keys(VARIANTS).map(async name => {
|
|
45
|
+
const path = join(directory, name + ".ttf");
|
|
46
|
+
await writeFile(path, createFont(Object.assign({ familyName: "Fidelity " + name }, VARIANTS[name])));
|
|
47
|
+
console.log("wrote " + path); // eslint-disable-line no-console
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createFont({ familyName, top, bottom, left, right }) {
|
|
52
|
+
const glyph = buildGlyph({ top, bottom, left, right });
|
|
53
|
+
const tables = [
|
|
54
|
+
["OS/2", buildOS2({ top, bottom })],
|
|
55
|
+
["cmap", buildCmap()],
|
|
56
|
+
["glyf", glyph],
|
|
57
|
+
["head", buildHead({ top, bottom, left, right })],
|
|
58
|
+
["hhea", buildHhea()],
|
|
59
|
+
["hmtx", buildHmtx()],
|
|
60
|
+
["loca", buildLoca(glyph.length)],
|
|
61
|
+
["maxp", buildMaxp()],
|
|
62
|
+
["name", buildName(familyName)],
|
|
63
|
+
["post", buildPost()]
|
|
64
|
+
];
|
|
65
|
+
return assemble(tables);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// the offset table, the table records and the tables themselves, followed by the one value that can
|
|
69
|
+
// only be computed once the whole file exists: the checksum adjustment in head
|
|
70
|
+
function assemble(tables) {
|
|
71
|
+
const headerSize = 12 + tables.length * TABLE_RECORD_SIZE;
|
|
72
|
+
const size = tables.reduce((total, [, content]) => total + align(content.length), headerSize);
|
|
73
|
+
const font = new Uint8Array(size);
|
|
74
|
+
const view = new DataView(font.buffer);
|
|
75
|
+
const entrySelector = Math.floor(Math.log2(tables.length));
|
|
76
|
+
const searchRange = 16 * (2 ** entrySelector);
|
|
77
|
+
view.setUint32(0, 0x00010000);
|
|
78
|
+
view.setUint16(4, tables.length);
|
|
79
|
+
view.setUint16(6, searchRange);
|
|
80
|
+
view.setUint16(8, entrySelector);
|
|
81
|
+
view.setUint16(10, tables.length * 16 - searchRange);
|
|
82
|
+
let offset = headerSize;
|
|
83
|
+
let headOffset;
|
|
84
|
+
tables.forEach(([tag, content], index) => {
|
|
85
|
+
const record = 12 + index * TABLE_RECORD_SIZE;
|
|
86
|
+
Array.from(tag).forEach((character, position) => view.setUint8(record + position, character.charCodeAt(0)));
|
|
87
|
+
view.setUint32(record + 4, checksum(content));
|
|
88
|
+
view.setUint32(record + 8, offset);
|
|
89
|
+
view.setUint32(record + 12, content.length);
|
|
90
|
+
font.set(content, offset);
|
|
91
|
+
if (tag == "head") {
|
|
92
|
+
headOffset = offset;
|
|
93
|
+
}
|
|
94
|
+
offset += align(content.length);
|
|
95
|
+
});
|
|
96
|
+
view.setUint32(headOffset + HEAD_CHECKSUM_ADJUSTMENT_OFFSET, (CHECKSUM_MAGIC - checksum(font)) >>> 0);
|
|
97
|
+
return font;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function checksum(content) {
|
|
101
|
+
const view = new DataView(content.buffer, content.byteOffset, content.byteLength);
|
|
102
|
+
let sum = 0;
|
|
103
|
+
for (let offset = 0; offset + 4 <= content.length; offset += 4) {
|
|
104
|
+
sum = (sum + view.getUint32(offset)) >>> 0;
|
|
105
|
+
}
|
|
106
|
+
// a table is padded to a multiple of four with zeroes, and the checksum is taken over the
|
|
107
|
+
// padded form: the trailing bytes are read as if those zeroes were already there
|
|
108
|
+
if (content.length % 4) {
|
|
109
|
+
let tail = 0;
|
|
110
|
+
for (let offset = content.length - content.length % 4; offset < content.length; offset++) {
|
|
111
|
+
tail = (tail << 8) | content[offset];
|
|
112
|
+
}
|
|
113
|
+
sum = (sum + (tail << (8 * (4 - content.length % 4)))) >>> 0;
|
|
114
|
+
}
|
|
115
|
+
return sum;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function align(length) {
|
|
119
|
+
return length + (length % 4 ? 4 - length % 4 : 0);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function buildHead({ top, bottom, left, right }) {
|
|
123
|
+
const writer = createWriter(54);
|
|
124
|
+
writer.uint32(0x00010000);
|
|
125
|
+
writer.uint32(0x00010000);
|
|
126
|
+
writer.uint32(0);
|
|
127
|
+
writer.uint32(MAGIC_NUMBER);
|
|
128
|
+
writer.uint16(0x0003);
|
|
129
|
+
writer.uint16(UNITS_PER_EM);
|
|
130
|
+
writer.uint32(0); writer.uint32(0);
|
|
131
|
+
writer.uint32(0); writer.uint32(0);
|
|
132
|
+
writer.int16(left); writer.int16(bottom); writer.int16(right); writer.int16(top);
|
|
133
|
+
writer.uint16(0);
|
|
134
|
+
writer.uint16(8);
|
|
135
|
+
writer.int16(2);
|
|
136
|
+
// the long form of loca, so that the offsets are plain byte counts rather than halves
|
|
137
|
+
writer.int16(1);
|
|
138
|
+
writer.int16(0);
|
|
139
|
+
return writer.content;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function buildHhea() {
|
|
143
|
+
const writer = createWriter(36);
|
|
144
|
+
writer.uint32(0x00010000);
|
|
145
|
+
writer.int16(ASCENT); writer.int16(DESCENT); writer.int16(0);
|
|
146
|
+
writer.uint16(ADVANCE_WIDTH);
|
|
147
|
+
writer.int16(0); writer.int16(0); writer.int16(ADVANCE_WIDTH);
|
|
148
|
+
writer.int16(1); writer.int16(0); writer.int16(0);
|
|
149
|
+
writer.int16(0); writer.int16(0); writer.int16(0); writer.int16(0);
|
|
150
|
+
writer.int16(0);
|
|
151
|
+
writer.uint16(2);
|
|
152
|
+
return writer.content;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function buildMaxp() {
|
|
156
|
+
const writer = createWriter(32);
|
|
157
|
+
writer.uint32(0x00010000);
|
|
158
|
+
writer.uint16(2);
|
|
159
|
+
writer.uint16(4); writer.uint16(1);
|
|
160
|
+
writer.uint16(0); writer.uint16(0);
|
|
161
|
+
writer.uint16(1); writer.uint16(0);
|
|
162
|
+
writer.uint16(0); writer.uint16(0); writer.uint16(0); writer.uint16(0); writer.uint16(0);
|
|
163
|
+
writer.uint16(0); writer.uint16(0);
|
|
164
|
+
return writer.content;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function buildHmtx() {
|
|
168
|
+
const writer = createWriter(8);
|
|
169
|
+
writer.uint16(ADVANCE_WIDTH); writer.int16(0);
|
|
170
|
+
writer.uint16(ADVANCE_WIDTH); writer.int16(0);
|
|
171
|
+
return writer.content;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// format 4, one segment covering printable ASCII and the terminator the format requires. Every
|
|
175
|
+
// character in the segment maps to the single glyph, including the space: text set in this font is
|
|
176
|
+
// one unbroken bar, which is exactly the point.
|
|
177
|
+
//
|
|
178
|
+
// The segment therefore has to name its glyph per character, through idRangeOffset and an array of
|
|
179
|
+
// indices. The shorter-looking route, a single idDelta added to the character code, maps a range
|
|
180
|
+
// LINEARLY — a font written that way claims a different glyph for every character, and the browser
|
|
181
|
+
// rejects the whole table as soon as one of them is past the last glyph
|
|
182
|
+
function buildCmap() {
|
|
183
|
+
const characterCount = LAST_CHARACTER_CODE - FIRST_CHARACTER_CODE + 1;
|
|
184
|
+
const subtableLength = 32 + characterCount * 2;
|
|
185
|
+
const writer = createWriter(12 + subtableLength);
|
|
186
|
+
writer.uint16(0); writer.uint16(1);
|
|
187
|
+
writer.uint16(3); writer.uint16(1); writer.uint32(12);
|
|
188
|
+
writer.uint16(4); writer.uint16(subtableLength); writer.uint16(0);
|
|
189
|
+
writer.uint16(4); writer.uint16(4); writer.uint16(1); writer.uint16(0);
|
|
190
|
+
writer.uint16(LAST_CHARACTER_CODE); writer.uint16(0xffff);
|
|
191
|
+
writer.uint16(0);
|
|
192
|
+
writer.uint16(FIRST_CHARACTER_CODE); writer.uint16(0xffff);
|
|
193
|
+
writer.uint16(0); writer.uint16(1);
|
|
194
|
+
// counted from the position of this very field, which is why the first segment's offset is the
|
|
195
|
+
// four bytes that the second segment's offset occupies
|
|
196
|
+
writer.uint16(4); writer.uint16(0);
|
|
197
|
+
for (let index = 0; index < characterCount; index++) {
|
|
198
|
+
writer.uint16(1);
|
|
199
|
+
}
|
|
200
|
+
return writer.content;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// glyph 0 is the empty .notdef, so the whole table is glyph 1: one closed contour of four on-curve
|
|
204
|
+
// points, given as deltas from the previous point. It is padded here rather than at assembly time,
|
|
205
|
+
// so that the offset loca gives for the end of the glyph stays inside the length glyf declares
|
|
206
|
+
function buildGlyph({ top, bottom, left, right }) {
|
|
207
|
+
const writer = createWriter(36);
|
|
208
|
+
writer.int16(1);
|
|
209
|
+
writer.int16(left); writer.int16(bottom); writer.int16(right); writer.int16(top);
|
|
210
|
+
writer.uint16(3);
|
|
211
|
+
writer.uint16(0);
|
|
212
|
+
writer.uint8(1); writer.uint8(1); writer.uint8(1); writer.uint8(1);
|
|
213
|
+
writer.int16(left); writer.int16(right - left); writer.int16(0); writer.int16(left - right);
|
|
214
|
+
writer.int16(bottom); writer.int16(0); writer.int16(top - bottom); writer.int16(0);
|
|
215
|
+
return writer.content;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function buildLoca(glyphLength) {
|
|
219
|
+
const writer = createWriter(12);
|
|
220
|
+
writer.uint32(0); writer.uint32(0); writer.uint32(align(glyphLength));
|
|
221
|
+
return writer.content;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function buildOS2({ top, bottom }) {
|
|
225
|
+
const writer = createWriter(96);
|
|
226
|
+
writer.uint16(4);
|
|
227
|
+
writer.int16(ADVANCE_WIDTH);
|
|
228
|
+
writer.uint16(400); writer.uint16(5); writer.uint16(0);
|
|
229
|
+
writer.int16(650); writer.int16(600); writer.int16(0); writer.int16(75);
|
|
230
|
+
writer.int16(650); writer.int16(600); writer.int16(0); writer.int16(350);
|
|
231
|
+
writer.int16(50); writer.int16(300);
|
|
232
|
+
writer.int16(0);
|
|
233
|
+
for (let index = 0; index < 10; index++) {
|
|
234
|
+
writer.uint8(0);
|
|
235
|
+
}
|
|
236
|
+
writer.uint32(1); writer.uint32(0); writer.uint32(0); writer.uint32(0);
|
|
237
|
+
Array.from("SFTD").forEach(character => writer.uint8(character.charCodeAt(0)));
|
|
238
|
+
writer.uint16(0x0040);
|
|
239
|
+
writer.uint16(FIRST_CHARACTER_CODE); writer.uint16(LAST_CHARACTER_CODE);
|
|
240
|
+
writer.int16(ASCENT); writer.int16(DESCENT); writer.int16(0);
|
|
241
|
+
writer.uint16(ASCENT); writer.uint16(-DESCENT);
|
|
242
|
+
writer.uint32(1); writer.uint32(0);
|
|
243
|
+
writer.int16(Math.round((top - bottom) / 2)); writer.int16(top);
|
|
244
|
+
writer.uint16(0); writer.uint16(FIRST_CHARACTER_CODE); writer.uint16(1);
|
|
245
|
+
return writer.content;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function buildName(familyName) {
|
|
249
|
+
const names = [[1, familyName], [2, "Regular"], [3, familyName + " Regular"], [4, familyName], [5, "Version 1.0"], [6, familyName.replace(/ /g, "")]];
|
|
250
|
+
const strings = names.map(([, value]) => encodeUTF16(value));
|
|
251
|
+
const header = 6 + names.length * 12;
|
|
252
|
+
const writer = createWriter(header + strings.reduce((total, string) => total + string.length, 0));
|
|
253
|
+
writer.uint16(0); writer.uint16(names.length); writer.uint16(header);
|
|
254
|
+
let offset = 0;
|
|
255
|
+
names.forEach(([identifier], index) => {
|
|
256
|
+
writer.uint16(3); writer.uint16(1); writer.uint16(0x0409); writer.uint16(identifier);
|
|
257
|
+
writer.uint16(strings[index].length); writer.uint16(offset);
|
|
258
|
+
offset += strings[index].length;
|
|
259
|
+
});
|
|
260
|
+
strings.forEach(string => string.forEach(byte => writer.uint8(byte)));
|
|
261
|
+
return writer.content;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function encodeUTF16(value) {
|
|
265
|
+
const bytes = [];
|
|
266
|
+
Array.from(value).forEach(character => {
|
|
267
|
+
const code = character.charCodeAt(0);
|
|
268
|
+
bytes.push(code >> 8, code & 0xff);
|
|
269
|
+
});
|
|
270
|
+
return bytes;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function buildPost() {
|
|
274
|
+
const writer = createWriter(32);
|
|
275
|
+
writer.uint32(0x00030000);
|
|
276
|
+
writer.uint32(0);
|
|
277
|
+
writer.int16(-100); writer.int16(50);
|
|
278
|
+
writer.uint32(1);
|
|
279
|
+
writer.uint32(0); writer.uint32(0); writer.uint32(0); writer.uint32(0);
|
|
280
|
+
return writer.content;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function createWriter(size) {
|
|
284
|
+
const content = new Uint8Array(size);
|
|
285
|
+
const view = new DataView(content.buffer);
|
|
286
|
+
let offset = 0;
|
|
287
|
+
return {
|
|
288
|
+
content,
|
|
289
|
+
uint8: value => view.setUint8(offset++, value),
|
|
290
|
+
int16: value => (view.setInt16(offset, value), offset += 2),
|
|
291
|
+
uint16: value => (view.setUint16(offset, value & 0xffff), offset += 2),
|
|
292
|
+
uint32: value => (view.setUint32(offset, value >>> 0), offset += 4)
|
|
293
|
+
};
|
|
294
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>Duplicate stylesheet</title>
|
|
7
|
+
<!--
|
|
8
|
+
Two <style> elements with byte-identical content. The archive writer groups them, stores the
|
|
9
|
+
content once as an entry, and points a <link> at it. Both elements are rewritten: the second
|
|
10
|
+
because it is a duplicate, the first because it is the one the duplicates were folded into —
|
|
11
|
+
leaving that one inline stored the same stylesheet twice, once as the entry and once in the
|
|
12
|
+
page.
|
|
13
|
+
|
|
14
|
+
The attributes are here because the rewrite replaces the element rather than editing it. An
|
|
15
|
+
id a script looks up and a class a selector matches have to survive that, and they were
|
|
16
|
+
being dropped: the archive came back with a bare <link>.
|
|
17
|
+
-->
|
|
18
|
+
<style id="palette" class="theme" data-role="tokens">
|
|
19
|
+
:root {
|
|
20
|
+
--ink: #17202a;
|
|
21
|
+
--paper: #f4f4f2;
|
|
22
|
+
--accent: #1f5673;
|
|
23
|
+
--edge: #c9c9c4;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
body {
|
|
27
|
+
background: var(--paper);
|
|
28
|
+
color: var(--ink);
|
|
29
|
+
font: 16px/1.6 Georgia, "Times New Roman", serif;
|
|
30
|
+
margin: 0;
|
|
31
|
+
padding: 40px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
h1 {
|
|
35
|
+
font-size: 34px;
|
|
36
|
+
margin: 0 0 24px;
|
|
37
|
+
border-bottom: 3px solid var(--accent);
|
|
38
|
+
padding-bottom: 12px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.grid {
|
|
42
|
+
display: grid;
|
|
43
|
+
grid-template-columns: repeat(3, 1fr);
|
|
44
|
+
gap: 16px;
|
|
45
|
+
margin: 24px 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.cell {
|
|
49
|
+
background: #fff;
|
|
50
|
+
border: 1px solid var(--edge);
|
|
51
|
+
padding: 20px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.cell b {
|
|
55
|
+
color: var(--accent);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.rule {
|
|
59
|
+
height: 8px;
|
|
60
|
+
background: var(--accent);
|
|
61
|
+
margin: 32px 0;
|
|
62
|
+
}
|
|
63
|
+
</style>
|
|
64
|
+
<style>
|
|
65
|
+
:root {
|
|
66
|
+
--ink: #17202a;
|
|
67
|
+
--paper: #f4f4f2;
|
|
68
|
+
--accent: #1f5673;
|
|
69
|
+
--edge: #c9c9c4;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
body {
|
|
73
|
+
background: var(--paper);
|
|
74
|
+
color: var(--ink);
|
|
75
|
+
font: 16px/1.6 Georgia, "Times New Roman", serif;
|
|
76
|
+
margin: 0;
|
|
77
|
+
padding: 40px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
h1 {
|
|
81
|
+
font-size: 34px;
|
|
82
|
+
margin: 0 0 24px;
|
|
83
|
+
border-bottom: 3px solid var(--accent);
|
|
84
|
+
padding-bottom: 12px;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.grid {
|
|
88
|
+
display: grid;
|
|
89
|
+
grid-template-columns: repeat(3, 1fr);
|
|
90
|
+
gap: 16px;
|
|
91
|
+
margin: 24px 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.cell {
|
|
95
|
+
background: #fff;
|
|
96
|
+
border: 1px solid var(--edge);
|
|
97
|
+
padding: 20px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.cell b {
|
|
101
|
+
color: var(--accent);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.rule {
|
|
105
|
+
height: 8px;
|
|
106
|
+
background: var(--accent);
|
|
107
|
+
margin: 32px 0;
|
|
108
|
+
}
|
|
109
|
+
</style>
|
|
110
|
+
</head>
|
|
111
|
+
|
|
112
|
+
<body>
|
|
113
|
+
<h1>Duplicate stylesheet</h1>
|
|
114
|
+
<p>The two style elements above hold the same declarations. Everything on this page is drawn by
|
|
115
|
+
them, so a stylesheet that goes missing in the save takes the whole layout with it.</p>
|
|
116
|
+
<div class="grid">
|
|
117
|
+
<div class="cell"><b>One</b><br>Bordered cell drawn by the shared stylesheet.</div>
|
|
118
|
+
<div class="cell"><b>Two</b><br>Bordered cell drawn by the shared stylesheet.</div>
|
|
119
|
+
<div class="cell"><b>Three</b><br>Bordered cell drawn by the shared stylesheet.</div>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="rule"></div>
|
|
122
|
+
<p>The grid, the rule above and the serif family all come from the custom properties declared in
|
|
123
|
+
the same block, so the comparison fails loudly rather than subtly.</p>
|
|
124
|
+
</body>
|
|
125
|
+
|
|
126
|
+
</html>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>Frame fonts</title>
|
|
7
|
+
<!--
|
|
8
|
+
The frame is sandboxed, which gives it an opaque origin and puts its contentDocument out of
|
|
9
|
+
reach. SingleFile then re-parses it from its srcdoc with DOMParser, and that document is
|
|
10
|
+
never rendered: it reports no font as used at all.
|
|
11
|
+
|
|
12
|
+
An empty list of used fonts is not a short one. Every rendered element has a computed
|
|
13
|
+
font-family, so nothing rendered can report none — an empty list means the styles could not
|
|
14
|
+
be read. Read as an answer instead of as an absence, it said "this frame uses no font" and
|
|
15
|
+
every face the frame declared was pruned. Measured on derstandard.at, where a newsletter box
|
|
16
|
+
inside such a frame fell back to a system font, and on MDN, where the text in the CSS demo
|
|
17
|
+
reflowed.
|
|
18
|
+
|
|
19
|
+
The frame declares its own face and uses it, and the face is not declared anywhere in the
|
|
20
|
+
parent: nothing outside the frame can keep it alive.
|
|
21
|
+
-->
|
|
22
|
+
<style>
|
|
23
|
+
body {
|
|
24
|
+
background: #fff;
|
|
25
|
+
margin: 0;
|
|
26
|
+
padding: 40px;
|
|
27
|
+
font: 20px/1.5 monospace;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
iframe {
|
|
31
|
+
width: 700px;
|
|
32
|
+
height: 220px;
|
|
33
|
+
border: 2px solid #333;
|
|
34
|
+
}
|
|
35
|
+
</style>
|
|
36
|
+
</head>
|
|
37
|
+
|
|
38
|
+
<body>
|
|
39
|
+
<p>The frame below declares and uses a face that the page around it never names.</p>
|
|
40
|
+
<iframe sandbox title="framed" srcdoc="
|
|
41
|
+
<!DOCTYPE html>
|
|
42
|
+
<meta charset="utf-8">
|
|
43
|
+
<style>
|
|
44
|
+
@font-face { font-family: "Fidelity Frame"; src: url(../fonts/bar.ttf) format("truetype") }
|
|
45
|
+
body { margin: 0; padding: 20px; background: #fff }
|
|
46
|
+
p { font: 48px "Fidelity Frame", serif; margin: 0 0 16px }
|
|
47
|
+
</style>
|
|
48
|
+
<p>abcdefghij</p>
|
|
49
|
+
<p>klmnopqrst</p>
|
|
50
|
+
"></iframe>
|
|
51
|
+
</body>
|
|
52
|
+
|
|
53
|
+
</html>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>Linked stylesheet</title>
|
|
7
|
+
<!--
|
|
8
|
+
An external stylesheet, linked with the attributes a page uses to find it again: an id a
|
|
9
|
+
theme switcher looks up, a class a selector matches, a data attribute a script reads.
|
|
10
|
+
|
|
11
|
+
A plain save has nowhere to put an external stylesheet, so the link becomes a style element
|
|
12
|
+
holding its content — a brand-new element, built with the media and the text and nothing
|
|
13
|
+
else. Everything the page used to identify that stylesheet was dropped, on every plain save
|
|
14
|
+
of every page with an external stylesheet. The archive path had the same defect in the other
|
|
15
|
+
direction and was fixed first; this is its mirror.
|
|
16
|
+
-->
|
|
17
|
+
<link rel="stylesheet" type="text/css" href="theme.css" id="theme" class="site-theme" data-role="tokens" title="Site theme">
|
|
18
|
+
</head>
|
|
19
|
+
|
|
20
|
+
<body>
|
|
21
|
+
<h1>Linked stylesheet</h1>
|
|
22
|
+
<p>Everything below is drawn by the linked stylesheet, so losing it in the save is not subtle.</p>
|
|
23
|
+
<div class="panel"><b>Panel</b> — bordered by the linked stylesheet.</div>
|
|
24
|
+
<div class="panel"><b>Panel</b> — bordered by the linked stylesheet.</div>
|
|
25
|
+
<div class="stripe"></div>
|
|
26
|
+
<p>The element carrying that stylesheet is looked up by id, by class and by data attribute, none
|
|
27
|
+
of which survives a rewrite that keeps only the text.</p>
|
|
28
|
+
</body>
|
|
29
|
+
|
|
30
|
+
</html>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--ink: #1b1b1f;
|
|
3
|
+
--paper: #fbfaf7;
|
|
4
|
+
--accent: #7a3b2e;
|
|
5
|
+
--edge: #d6d2c8;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
body {
|
|
9
|
+
background: var(--paper);
|
|
10
|
+
color: var(--ink);
|
|
11
|
+
font: 16px/1.6 Georgia, "Times New Roman", serif;
|
|
12
|
+
margin: 0;
|
|
13
|
+
padding: 40px;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
h1 {
|
|
17
|
+
font-size: 32px;
|
|
18
|
+
margin: 0 0 20px;
|
|
19
|
+
border-bottom: 4px solid var(--accent);
|
|
20
|
+
padding-bottom: 10px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.panel {
|
|
24
|
+
border: 1px solid var(--edge);
|
|
25
|
+
background: #fff;
|
|
26
|
+
padding: 20px;
|
|
27
|
+
margin: 0 0 16px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.panel b {
|
|
31
|
+
color: var(--accent);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.stripe {
|
|
35
|
+
height: 10px;
|
|
36
|
+
background: var(--accent);
|
|
37
|
+
margin: 24px 0;
|
|
38
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>Synthetic italic</title>
|
|
7
|
+
<!--
|
|
8
|
+
Two faces, both declared with no style of their own, both drawn. The second is asked for in
|
|
9
|
+
italic, and since no italic face is declared the browser slants the normal one — a synthetic
|
|
10
|
+
oblique. The face is used; it is simply used at an angle.
|
|
11
|
+
|
|
12
|
+
What the page reports as its used fonts is read off the computed styles, and the family is
|
|
13
|
+
kept only when a loaded face answers for the computed font-style. A synthesized style has no
|
|
14
|
+
face behind it, so the match fails and the family is not recorded as used at all — while the
|
|
15
|
+
first sample keeps the list non-empty, so the guard for "nothing could be read" does not
|
|
16
|
+
apply either. The italic family is then declared, drawn, and pruned.
|
|
17
|
+
-->
|
|
18
|
+
<style>
|
|
19
|
+
@font-face {
|
|
20
|
+
font-family: "Fidelity Block";
|
|
21
|
+
src: url(../fonts/block.ttf) format("truetype");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@font-face {
|
|
25
|
+
font-family: "Fidelity Bar";
|
|
26
|
+
src: url(../fonts/bar.ttf) format("truetype");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
body {
|
|
30
|
+
background: #fff;
|
|
31
|
+
margin: 0;
|
|
32
|
+
padding: 40px;
|
|
33
|
+
font: 20px/1.5 monospace;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.sample {
|
|
37
|
+
font-size: 48px;
|
|
38
|
+
margin: 0 0 24px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.upright {
|
|
42
|
+
font-family: "Fidelity Block";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.slanted {
|
|
46
|
+
font-family: "Fidelity Bar";
|
|
47
|
+
font-style: italic;
|
|
48
|
+
}
|
|
49
|
+
</style>
|
|
50
|
+
</head>
|
|
51
|
+
|
|
52
|
+
<body>
|
|
53
|
+
<p>Upright, drawn with a face declared for that style:</p>
|
|
54
|
+
<p class="sample upright">abcdefghij</p>
|
|
55
|
+
<p>Italic, drawn by slanting a face declared without one:</p>
|
|
56
|
+
<p class="sample slanted">abcdefghij</p>
|
|
57
|
+
</body>
|
|
58
|
+
|
|
59
|
+
</html>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>Unresolved font property</title>
|
|
7
|
+
<!--
|
|
8
|
+
The property holds a whole font shorthand, not a family list. The browser substitutes it and
|
|
9
|
+
draws the text in the face it names; the minifier must not read it as a list of families,
|
|
10
|
+
because the words in it are a style and a size and reading them as names would invent
|
|
11
|
+
families the document never had. So the family is genuinely unnameable from the stylesheets,
|
|
12
|
+
and the only thing left that knows it is the list of fonts the rendering reported.
|
|
13
|
+
|
|
14
|
+
The shape matters. A var() in family position — even one nested in a fallback — is walked
|
|
15
|
+
branch by branch and usually resolves, so a fixture written that way keeps its face through
|
|
16
|
+
the ordinary path and proves nothing about the case it was built for. This one was.
|
|
17
|
+
|
|
18
|
+
This is the page the pruning policy for undetermined values is decided on. Keeping every
|
|
19
|
+
declared face renders exactly like keeping the right one, so the two halves are checked
|
|
20
|
+
apart: the pixels say the face that was drawn survived, and the markup says the faces that
|
|
21
|
+
were not drawn are gone. A build that gives up on the document passes the first and fails
|
|
22
|
+
the second, which is what used to happen for every page holding one unreadable value.
|
|
23
|
+
-->
|
|
24
|
+
<style>
|
|
25
|
+
@font-face {
|
|
26
|
+
font-family: "Fidelity Block";
|
|
27
|
+
src: url(../fonts/block.ttf) format("truetype");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@font-face {
|
|
31
|
+
font-family: "Fidelity Unused One";
|
|
32
|
+
src: url(../fonts/band.ttf) format("truetype");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@font-face {
|
|
36
|
+
font-family: "Fidelity Unused Two";
|
|
37
|
+
src: url(../fonts/bar.ttf) format("truetype");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
body {
|
|
41
|
+
background: #fff;
|
|
42
|
+
margin: 0;
|
|
43
|
+
padding: 40px;
|
|
44
|
+
font: 20px/1.5 monospace;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.card {
|
|
48
|
+
--sample-font: 48px "Fidelity Block";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* a second declaration, so that the property has no single value to substitute into the
|
|
52
|
+
shorthand. It names a face nothing draws with: a resolver that read these values as
|
|
53
|
+
family lists would keep that one, which is the other way this can go wrong */
|
|
54
|
+
.note {
|
|
55
|
+
--sample-font: 48px "Fidelity Unused One";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.card .sample {
|
|
59
|
+
font: var(--sample-font);
|
|
60
|
+
margin: 0;
|
|
61
|
+
}
|
|
62
|
+
</style>
|
|
63
|
+
</head>
|
|
64
|
+
|
|
65
|
+
<body>
|
|
66
|
+
<div class="card">
|
|
67
|
+
<p>The family below is named inside a property holding a whole font shorthand.</p>
|
|
68
|
+
<p class="sample">abcdefghij</p>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="note">
|
|
71
|
+
<p>This section declares the same property with another value and draws nothing with it.</p>
|
|
72
|
+
</div>
|
|
73
|
+
</body>
|
|
74
|
+
|
|
75
|
+
</html>
|