docxodus 9.8.0 → 10.0.0
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/README.md +178 -0
- package/dist/canonical.d.ts +7 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/canonical.js +56 -0
- package/dist/canonical.js.map +1 -0
- package/dist/docxodus.worker.d.ts +2 -1
- package/dist/docxodus.worker.d.ts.map +1 -1
- package/dist/docxodus.worker.js +244 -3
- package/dist/docxodus.worker.js.map +1 -1
- package/dist/editor.bundle.js +2024 -388
- package/dist/editor.d.ts +22 -2
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +94 -9
- package/dist/editor.js.map +1 -1
- package/dist/embed.bundle.js +2912 -437
- package/dist/embed.iife.js +2911 -436
- package/dist/export-assets.json +283 -0
- package/dist/export-browser.bundle.js +7990 -0
- package/dist/export-browser.d.ts +266 -0
- package/dist/export-browser.d.ts.map +1 -0
- package/dist/export-browser.js +2915 -0
- package/dist/export-browser.js.map +1 -0
- package/dist/export-resource-limits-v1.json +57 -0
- package/dist/font-contract.d.ts +150 -0
- package/dist/font-contract.d.ts.map +1 -0
- package/dist/font-contract.js +59 -0
- package/dist/font-contract.js.map +1 -0
- package/dist/font-runtime.d.ts +33 -0
- package/dist/font-runtime.d.ts.map +1 -0
- package/dist/font-runtime.js +1100 -0
- package/dist/font-runtime.js.map +1 -0
- package/dist/index.d.ts +62 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +130 -16
- package/dist/index.js.map +1 -1
- package/dist/page-geometry.d.ts +8 -1
- package/dist/page-geometry.d.ts.map +1 -1
- package/dist/page-geometry.js +37 -11
- package/dist/page-geometry.js.map +1 -1
- package/dist/pagination.bundle.js +1937 -380
- package/dist/pagination.d.ts +277 -15
- package/dist/pagination.d.ts.map +1 -1
- package/dist/pagination.js +2055 -438
- package/dist/pagination.js.map +1 -1
- package/dist/react.d.ts +11 -4
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +38 -5
- package/dist/react.js.map +1 -1
- package/dist/render-report-v2.schema.json +1581 -0
- package/dist/ribbon-chrome.d.ts +1 -1
- package/dist/ribbon-chrome.d.ts.map +1 -1
- package/dist/ribbon-chrome.js +46 -0
- package/dist/ribbon-chrome.js.map +1 -1
- package/dist/ribbon.js +70 -0
- package/dist/ribbon.js.map +1 -1
- package/dist/session.bundle.js +695 -29
- package/dist/session.d.ts +134 -19
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +562 -25
- package/dist/session.js.map +1 -1
- package/dist/types.d.ts +1207 -16
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/wasm/_framework/DocumentFormat.OpenXml.Framework.wasm +0 -0
- package/dist/wasm/_framework/DocumentFormat.OpenXml.Framework.wasm.br +0 -0
- package/dist/wasm/_framework/DocumentFormat.OpenXml.wasm +0 -0
- package/dist/wasm/_framework/DocumentFormat.OpenXml.wasm.br +0 -0
- package/dist/wasm/_framework/Docxodus.wasm +0 -0
- package/dist/wasm/_framework/Docxodus.wasm.br +0 -0
- package/dist/wasm/_framework/DocxodusWasm.wasm +0 -0
- package/dist/wasm/_framework/DocxodusWasm.wasm.br +0 -0
- package/dist/wasm/_framework/System.Collections.Concurrent.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.Concurrent.wasm.br +0 -0
- package/dist/wasm/_framework/System.Collections.Immutable.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.Immutable.wasm.br +0 -0
- package/dist/wasm/_framework/System.Collections.NonGeneric.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.NonGeneric.wasm.br +0 -0
- package/dist/wasm/_framework/System.Collections.Specialized.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.Specialized.wasm.br +0 -0
- package/dist/wasm/_framework/System.Collections.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.wasm.br +0 -0
- package/dist/wasm/_framework/System.ComponentModel.Primitives.wasm +0 -0
- package/dist/wasm/_framework/System.ComponentModel.Primitives.wasm.br +0 -0
- package/dist/wasm/_framework/System.ComponentModel.TypeConverter.wasm +0 -0
- package/dist/wasm/_framework/System.ComponentModel.TypeConverter.wasm.br +0 -0
- package/dist/wasm/_framework/System.ComponentModel.wasm +0 -0
- package/dist/wasm/_framework/System.ComponentModel.wasm.br +0 -0
- package/dist/wasm/_framework/System.Console.wasm +0 -0
- package/dist/wasm/_framework/System.Console.wasm.br +0 -0
- package/dist/wasm/_framework/System.Diagnostics.Process.wasm +0 -0
- package/dist/wasm/_framework/System.Diagnostics.Process.wasm.br +0 -0
- package/dist/wasm/_framework/System.IO.Compression.wasm +0 -0
- package/dist/wasm/_framework/System.IO.Compression.wasm.br +0 -0
- package/dist/wasm/_framework/System.IO.Packaging.wasm +0 -0
- package/dist/wasm/_framework/System.IO.Packaging.wasm.br +0 -0
- package/dist/wasm/_framework/System.IO.Pipelines.wasm +0 -0
- package/dist/wasm/_framework/System.IO.Pipelines.wasm.br +0 -0
- package/dist/wasm/_framework/System.Linq.Expressions.wasm +0 -0
- package/dist/wasm/_framework/System.Linq.Expressions.wasm.br +0 -0
- package/dist/wasm/_framework/System.Linq.wasm +0 -0
- package/dist/wasm/_framework/System.Linq.wasm.br +0 -0
- package/dist/wasm/_framework/System.Memory.wasm +0 -0
- package/dist/wasm/_framework/System.Memory.wasm.br +0 -0
- package/dist/wasm/_framework/System.Net.Http.wasm +0 -0
- package/dist/wasm/_framework/System.Net.Http.wasm.br +0 -0
- package/dist/wasm/_framework/System.Net.Primitives.wasm +0 -0
- package/dist/wasm/_framework/System.Net.Primitives.wasm.br +0 -0
- package/dist/wasm/_framework/System.ObjectModel.wasm +0 -0
- package/dist/wasm/_framework/System.ObjectModel.wasm.br +0 -0
- package/dist/wasm/_framework/System.Private.CoreLib.wasm +0 -0
- package/dist/wasm/_framework/System.Private.CoreLib.wasm.br +0 -0
- package/dist/wasm/_framework/System.Private.Uri.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Uri.wasm.br +0 -0
- package/dist/wasm/_framework/System.Private.Xml.Linq.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Xml.Linq.wasm.br +0 -0
- package/dist/wasm/_framework/System.Private.Xml.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Xml.wasm.br +0 -0
- package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm +0 -0
- package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm.br +0 -0
- package/dist/wasm/_framework/System.Runtime.wasm +0 -0
- package/dist/wasm/_framework/System.Runtime.wasm.br +0 -0
- package/dist/wasm/_framework/System.Security.Cryptography.wasm +0 -0
- package/dist/wasm/_framework/System.Security.Cryptography.wasm.br +0 -0
- package/dist/wasm/_framework/System.Text.Encodings.Web.wasm +0 -0
- package/dist/wasm/_framework/System.Text.Encodings.Web.wasm.br +0 -0
- package/dist/wasm/_framework/System.Text.Json.wasm +0 -0
- package/dist/wasm/_framework/System.Text.Json.wasm.br +0 -0
- package/dist/wasm/_framework/System.Text.RegularExpressions.wasm +0 -0
- package/dist/wasm/_framework/System.Text.RegularExpressions.wasm.br +0 -0
- package/dist/wasm/_framework/System.Xml.Linq.wasm +0 -0
- package/dist/wasm/_framework/System.Xml.Linq.wasm.br +0 -0
- package/dist/wasm/_framework/System.Xml.XDocument.wasm +0 -0
- package/dist/wasm/_framework/System.Xml.XDocument.wasm.br +0 -0
- package/dist/wasm/_framework/System.wasm +0 -0
- package/dist/wasm/_framework/System.wasm.br +0 -0
- package/dist/wasm/_framework/dotnet.boot.js +38 -38
- package/dist/wasm/_framework/dotnet.boot.js.br +0 -0
- package/dist/wasm/_framework/dotnet.js +1 -1
- package/dist/wasm/_framework/dotnet.js.br +0 -0
- package/dist/wasm/_framework/dotnet.native.js +3 -3
- package/dist/wasm/_framework/dotnet.native.js.br +0 -0
- package/dist/wasm/_framework/dotnet.native.wasm +0 -0
- package/dist/wasm/_framework/dotnet.native.wasm.br +0 -0
- package/dist/wasm/_framework/dotnet.runtime.js +1 -1
- package/dist/wasm/_framework/dotnet.runtime.js.br +0 -0
- package/dist/worker-proxy.bundle.js +196 -29
- package/dist/worker-proxy.d.ts +36 -3
- package/dist/worker-proxy.d.ts.map +1 -1
- package/dist/worker-proxy.js +186 -33
- package/dist/worker-proxy.js.map +1 -1
- package/package.json +24 -7
|
@@ -0,0 +1,1100 @@
|
|
|
1
|
+
import { canonicalJson, isWellFormedUnicode } from "./canonical.js";
|
|
2
|
+
import { FONT_RESOLVER_CONTRACT_ID, FONT_RESOLVER_SCHEMA_VERSION, FONT_SUBSTITUTION_CONTRACT_MATERIAL, FONT_SUBSTITUTION_CONTRACT_VERSION, fontFamilyKey, normalizeFontFamilyName, } from "./font-contract.js";
|
|
3
|
+
export class BrowserFontError extends Error {
|
|
4
|
+
constructor(kind, message, detail, cause) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "BrowserFontError";
|
|
7
|
+
this.kind = kind;
|
|
8
|
+
this.detail = detail;
|
|
9
|
+
this.cause = cause;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const TEXT_ENCODER = new TextEncoder();
|
|
13
|
+
const MAX_FAMILY_COUNT = 64;
|
|
14
|
+
const MAX_FAMILY_CHARACTERS = 256;
|
|
15
|
+
const MAX_FAMILY_STACK_CHARACTERS = 4096;
|
|
16
|
+
const FONT_LOAD_CONCURRENCY = 16;
|
|
17
|
+
const GENERIC_FAMILIES = new Set([
|
|
18
|
+
"cursive", "emoji", "fangsong", "fantasy", "math", "monospace", "sans-serif",
|
|
19
|
+
"serif", "system-ui", "ui-monospace", "ui-rounded", "ui-sans-serif", "ui-serif",
|
|
20
|
+
]);
|
|
21
|
+
const FONT_STRETCH_PERCENT = new Map([
|
|
22
|
+
["ultra-condensed", 50],
|
|
23
|
+
["extra-condensed", 62.5],
|
|
24
|
+
["condensed", 75],
|
|
25
|
+
["semi-condensed", 87.5],
|
|
26
|
+
["normal", 100],
|
|
27
|
+
["semi-expanded", 112.5],
|
|
28
|
+
["expanded", 125],
|
|
29
|
+
["extra-expanded", 150],
|
|
30
|
+
["ultra-expanded", 200],
|
|
31
|
+
]);
|
|
32
|
+
const FORMAT_MEDIA = new Map([
|
|
33
|
+
["ttf", "font/ttf"],
|
|
34
|
+
["otf", "font/otf"],
|
|
35
|
+
["woff", "font/woff"],
|
|
36
|
+
["woff2", "font/woff2"],
|
|
37
|
+
]);
|
|
38
|
+
const FORMAT_HINT = new Map([
|
|
39
|
+
["ttf", "truetype"],
|
|
40
|
+
["otf", "opentype"],
|
|
41
|
+
["woff", "woff"],
|
|
42
|
+
["woff2", "woff2"],
|
|
43
|
+
]);
|
|
44
|
+
function compareText(left, right) {
|
|
45
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
46
|
+
}
|
|
47
|
+
async function sha256(bytes) {
|
|
48
|
+
if (!globalThis.crypto?.subtle) {
|
|
49
|
+
throw new BrowserFontError("invalid_response", "Web Crypto SHA-256 is unavailable.");
|
|
50
|
+
}
|
|
51
|
+
const owned = new Uint8Array(bytes);
|
|
52
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", owned.buffer);
|
|
53
|
+
return Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, "0")).join("");
|
|
54
|
+
}
|
|
55
|
+
async function digestJson(value) {
|
|
56
|
+
return sha256(TEXT_ENCODER.encode(canonicalJson(value)));
|
|
57
|
+
}
|
|
58
|
+
async function abortable(promise, signal) {
|
|
59
|
+
if (signal.aborted)
|
|
60
|
+
throw new DOMException("Font loading was aborted", "AbortError");
|
|
61
|
+
let rejectAbort;
|
|
62
|
+
const aborted = new Promise((_, reject) => { rejectAbort = reject; });
|
|
63
|
+
const onAbort = () => rejectAbort?.(new DOMException("Font loading was aborted", "AbortError"));
|
|
64
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
65
|
+
try {
|
|
66
|
+
return await Promise.race([promise, aborted]);
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
signal.removeEventListener("abort", onAbort);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function forEachBounded(values, operation) {
|
|
73
|
+
let cursor = 0;
|
|
74
|
+
const worker = async () => {
|
|
75
|
+
while (cursor < values.length) {
|
|
76
|
+
const index = cursor++;
|
|
77
|
+
await operation(values[index], index);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
await Promise.all(Array.from({ length: Math.min(FONT_LOAD_CONCURRENCY, values.length) }, () => worker()));
|
|
81
|
+
}
|
|
82
|
+
function cssEscape(source, start) {
|
|
83
|
+
let cursor = start + 1;
|
|
84
|
+
if (cursor >= source.length)
|
|
85
|
+
return { value: "\ufffd", end: cursor };
|
|
86
|
+
if (source[cursor] === "\r" && source[cursor + 1] === "\n")
|
|
87
|
+
return { value: "", end: cursor + 2 };
|
|
88
|
+
if (source[cursor] === "\n" || source[cursor] === "\r" || source[cursor] === "\f") {
|
|
89
|
+
return { value: "", end: cursor + 1 };
|
|
90
|
+
}
|
|
91
|
+
const hexStart = cursor;
|
|
92
|
+
while (cursor < source.length && cursor - hexStart < 6 && /[0-9a-f]/i.test(source[cursor]))
|
|
93
|
+
cursor++;
|
|
94
|
+
if (cursor > hexStart) {
|
|
95
|
+
const point = Number.parseInt(source.slice(hexStart, cursor), 16);
|
|
96
|
+
if (/\s/.test(source[cursor] ?? "")) {
|
|
97
|
+
if (source[cursor] === "\r" && source[cursor + 1] === "\n")
|
|
98
|
+
cursor += 2;
|
|
99
|
+
else
|
|
100
|
+
cursor++;
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
value: point === 0 || point > 0x10ffff || (point >= 0xd800 && point <= 0xdfff)
|
|
104
|
+
? "\ufffd"
|
|
105
|
+
: String.fromCodePoint(point),
|
|
106
|
+
end: cursor,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return { value: source[cursor], end: cursor + 1 };
|
|
110
|
+
}
|
|
111
|
+
function parseCssFontFamilyTokens(value) {
|
|
112
|
+
if (!isWellFormedUnicode(value)) {
|
|
113
|
+
throw new BrowserFontError("invalid_response", "A computed font-family value contains an unpaired UTF-16 surrogate.");
|
|
114
|
+
}
|
|
115
|
+
const families = [];
|
|
116
|
+
let familyCharacters = 0;
|
|
117
|
+
let family = "";
|
|
118
|
+
let quote = "";
|
|
119
|
+
let quoted = false;
|
|
120
|
+
let cursor = 0;
|
|
121
|
+
const finish = () => {
|
|
122
|
+
const normalized = normalizeFontFamilyName(family);
|
|
123
|
+
if (normalized) {
|
|
124
|
+
if (!isWellFormedUnicode(normalized)
|
|
125
|
+
|| normalized.length > MAX_FAMILY_CHARACTERS
|
|
126
|
+
|| /[\u0000-\u001f\u007f]/u.test(normalized)) {
|
|
127
|
+
throw new BrowserFontError("resource_limit", `A computed font family must contain at most ${MAX_FAMILY_CHARACTERS} bounded characters.`, "fontFamilyCharacters");
|
|
128
|
+
}
|
|
129
|
+
if (families.length >= MAX_FAMILY_COUNT) {
|
|
130
|
+
throw new BrowserFontError("resource_limit", `A computed font-family stack may contain at most ${MAX_FAMILY_COUNT} families.`, "fontFamilyCount");
|
|
131
|
+
}
|
|
132
|
+
familyCharacters += normalized.length;
|
|
133
|
+
if (familyCharacters > MAX_FAMILY_STACK_CHARACTERS) {
|
|
134
|
+
throw new BrowserFontError("resource_limit", `A computed font-family stack may contain at most ${MAX_FAMILY_STACK_CHARACTERS} characters.`, "fontFamilyCharacters");
|
|
135
|
+
}
|
|
136
|
+
families.push({
|
|
137
|
+
name: normalized,
|
|
138
|
+
kind: quoted || !GENERIC_FAMILIES.has(fontFamilyKey(normalized)) ? "named" : "generic",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
family = "";
|
|
142
|
+
quoted = false;
|
|
143
|
+
};
|
|
144
|
+
while (cursor < value.length) {
|
|
145
|
+
const character = value[cursor];
|
|
146
|
+
if (character === "\\") {
|
|
147
|
+
const escape = cssEscape(value, cursor);
|
|
148
|
+
family += escape.value;
|
|
149
|
+
cursor = escape.end;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (quote) {
|
|
153
|
+
if (character === quote)
|
|
154
|
+
quote = "";
|
|
155
|
+
else
|
|
156
|
+
family += character;
|
|
157
|
+
cursor++;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (character === "\"" || character === "'") {
|
|
161
|
+
quote = character;
|
|
162
|
+
quoted = true;
|
|
163
|
+
cursor++;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (character === ",") {
|
|
167
|
+
finish();
|
|
168
|
+
cursor++;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
family += character;
|
|
172
|
+
cursor++;
|
|
173
|
+
}
|
|
174
|
+
finish();
|
|
175
|
+
return families;
|
|
176
|
+
}
|
|
177
|
+
/** Parse a computed CSS font-family value without splitting quoted or escaped commas. */
|
|
178
|
+
export function parseCssFontFamily(value) {
|
|
179
|
+
return parseCssFontFamilyTokens(value).map(({ name }) => name);
|
|
180
|
+
}
|
|
181
|
+
function faceStyle(value) {
|
|
182
|
+
const key = value.trim().toLowerCase();
|
|
183
|
+
if (key.startsWith("italic"))
|
|
184
|
+
return "italic";
|
|
185
|
+
if (key.startsWith("oblique"))
|
|
186
|
+
return "oblique";
|
|
187
|
+
return "normal";
|
|
188
|
+
}
|
|
189
|
+
function faceWeight(value) {
|
|
190
|
+
if (value === "bold")
|
|
191
|
+
return 700;
|
|
192
|
+
if (value === "normal")
|
|
193
|
+
return 400;
|
|
194
|
+
const weight = Number.parseFloat(value);
|
|
195
|
+
return Number.isFinite(weight) ? Math.max(1, Math.min(1000, Math.round(weight))) : 400;
|
|
196
|
+
}
|
|
197
|
+
function faceStretch(value) {
|
|
198
|
+
const key = value.trim().toLowerCase();
|
|
199
|
+
const named = FONT_STRETCH_PERCENT.get(key);
|
|
200
|
+
if (named !== undefined)
|
|
201
|
+
return named;
|
|
202
|
+
const percentage = /^([0-9]+(?:\.[0-9]+)?)%$/.exec(key);
|
|
203
|
+
if (!percentage)
|
|
204
|
+
return 100;
|
|
205
|
+
const stretch = Number.parseFloat(percentage[1]);
|
|
206
|
+
return Number.isFinite(stretch) && stretch > 0 ? stretch : 100;
|
|
207
|
+
}
|
|
208
|
+
function requestKey(request) {
|
|
209
|
+
return canonicalJson({
|
|
210
|
+
familyStack: request.familyStack,
|
|
211
|
+
familyKinds: request.familyKinds,
|
|
212
|
+
stretch: request.stretch,
|
|
213
|
+
style: request.style,
|
|
214
|
+
weight: request.weight,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Whether the element is *being rendered* in the HTML sense: it generates a painted box and
|
|
219
|
+
* therefore resolves a font. Content the author hid contributes no glyphs to the output and
|
|
220
|
+
* is deliberately left out of the inventory.
|
|
221
|
+
*
|
|
222
|
+
* The converter's measurement staging area is itself `visibility: hidden`, and at
|
|
223
|
+
* `font_loading` every piece of document content still lives inside it. The export pipeline
|
|
224
|
+
* lifts that one container's own visibility for the duration of the phase (see
|
|
225
|
+
* `revealMeasurementStaging` in export-browser.ts) so this predicate keeps its meaning:
|
|
226
|
+
* a descendant that the document itself hid still computes to hidden and stays out.
|
|
227
|
+
*/
|
|
228
|
+
function participatesInRendering(element, view) {
|
|
229
|
+
const leaf = view.getComputedStyle(element);
|
|
230
|
+
if (leaf.visibility === "hidden" || leaf.visibility === "collapse")
|
|
231
|
+
return false;
|
|
232
|
+
for (let current = element; current; current = current.parentElement) {
|
|
233
|
+
const computed = current === element ? leaf : view.getComputedStyle(current);
|
|
234
|
+
if (computed.display === "none" || computed.contentVisibility === "hidden")
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
function collectFontInventory(document, limits) {
|
|
240
|
+
const view = document.defaultView;
|
|
241
|
+
if (!view || !document.body)
|
|
242
|
+
throw new BrowserFontError("invalid_response", "The render document has no font realm.");
|
|
243
|
+
const requests = new Map();
|
|
244
|
+
const uses = [];
|
|
245
|
+
let sampledCodePoints = 0;
|
|
246
|
+
let renderedTextNodeCount = 0;
|
|
247
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
248
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
249
|
+
const text = node.nodeValue ?? "";
|
|
250
|
+
const element = node.parentElement;
|
|
251
|
+
if (!text || !element || /^(?:script|style|template|noscript)$/i.test(element.localName))
|
|
252
|
+
continue;
|
|
253
|
+
if (!participatesInRendering(element, view))
|
|
254
|
+
continue;
|
|
255
|
+
renderedTextNodeCount++;
|
|
256
|
+
const computed = view.getComputedStyle(element);
|
|
257
|
+
const parsedFamilies = parseCssFontFamilyTokens(computed.fontFamily);
|
|
258
|
+
if (parsedFamilies.length === 0)
|
|
259
|
+
continue;
|
|
260
|
+
const descriptor = {
|
|
261
|
+
familyStack: parsedFamilies.map(({ name }) => name),
|
|
262
|
+
familyKinds: parsedFamilies.map(({ kind }) => kind),
|
|
263
|
+
style: faceStyle(computed.fontStyle),
|
|
264
|
+
weight: faceWeight(computed.fontWeight),
|
|
265
|
+
stretch: faceStretch(computed.fontStretch),
|
|
266
|
+
};
|
|
267
|
+
const key = requestKey(descriptor);
|
|
268
|
+
let request = requests.get(key);
|
|
269
|
+
if (!request) {
|
|
270
|
+
if (requests.size >= limits.fontRequests) {
|
|
271
|
+
throw new BrowserFontError("resource_limit", `fontRequests limit exceeded (${requests.size + 1} > ${limits.fontRequests}).`, "fontRequests");
|
|
272
|
+
}
|
|
273
|
+
request = { ...descriptor, sampleCodePoints: new Set() };
|
|
274
|
+
requests.set(key, request);
|
|
275
|
+
}
|
|
276
|
+
for (const character of text) {
|
|
277
|
+
const scalar = character.codePointAt(0);
|
|
278
|
+
const point = scalar >= 0xd800 && scalar <= 0xdfff ? 0xfffd : scalar;
|
|
279
|
+
if (request.sampleCodePoints.has(point))
|
|
280
|
+
continue;
|
|
281
|
+
sampledCodePoints++;
|
|
282
|
+
if (sampledCodePoints > limits.fontSampleCodePoints) {
|
|
283
|
+
throw new BrowserFontError("resource_limit", `fontSampleCodePoints limit exceeded (${sampledCodePoints} > ${limits.fontSampleCodePoints}).`, "fontSampleCodePoints");
|
|
284
|
+
}
|
|
285
|
+
request.sampleCodePoints.add(point);
|
|
286
|
+
}
|
|
287
|
+
uses.push({ element, requestKey: key, originalStyle: element.getAttribute("style") });
|
|
288
|
+
}
|
|
289
|
+
const ordered = Array.from(requests, ([key, request]) => ({ key, request }))
|
|
290
|
+
.sort((left, right) => compareText(left.key, right.key));
|
|
291
|
+
const idByKey = new Map();
|
|
292
|
+
const result = Object.freeze(ordered.map(({ key, request }, index) => {
|
|
293
|
+
const id = `font-${String(index + 1).padStart(4, "0")}`;
|
|
294
|
+
idByKey.set(key, id);
|
|
295
|
+
return Object.freeze({
|
|
296
|
+
id,
|
|
297
|
+
familyStack: Object.freeze([...request.familyStack]),
|
|
298
|
+
familyKinds: Object.freeze([...request.familyKinds]),
|
|
299
|
+
style: request.style,
|
|
300
|
+
weight: request.weight,
|
|
301
|
+
stretch: request.stretch,
|
|
302
|
+
sampleCodePoints: Object.freeze(Array.from(request.sampleCodePoints).sort((left, right) => left - right)),
|
|
303
|
+
});
|
|
304
|
+
}));
|
|
305
|
+
return {
|
|
306
|
+
requests: result,
|
|
307
|
+
uses: uses.map((use) => ({ ...use, requestKey: idByKey.get(use.requestKey) })),
|
|
308
|
+
renderedTextNodeCount,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/** Browser-readable request inventory; element associations remain internal. */
|
|
312
|
+
export function inventoryDocumentFontRequests(document, limits) {
|
|
313
|
+
return [...collectFontInventory(document, {
|
|
314
|
+
...limits,
|
|
315
|
+
fontFiles: Number.MAX_SAFE_INTEGER,
|
|
316
|
+
fontFileBytes: Number.MAX_SAFE_INTEGER,
|
|
317
|
+
fontTotalBytes: Number.MAX_SAFE_INTEGER,
|
|
318
|
+
}).requests];
|
|
319
|
+
}
|
|
320
|
+
function requireObject(value, label) {
|
|
321
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
322
|
+
throw new BrowserFontError("invalid_response", `${label} must be an object.`);
|
|
323
|
+
}
|
|
324
|
+
return value;
|
|
325
|
+
}
|
|
326
|
+
// Same shape as npm-export/src/fonts/resolver.ts's exactKeys, duplicated rather than shared
|
|
327
|
+
// across the package boundary; each closes over its own error type.
|
|
328
|
+
function exactKeys(record, allowed, label) {
|
|
329
|
+
const unknown = Object.keys(record).filter((key) => !allowed.includes(key));
|
|
330
|
+
if (unknown.length > 0) {
|
|
331
|
+
throw new BrowserFontError("invalid_response", `${label} contains unknown fields: ${unknown.sort(compareText).join(", ")}.`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function shortString(value, label, maximum = 512) {
|
|
335
|
+
if (typeof value !== "string" || value.trim() === "" || value.length > maximum
|
|
336
|
+
|| !isWellFormedUnicode(value) || /[\u0000-\u001f\u007f]/u.test(value)) {
|
|
337
|
+
throw new BrowserFontError("invalid_response", `${label} must be a non-empty string of at most ${maximum} characters.`);
|
|
338
|
+
}
|
|
339
|
+
return value;
|
|
340
|
+
}
|
|
341
|
+
function digestString(value, label) {
|
|
342
|
+
if (typeof value !== "string" || !/^[0-9a-f]{64}$/.test(value)) {
|
|
343
|
+
throw new BrowserFontError("invalid_response", `${label} must be a lowercase SHA-256 digest.`);
|
|
344
|
+
}
|
|
345
|
+
return value;
|
|
346
|
+
}
|
|
347
|
+
function boundedNumber(value, label, minimum, maximum) {
|
|
348
|
+
if (typeof value !== "number" || !Number.isFinite(value)
|
|
349
|
+
|| value < minimum || value > maximum) {
|
|
350
|
+
throw new BrowserFontError("invalid_response", `${label} must be a finite number from ${minimum} through ${maximum}.`);
|
|
351
|
+
}
|
|
352
|
+
return value;
|
|
353
|
+
}
|
|
354
|
+
function integer(value, label, minimum, maximum) {
|
|
355
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
356
|
+
throw new BrowserFontError("invalid_response", `${label} must be an integer from ${minimum} through ${maximum}.`);
|
|
357
|
+
}
|
|
358
|
+
return value;
|
|
359
|
+
}
|
|
360
|
+
function canonicalBase64(value, expectedBytes, label) {
|
|
361
|
+
const expectedCharacters = 4 * Math.ceil(expectedBytes / 3);
|
|
362
|
+
if (typeof value !== "string" || value.length !== expectedCharacters || value.length % 4 !== 0
|
|
363
|
+
|| !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) {
|
|
364
|
+
throw new BrowserFontError("invalid_response", `${label} must be canonical padded base64 without whitespace.`);
|
|
365
|
+
}
|
|
366
|
+
let decoded;
|
|
367
|
+
try {
|
|
368
|
+
decoded = globalThis.atob(value);
|
|
369
|
+
}
|
|
370
|
+
catch (cause) {
|
|
371
|
+
throw new BrowserFontError("invalid_response", `${label} is not valid base64.`, String(cause));
|
|
372
|
+
}
|
|
373
|
+
if (decoded.length !== expectedBytes) {
|
|
374
|
+
throw new BrowserFontError("invalid_response", `${label} byte length does not match its metadata.`);
|
|
375
|
+
}
|
|
376
|
+
const bytes = new Uint8Array(decoded.length);
|
|
377
|
+
for (let index = 0; index < decoded.length; index++)
|
|
378
|
+
bytes[index] = decoded.charCodeAt(index);
|
|
379
|
+
let binary = "";
|
|
380
|
+
for (let index = 0; index < bytes.length; index += 0x8000) {
|
|
381
|
+
binary += String.fromCharCode(...bytes.subarray(index, index + 0x8000));
|
|
382
|
+
}
|
|
383
|
+
if (globalThis.btoa(binary) !== value) {
|
|
384
|
+
throw new BrowserFontError("invalid_response", `${label} is not canonical base64.`);
|
|
385
|
+
}
|
|
386
|
+
return bytes;
|
|
387
|
+
}
|
|
388
|
+
// Duplicated (not shared) with npm-export/src/fonts/discovery.ts's readU32 — that package
|
|
389
|
+
// can't reach this realm's internals, and the two throw genuinely different error types.
|
|
390
|
+
function readU32(bytes, offset) {
|
|
391
|
+
if (bytes.byteLength < offset + 4) {
|
|
392
|
+
throw new BrowserFontError("invalid_response", "A configured webfont has a truncated format header.");
|
|
393
|
+
}
|
|
394
|
+
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(offset, false);
|
|
395
|
+
}
|
|
396
|
+
function expandedFontByteLength(bytes, format, label, maximum) {
|
|
397
|
+
if (format !== "woff" && format !== "woff2")
|
|
398
|
+
return bytes.byteLength;
|
|
399
|
+
const declaredLength = readU32(bytes, 8);
|
|
400
|
+
if (declaredLength !== bytes.byteLength) {
|
|
401
|
+
throw new BrowserFontError("invalid_response", `${label} declared length does not match its decoded bytes.`);
|
|
402
|
+
}
|
|
403
|
+
const expanded = readU32(bytes, 16);
|
|
404
|
+
if (expanded === 0) {
|
|
405
|
+
throw new BrowserFontError("invalid_response", `${label} declares an invalid expanded size.`);
|
|
406
|
+
}
|
|
407
|
+
if (expanded > maximum) {
|
|
408
|
+
throw new BrowserFontError("resource_limit", `fontFileBytes limit exceeded by ${label} expanded bytes (${expanded} > ${maximum}).`, "fontFileBytes");
|
|
409
|
+
}
|
|
410
|
+
return expanded;
|
|
411
|
+
}
|
|
412
|
+
// The signature bytes asserted here are also decoded independently by npm-export's
|
|
413
|
+
// discovery.ts (decodedFormat), which detects a discovered file's own format from the same
|
|
414
|
+
// bytes. A new font format needs both updated, or discovery will accept what this rejects.
|
|
415
|
+
function fontSignatureMatches(bytes, format) {
|
|
416
|
+
if (bytes.length < 4)
|
|
417
|
+
return false;
|
|
418
|
+
const signature = String.fromCharCode(bytes[0], bytes[1], bytes[2], bytes[3]);
|
|
419
|
+
if (format === "otf")
|
|
420
|
+
return signature === "OTTO";
|
|
421
|
+
if (format === "woff")
|
|
422
|
+
return signature === "wOFF";
|
|
423
|
+
if (format === "woff2")
|
|
424
|
+
return signature === "wOF2";
|
|
425
|
+
return (bytes[0] === 0 && bytes[1] === 1 && bytes[2] === 0 && bytes[3] === 0)
|
|
426
|
+
|| signature === "true";
|
|
427
|
+
}
|
|
428
|
+
function scalarArray(value, label, request) {
|
|
429
|
+
if (!Array.isArray(value))
|
|
430
|
+
throw new BrowserFontError("invalid_response", `${label} must be an array.`);
|
|
431
|
+
const requestPoints = new Set(request.sampleCodePoints);
|
|
432
|
+
const result = value.map((point, index) => integer(point, `${label}[${index}]`, 0, 0x10ffff));
|
|
433
|
+
if (result.some((point) => point >= 0xd800 && point <= 0xdfff)
|
|
434
|
+
|| result.some((point, index) => index > 0 && point <= result[index - 1])
|
|
435
|
+
|| result.some((point) => !requestPoints.has(point))) {
|
|
436
|
+
throw new BrowserFontError("invalid_response", `${label} must be sorted, distinct scalar values from the request sample.`);
|
|
437
|
+
}
|
|
438
|
+
return result;
|
|
439
|
+
}
|
|
440
|
+
function snapshotFields(value, allowed, label) {
|
|
441
|
+
const record = requireObject(value, label);
|
|
442
|
+
exactKeys(record, allowed, label);
|
|
443
|
+
const snapshot = {};
|
|
444
|
+
for (const key of allowed) {
|
|
445
|
+
if (Object.prototype.hasOwnProperty.call(record, key))
|
|
446
|
+
snapshot[key] = record[key];
|
|
447
|
+
}
|
|
448
|
+
return Object.freeze(snapshot);
|
|
449
|
+
}
|
|
450
|
+
function snapshotResolverResponse(value, requestCount, limits) {
|
|
451
|
+
const response = snapshotFields(value, [
|
|
452
|
+
"schemaVersion", "resolverContract", "substitutionContractVersion",
|
|
453
|
+
"substitutionContractDigest", "outcomes", "faces",
|
|
454
|
+
], "font resolver response");
|
|
455
|
+
const faceValues = response.faces;
|
|
456
|
+
if (!Array.isArray(faceValues)) {
|
|
457
|
+
throw new BrowserFontError("invalid_response", "font resolver response faces must be an array.");
|
|
458
|
+
}
|
|
459
|
+
if (faceValues.length > limits.fontFiles) {
|
|
460
|
+
throw new BrowserFontError("resource_limit", `fontFiles limit exceeded (${faceValues.length} > ${limits.fontFiles}).`, "fontFiles");
|
|
461
|
+
}
|
|
462
|
+
const faceSnapshots = [];
|
|
463
|
+
for (let index = 0; index < faceValues.length; index++) {
|
|
464
|
+
const value = faceValues[index];
|
|
465
|
+
const face = snapshotFields(value, [
|
|
466
|
+
"id", "resolvedFamily", "postscriptName", "version", "style", "weight", "stretch",
|
|
467
|
+
"format", "mediaType", "byteLength", "sha256", "bytesBase64", "licenseEvidence",
|
|
468
|
+
], `faces[${index}]`);
|
|
469
|
+
const evidence = snapshotFields(face.licenseEvidence, ["kind", "identity", "noSubsetting"], `faces[${index}].licenseEvidence`);
|
|
470
|
+
faceSnapshots.push(Object.freeze({ ...face, licenseEvidence: evidence }));
|
|
471
|
+
}
|
|
472
|
+
const faces = Object.freeze(faceSnapshots);
|
|
473
|
+
const outcomeValues = response.outcomes;
|
|
474
|
+
if (!Array.isArray(outcomeValues) || outcomeValues.length !== requestCount) {
|
|
475
|
+
throw new BrowserFontError("invalid_response", "The font resolver must return exactly one outcome per request.");
|
|
476
|
+
}
|
|
477
|
+
const outcomeSnapshots = [];
|
|
478
|
+
for (let index = 0; index < outcomeValues.length; index++) {
|
|
479
|
+
const value = outcomeValues[index];
|
|
480
|
+
const outcome = snapshotFields(value, [
|
|
481
|
+
"requestId", "status", "faceId", "requestedFamily", "resolvedFamily", "metricCompatible",
|
|
482
|
+
"faceMatch", "glyphCoverage", "missingCodePoints",
|
|
483
|
+
], `outcomes[${index}]`);
|
|
484
|
+
const missing = outcome.missingCodePoints;
|
|
485
|
+
if (missing !== undefined && !Array.isArray(missing)) {
|
|
486
|
+
throw new BrowserFontError("invalid_response", `outcomes[${index}].missingCodePoints must be an array.`);
|
|
487
|
+
}
|
|
488
|
+
if (Array.isArray(missing) && missing.length > limits.fontSampleCodePoints) {
|
|
489
|
+
throw new BrowserFontError("resource_limit", `fontSampleCodePoints limit exceeded by outcomes[${index}].missingCodePoints.`, "fontSampleCodePoints");
|
|
490
|
+
}
|
|
491
|
+
let missingSnapshot;
|
|
492
|
+
if (Array.isArray(missing)) {
|
|
493
|
+
const copied = [];
|
|
494
|
+
for (let missingIndex = 0; missingIndex < missing.length; missingIndex++) {
|
|
495
|
+
copied.push(missing[missingIndex]);
|
|
496
|
+
}
|
|
497
|
+
missingSnapshot = Object.freeze(copied);
|
|
498
|
+
}
|
|
499
|
+
outcomeSnapshots.push(Object.freeze({
|
|
500
|
+
...outcome,
|
|
501
|
+
...(missingSnapshot ? { missingCodePoints: missingSnapshot } : {}),
|
|
502
|
+
}));
|
|
503
|
+
}
|
|
504
|
+
const outcomes = Object.freeze(outcomeSnapshots);
|
|
505
|
+
return Object.freeze({ ...response, faces, outcomes });
|
|
506
|
+
}
|
|
507
|
+
async function validateResolverResponse(value, requests, limits, expectedContractDigest) {
|
|
508
|
+
const record = snapshotResolverResponse(value, requests.length, limits);
|
|
509
|
+
if (record.schemaVersion !== FONT_RESOLVER_SCHEMA_VERSION
|
|
510
|
+
|| record.resolverContract !== FONT_RESOLVER_CONTRACT_ID
|
|
511
|
+
|| record.substitutionContractVersion !== FONT_SUBSTITUTION_CONTRACT_VERSION
|
|
512
|
+
|| record.substitutionContractDigest !== expectedContractDigest) {
|
|
513
|
+
throw new BrowserFontError("invalid_response", "The font resolver response uses a different contract identity.");
|
|
514
|
+
}
|
|
515
|
+
const faceValues = record.faces;
|
|
516
|
+
const faces = new Map();
|
|
517
|
+
let totalBytes = 0;
|
|
518
|
+
let totalExpandedBytes = 0;
|
|
519
|
+
for (const [index, face] of faceValues.entries()) {
|
|
520
|
+
const id = shortString(face.id, `faces[${index}].id`, 128);
|
|
521
|
+
if (faces.has(id))
|
|
522
|
+
throw new BrowserFontError("invalid_response", `Duplicate configured face id: ${id}.`);
|
|
523
|
+
const format = face.format;
|
|
524
|
+
if (format !== "ttf" && format !== "otf" && format !== "woff" && format !== "woff2") {
|
|
525
|
+
throw new BrowserFontError("invalid_response", `faces[${index}].format is unsupported.`);
|
|
526
|
+
}
|
|
527
|
+
if (face.mediaType !== FORMAT_MEDIA.get(format)) {
|
|
528
|
+
throw new BrowserFontError("invalid_response", `faces[${index}].mediaType does not match its format.`);
|
|
529
|
+
}
|
|
530
|
+
const byteLength = integer(face.byteLength, `faces[${index}].byteLength`, 1, limits.fontFileBytes);
|
|
531
|
+
totalBytes += byteLength;
|
|
532
|
+
if (totalBytes > limits.fontTotalBytes) {
|
|
533
|
+
throw new BrowserFontError("resource_limit", `fontTotalBytes limit exceeded (${totalBytes} > ${limits.fontTotalBytes}).`, "fontTotalBytes");
|
|
534
|
+
}
|
|
535
|
+
const bytesBase64 = face.bytesBase64;
|
|
536
|
+
const bytes = canonicalBase64(bytesBase64, byteLength, `faces[${index}].bytesBase64`);
|
|
537
|
+
if (!fontSignatureMatches(bytes, format)) {
|
|
538
|
+
throw new BrowserFontError("invalid_response", `faces[${index}] decoded signature does not match its format.`);
|
|
539
|
+
}
|
|
540
|
+
const expandedBytes = expandedFontByteLength(bytes, format, `faces[${index}]`, limits.fontFileBytes);
|
|
541
|
+
totalExpandedBytes += expandedBytes;
|
|
542
|
+
if (totalExpandedBytes > limits.fontTotalBytes) {
|
|
543
|
+
throw new BrowserFontError("resource_limit", `fontTotalBytes limit exceeded by expanded font bytes (${totalExpandedBytes} > ${limits.fontTotalBytes}).`, "fontTotalBytes");
|
|
544
|
+
}
|
|
545
|
+
const expectedDigest = digestString(face.sha256, `faces[${index}].sha256`);
|
|
546
|
+
if (await sha256(bytes) !== expectedDigest) {
|
|
547
|
+
throw new BrowserFontError("invalid_response", `faces[${index}] bytes do not match sha256.`);
|
|
548
|
+
}
|
|
549
|
+
const evidence = face.licenseEvidence;
|
|
550
|
+
if (evidence.kind !== "installable" && evidence.kind !== "previewPrint"
|
|
551
|
+
&& evidence.kind !== "editable" && evidence.kind !== "attested") {
|
|
552
|
+
throw new BrowserFontError("invalid_response", `faces[${index}].licenseEvidence.kind is invalid.`);
|
|
553
|
+
}
|
|
554
|
+
if (evidence.noSubsetting !== true && evidence.noSubsetting !== false) {
|
|
555
|
+
throw new BrowserFontError("invalid_response", `faces[${index}].licenseEvidence.noSubsetting must be boolean.`);
|
|
556
|
+
}
|
|
557
|
+
const style = face.style;
|
|
558
|
+
if (style !== "normal" && style !== "italic" && style !== "oblique") {
|
|
559
|
+
throw new BrowserFontError("invalid_response", `faces[${index}].style is invalid.`);
|
|
560
|
+
}
|
|
561
|
+
const licenseEvidence = Object.freeze({
|
|
562
|
+
kind: evidence.kind,
|
|
563
|
+
identity: digestString(evidence.identity, `faces[${index}].licenseEvidence.identity`),
|
|
564
|
+
noSubsetting: evidence.noSubsetting,
|
|
565
|
+
});
|
|
566
|
+
const resolved = Object.freeze({
|
|
567
|
+
id,
|
|
568
|
+
resolvedFamily: normalizeFontFamilyName(shortString(face.resolvedFamily, `faces[${index}].resolvedFamily`, MAX_FAMILY_CHARACTERS)),
|
|
569
|
+
...(face.postscriptName === undefined
|
|
570
|
+
? {}
|
|
571
|
+
: { postscriptName: shortString(face.postscriptName, `faces[${index}].postscriptName`) }),
|
|
572
|
+
version: shortString(face.version, `faces[${index}].version`),
|
|
573
|
+
style,
|
|
574
|
+
weight: integer(face.weight, `faces[${index}].weight`, 1, 1000),
|
|
575
|
+
stretch: boundedNumber(face.stretch, `faces[${index}].stretch`, 50, 200),
|
|
576
|
+
format,
|
|
577
|
+
mediaType: FORMAT_MEDIA.get(format),
|
|
578
|
+
byteLength,
|
|
579
|
+
sha256: expectedDigest,
|
|
580
|
+
bytesBase64: bytesBase64,
|
|
581
|
+
licenseEvidence,
|
|
582
|
+
});
|
|
583
|
+
faces.set(id, { record: resolved, bytes });
|
|
584
|
+
}
|
|
585
|
+
const outcomeValues = record.outcomes;
|
|
586
|
+
const requestById = new Map(requests.map((request) => [request.id, request]));
|
|
587
|
+
const seen = new Set();
|
|
588
|
+
const outcomes = [];
|
|
589
|
+
for (const [index, outcome] of outcomeValues.entries()) {
|
|
590
|
+
const requestId = shortString(outcome.requestId, `outcomes[${index}].requestId`, 128);
|
|
591
|
+
const request = requestById.get(requestId);
|
|
592
|
+
if (!request || seen.has(requestId)) {
|
|
593
|
+
throw new BrowserFontError("invalid_response", `outcomes[${index}].requestId is unknown or duplicated.`);
|
|
594
|
+
}
|
|
595
|
+
seen.add(requestId);
|
|
596
|
+
const status = outcome.status;
|
|
597
|
+
if (status !== "resolved" && status !== "substituted" && status !== "missing" && status !== "unverified") {
|
|
598
|
+
throw new BrowserFontError("invalid_response", `outcomes[${index}].status is invalid.`);
|
|
599
|
+
}
|
|
600
|
+
const faceId = outcome.faceId === undefined
|
|
601
|
+
? undefined
|
|
602
|
+
: shortString(outcome.faceId, `outcomes[${index}].faceId`, 128);
|
|
603
|
+
if ((status === "resolved" || status === "substituted") !== (faceId !== undefined)) {
|
|
604
|
+
throw new BrowserFontError("invalid_response", `outcomes[${index}] has an inconsistent face selection.`);
|
|
605
|
+
}
|
|
606
|
+
const selectedFace = faceId === undefined ? undefined : faces.get(faceId)?.record;
|
|
607
|
+
if (faceId && !selectedFace)
|
|
608
|
+
throw new BrowserFontError("invalid_response", `outcomes[${index}] references an unknown face.`);
|
|
609
|
+
const requestedFamily = outcome.requestedFamily === undefined
|
|
610
|
+
? request.familyStack[0]
|
|
611
|
+
: normalizeFontFamilyName(shortString(outcome.requestedFamily, `outcomes[${index}].requestedFamily`, MAX_FAMILY_CHARACTERS));
|
|
612
|
+
if (!request.familyStack.some((family) => fontFamilyKey(family) === fontFamilyKey(requestedFamily))) {
|
|
613
|
+
throw new BrowserFontError("invalid_response", `outcomes[${index}].requestedFamily is not in the request stack.`);
|
|
614
|
+
}
|
|
615
|
+
const resolvedFamily = outcome.resolvedFamily === undefined
|
|
616
|
+
? selectedFace?.resolvedFamily
|
|
617
|
+
: normalizeFontFamilyName(shortString(outcome.resolvedFamily, `outcomes[${index}].resolvedFamily`, MAX_FAMILY_CHARACTERS));
|
|
618
|
+
if (selectedFace && (!resolvedFamily || fontFamilyKey(resolvedFamily) !== fontFamilyKey(selectedFace.resolvedFamily))) {
|
|
619
|
+
throw new BrowserFontError("invalid_response", `outcomes[${index}].resolvedFamily does not match its face.`);
|
|
620
|
+
}
|
|
621
|
+
if (status === "resolved"
|
|
622
|
+
&& (request.familyKinds[0] !== "named"
|
|
623
|
+
|| fontFamilyKey(requestedFamily) !== fontFamilyKey(request.familyStack[0])
|
|
624
|
+
|| !resolvedFamily
|
|
625
|
+
|| fontFamilyKey(resolvedFamily) !== fontFamilyKey(request.familyStack[0]))) {
|
|
626
|
+
throw new BrowserFontError("invalid_response", `outcomes[${index}] resolved status must match the request's primary family.`);
|
|
627
|
+
}
|
|
628
|
+
if (outcome.metricCompatible !== undefined && typeof outcome.metricCompatible !== "boolean") {
|
|
629
|
+
throw new BrowserFontError("invalid_response", `outcomes[${index}].metricCompatible must be boolean.`);
|
|
630
|
+
}
|
|
631
|
+
if (outcome.faceMatch !== undefined && outcome.faceMatch !== "exact" && outcome.faceMatch !== "synthesized") {
|
|
632
|
+
throw new BrowserFontError("invalid_response", `outcomes[${index}].faceMatch is invalid.`);
|
|
633
|
+
}
|
|
634
|
+
if (outcome.glyphCoverage !== undefined
|
|
635
|
+
&& outcome.glyphCoverage !== "complete" && outcome.glyphCoverage !== "partial"
|
|
636
|
+
&& outcome.glyphCoverage !== "unverified") {
|
|
637
|
+
throw new BrowserFontError("invalid_response", `outcomes[${index}].glyphCoverage is invalid.`);
|
|
638
|
+
}
|
|
639
|
+
const missingCodePoints = outcome.missingCodePoints === undefined
|
|
640
|
+
? undefined
|
|
641
|
+
: scalarArray(outcome.missingCodePoints, `outcomes[${index}].missingCodePoints`, request);
|
|
642
|
+
const missingCodePointCount = missingCodePoints?.length ?? 0;
|
|
643
|
+
if (outcome.glyphCoverage === "complete" && missingCodePointCount > 0) {
|
|
644
|
+
throw new BrowserFontError("invalid_response", `outcomes[${index}] complete coverage cannot name missing code points.`);
|
|
645
|
+
}
|
|
646
|
+
if ((outcome.glyphCoverage === "partial" && missingCodePointCount === 0)
|
|
647
|
+
|| (missingCodePointCount > 0 && outcome.glyphCoverage !== "partial")) {
|
|
648
|
+
throw new BrowserFontError("invalid_response", `outcomes[${index}] partial coverage must exactly match a non-empty missing-code-point list.`);
|
|
649
|
+
}
|
|
650
|
+
if (selectedFace && missingCodePointCount === 0 && outcome.glyphCoverage !== "complete") {
|
|
651
|
+
throw new BrowserFontError("invalid_response", `outcomes[${index}] must declare complete coverage when no code points are missing.`);
|
|
652
|
+
}
|
|
653
|
+
if (outcome.faceMatch === "exact" && selectedFace
|
|
654
|
+
&& (selectedFace.style !== request.style
|
|
655
|
+
|| selectedFace.weight !== request.weight
|
|
656
|
+
|| selectedFace.stretch !== request.stretch)) {
|
|
657
|
+
throw new BrowserFontError("invalid_response", `outcomes[${index}] claims an exact face with different style, weight, or stretch.`);
|
|
658
|
+
}
|
|
659
|
+
if (status === "missing" || status === "unverified") {
|
|
660
|
+
const hasSelectionMetadata = outcome.resolvedFamily !== undefined
|
|
661
|
+
|| outcome.metricCompatible !== undefined
|
|
662
|
+
|| outcome.faceMatch !== undefined
|
|
663
|
+
|| outcome.missingCodePoints !== undefined
|
|
664
|
+
|| (outcome.glyphCoverage !== undefined && outcome.glyphCoverage !== "unverified");
|
|
665
|
+
if (hasSelectionMetadata) {
|
|
666
|
+
throw new BrowserFontError("invalid_response", `outcomes[${index}] cannot attach selection metadata to ${status} status.`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
outcomes.push(Object.freeze({
|
|
670
|
+
requestId,
|
|
671
|
+
status,
|
|
672
|
+
...(faceId ? { faceId } : {}),
|
|
673
|
+
requestedFamily,
|
|
674
|
+
...(resolvedFamily ? { resolvedFamily } : {}),
|
|
675
|
+
...(outcome.metricCompatible === undefined ? {} : { metricCompatible: outcome.metricCompatible }),
|
|
676
|
+
...(outcome.faceMatch === undefined ? {} : { faceMatch: outcome.faceMatch }),
|
|
677
|
+
...(outcome.glyphCoverage === undefined ? {} : { glyphCoverage: outcome.glyphCoverage }),
|
|
678
|
+
...(missingCodePoints === undefined ? {} : { missingCodePoints: Object.freeze(missingCodePoints) }),
|
|
679
|
+
}));
|
|
680
|
+
}
|
|
681
|
+
const referencedFaces = new Set(outcomes.flatMap((outcome) => outcome.faceId ? [outcome.faceId] : []));
|
|
682
|
+
if (Array.from(faces.keys()).some((id) => !referencedFaces.has(id))) {
|
|
683
|
+
throw new BrowserFontError("invalid_response", "The font resolver returned an unreferenced face.");
|
|
684
|
+
}
|
|
685
|
+
outcomes.sort((left, right) => compareText(left.requestId, right.requestId));
|
|
686
|
+
const faceRecords = Array.from(faces.values(), ({ record }) => record)
|
|
687
|
+
.sort((left, right) => compareText(left.id, right.id));
|
|
688
|
+
return {
|
|
689
|
+
response: Object.freeze({
|
|
690
|
+
schemaVersion: FONT_RESOLVER_SCHEMA_VERSION,
|
|
691
|
+
resolverContract: FONT_RESOLVER_CONTRACT_ID,
|
|
692
|
+
substitutionContractVersion: FONT_SUBSTITUTION_CONTRACT_VERSION,
|
|
693
|
+
substitutionContractDigest: expectedContractDigest,
|
|
694
|
+
outcomes: Object.freeze(outcomes),
|
|
695
|
+
faces: Object.freeze(faceRecords),
|
|
696
|
+
}),
|
|
697
|
+
faces,
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
function cssString(value) {
|
|
701
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")
|
|
702
|
+
.replace(/[\n\r\f]/g, (character) => `\\${character.codePointAt(0).toString(16)} `)}"`;
|
|
703
|
+
}
|
|
704
|
+
function serializeFamilyStack(families, kinds = families.map(() => "named")) {
|
|
705
|
+
return families.map((family, index) => kinds[index] === "generic"
|
|
706
|
+
? fontFamilyKey(family)
|
|
707
|
+
: cssString(family)).join(", ");
|
|
708
|
+
}
|
|
709
|
+
function fontSpecification(request, families, kinds) {
|
|
710
|
+
// Chromium's FontFace descriptor accepts percentage stretches, but
|
|
711
|
+
// FontFaceSet.load() still parses the legacy font shorthand and rejects
|
|
712
|
+
// percentage tokens. Preserve exact standard widths through their keyword;
|
|
713
|
+
// an uncommon percentage remains enforced by the installed @font-face rule.
|
|
714
|
+
const stretch = Array.from(FONT_STRETCH_PERCENT).find(([, value]) => value === request.stretch)?.[0];
|
|
715
|
+
return `${request.style} ${request.weight}${stretch ? ` ${stretch}` : ""} 12px ${serializeFamilyStack(families, kinds)}`;
|
|
716
|
+
}
|
|
717
|
+
function responseIdentityMaterial(response) {
|
|
718
|
+
return {
|
|
719
|
+
schemaVersion: response.schemaVersion,
|
|
720
|
+
resolverContract: response.resolverContract,
|
|
721
|
+
substitutionContractVersion: response.substitutionContractVersion,
|
|
722
|
+
substitutionContractDigest: response.substitutionContractDigest,
|
|
723
|
+
outcomes: response.outcomes,
|
|
724
|
+
faces: response.faces.map(({ bytesBase64: _bytes, ...face }) => face),
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
async function syntheticFamily(resolverDigest, faceId) {
|
|
728
|
+
const mapping = await digestJson({
|
|
729
|
+
faceId,
|
|
730
|
+
resolverDigest,
|
|
731
|
+
});
|
|
732
|
+
return `__DocxodusConfigured_${resolverDigest.slice(0, 16)}_${mapping.slice(0, 16)}`;
|
|
733
|
+
}
|
|
734
|
+
function restoreUse(use) {
|
|
735
|
+
if (use.originalStyle === null)
|
|
736
|
+
use.element.removeAttribute("style");
|
|
737
|
+
else
|
|
738
|
+
use.element.setAttribute("style", use.originalStyle);
|
|
739
|
+
}
|
|
740
|
+
function faceRule(family, face) {
|
|
741
|
+
return `@font-face{font-family:${cssString(family)};src:url("data:${face.mediaType};base64,${face.bytesBase64}") format("${FORMAT_HINT.get(face.format)}");font-style:${face.style};font-weight:${face.weight};font-stretch:${face.stretch}%;font-display:block}`;
|
|
742
|
+
}
|
|
743
|
+
function baseResolution(request) {
|
|
744
|
+
return {
|
|
745
|
+
requestId: request.id,
|
|
746
|
+
requestedFamily: request.familyStack[0],
|
|
747
|
+
requestedFamilies: [...request.familyStack],
|
|
748
|
+
requestedFamilyKinds: [...request.familyKinds],
|
|
749
|
+
requestedStyle: request.style,
|
|
750
|
+
requestedWeight: request.weight,
|
|
751
|
+
requestedStretch: request.stretch,
|
|
752
|
+
sampleCodePointCount: request.sampleCodePoints.length,
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
// Availability probing needs glyphs whose advance widths differ between typefaces;
|
|
756
|
+
// digits and mixed-case letters separate metric-compatible families far better than
|
|
757
|
+
// a single repeated character does.
|
|
758
|
+
const FONT_AVAILABILITY_SAMPLE = "MWmwilAaGg0189";
|
|
759
|
+
const FONT_AVAILABILITY_FALLBACKS = ["monospace", "serif", "sans-serif"];
|
|
760
|
+
/**
|
|
761
|
+
* Whether the render environment can actually paint `family`.
|
|
762
|
+
*
|
|
763
|
+
* `FontFaceSet.check()` cannot answer this. It reports whether pending downloads have
|
|
764
|
+
* settled, so Chromium returns true for a family it has never heard of — every missing
|
|
765
|
+
* font would be recorded as merely unverified. Measuring instead: a family that moves the
|
|
766
|
+
* advance width away from every generic fallback is present, and one that matches all
|
|
767
|
+
* three is being silently substituted for.
|
|
768
|
+
*/
|
|
769
|
+
function familyAvailable(document, family, kind) {
|
|
770
|
+
// A generic family always resolves to whatever the environment maps it to.
|
|
771
|
+
if (kind === "generic")
|
|
772
|
+
return true;
|
|
773
|
+
const context = document.createElement("canvas").getContext("2d");
|
|
774
|
+
if (!context)
|
|
775
|
+
return true;
|
|
776
|
+
const token = serializeFamilyStack([family], ["named"]);
|
|
777
|
+
for (const fallback of FONT_AVAILABILITY_FALLBACKS) {
|
|
778
|
+
context.font = `72px ${fallback}`;
|
|
779
|
+
const baseline = context.measureText(FONT_AVAILABILITY_SAMPLE).width;
|
|
780
|
+
context.font = `72px ${token}, ${fallback}`;
|
|
781
|
+
if (context.measureText(FONT_AVAILABILITY_SAMPLE).width !== baseline)
|
|
782
|
+
return true;
|
|
783
|
+
}
|
|
784
|
+
return false;
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* The first family a request names is the one the document asked for; anything after it is
|
|
788
|
+
* already a substitution the author accepted, and a trailing generic always resolves.
|
|
789
|
+
*/
|
|
790
|
+
function requestedFamilyAvailable(document, request) {
|
|
791
|
+
const index = request.familyStack.findIndex((_, position) => request.familyKinds[position] === "named");
|
|
792
|
+
if (index < 0)
|
|
793
|
+
return true;
|
|
794
|
+
return familyAvailable(document, request.familyStack[index], "named");
|
|
795
|
+
}
|
|
796
|
+
async function observedFonts(document, inventory, pending, signal, contractDigest) {
|
|
797
|
+
const probes = new Map();
|
|
798
|
+
if (document.fonts) {
|
|
799
|
+
await forEachBounded(inventory.requests, async (request) => {
|
|
800
|
+
const sample = String.fromCodePoint(...request.sampleCodePoints.slice(0, 4096)) || " ";
|
|
801
|
+
const specification = fontSpecification(request, request.familyStack, request.familyKinds);
|
|
802
|
+
try {
|
|
803
|
+
// Loading first settles any webface the document declared; availability of the
|
|
804
|
+
// requested family itself is then measured, not asked of check().
|
|
805
|
+
await abortable(document.fonts.load(specification, sample), signal);
|
|
806
|
+
probes.set(request.id, requestedFamilyAvailable(document, request));
|
|
807
|
+
}
|
|
808
|
+
catch (error) {
|
|
809
|
+
if (signal.aborted)
|
|
810
|
+
throw error;
|
|
811
|
+
probes.set(request.id, false);
|
|
812
|
+
}
|
|
813
|
+
finally {
|
|
814
|
+
pending.delete(`request:${request.id}`);
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
await abortable(document.fonts.ready.then(() => undefined), signal);
|
|
818
|
+
}
|
|
819
|
+
else {
|
|
820
|
+
inventory.requests.forEach((request) => pending.delete(`request:${request.id}`));
|
|
821
|
+
}
|
|
822
|
+
pending.delete("document.fonts.ready");
|
|
823
|
+
const resolutions = [];
|
|
824
|
+
for (const request of inventory.requests) {
|
|
825
|
+
resolutions.push({
|
|
826
|
+
...baseResolution(request),
|
|
827
|
+
sampleDigest: await digestJson(request.sampleCodePoints),
|
|
828
|
+
status: probes.get(request.id) === false ? "missing" : "unverified",
|
|
829
|
+
source: "browser",
|
|
830
|
+
glyphCoverage: "unverified",
|
|
831
|
+
verified: false,
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
const resolutionDigest = await digestJson({ contractDigest, requests: inventory.requests, resolutions });
|
|
835
|
+
return {
|
|
836
|
+
identity: {
|
|
837
|
+
resolverContract: FONT_RESOLVER_CONTRACT_ID,
|
|
838
|
+
substitutionContractVersion: FONT_SUBSTITUTION_CONTRACT_VERSION,
|
|
839
|
+
substitutionContractDigest: contractDigest,
|
|
840
|
+
resolutionDigest,
|
|
841
|
+
},
|
|
842
|
+
resolutions,
|
|
843
|
+
renderedTextNodeCount: inventory.renderedTextNodeCount,
|
|
844
|
+
resolverConfigured: false,
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
async function configuredFonts(document, inventory, resolver, limits, pending, signal, contractDigest) {
|
|
848
|
+
pending.add("resolver");
|
|
849
|
+
let responseValue;
|
|
850
|
+
const resolverRequests = Object.freeze(inventory.requests.map((request) => Object.freeze({
|
|
851
|
+
id: request.id,
|
|
852
|
+
familyStack: Object.freeze([...request.familyStack]),
|
|
853
|
+
familyKinds: Object.freeze([...request.familyKinds]),
|
|
854
|
+
style: request.style,
|
|
855
|
+
weight: request.weight,
|
|
856
|
+
stretch: request.stretch,
|
|
857
|
+
sampleCodePoints: Object.freeze([...request.sampleCodePoints]),
|
|
858
|
+
})));
|
|
859
|
+
const resolverRequest = Object.freeze({
|
|
860
|
+
schemaVersion: FONT_RESOLVER_SCHEMA_VERSION,
|
|
861
|
+
requests: resolverRequests,
|
|
862
|
+
});
|
|
863
|
+
try {
|
|
864
|
+
responseValue = await abortable(resolver(resolverRequest, signal), signal);
|
|
865
|
+
}
|
|
866
|
+
catch (cause) {
|
|
867
|
+
if (signal.aborted)
|
|
868
|
+
throw cause;
|
|
869
|
+
// The resolver is a trusted caller policy authority, not necessarily this validated
|
|
870
|
+
// wire contract — a Node-backed resolver's own filesystem/policy failure crosses here
|
|
871
|
+
// with its original code/phase/remediation intact (see reconstructDocxodusExportError
|
|
872
|
+
// in export-browser.ts); preserve it as `cause` so rethrowBrowserFontError can report
|
|
873
|
+
// the real failure instead of a generic "fix your resolver" message.
|
|
874
|
+
throw new BrowserFontError("invalid_response", `The configured font resolver failed: ${cause instanceof Error ? cause.message : String(cause)}`, undefined, cause);
|
|
875
|
+
}
|
|
876
|
+
finally {
|
|
877
|
+
pending.delete("resolver");
|
|
878
|
+
}
|
|
879
|
+
const validated = await validateResolverResponse(responseValue, inventory.requests, limits, contractDigest);
|
|
880
|
+
const resolverDigest = await digestJson({
|
|
881
|
+
requests: inventory.requests,
|
|
882
|
+
response: responseIdentityMaterial(validated.response),
|
|
883
|
+
});
|
|
884
|
+
// A missing document realm is an engine invariant break, not a per-face decode failure —
|
|
885
|
+
// check it once, outside the per-face catch below, so it fails loud instead of being
|
|
886
|
+
// silently reclassified as "this one font is corrupt" for every face in flight.
|
|
887
|
+
const view = document.defaultView;
|
|
888
|
+
if (!view)
|
|
889
|
+
throw new BrowserFontError("invalid_response", "The render document has no font realm.");
|
|
890
|
+
const successfulFaces = new Set();
|
|
891
|
+
const faceLoadFailures = new Map();
|
|
892
|
+
for (const id of validated.faces.keys())
|
|
893
|
+
pending.add(`face:${id}`);
|
|
894
|
+
await forEachBounded(Array.from(validated.faces), async ([id, face]) => {
|
|
895
|
+
try {
|
|
896
|
+
const candidate = new view.FontFace("__DocxodusValidation", new Uint8Array(face.bytes).buffer, {
|
|
897
|
+
style: face.record.style,
|
|
898
|
+
weight: String(face.record.weight),
|
|
899
|
+
stretch: `${face.record.stretch}%`,
|
|
900
|
+
});
|
|
901
|
+
await abortable(candidate.load(), signal);
|
|
902
|
+
successfulFaces.add(id);
|
|
903
|
+
}
|
|
904
|
+
catch (error) {
|
|
905
|
+
if (signal.aborted)
|
|
906
|
+
throw error;
|
|
907
|
+
// The Font Loading API's own reason (SyntaxError for undecodable bytes, NetworkError,
|
|
908
|
+
// ...) is the one piece of information that tells a caller *why* a face failed — it
|
|
909
|
+
// never otherwise reaches the render report.
|
|
910
|
+
faceLoadFailures.set(id, error instanceof Error
|
|
911
|
+
? `${error.name}: ${error.message}`
|
|
912
|
+
: String(error));
|
|
913
|
+
}
|
|
914
|
+
finally {
|
|
915
|
+
pending.delete(`face:${id}`);
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
const outcomeById = new Map(validated.response.outcomes.map((outcome) => [outcome.requestId, outcome]));
|
|
919
|
+
const successfulRequests = new Set(validated.response.outcomes
|
|
920
|
+
.filter((outcome) => outcome.faceId && successfulFaces.has(outcome.faceId))
|
|
921
|
+
.map((outcome) => outcome.requestId));
|
|
922
|
+
const syntheticByRequest = new Map();
|
|
923
|
+
const syntheticByFaceId = new Map();
|
|
924
|
+
for (const outcome of validated.response.outcomes) {
|
|
925
|
+
if (!outcome.faceId || !successfulFaces.has(outcome.faceId))
|
|
926
|
+
continue;
|
|
927
|
+
let synthetic = syntheticByFaceId.get(outcome.faceId);
|
|
928
|
+
if (!synthetic) {
|
|
929
|
+
synthetic = await syntheticFamily(resolverDigest, outcome.faceId);
|
|
930
|
+
syntheticByFaceId.set(outcome.faceId, synthetic);
|
|
931
|
+
}
|
|
932
|
+
syntheticByRequest.set(outcome.requestId, synthetic);
|
|
933
|
+
}
|
|
934
|
+
const installedMappings = new Set();
|
|
935
|
+
const style = document.createElement("style");
|
|
936
|
+
style.id = "docxodus-configured-fonts";
|
|
937
|
+
const rebuildStyle = () => {
|
|
938
|
+
const rules = [];
|
|
939
|
+
installedMappings.clear();
|
|
940
|
+
for (const outcome of validated.response.outcomes) {
|
|
941
|
+
const synthetic = syntheticByRequest.get(outcome.requestId);
|
|
942
|
+
const face = outcome.faceId ? validated.faces.get(outcome.faceId)?.record : undefined;
|
|
943
|
+
if (!synthetic || !face || !successfulRequests.has(outcome.requestId))
|
|
944
|
+
continue;
|
|
945
|
+
const mapping = `${synthetic}\u0000${face.id}`;
|
|
946
|
+
if (installedMappings.has(mapping))
|
|
947
|
+
continue;
|
|
948
|
+
installedMappings.add(mapping);
|
|
949
|
+
rules.push(faceRule(synthetic, face));
|
|
950
|
+
}
|
|
951
|
+
style.textContent = rules.sort(compareText).join("\n");
|
|
952
|
+
};
|
|
953
|
+
rebuildStyle();
|
|
954
|
+
if (style.textContent)
|
|
955
|
+
document.head.appendChild(style);
|
|
956
|
+
const usesByRequest = new Map();
|
|
957
|
+
for (const use of inventory.uses) {
|
|
958
|
+
const list = usesByRequest.get(use.requestKey) ?? [];
|
|
959
|
+
list.push(use);
|
|
960
|
+
usesByRequest.set(use.requestKey, list);
|
|
961
|
+
}
|
|
962
|
+
for (const request of inventory.requests) {
|
|
963
|
+
const synthetic = syntheticByRequest.get(request.id);
|
|
964
|
+
if (!synthetic)
|
|
965
|
+
continue;
|
|
966
|
+
for (const use of usesByRequest.get(request.id) ?? []) {
|
|
967
|
+
use.element.style.setProperty("font-family", `${cssString(synthetic)}, ${serializeFamilyStack(request.familyStack, request.familyKinds)}`, "important");
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
const fallbackAvailable = new Map();
|
|
971
|
+
const requestLoadFailures = new Map();
|
|
972
|
+
if (document.fonts) {
|
|
973
|
+
await forEachBounded(inventory.requests, async (request) => {
|
|
974
|
+
const synthetic = syntheticByRequest.get(request.id);
|
|
975
|
+
try {
|
|
976
|
+
if (synthetic) {
|
|
977
|
+
const sample = String.fromCodePoint(...request.sampleCodePoints.slice(0, 4096)) || " ";
|
|
978
|
+
const loaded = await abortable(document.fonts.load(fontSpecification(request, [synthetic]), sample), signal);
|
|
979
|
+
if (loaded.length === 0) {
|
|
980
|
+
requestLoadFailures.set(request.id, "The face decoded, but the browser did not apply it to the requested style.");
|
|
981
|
+
successfulRequests.delete(request.id);
|
|
982
|
+
syntheticByRequest.delete(request.id);
|
|
983
|
+
for (const use of usesByRequest.get(request.id) ?? [])
|
|
984
|
+
restoreUse(use);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
else {
|
|
988
|
+
const specification = fontSpecification(request, request.familyStack, request.familyKinds);
|
|
989
|
+
const sample = String.fromCodePoint(...request.sampleCodePoints.slice(0, 4096)) || " ";
|
|
990
|
+
// document.fonts.check() cannot answer this — see familyAvailable() above for why.
|
|
991
|
+
// Measure the same way observedFonts() does instead of asking check().
|
|
992
|
+
await abortable(document.fonts.load(specification, sample), signal);
|
|
993
|
+
fallbackAvailable.set(request.id, requestedFamilyAvailable(document, request));
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
catch (error) {
|
|
997
|
+
if (signal.aborted)
|
|
998
|
+
throw error;
|
|
999
|
+
// Same rationale as faceLoadFailures above: capture what the Font Loading API says,
|
|
1000
|
+
// not just that it rejected.
|
|
1001
|
+
requestLoadFailures.set(request.id, error instanceof Error
|
|
1002
|
+
? `${error.name}: ${error.message}`
|
|
1003
|
+
: String(error));
|
|
1004
|
+
fallbackAvailable.set(request.id, false);
|
|
1005
|
+
successfulRequests.delete(request.id);
|
|
1006
|
+
syntheticByRequest.delete(request.id);
|
|
1007
|
+
for (const use of usesByRequest.get(request.id) ?? [])
|
|
1008
|
+
restoreUse(use);
|
|
1009
|
+
}
|
|
1010
|
+
finally {
|
|
1011
|
+
pending.delete(`request:${request.id}`);
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
rebuildStyle();
|
|
1015
|
+
if (!style.textContent)
|
|
1016
|
+
style.remove();
|
|
1017
|
+
await abortable(document.fonts.ready.then(() => undefined), signal);
|
|
1018
|
+
}
|
|
1019
|
+
else {
|
|
1020
|
+
inventory.requests.forEach((request) => pending.delete(`request:${request.id}`));
|
|
1021
|
+
}
|
|
1022
|
+
pending.delete("document.fonts.ready");
|
|
1023
|
+
const resolutions = [];
|
|
1024
|
+
for (const request of inventory.requests) {
|
|
1025
|
+
const outcome = outcomeById.get(request.id);
|
|
1026
|
+
const selected = outcome.faceId ? validated.faces.get(outcome.faceId)?.record : undefined;
|
|
1027
|
+
const loadFailed = selected !== undefined && !successfulRequests.has(request.id);
|
|
1028
|
+
// The face's own decode failure (a corrupt byte stream) is the more fundamental reason
|
|
1029
|
+
// when both are present; the request-level detail only ever fires once a face already
|
|
1030
|
+
// decoded successfully but the browser then declined to apply it.
|
|
1031
|
+
const loadFailureDetail = loadFailed
|
|
1032
|
+
? (outcome.faceId && faceLoadFailures.get(outcome.faceId)) || requestLoadFailures.get(request.id)
|
|
1033
|
+
: undefined;
|
|
1034
|
+
const browserFallbackAvailable = !selected && outcome.status === "missing"
|
|
1035
|
+
? fallbackAvailable.get(request.id) === true
|
|
1036
|
+
: undefined;
|
|
1037
|
+
const glyphCoverage = selected ? outcome.glyphCoverage : "unverified";
|
|
1038
|
+
// strictFonts' whole question, computed once here where every raw signal is in scope: an
|
|
1039
|
+
// exact, digest-identified, license-evidenced face with complete coverage that actually
|
|
1040
|
+
// loaded. `selected` truthy already implies `source` is configured/attested and
|
|
1041
|
+
// `fileSha256`/`licenseEvidence` are set below (both are always present on a validated
|
|
1042
|
+
// face record), so only faceMatch and glyphCoverage are independent conditions here.
|
|
1043
|
+
const verified = !loadFailed && outcome.status === "resolved" && selected !== undefined
|
|
1044
|
+
&& outcome.faceMatch === "exact" && glyphCoverage === "complete";
|
|
1045
|
+
resolutions.push({
|
|
1046
|
+
...baseResolution(request),
|
|
1047
|
+
sampleDigest: await digestJson(request.sampleCodePoints),
|
|
1048
|
+
...(outcome.resolvedFamily ? { resolvedFamily: outcome.resolvedFamily } : {}),
|
|
1049
|
+
...(selected?.postscriptName ? { resolvedFace: selected.postscriptName } : selected ? { resolvedFace: selected.id } : {}),
|
|
1050
|
+
// A browser-generic fallback can make document.fonts.check() succeed, but
|
|
1051
|
+
// it cannot turn an explicit resolver miss into a verified resolution.
|
|
1052
|
+
status: loadFailed ? "load_failed" : outcome.status,
|
|
1053
|
+
source: selected
|
|
1054
|
+
? selected.licenseEvidence.kind === "attested" ? "attested" : "configured"
|
|
1055
|
+
: "browser",
|
|
1056
|
+
...(selected ? {
|
|
1057
|
+
format: selected.format,
|
|
1058
|
+
fileSha256: selected.sha256,
|
|
1059
|
+
version: selected.version,
|
|
1060
|
+
licenseEvidence: { ...selected.licenseEvidence },
|
|
1061
|
+
} : {}),
|
|
1062
|
+
...(outcome.faceMatch ? { faceMatch: outcome.faceMatch } : {}),
|
|
1063
|
+
...(outcome.metricCompatible === undefined ? {} : { metricCompatible: outcome.metricCompatible }),
|
|
1064
|
+
...(glyphCoverage ? { glyphCoverage } : {}),
|
|
1065
|
+
...(outcome.missingCodePoints ? { missingCodePointCount: outcome.missingCodePoints.length } : {}),
|
|
1066
|
+
...(browserFallbackAvailable === undefined ? {} : { browserFallbackAvailable }),
|
|
1067
|
+
...(loadFailureDetail ? { loadFailureDetail } : {}),
|
|
1068
|
+
verified,
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
const resolutionDigest = await digestJson({ resolverDigest, resolutions });
|
|
1072
|
+
return {
|
|
1073
|
+
identity: {
|
|
1074
|
+
resolverContract: FONT_RESOLVER_CONTRACT_ID,
|
|
1075
|
+
substitutionContractVersion: FONT_SUBSTITUTION_CONTRACT_VERSION,
|
|
1076
|
+
substitutionContractDigest: contractDigest,
|
|
1077
|
+
resolutionDigest,
|
|
1078
|
+
resolverDigest,
|
|
1079
|
+
},
|
|
1080
|
+
resolutions,
|
|
1081
|
+
renderedTextNodeCount: inventory.renderedTextNodeCount,
|
|
1082
|
+
resolverConfigured: true,
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
export function createBrowserFontTask(document, resolver, limits) {
|
|
1086
|
+
const inventory = collectFontInventory(document, limits);
|
|
1087
|
+
const pending = new Set(inventory.requests.map((request) => `request:${request.id}`));
|
|
1088
|
+
if (document.fonts)
|
|
1089
|
+
pending.add("document.fonts.ready");
|
|
1090
|
+
return {
|
|
1091
|
+
pending: () => Array.from(pending, (item) => `font:${item}`).sort(compareText),
|
|
1092
|
+
async wait(signal) {
|
|
1093
|
+
const contractDigest = await digestJson(FONT_SUBSTITUTION_CONTRACT_MATERIAL);
|
|
1094
|
+
return resolver
|
|
1095
|
+
? configuredFonts(document, inventory, resolver, limits, pending, signal, contractDigest)
|
|
1096
|
+
: observedFonts(document, inventory, pending, signal, contractDigest);
|
|
1097
|
+
},
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
//# sourceMappingURL=font-runtime.js.map
|