@json-to-office/shared 0.7.0 → 0.9.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/dist/chunk-CP2I5NPP.js +40 -0
- package/dist/chunk-CP2I5NPP.js.map +1 -0
- package/dist/fonts/node.d.ts +86 -0
- package/dist/fonts/node.js +239 -0
- package/dist/fonts/node.js.map +1 -0
- package/dist/index.d.ts +451 -1
- package/dist/index.js +1513 -1
- package/dist/index.js.map +1 -1
- package/dist/types-CL0Hbw6x.d.ts +215 -0
- package/package.json +10 -3
package/dist/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
detectFontFormat,
|
|
3
|
+
isAllowedFontUrl
|
|
4
|
+
} from "./chunk-CP2I5NPP.js";
|
|
1
5
|
import {
|
|
2
6
|
ComponentValidationError,
|
|
3
7
|
DuplicateComponentError,
|
|
@@ -41,13 +45,1511 @@ import {
|
|
|
41
45
|
transformValueError,
|
|
42
46
|
transformValueErrors
|
|
43
47
|
} from "./chunk-ZKD5BAMU.js";
|
|
48
|
+
|
|
49
|
+
// src/schemas/font-catalog.ts
|
|
50
|
+
import { Type } from "@sinclair/typebox";
|
|
51
|
+
var SAFE_FONTS = [
|
|
52
|
+
"Arial",
|
|
53
|
+
"Calibri",
|
|
54
|
+
"Cambria",
|
|
55
|
+
"Consolas",
|
|
56
|
+
"Courier New",
|
|
57
|
+
"Georgia",
|
|
58
|
+
"Segoe UI",
|
|
59
|
+
"Tahoma",
|
|
60
|
+
"Times New Roman",
|
|
61
|
+
"Trebuchet MS",
|
|
62
|
+
"Verdana",
|
|
63
|
+
"Helvetica",
|
|
64
|
+
"Helvetica Neue",
|
|
65
|
+
"Menlo",
|
|
66
|
+
"Monaco"
|
|
67
|
+
];
|
|
68
|
+
function isSafeFont(name) {
|
|
69
|
+
const lower = name.toLowerCase();
|
|
70
|
+
return SAFE_FONTS.some((f) => f.toLowerCase() === lower);
|
|
71
|
+
}
|
|
72
|
+
var FontFamilyNameSchema = Type.String({
|
|
73
|
+
description: "Font family name. Prefer a SAFE_FONTS entry (Arial, Calibri, Cambria, Consolas, Courier New, Georgia, Segoe UI, Tahoma, Times New Roman, Trebuchet MS, Verdana, Helvetica, Helvetica Neue, Menlo, Monaco) for zero-setup rendering. For any other font, register it in props.fontRegistry[] and reference it here by family. Unregistered non-safe names render with a host fallback.",
|
|
74
|
+
examples: ["Arial", "Calibri", "Georgia", "Inter", "Roboto"]
|
|
75
|
+
});
|
|
76
|
+
var FontWeightSchema = Type.Number({
|
|
77
|
+
minimum: 100,
|
|
78
|
+
maximum: 900,
|
|
79
|
+
description: "OpenType weight (100 thin ... 900 black). Default 400."
|
|
80
|
+
});
|
|
81
|
+
var FontItalicSchema = Type.Boolean({
|
|
82
|
+
description: "Whether this source is italic. Default false."
|
|
83
|
+
});
|
|
84
|
+
var SafeFontSourceSchema = Type.Object(
|
|
85
|
+
{
|
|
86
|
+
kind: Type.Literal("safe"),
|
|
87
|
+
family: Type.String({
|
|
88
|
+
description: "A SAFE_FONTS name \u2014 installed with Office."
|
|
89
|
+
})
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
additionalProperties: false,
|
|
93
|
+
description: "Office-installed font \u2014 no embedding"
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
var GoogleFontSourceSchema = Type.Object(
|
|
97
|
+
{
|
|
98
|
+
kind: Type.Literal("google"),
|
|
99
|
+
family: Type.String({
|
|
100
|
+
description: 'Exact Google Fonts family name (e.g. "Inter").'
|
|
101
|
+
}),
|
|
102
|
+
weights: Type.Optional(
|
|
103
|
+
Type.Array(FontWeightSchema, {
|
|
104
|
+
description: "Weights to fetch. Default [400, 700]."
|
|
105
|
+
})
|
|
106
|
+
),
|
|
107
|
+
italics: Type.Optional(
|
|
108
|
+
Type.Boolean({ description: "Include italic variants. Default false." })
|
|
109
|
+
)
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
additionalProperties: false,
|
|
113
|
+
description: "Google Fonts \u2014 auto-fetched and embedded"
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
var FileFontSourceSchema = Type.Object(
|
|
117
|
+
{
|
|
118
|
+
kind: Type.Literal("file"),
|
|
119
|
+
path: Type.String({
|
|
120
|
+
description: "Path to a .ttf/.otf file. Relative paths are resolved against the JSON document file."
|
|
121
|
+
}),
|
|
122
|
+
weight: Type.Optional(FontWeightSchema),
|
|
123
|
+
italic: Type.Optional(FontItalicSchema)
|
|
124
|
+
},
|
|
125
|
+
{ additionalProperties: false, description: "Local font file to embed" }
|
|
126
|
+
);
|
|
127
|
+
var UrlFontSourceSchema = Type.Object(
|
|
128
|
+
{
|
|
129
|
+
kind: Type.Literal("url"),
|
|
130
|
+
url: Type.String({
|
|
131
|
+
description: "HTTPS URL of a TTF or OTF file."
|
|
132
|
+
}),
|
|
133
|
+
weight: Type.Optional(FontWeightSchema),
|
|
134
|
+
italic: Type.Optional(FontItalicSchema)
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
additionalProperties: false,
|
|
138
|
+
description: "Direct TTF/OTF URL (non-Google CDN)"
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
var VariableFontSourceSchema = Type.Object(
|
|
142
|
+
{
|
|
143
|
+
kind: Type.Literal("variable"),
|
|
144
|
+
url: Type.String({
|
|
145
|
+
description: "HTTPS URL of a variable TTF (`fvar` axis table required)."
|
|
146
|
+
}),
|
|
147
|
+
weight: FontWeightSchema,
|
|
148
|
+
italic: Type.Optional(FontItalicSchema),
|
|
149
|
+
axes: Type.Optional(
|
|
150
|
+
Type.Record(Type.String(), Type.Number(), {
|
|
151
|
+
description: "Additional axis pin values (e.g. `{ ital: 1, opsz: 14 }`) merged on top of the derived `wght` pin. Uncommon \u2014 the `weight` field is usually enough."
|
|
152
|
+
})
|
|
153
|
+
)
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
additionalProperties: false,
|
|
157
|
+
description: "Variable-font instancer. The fetcher pins `wght` to `weight` (plus any `axes` overrides) and emits a clean static TTF, bypassing upstream distributions that collapse multiple static weights onto the same glyph geometry."
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
var DataFontSourceSchema = Type.Object(
|
|
161
|
+
{
|
|
162
|
+
kind: Type.Literal("data"),
|
|
163
|
+
data: Type.String({
|
|
164
|
+
description: "Base64-encoded TTF/OTF or data: URL (data:font/ttf;base64,...). Makes the JSON self-contained at the cost of size."
|
|
165
|
+
}),
|
|
166
|
+
weight: Type.Optional(FontWeightSchema),
|
|
167
|
+
italic: Type.Optional(FontItalicSchema)
|
|
168
|
+
},
|
|
169
|
+
{ additionalProperties: false, description: "Inline base64 font \u2014 portable" }
|
|
170
|
+
);
|
|
171
|
+
var FontSourceSchema = Type.Union(
|
|
172
|
+
[
|
|
173
|
+
SafeFontSourceSchema,
|
|
174
|
+
GoogleFontSourceSchema,
|
|
175
|
+
FileFontSourceSchema,
|
|
176
|
+
DataFontSourceSchema,
|
|
177
|
+
UrlFontSourceSchema,
|
|
178
|
+
VariableFontSourceSchema
|
|
179
|
+
],
|
|
180
|
+
{
|
|
181
|
+
description: 'A single font variant source. Use kind:"safe" for installed fonts, kind:"google" for Google Fonts, kind:"file" for local files, kind:"data" for base64, kind:"url" for direct HTTPS TTF/OTF URLs, kind:"variable" to instance a variable TTF at a specific weight.'
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
var FontCategorySchema = Type.Union(
|
|
185
|
+
[
|
|
186
|
+
Type.Literal("sans"),
|
|
187
|
+
Type.Literal("serif"),
|
|
188
|
+
Type.Literal("mono"),
|
|
189
|
+
Type.Literal("display"),
|
|
190
|
+
Type.Literal("handwriting")
|
|
191
|
+
],
|
|
192
|
+
{ description: "Broad category used for fallback selection" }
|
|
193
|
+
);
|
|
194
|
+
var FontRegistryEntrySchema = Type.Object(
|
|
195
|
+
{
|
|
196
|
+
id: Type.String({
|
|
197
|
+
description: 'Registry key. By convention, match the display family name ("Inter", "Roboto Slab").'
|
|
198
|
+
}),
|
|
199
|
+
family: Type.String({
|
|
200
|
+
description: "Display family used in font.family / fontFace / theme.fonts.*. Usually identical to id."
|
|
201
|
+
}),
|
|
202
|
+
category: Type.Optional(FontCategorySchema),
|
|
203
|
+
sources: Type.Array(FontSourceSchema, {
|
|
204
|
+
minItems: 1,
|
|
205
|
+
description: "One or more weight/style variants. List at least a regular (weight 400, italic false)."
|
|
206
|
+
})
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
additionalProperties: false,
|
|
210
|
+
description: "A font registered for this document. Referenced by family from font.family, fontFace, and theme.fonts.*."
|
|
211
|
+
}
|
|
212
|
+
);
|
|
213
|
+
var FontRegistrySchema = Type.Array(FontRegistryEntrySchema, {
|
|
214
|
+
description: "Document-scoped font registry. Every non-safe font used in this document must be registered here."
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// src/fonts/collect.ts
|
|
218
|
+
var FONT_NAME_KEYS = /* @__PURE__ */ new Set([
|
|
219
|
+
"family",
|
|
220
|
+
"fontFace",
|
|
221
|
+
"titleFontFace",
|
|
222
|
+
"legendFontFace",
|
|
223
|
+
"dataLabelFontFace",
|
|
224
|
+
"catAxisLabelFontFace",
|
|
225
|
+
"valAxisLabelFontFace"
|
|
226
|
+
]);
|
|
227
|
+
var THEME_FONT_KEYS = /* @__PURE__ */ new Set(["heading", "body", "mono", "light"]);
|
|
228
|
+
function collect(node, out, parentKey) {
|
|
229
|
+
if (node == null) return;
|
|
230
|
+
if (typeof node === "string") {
|
|
231
|
+
if (parentKey && (FONT_NAME_KEYS.has(parentKey) || THEME_FONT_KEYS.has(parentKey))) {
|
|
232
|
+
const trimmed = node.trim();
|
|
233
|
+
if (trimmed.length > 0) out.add(trimmed);
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (Array.isArray(node)) {
|
|
238
|
+
for (const item of node) collect(item, out, parentKey);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (typeof node === "object") {
|
|
242
|
+
const maybeFonts = node.fonts;
|
|
243
|
+
if (parentKey === "theme" && maybeFonts && typeof maybeFonts === "object") {
|
|
244
|
+
for (const [k, v] of Object.entries(
|
|
245
|
+
maybeFonts
|
|
246
|
+
)) {
|
|
247
|
+
if (typeof v === "string") {
|
|
248
|
+
const trimmed = v.trim();
|
|
249
|
+
if (trimmed.length > 0) out.add(trimmed);
|
|
250
|
+
} else if (v && typeof v === "object") {
|
|
251
|
+
const fam = v.family;
|
|
252
|
+
if (typeof fam === "string" && fam.trim().length > 0) {
|
|
253
|
+
out.add(fam.trim());
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
void k;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
for (const [k, v] of Object.entries(node)) {
|
|
260
|
+
collect(v, out, k);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function collectFontNames(doc) {
|
|
265
|
+
const out = /* @__PURE__ */ new Set();
|
|
266
|
+
collect(doc, out);
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
var collectFontNamesFromDocx = collectFontNames;
|
|
270
|
+
var collectFontNamesFromPptx = collectFontNames;
|
|
271
|
+
|
|
272
|
+
// src/fonts/validator.ts
|
|
273
|
+
function buildRegistryIndex(registeredEntries) {
|
|
274
|
+
const idx = /* @__PURE__ */ new Set();
|
|
275
|
+
for (const e of registeredEntries ?? []) {
|
|
276
|
+
idx.add(e.family.toLowerCase());
|
|
277
|
+
idx.add(e.id.toLowerCase());
|
|
278
|
+
}
|
|
279
|
+
return idx;
|
|
280
|
+
}
|
|
281
|
+
function validateFontReferences(input) {
|
|
282
|
+
const registryIdx = buildRegistryIndex(input.registeredEntries);
|
|
283
|
+
const resolved = [];
|
|
284
|
+
const unresolved = [];
|
|
285
|
+
const warnings = [];
|
|
286
|
+
for (const name of input.referencedNames) {
|
|
287
|
+
if (isSafeFont(name) || registryIdx.has(name.toLowerCase())) {
|
|
288
|
+
resolved.push(name);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
unresolved.push(name);
|
|
292
|
+
warnings.push({
|
|
293
|
+
code: "FONT_UNRESOLVED",
|
|
294
|
+
family: name,
|
|
295
|
+
message: `Font "${name}" is not a SAFE_FONTS entry and is not registered via fonts.extraEntries. It will render with a host fallback on machines lacking the font. Safe fonts: ${SAFE_FONTS.join(", ")}.`
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
return { resolved, unresolved, warnings };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/fonts/synthesize.ts
|
|
302
|
+
var WEIGHT_LABELS = {
|
|
303
|
+
100: "Thin",
|
|
304
|
+
200: "ExtraLight",
|
|
305
|
+
300: "Light",
|
|
306
|
+
400: "Regular",
|
|
307
|
+
500: "Medium",
|
|
308
|
+
600: "SemiBold",
|
|
309
|
+
700: "Bold",
|
|
310
|
+
800: "ExtraBold",
|
|
311
|
+
900: "Black"
|
|
312
|
+
};
|
|
313
|
+
function synthesizeFamilyName(family, weight, italic) {
|
|
314
|
+
if (weight == null) {
|
|
315
|
+
return { family, bold: false, italic, nonCanonicalWeight: false };
|
|
316
|
+
}
|
|
317
|
+
if (weight === 400) {
|
|
318
|
+
return { family, bold: false, italic, nonCanonicalWeight: false };
|
|
319
|
+
}
|
|
320
|
+
if (weight === 700) {
|
|
321
|
+
return { family, bold: true, italic, nonCanonicalWeight: false };
|
|
322
|
+
}
|
|
323
|
+
const label = WEIGHT_LABELS[weight];
|
|
324
|
+
if (!label) {
|
|
325
|
+
return {
|
|
326
|
+
family,
|
|
327
|
+
bold: weight >= 600,
|
|
328
|
+
italic,
|
|
329
|
+
nonCanonicalWeight: true
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
const suffix = italic ? ` ${label} Italic` : ` ${label}`;
|
|
333
|
+
return {
|
|
334
|
+
family: `${family}${suffix}`,
|
|
335
|
+
bold: false,
|
|
336
|
+
italic: false,
|
|
337
|
+
nonCanonicalWeight: false
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/fonts/sources/ttf-name.ts
|
|
342
|
+
var TARGET_NAME_IDS = /* @__PURE__ */ new Set([
|
|
343
|
+
1,
|
|
344
|
+
// Font Family
|
|
345
|
+
4,
|
|
346
|
+
// Full Font Name
|
|
347
|
+
6,
|
|
348
|
+
// PostScript Name
|
|
349
|
+
16
|
|
350
|
+
// Typographic/Preferred Family
|
|
351
|
+
]);
|
|
352
|
+
var MAGIC_HEAD_CHECKSUM = 2981146554;
|
|
353
|
+
function sfntChecksum(buf) {
|
|
354
|
+
let sum = 0;
|
|
355
|
+
const end = buf.length;
|
|
356
|
+
const aligned = end - end % 4;
|
|
357
|
+
for (let i = 0; i < aligned; i += 4) {
|
|
358
|
+
sum = sum + buf.readUInt32BE(i) >>> 0;
|
|
359
|
+
}
|
|
360
|
+
if (aligned < end) {
|
|
361
|
+
let chunk = 0;
|
|
362
|
+
const remaining = end - aligned;
|
|
363
|
+
if (remaining >= 1) chunk |= buf[aligned] << 24;
|
|
364
|
+
if (remaining >= 2) chunk |= buf[aligned + 1] << 16;
|
|
365
|
+
if (remaining >= 3) chunk |= buf[aligned + 2] << 8;
|
|
366
|
+
sum = sum + chunk >>> 0;
|
|
367
|
+
}
|
|
368
|
+
return sum >>> 0;
|
|
369
|
+
}
|
|
370
|
+
function encodeString(record, value) {
|
|
371
|
+
if (record.platformID === 1) {
|
|
372
|
+
return Buffer.from(value, "ascii");
|
|
373
|
+
}
|
|
374
|
+
const out = Buffer.alloc(value.length * 2);
|
|
375
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
376
|
+
out.writeUInt16BE(value.charCodeAt(i), i * 2);
|
|
377
|
+
}
|
|
378
|
+
return out;
|
|
379
|
+
}
|
|
380
|
+
function buildNameTable(records) {
|
|
381
|
+
const count = records.length;
|
|
382
|
+
const headerSize = 6 + count * 12;
|
|
383
|
+
let heapSize = 0;
|
|
384
|
+
for (const r of records) heapSize += r.bytes.length;
|
|
385
|
+
const raw = Buffer.alloc(headerSize + heapSize);
|
|
386
|
+
raw.writeUInt16BE(0, 0);
|
|
387
|
+
raw.writeUInt16BE(count, 2);
|
|
388
|
+
raw.writeUInt16BE(headerSize, 4);
|
|
389
|
+
let heapCursor = 0;
|
|
390
|
+
for (let i = 0; i < count; i += 1) {
|
|
391
|
+
const r = records[i];
|
|
392
|
+
const ro = 6 + i * 12;
|
|
393
|
+
raw.writeUInt16BE(r.platformID, ro);
|
|
394
|
+
raw.writeUInt16BE(r.encodingID, ro + 2);
|
|
395
|
+
raw.writeUInt16BE(r.languageID, ro + 4);
|
|
396
|
+
raw.writeUInt16BE(r.nameID, ro + 6);
|
|
397
|
+
raw.writeUInt16BE(r.bytes.length, ro + 8);
|
|
398
|
+
raw.writeUInt16BE(heapCursor, ro + 10);
|
|
399
|
+
r.bytes.copy(raw, headerSize + heapCursor);
|
|
400
|
+
heapCursor += r.bytes.length;
|
|
401
|
+
}
|
|
402
|
+
return raw;
|
|
403
|
+
}
|
|
404
|
+
function rewriteFontFamilyName(input, newFamily) {
|
|
405
|
+
if (input.length < 12) return input;
|
|
406
|
+
const version = input.readUInt32BE(0);
|
|
407
|
+
const isSfnt = version === 65536 || version === 1330926671 || version === 1953658213 || version === 1954115633;
|
|
408
|
+
if (!isSfnt) return input;
|
|
409
|
+
const numTables = input.readUInt16BE(4);
|
|
410
|
+
if (numTables === 0 || input.length < 12 + numTables * 16) return input;
|
|
411
|
+
const tables = [];
|
|
412
|
+
for (let i = 0; i < numTables; i += 1) {
|
|
413
|
+
const eo = 12 + i * 16;
|
|
414
|
+
const tag = input.toString("ascii", eo, eo + 4);
|
|
415
|
+
const checksum = input.readUInt32BE(eo + 4);
|
|
416
|
+
const offset = input.readUInt32BE(eo + 8);
|
|
417
|
+
const length = input.readUInt32BE(eo + 12);
|
|
418
|
+
if (offset + length > input.length) return input;
|
|
419
|
+
tables.push({
|
|
420
|
+
tag,
|
|
421
|
+
checksum,
|
|
422
|
+
offset: 0,
|
|
423
|
+
originalOffset: offset,
|
|
424
|
+
data: input.slice(offset, offset + length)
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
const nameIdx = tables.findIndex((t) => t.tag === "name");
|
|
428
|
+
if (nameIdx === -1) return input;
|
|
429
|
+
const nameBuf = tables[nameIdx].data;
|
|
430
|
+
if (nameBuf.length < 6) return input;
|
|
431
|
+
const recordCount = nameBuf.readUInt16BE(2);
|
|
432
|
+
const stringOffset = nameBuf.readUInt16BE(4);
|
|
433
|
+
if (nameBuf.length < 6 + recordCount * 12) return input;
|
|
434
|
+
const records = [];
|
|
435
|
+
for (let i = 0; i < recordCount; i += 1) {
|
|
436
|
+
const ro = 6 + i * 12;
|
|
437
|
+
const platformID = nameBuf.readUInt16BE(ro);
|
|
438
|
+
const encodingID = nameBuf.readUInt16BE(ro + 2);
|
|
439
|
+
const languageID = nameBuf.readUInt16BE(ro + 4);
|
|
440
|
+
const nameID = nameBuf.readUInt16BE(ro + 6);
|
|
441
|
+
const length = nameBuf.readUInt16BE(ro + 8);
|
|
442
|
+
const offset = nameBuf.readUInt16BE(ro + 10);
|
|
443
|
+
const bytes = nameBuf.slice(
|
|
444
|
+
stringOffset + offset,
|
|
445
|
+
stringOffset + offset + length
|
|
446
|
+
);
|
|
447
|
+
records.push({ platformID, encodingID, languageID, nameID, bytes });
|
|
448
|
+
}
|
|
449
|
+
const psForbidden = /[[\](){}<>/%]/g;
|
|
450
|
+
const isAscii = /^[\x00-\x7f]*$/.test(newFamily);
|
|
451
|
+
const survivors = [];
|
|
452
|
+
for (const r of records) {
|
|
453
|
+
if (!TARGET_NAME_IDS.has(r.nameID)) {
|
|
454
|
+
survivors.push(r);
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
if (r.platformID === 1 && !isAscii) {
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
const value = r.nameID === 6 ? newFamily.replace(/\s+/g, "").replace(psForbidden, "").replace(/[^\x21-\x7e]/g, "") : newFamily;
|
|
461
|
+
r.bytes = encodeString(r, value);
|
|
462
|
+
survivors.push(r);
|
|
463
|
+
}
|
|
464
|
+
records.length = 0;
|
|
465
|
+
records.push(...survivors);
|
|
466
|
+
const rebuiltName = buildNameTable(records);
|
|
467
|
+
tables[nameIdx] = {
|
|
468
|
+
...tables[nameIdx],
|
|
469
|
+
data: rebuiltName,
|
|
470
|
+
checksum: sfntChecksum(rebuiltName)
|
|
471
|
+
};
|
|
472
|
+
tables.sort((a, b) => a.originalOffset - b.originalOffset);
|
|
473
|
+
let cursor = 12 + tables.length * 16;
|
|
474
|
+
for (const t of tables) {
|
|
475
|
+
cursor = cursor + 3 & ~3;
|
|
476
|
+
t.offset = cursor;
|
|
477
|
+
cursor += t.data.length;
|
|
478
|
+
}
|
|
479
|
+
const totalSize = cursor + 3 & ~3;
|
|
480
|
+
const out = Buffer.alloc(totalSize);
|
|
481
|
+
input.copy(out, 0, 0, 12);
|
|
482
|
+
out.writeUInt16BE(tables.length, 4);
|
|
483
|
+
const dirTables = [...tables].sort(
|
|
484
|
+
(a, b) => a.tag < b.tag ? -1 : a.tag > b.tag ? 1 : 0
|
|
485
|
+
);
|
|
486
|
+
for (let i = 0; i < dirTables.length; i += 1) {
|
|
487
|
+
const t = dirTables[i];
|
|
488
|
+
const eo = 12 + i * 16;
|
|
489
|
+
out.write(t.tag, eo, 4, "ascii");
|
|
490
|
+
out.writeUInt32BE(t.checksum, eo + 4);
|
|
491
|
+
out.writeUInt32BE(t.offset, eo + 8);
|
|
492
|
+
out.writeUInt32BE(t.data.length, eo + 12);
|
|
493
|
+
}
|
|
494
|
+
for (const t of tables) {
|
|
495
|
+
t.data.copy(out, t.offset);
|
|
496
|
+
}
|
|
497
|
+
const headTable = tables.find((t) => t.tag === "head");
|
|
498
|
+
if (headTable && headTable.data.length >= 12) {
|
|
499
|
+
out.writeUInt32BE(0, headTable.offset + 8);
|
|
500
|
+
const fontSum = sfntChecksum(out);
|
|
501
|
+
const adjustment = MAGIC_HEAD_CHECKSUM - fontSum >>> 0;
|
|
502
|
+
out.writeUInt32BE(adjustment, headTable.offset + 8);
|
|
503
|
+
}
|
|
504
|
+
return out;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/fonts/sources/data-loader.ts
|
|
508
|
+
var MAX_DATA_FONT_BYTES = 5 * 1024 * 1024;
|
|
509
|
+
function loadDataFontSource(input) {
|
|
510
|
+
const raw = input.data.trim();
|
|
511
|
+
let b64;
|
|
512
|
+
if (raw.startsWith("data:")) {
|
|
513
|
+
const comma = raw.indexOf(",");
|
|
514
|
+
if (comma < 0) throw new Error("Invalid data URL: no payload separator");
|
|
515
|
+
const header = raw.slice(5, comma);
|
|
516
|
+
if (!header.includes(";base64")) {
|
|
517
|
+
throw new Error("Data URL must be base64-encoded");
|
|
518
|
+
}
|
|
519
|
+
b64 = raw.slice(comma + 1);
|
|
520
|
+
} else {
|
|
521
|
+
b64 = raw;
|
|
522
|
+
}
|
|
523
|
+
const approxDecodedBytes = Math.floor(b64.length * 3 / 4);
|
|
524
|
+
if (approxDecodedBytes > MAX_DATA_FONT_BYTES) {
|
|
525
|
+
throw new Error(
|
|
526
|
+
`Font data payload exceeds ${MAX_DATA_FONT_BYTES} byte limit`
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
const data = Buffer.from(b64, "base64");
|
|
530
|
+
if (data.length === 0) throw new Error("Decoded font buffer is empty");
|
|
531
|
+
if (data.length > MAX_DATA_FONT_BYTES) {
|
|
532
|
+
throw new Error(
|
|
533
|
+
`Font data payload exceeds ${MAX_DATA_FONT_BYTES} byte limit`
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
const format = detectFontFormat(data);
|
|
537
|
+
if (format === "unknown") {
|
|
538
|
+
throw new Error(
|
|
539
|
+
"Decoded font buffer is not a recognized font (expected TTF/OTF/WOFF/WOFF2)"
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
return {
|
|
543
|
+
data,
|
|
544
|
+
weight: input.weight ?? 400,
|
|
545
|
+
italic: input.italic ?? false,
|
|
546
|
+
format
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/fonts/sources/google-fetcher.ts
|
|
551
|
+
var TTF_UA = "Mozilla/4.0";
|
|
552
|
+
async function fetchWithTimeout(url, opts) {
|
|
553
|
+
const ctrl = new AbortController();
|
|
554
|
+
const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 5e3);
|
|
555
|
+
try {
|
|
556
|
+
const f = opts.fetcher ?? fetch;
|
|
557
|
+
return await f(url, { headers: opts.headers, signal: ctrl.signal });
|
|
558
|
+
} finally {
|
|
559
|
+
clearTimeout(timer);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
function buildCssUrl(family, weights, italics) {
|
|
563
|
+
const famPart = encodeURIComponent(family).replace(/%20/g, "+");
|
|
564
|
+
const sortedWeights = [...weights].sort((a, b) => a - b);
|
|
565
|
+
if (italics) {
|
|
566
|
+
const axis = sortedWeights.flatMap((w) => [`0,${w}`, `1,${w}`]).join(";");
|
|
567
|
+
return `https://fonts.googleapis.com/css2?family=${famPart}:ital,wght@${axis}&display=swap`;
|
|
568
|
+
}
|
|
569
|
+
const wghtPart = sortedWeights.join(";");
|
|
570
|
+
return `https://fonts.googleapis.com/css2?family=${famPart}:wght@${wghtPart}&display=swap`;
|
|
571
|
+
}
|
|
572
|
+
function parseCssFaces(css) {
|
|
573
|
+
const out = [];
|
|
574
|
+
const faceRe = /@font-face\s*\{([^}]*)\}/g;
|
|
575
|
+
let m;
|
|
576
|
+
while ((m = faceRe.exec(css)) !== null) {
|
|
577
|
+
const block = m[1];
|
|
578
|
+
const urlM = block.match(
|
|
579
|
+
/src:\s*url\((https:\/\/fonts\.gstatic\.com\/[^)]+\.ttf)\)/
|
|
580
|
+
);
|
|
581
|
+
if (!urlM) continue;
|
|
582
|
+
const weightM = block.match(/font-weight:\s*(\d+)/);
|
|
583
|
+
const italicM = block.match(/font-style:\s*italic/);
|
|
584
|
+
out.push({
|
|
585
|
+
weight: weightM ? parseInt(weightM[1], 10) : 400,
|
|
586
|
+
italic: Boolean(italicM),
|
|
587
|
+
ttfUrl: urlM[1]
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
return out;
|
|
591
|
+
}
|
|
592
|
+
function cacheKey(family, weight, italic) {
|
|
593
|
+
return `google|${family}|${weight}|${italic ? "i" : "r"}`;
|
|
594
|
+
}
|
|
595
|
+
async function fetchGoogleFontSources(opts) {
|
|
596
|
+
const weights = opts.weights?.length ? opts.weights : [400, 700];
|
|
597
|
+
const italics = opts.italics ?? false;
|
|
598
|
+
const warnings = [];
|
|
599
|
+
const sources = [];
|
|
600
|
+
const wanted = [];
|
|
601
|
+
for (const w of weights) {
|
|
602
|
+
wanted.push({ weight: w, italic: false });
|
|
603
|
+
if (italics) wanted.push({ weight: w, italic: true });
|
|
604
|
+
}
|
|
605
|
+
const pending = [];
|
|
606
|
+
for (const w of wanted) {
|
|
607
|
+
const key = cacheKey(opts.family, w.weight, w.italic);
|
|
608
|
+
const mem = opts.memoryCache?.get(key);
|
|
609
|
+
if (mem) {
|
|
610
|
+
sources.push({
|
|
611
|
+
data: mem,
|
|
612
|
+
weight: w.weight,
|
|
613
|
+
italic: w.italic,
|
|
614
|
+
format: detectFontFormat(mem)
|
|
615
|
+
});
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
const disk = await opts.diskCache?.get(key);
|
|
619
|
+
if (disk) {
|
|
620
|
+
opts.memoryCache?.set(key, disk);
|
|
621
|
+
sources.push({
|
|
622
|
+
data: disk,
|
|
623
|
+
weight: w.weight,
|
|
624
|
+
italic: w.italic,
|
|
625
|
+
format: detectFontFormat(disk)
|
|
626
|
+
});
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
pending.push(w);
|
|
630
|
+
}
|
|
631
|
+
if (pending.length === 0) {
|
|
632
|
+
return { sources, warnings };
|
|
633
|
+
}
|
|
634
|
+
const needItalics = pending.some((p) => p.italic);
|
|
635
|
+
const cssUrl = buildCssUrl(
|
|
636
|
+
opts.family,
|
|
637
|
+
Array.from(new Set(pending.map((p) => p.weight))),
|
|
638
|
+
needItalics
|
|
639
|
+
);
|
|
640
|
+
let faces;
|
|
641
|
+
try {
|
|
642
|
+
const cssRes = await fetchWithTimeout(cssUrl, {
|
|
643
|
+
headers: { "User-Agent": TTF_UA },
|
|
644
|
+
timeoutMs: opts.fetchTimeoutMs,
|
|
645
|
+
fetcher: opts.fetcher
|
|
646
|
+
});
|
|
647
|
+
if (!cssRes.ok) {
|
|
648
|
+
warnings.push(
|
|
649
|
+
`Google Fonts CSS fetch for "${opts.family}" returned ${cssRes.status}`
|
|
650
|
+
);
|
|
651
|
+
return { sources, warnings };
|
|
652
|
+
}
|
|
653
|
+
const css = await cssRes.text();
|
|
654
|
+
faces = parseCssFaces(css);
|
|
655
|
+
} catch (err) {
|
|
656
|
+
warnings.push(
|
|
657
|
+
`Google Fonts CSS fetch for "${opts.family}" failed: ${err.message}`
|
|
658
|
+
);
|
|
659
|
+
return { sources, warnings };
|
|
660
|
+
}
|
|
661
|
+
for (const need of pending) {
|
|
662
|
+
const match = faces.find(
|
|
663
|
+
(f) => f.weight === need.weight && f.italic === need.italic
|
|
664
|
+
);
|
|
665
|
+
if (!match) {
|
|
666
|
+
warnings.push(
|
|
667
|
+
`Google Fonts "${opts.family}" missing weight ${need.weight}${need.italic ? " italic" : ""}`
|
|
668
|
+
);
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
try {
|
|
672
|
+
const res = await fetchWithTimeout(match.ttfUrl, {
|
|
673
|
+
timeoutMs: opts.fetchTimeoutMs,
|
|
674
|
+
fetcher: opts.fetcher
|
|
675
|
+
});
|
|
676
|
+
if (!res.ok) {
|
|
677
|
+
warnings.push(
|
|
678
|
+
`Google Fonts TTF fetch for "${opts.family}" ${need.weight} returned ${res.status}`
|
|
679
|
+
);
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
const ab = await res.arrayBuffer();
|
|
683
|
+
const buf = Buffer.from(ab);
|
|
684
|
+
const key = cacheKey(opts.family, need.weight, need.italic);
|
|
685
|
+
opts.memoryCache?.set(key, buf);
|
|
686
|
+
await opts.diskCache?.set(key, buf);
|
|
687
|
+
sources.push({
|
|
688
|
+
data: buf,
|
|
689
|
+
weight: need.weight,
|
|
690
|
+
italic: need.italic,
|
|
691
|
+
format: detectFontFormat(buf)
|
|
692
|
+
});
|
|
693
|
+
} catch (err) {
|
|
694
|
+
warnings.push(
|
|
695
|
+
`Google Fonts TTF fetch for "${opts.family}" ${need.weight} failed: ${err.message}`
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return { sources, warnings };
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// src/fonts/sources/url-fetcher.ts
|
|
703
|
+
function cacheKey2(url, weight, italic) {
|
|
704
|
+
return `url|${url}|${weight}|${italic ? "i" : "r"}`;
|
|
705
|
+
}
|
|
706
|
+
async function fetchUrlFontSource(opts) {
|
|
707
|
+
if (!isAllowedFontUrl(opts.url)) {
|
|
708
|
+
return {
|
|
709
|
+
warnings: [
|
|
710
|
+
`URL font fetch rejected (host not in allowlist or non-HTTPS): ${opts.url}`
|
|
711
|
+
]
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
const key = cacheKey2(opts.url, opts.weight, opts.italic);
|
|
715
|
+
const mem = opts.memoryCache?.get(key);
|
|
716
|
+
if (mem) {
|
|
717
|
+
return {
|
|
718
|
+
source: {
|
|
719
|
+
data: mem,
|
|
720
|
+
weight: opts.weight,
|
|
721
|
+
italic: opts.italic,
|
|
722
|
+
format: detectFontFormat(mem)
|
|
723
|
+
},
|
|
724
|
+
warnings: []
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
const disk = await opts.diskCache?.get(key);
|
|
728
|
+
if (disk) {
|
|
729
|
+
opts.memoryCache?.set(key, disk);
|
|
730
|
+
return {
|
|
731
|
+
source: {
|
|
732
|
+
data: disk,
|
|
733
|
+
weight: opts.weight,
|
|
734
|
+
italic: opts.italic,
|
|
735
|
+
format: detectFontFormat(disk)
|
|
736
|
+
},
|
|
737
|
+
warnings: []
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
const ctrl = new AbortController();
|
|
741
|
+
const timer = setTimeout(() => ctrl.abort(), opts.fetchTimeoutMs ?? 1e4);
|
|
742
|
+
try {
|
|
743
|
+
const f = opts.fetcher ?? fetch;
|
|
744
|
+
let res = await f(opts.url, { signal: ctrl.signal, redirect: "manual" });
|
|
745
|
+
let hops = 0;
|
|
746
|
+
while (res.status >= 300 && res.status < 400 && res.status !== 304) {
|
|
747
|
+
const next = res.headers.get("location");
|
|
748
|
+
if (!next) {
|
|
749
|
+
return {
|
|
750
|
+
warnings: [
|
|
751
|
+
`URL font fetch "${opts.url}" ${res.status} with no Location header`
|
|
752
|
+
]
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
const resolved = new URL(next, opts.url).toString();
|
|
756
|
+
if (!isAllowedFontUrl(resolved)) {
|
|
757
|
+
return {
|
|
758
|
+
warnings: [
|
|
759
|
+
`URL font fetch "${opts.url}" redirected to disallowed host: ${resolved}`
|
|
760
|
+
]
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
if (++hops > 3) {
|
|
764
|
+
return {
|
|
765
|
+
warnings: [`URL font fetch "${opts.url}" too many redirects`]
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
res = await f(resolved, { signal: ctrl.signal, redirect: "manual" });
|
|
769
|
+
}
|
|
770
|
+
if (!res.ok) {
|
|
771
|
+
return {
|
|
772
|
+
warnings: [`URL font fetch "${opts.url}" returned ${res.status}`]
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
const ab = await res.arrayBuffer();
|
|
776
|
+
const raw = Buffer.from(ab);
|
|
777
|
+
const format = detectFontFormat(raw);
|
|
778
|
+
if (format === "unknown" || raw.length < 512) {
|
|
779
|
+
return {
|
|
780
|
+
warnings: [
|
|
781
|
+
`URL font fetch "${opts.url}" returned ${raw.length} bytes of ${format} \u2014 not a TTF/OTF. Skipping.`
|
|
782
|
+
]
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
const buf = raw;
|
|
786
|
+
opts.memoryCache?.set(key, buf);
|
|
787
|
+
await opts.diskCache?.set(key, buf);
|
|
788
|
+
return {
|
|
789
|
+
source: {
|
|
790
|
+
data: buf,
|
|
791
|
+
weight: opts.weight,
|
|
792
|
+
italic: opts.italic,
|
|
793
|
+
format
|
|
794
|
+
},
|
|
795
|
+
warnings: []
|
|
796
|
+
};
|
|
797
|
+
} catch (err) {
|
|
798
|
+
return {
|
|
799
|
+
warnings: [
|
|
800
|
+
`URL font fetch "${opts.url}" failed: ${err.message}`
|
|
801
|
+
]
|
|
802
|
+
};
|
|
803
|
+
} finally {
|
|
804
|
+
clearTimeout(timer);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// src/fonts/sources/ttf-validate.ts
|
|
809
|
+
var HEADER_SIZE = 12;
|
|
810
|
+
var TABLE_RECORD_SIZE = 16;
|
|
811
|
+
function readTable(ttf, tag) {
|
|
812
|
+
if (ttf.length < HEADER_SIZE) return null;
|
|
813
|
+
const version = ttf.readUInt32BE(0);
|
|
814
|
+
if (version !== 65536 && version !== 1330926671) return null;
|
|
815
|
+
const numTables = ttf.readUInt16BE(4);
|
|
816
|
+
for (let i = 0; i < numTables; i++) {
|
|
817
|
+
const r = HEADER_SIZE + i * TABLE_RECORD_SIZE;
|
|
818
|
+
if (r + TABLE_RECORD_SIZE > ttf.length) return null;
|
|
819
|
+
if (ttf.toString("ascii", r, r + 4) === tag) {
|
|
820
|
+
return { off: ttf.readUInt32BE(r + 8), len: ttf.readUInt32BE(r + 12) };
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
function readUsWeightClass(ttf) {
|
|
826
|
+
const os2 = readTable(ttf, "OS/2");
|
|
827
|
+
if (!os2) return null;
|
|
828
|
+
if (os2.off + 6 > ttf.length) return null;
|
|
829
|
+
return ttf.readUInt16BE(os2.off + 4);
|
|
830
|
+
}
|
|
831
|
+
function readNames(ttf, wanted) {
|
|
832
|
+
const nt = readTable(ttf, "name");
|
|
833
|
+
if (!nt) return [];
|
|
834
|
+
const tableOff = nt.off;
|
|
835
|
+
if (tableOff + 6 > ttf.length) return [];
|
|
836
|
+
const count = ttf.readUInt16BE(tableOff + 2);
|
|
837
|
+
const storageRel = ttf.readUInt16BE(tableOff + 4);
|
|
838
|
+
const storage = tableOff + storageRel;
|
|
839
|
+
const out = [];
|
|
840
|
+
for (let j = 0; j < count; j++) {
|
|
841
|
+
const r = tableOff + 6 + j * 12;
|
|
842
|
+
if (r + 12 > ttf.length) break;
|
|
843
|
+
const platformID = ttf.readUInt16BE(r);
|
|
844
|
+
const nameID = ttf.readUInt16BE(r + 6);
|
|
845
|
+
if (!wanted.has(nameID)) continue;
|
|
846
|
+
const length = ttf.readUInt16BE(r + 8);
|
|
847
|
+
const offset = ttf.readUInt16BE(r + 10);
|
|
848
|
+
const raw = ttf.slice(storage + offset, storage + offset + length);
|
|
849
|
+
let value;
|
|
850
|
+
if (platformID === 1) {
|
|
851
|
+
value = raw.toString("ascii");
|
|
852
|
+
} else {
|
|
853
|
+
const swapped = Buffer.from(raw);
|
|
854
|
+
if (swapped.length % 2 === 0) swapped.swap16();
|
|
855
|
+
value = swapped.toString("utf16le");
|
|
856
|
+
}
|
|
857
|
+
out.push({ platformID, nameID, value });
|
|
858
|
+
}
|
|
859
|
+
return out;
|
|
860
|
+
}
|
|
861
|
+
var STANDARD_SUBFAMILY = {
|
|
862
|
+
100: "Thin",
|
|
863
|
+
200: "ExtraLight",
|
|
864
|
+
300: "Light",
|
|
865
|
+
400: "Regular",
|
|
866
|
+
500: "Medium",
|
|
867
|
+
600: "SemiBold",
|
|
868
|
+
700: "Bold",
|
|
869
|
+
800: "ExtraBold",
|
|
870
|
+
900: "Black"
|
|
871
|
+
};
|
|
872
|
+
function validateFontMetadata(ttf, weight, italic, familyLabel) {
|
|
873
|
+
const diags = [];
|
|
874
|
+
const usWeight = readUsWeightClass(ttf);
|
|
875
|
+
if (usWeight != null && usWeight !== weight) {
|
|
876
|
+
diags.push({
|
|
877
|
+
code: "WEIGHT_CLASS_MISMATCH",
|
|
878
|
+
message: `Font "${familyLabel}" weight ${weight}: OS/2.usWeightClass reports ${usWeight}. Likely a defective redistribution \u2014 consider adding an upstream override.`
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
const subfamily = STANDARD_SUBFAMILY[weight];
|
|
882
|
+
if (!subfamily) return diags;
|
|
883
|
+
const expected17 = italic ? `${subfamily} Italic` : subfamily;
|
|
884
|
+
const expected2 = weight >= 600 ? italic ? "Bold Italic" : "Bold" : italic ? "Italic" : "Regular";
|
|
885
|
+
const names = readNames(ttf, /* @__PURE__ */ new Set([2, 17]));
|
|
886
|
+
for (const n of names) {
|
|
887
|
+
if (n.nameID === 17 && n.value !== expected17) {
|
|
888
|
+
diags.push({
|
|
889
|
+
code: "SUBFAMILY_MISMATCH",
|
|
890
|
+
message: `Font "${familyLabel}" weight ${weight}${italic ? " italic" : ""}: name record (platform ${n.platformID}) nameID 17 = "${n.value}", expected "${expected17}".`
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
if (n.nameID === 2 && n.value !== expected2) {
|
|
894
|
+
diags.push({
|
|
895
|
+
code: "LEGACY_SUBFAMILY_MISMATCH",
|
|
896
|
+
message: `Font "${familyLabel}" weight ${weight}${italic ? " italic" : ""}: name record (platform ${n.platformID}) nameID 2 = "${n.value}", expected "${expected2}".`
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
return diags;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// src/fonts/cache/memory-cache.ts
|
|
904
|
+
var FontMemoryCache = class {
|
|
905
|
+
store = /* @__PURE__ */ new Map();
|
|
906
|
+
bytes = 0;
|
|
907
|
+
maxBytes;
|
|
908
|
+
constructor(opts = {}) {
|
|
909
|
+
this.maxBytes = opts.maxBytes ?? 20 * 1024 * 1024;
|
|
910
|
+
}
|
|
911
|
+
get(key) {
|
|
912
|
+
const v = this.store.get(key);
|
|
913
|
+
if (!v) return void 0;
|
|
914
|
+
this.store.delete(key);
|
|
915
|
+
this.store.set(key, v);
|
|
916
|
+
return v;
|
|
917
|
+
}
|
|
918
|
+
set(key, value) {
|
|
919
|
+
const existing = this.store.get(key);
|
|
920
|
+
if (existing) this.bytes -= existing.byteLength;
|
|
921
|
+
this.store.set(key, value);
|
|
922
|
+
this.bytes += value.byteLength;
|
|
923
|
+
while (this.bytes > this.maxBytes && this.store.size > 0) {
|
|
924
|
+
const oldest = this.store.keys().next().value;
|
|
925
|
+
if (!oldest) break;
|
|
926
|
+
const removed = this.store.get(oldest);
|
|
927
|
+
this.store.delete(oldest);
|
|
928
|
+
if (removed) this.bytes -= removed.byteLength;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
size() {
|
|
932
|
+
return this.store.size;
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
|
|
936
|
+
// src/fonts/registry.ts
|
|
937
|
+
var FontRegistry = class {
|
|
938
|
+
index;
|
|
939
|
+
cache;
|
|
940
|
+
opts;
|
|
941
|
+
memoryCache;
|
|
942
|
+
diskCache;
|
|
943
|
+
fileLoader;
|
|
944
|
+
variableLoader;
|
|
945
|
+
constructor(input = {}) {
|
|
946
|
+
this.opts = input.opts ?? {};
|
|
947
|
+
this.index = /* @__PURE__ */ new Map();
|
|
948
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
949
|
+
this.memoryCache = new FontMemoryCache();
|
|
950
|
+
this.diskCache = input.diskCache;
|
|
951
|
+
this.fileLoader = input.fileLoader;
|
|
952
|
+
this.variableLoader = input.variableLoader;
|
|
953
|
+
for (const e of this.opts.extraEntries ?? []) this.addEntry(e);
|
|
954
|
+
}
|
|
955
|
+
addEntry(entry) {
|
|
956
|
+
this.index.set(entry.family.toLowerCase(), entry);
|
|
957
|
+
this.index.set(entry.id.toLowerCase(), entry);
|
|
958
|
+
}
|
|
959
|
+
/** Resolve every referenced name in one pass. Order preserved. */
|
|
960
|
+
async resolveMany(names) {
|
|
961
|
+
const out = [];
|
|
962
|
+
for (const n of names) out.push(await this.resolve(n));
|
|
963
|
+
return out;
|
|
964
|
+
}
|
|
965
|
+
async resolve(name) {
|
|
966
|
+
const key = name.toLowerCase();
|
|
967
|
+
const cached = this.cache.get(key);
|
|
968
|
+
if (cached) return cached;
|
|
969
|
+
const entry = this.index.get(key);
|
|
970
|
+
let result;
|
|
971
|
+
if (entry) {
|
|
972
|
+
result = await this.materializeEntry(entry);
|
|
973
|
+
} else if (isSafeFont(name)) {
|
|
974
|
+
result = { family: name, sources: [], warnings: [] };
|
|
975
|
+
} else {
|
|
976
|
+
result = {
|
|
977
|
+
family: name,
|
|
978
|
+
sources: [],
|
|
979
|
+
warnings: [
|
|
980
|
+
`Font "${name}" is not registered and not in SAFE_FONTS; will rely on host fallback.`
|
|
981
|
+
]
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
this.cache.set(key, result);
|
|
985
|
+
return result;
|
|
986
|
+
}
|
|
987
|
+
async materializeEntry(entry) {
|
|
988
|
+
const sources = [];
|
|
989
|
+
const warnings = [];
|
|
990
|
+
for (const source of entry.sources) {
|
|
991
|
+
try {
|
|
992
|
+
const materialized = await this.materializeSource(source, warnings);
|
|
993
|
+
for (const s of materialized) {
|
|
994
|
+
if (s.format === "ttf" || s.format === "otf") {
|
|
995
|
+
for (const d of validateFontMetadata(
|
|
996
|
+
s.data,
|
|
997
|
+
s.weight,
|
|
998
|
+
s.italic,
|
|
999
|
+
entry.family
|
|
1000
|
+
)) {
|
|
1001
|
+
warnings.push(`[FONT_METADATA_DEFECT:${d.code}] ${d.message}`);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
sources.push(s);
|
|
1005
|
+
}
|
|
1006
|
+
} catch (err) {
|
|
1007
|
+
warnings.push(
|
|
1008
|
+
`Font "${entry.family}" source (${source.kind}) failed: ${err.message}`
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return {
|
|
1013
|
+
family: entry.family,
|
|
1014
|
+
sources,
|
|
1015
|
+
warnings
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
async materializeSource(source, warnings) {
|
|
1019
|
+
switch (source.kind) {
|
|
1020
|
+
case "safe":
|
|
1021
|
+
return [];
|
|
1022
|
+
case "file": {
|
|
1023
|
+
if (!this.fileLoader) {
|
|
1024
|
+
warnings.push(
|
|
1025
|
+
`kind:"file" source for "${source.path}" requires a fileLoader (Node-only); skipping.`
|
|
1026
|
+
);
|
|
1027
|
+
return [];
|
|
1028
|
+
}
|
|
1029
|
+
return [
|
|
1030
|
+
await this.fileLoader({
|
|
1031
|
+
path: source.path,
|
|
1032
|
+
weight: source.weight,
|
|
1033
|
+
italic: source.italic,
|
|
1034
|
+
baseDir: this.opts.baseDir
|
|
1035
|
+
})
|
|
1036
|
+
];
|
|
1037
|
+
}
|
|
1038
|
+
case "data":
|
|
1039
|
+
return [
|
|
1040
|
+
loadDataFontSource({
|
|
1041
|
+
data: source.data,
|
|
1042
|
+
weight: source.weight,
|
|
1043
|
+
italic: source.italic
|
|
1044
|
+
})
|
|
1045
|
+
];
|
|
1046
|
+
case "google": {
|
|
1047
|
+
const gf = this.opts.googleFonts;
|
|
1048
|
+
if (gf?.enabled === false) {
|
|
1049
|
+
warnings.push(
|
|
1050
|
+
`Google Fonts fetch disabled \u2014 skipping "${source.family}".`
|
|
1051
|
+
);
|
|
1052
|
+
return [];
|
|
1053
|
+
}
|
|
1054
|
+
const { sources: fetched, warnings: fetchWarnings } = await fetchGoogleFontSources({
|
|
1055
|
+
family: source.family,
|
|
1056
|
+
weights: source.weights ?? [400, 700],
|
|
1057
|
+
italics: source.italics ?? false,
|
|
1058
|
+
memoryCache: this.memoryCache,
|
|
1059
|
+
diskCache: this.diskCache,
|
|
1060
|
+
fetchTimeoutMs: gf?.fetchTimeoutMs
|
|
1061
|
+
});
|
|
1062
|
+
warnings.push(...fetchWarnings);
|
|
1063
|
+
return fetched;
|
|
1064
|
+
}
|
|
1065
|
+
case "url": {
|
|
1066
|
+
const gf = this.opts.googleFonts;
|
|
1067
|
+
const { source: fetched, warnings: fetchWarnings } = await fetchUrlFontSource({
|
|
1068
|
+
url: source.url,
|
|
1069
|
+
weight: source.weight ?? 400,
|
|
1070
|
+
italic: source.italic ?? false,
|
|
1071
|
+
memoryCache: this.memoryCache,
|
|
1072
|
+
diskCache: this.diskCache,
|
|
1073
|
+
fetchTimeoutMs: gf?.fetchTimeoutMs
|
|
1074
|
+
});
|
|
1075
|
+
if (fetchWarnings) warnings.push(...fetchWarnings);
|
|
1076
|
+
return fetched ? [fetched] : [];
|
|
1077
|
+
}
|
|
1078
|
+
case "variable": {
|
|
1079
|
+
if (!this.variableLoader) {
|
|
1080
|
+
warnings.push(
|
|
1081
|
+
`kind:"variable" source for "${source.url}" requires a variableLoader (Node-only); skipping.`
|
|
1082
|
+
);
|
|
1083
|
+
return [];
|
|
1084
|
+
}
|
|
1085
|
+
const gf = this.opts.googleFonts;
|
|
1086
|
+
const { source: fetched, warnings: fetchWarnings } = await this.variableLoader({
|
|
1087
|
+
url: source.url,
|
|
1088
|
+
weight: source.weight,
|
|
1089
|
+
italic: source.italic ?? false,
|
|
1090
|
+
axes: source.axes,
|
|
1091
|
+
memoryCache: this.memoryCache,
|
|
1092
|
+
diskCache: this.diskCache,
|
|
1093
|
+
fetchTimeoutMs: gf?.fetchTimeoutMs
|
|
1094
|
+
});
|
|
1095
|
+
if (fetchWarnings) warnings.push(...fetchWarnings);
|
|
1096
|
+
return fetched ? [fetched] : [];
|
|
1097
|
+
}
|
|
1098
|
+
default:
|
|
1099
|
+
throw new Error(
|
|
1100
|
+
`Unknown font source kind: ${source.kind}`
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
|
|
1106
|
+
// src/fonts/catalog/popular-google.ts
|
|
1107
|
+
var POPULAR_GOOGLE_FONTS = [
|
|
1108
|
+
// Sans-serif
|
|
1109
|
+
{
|
|
1110
|
+
family: "Inter",
|
|
1111
|
+
category: "sans",
|
|
1112
|
+
weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
1113
|
+
hasItalic: false
|
|
1114
|
+
},
|
|
1115
|
+
{
|
|
1116
|
+
family: "Roboto",
|
|
1117
|
+
category: "sans",
|
|
1118
|
+
weights: [100, 300, 400, 500, 700, 900],
|
|
1119
|
+
hasItalic: true
|
|
1120
|
+
},
|
|
1121
|
+
{
|
|
1122
|
+
family: "Open Sans",
|
|
1123
|
+
category: "sans",
|
|
1124
|
+
weights: [300, 400, 500, 600, 700, 800],
|
|
1125
|
+
hasItalic: true
|
|
1126
|
+
},
|
|
1127
|
+
{
|
|
1128
|
+
family: "Lato",
|
|
1129
|
+
category: "sans",
|
|
1130
|
+
weights: [100, 300, 400, 700, 900],
|
|
1131
|
+
hasItalic: true
|
|
1132
|
+
},
|
|
1133
|
+
{
|
|
1134
|
+
family: "Montserrat",
|
|
1135
|
+
category: "sans",
|
|
1136
|
+
weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
1137
|
+
hasItalic: true
|
|
1138
|
+
},
|
|
1139
|
+
{
|
|
1140
|
+
family: "Poppins",
|
|
1141
|
+
category: "sans",
|
|
1142
|
+
weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
1143
|
+
hasItalic: true
|
|
1144
|
+
},
|
|
1145
|
+
{
|
|
1146
|
+
family: "Work Sans",
|
|
1147
|
+
category: "sans",
|
|
1148
|
+
weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
1149
|
+
hasItalic: true
|
|
1150
|
+
},
|
|
1151
|
+
{
|
|
1152
|
+
family: "Nunito",
|
|
1153
|
+
category: "sans",
|
|
1154
|
+
weights: [200, 300, 400, 500, 600, 700, 800, 900],
|
|
1155
|
+
hasItalic: true
|
|
1156
|
+
},
|
|
1157
|
+
{
|
|
1158
|
+
family: "DM Sans",
|
|
1159
|
+
category: "sans",
|
|
1160
|
+
weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
1161
|
+
hasItalic: true
|
|
1162
|
+
},
|
|
1163
|
+
{
|
|
1164
|
+
family: "Rubik",
|
|
1165
|
+
category: "sans",
|
|
1166
|
+
weights: [300, 400, 500, 600, 700, 800, 900],
|
|
1167
|
+
hasItalic: true
|
|
1168
|
+
},
|
|
1169
|
+
{
|
|
1170
|
+
family: "Source Sans 3",
|
|
1171
|
+
category: "sans",
|
|
1172
|
+
weights: [200, 300, 400, 500, 600, 700, 800, 900],
|
|
1173
|
+
hasItalic: true
|
|
1174
|
+
},
|
|
1175
|
+
{
|
|
1176
|
+
family: "Manrope",
|
|
1177
|
+
category: "sans",
|
|
1178
|
+
weights: [200, 300, 400, 500, 600, 700, 800],
|
|
1179
|
+
hasItalic: false
|
|
1180
|
+
},
|
|
1181
|
+
{
|
|
1182
|
+
family: "Plus Jakarta Sans",
|
|
1183
|
+
category: "sans",
|
|
1184
|
+
weights: [200, 300, 400, 500, 600, 700, 800],
|
|
1185
|
+
hasItalic: true
|
|
1186
|
+
},
|
|
1187
|
+
{
|
|
1188
|
+
family: "IBM Plex Sans",
|
|
1189
|
+
category: "sans",
|
|
1190
|
+
weights: [100, 200, 300, 400, 500, 600, 700],
|
|
1191
|
+
hasItalic: true
|
|
1192
|
+
},
|
|
1193
|
+
// Serif
|
|
1194
|
+
{
|
|
1195
|
+
family: "Playfair Display",
|
|
1196
|
+
category: "serif",
|
|
1197
|
+
weights: [400, 500, 600, 700, 800, 900],
|
|
1198
|
+
hasItalic: true
|
|
1199
|
+
},
|
|
1200
|
+
{
|
|
1201
|
+
family: "Merriweather",
|
|
1202
|
+
category: "serif",
|
|
1203
|
+
weights: [300, 400, 700, 900],
|
|
1204
|
+
hasItalic: true
|
|
1205
|
+
},
|
|
1206
|
+
{
|
|
1207
|
+
family: "Lora",
|
|
1208
|
+
category: "serif",
|
|
1209
|
+
weights: [400, 500, 600, 700],
|
|
1210
|
+
hasItalic: true
|
|
1211
|
+
},
|
|
1212
|
+
{
|
|
1213
|
+
family: "Source Serif 4",
|
|
1214
|
+
category: "serif",
|
|
1215
|
+
weights: [200, 300, 400, 500, 600, 700, 800, 900],
|
|
1216
|
+
hasItalic: true
|
|
1217
|
+
},
|
|
1218
|
+
{
|
|
1219
|
+
family: "DM Serif Display",
|
|
1220
|
+
category: "serif",
|
|
1221
|
+
weights: [400],
|
|
1222
|
+
hasItalic: true
|
|
1223
|
+
},
|
|
1224
|
+
{
|
|
1225
|
+
family: "Crimson Pro",
|
|
1226
|
+
category: "serif",
|
|
1227
|
+
weights: [200, 300, 400, 500, 600, 700, 800, 900],
|
|
1228
|
+
hasItalic: true
|
|
1229
|
+
},
|
|
1230
|
+
{
|
|
1231
|
+
family: "Cormorant Garamond",
|
|
1232
|
+
category: "serif",
|
|
1233
|
+
weights: [300, 400, 500, 600, 700],
|
|
1234
|
+
hasItalic: true
|
|
1235
|
+
},
|
|
1236
|
+
// Monospace
|
|
1237
|
+
{
|
|
1238
|
+
family: "JetBrains Mono",
|
|
1239
|
+
category: "mono",
|
|
1240
|
+
weights: [100, 200, 300, 400, 500, 600, 700, 800],
|
|
1241
|
+
hasItalic: true
|
|
1242
|
+
},
|
|
1243
|
+
{
|
|
1244
|
+
family: "Fira Code",
|
|
1245
|
+
category: "mono",
|
|
1246
|
+
weights: [300, 400, 500, 600, 700],
|
|
1247
|
+
hasItalic: false
|
|
1248
|
+
},
|
|
1249
|
+
{
|
|
1250
|
+
family: "IBM Plex Mono",
|
|
1251
|
+
category: "mono",
|
|
1252
|
+
weights: [100, 200, 300, 400, 500, 600, 700],
|
|
1253
|
+
hasItalic: true
|
|
1254
|
+
},
|
|
1255
|
+
{
|
|
1256
|
+
family: "Source Code Pro",
|
|
1257
|
+
category: "mono",
|
|
1258
|
+
weights: [200, 300, 400, 500, 600, 700, 800, 900],
|
|
1259
|
+
hasItalic: true
|
|
1260
|
+
},
|
|
1261
|
+
{
|
|
1262
|
+
family: "Space Mono",
|
|
1263
|
+
category: "mono",
|
|
1264
|
+
weights: [400, 700],
|
|
1265
|
+
hasItalic: true
|
|
1266
|
+
},
|
|
1267
|
+
// Display
|
|
1268
|
+
{
|
|
1269
|
+
family: "Bebas Neue",
|
|
1270
|
+
category: "display",
|
|
1271
|
+
weights: [400],
|
|
1272
|
+
hasItalic: false
|
|
1273
|
+
},
|
|
1274
|
+
{
|
|
1275
|
+
family: "Abril Fatface",
|
|
1276
|
+
category: "display",
|
|
1277
|
+
weights: [400],
|
|
1278
|
+
hasItalic: false
|
|
1279
|
+
},
|
|
1280
|
+
{
|
|
1281
|
+
family: "Archivo Black",
|
|
1282
|
+
category: "display",
|
|
1283
|
+
weights: [400],
|
|
1284
|
+
hasItalic: false
|
|
1285
|
+
},
|
|
1286
|
+
{
|
|
1287
|
+
family: "Oswald",
|
|
1288
|
+
category: "display",
|
|
1289
|
+
weights: [200, 300, 400, 500, 600, 700],
|
|
1290
|
+
hasItalic: false
|
|
1291
|
+
},
|
|
1292
|
+
// Handwriting
|
|
1293
|
+
{
|
|
1294
|
+
family: "Caveat",
|
|
1295
|
+
category: "handwriting",
|
|
1296
|
+
weights: [400, 500, 600, 700],
|
|
1297
|
+
hasItalic: false
|
|
1298
|
+
},
|
|
1299
|
+
{
|
|
1300
|
+
family: "Pacifico",
|
|
1301
|
+
category: "handwriting",
|
|
1302
|
+
weights: [400],
|
|
1303
|
+
hasItalic: false
|
|
1304
|
+
}
|
|
1305
|
+
];
|
|
1306
|
+
|
|
1307
|
+
// src/fonts/catalog/upstream-overrides.ts
|
|
1308
|
+
var INTER_VARIABLE_URL = "https://cdn.jsdelivr.net/gh/rsms/inter@v4.1/docs/font-files/InterVariable.ttf";
|
|
1309
|
+
var INTER_VARIABLE_ITALIC_URL = "https://cdn.jsdelivr.net/gh/rsms/inter@v4.1/docs/font-files/InterVariable-Italic.ttf";
|
|
1310
|
+
function interVariants() {
|
|
1311
|
+
const weights = [100, 200, 300, 400, 500, 600, 700, 800, 900];
|
|
1312
|
+
const upright = weights.map((weight) => ({
|
|
1313
|
+
kind: "variable",
|
|
1314
|
+
url: INTER_VARIABLE_URL,
|
|
1315
|
+
weight,
|
|
1316
|
+
italic: false
|
|
1317
|
+
}));
|
|
1318
|
+
const italic = weights.map((weight) => ({
|
|
1319
|
+
kind: "variable",
|
|
1320
|
+
url: INTER_VARIABLE_ITALIC_URL,
|
|
1321
|
+
weight,
|
|
1322
|
+
italic: true
|
|
1323
|
+
}));
|
|
1324
|
+
return [...upright, ...italic];
|
|
1325
|
+
}
|
|
1326
|
+
var UPSTREAM_OVERRIDES = {
|
|
1327
|
+
inter: {
|
|
1328
|
+
reason: "Google's static Inter Thin/ExtraLight both carry usWeightClass=250 and near-identical glyph outlines (xAvgCharWidth differs by 1.8%); instancing the upstream variable font per weight produces properly distinct statics.",
|
|
1329
|
+
variants: interVariants()
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
function getUpstreamOverride(family) {
|
|
1333
|
+
return UPSTREAM_OVERRIDES[family.toLowerCase()];
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// src/fonts/substitute.ts
|
|
1337
|
+
function applyFontSubstitution(doc, mapping) {
|
|
1338
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1339
|
+
const rewritten = rewrite(doc, mapping, seen);
|
|
1340
|
+
const substitutions = [];
|
|
1341
|
+
for (const [from, to] of seen) substitutions.push({ from, to });
|
|
1342
|
+
return { doc: rewritten, substitutions };
|
|
1343
|
+
}
|
|
1344
|
+
function rewrite(node, mapping, seen, parentKey) {
|
|
1345
|
+
if (node == null) return node;
|
|
1346
|
+
if (typeof node === "string") {
|
|
1347
|
+
if (parentKey && (FONT_NAME_KEYS.has(parentKey) || THEME_FONT_KEYS.has(parentKey))) {
|
|
1348
|
+
return maybeSwap(node, mapping, seen);
|
|
1349
|
+
}
|
|
1350
|
+
return node;
|
|
1351
|
+
}
|
|
1352
|
+
if (Array.isArray(node)) {
|
|
1353
|
+
return node.map((item) => rewrite(item, mapping, seen, parentKey));
|
|
1354
|
+
}
|
|
1355
|
+
if (typeof node === "object") {
|
|
1356
|
+
const out = {};
|
|
1357
|
+
for (const [k, v] of Object.entries(node)) {
|
|
1358
|
+
if (parentKey === "theme" && k === "fonts" && v && typeof v === "object") {
|
|
1359
|
+
const nextFonts = {};
|
|
1360
|
+
for (const [fk, fv] of Object.entries(v)) {
|
|
1361
|
+
if (typeof fv === "string") {
|
|
1362
|
+
nextFonts[fk] = maybeSwap(fv, mapping, seen);
|
|
1363
|
+
} else if (fv && typeof fv === "object") {
|
|
1364
|
+
const fam = fv.family;
|
|
1365
|
+
nextFonts[fk] = typeof fam === "string" ? { ...fv, family: maybeSwap(fam, mapping, seen) } : rewrite(fv, mapping, seen, fk);
|
|
1366
|
+
} else {
|
|
1367
|
+
nextFonts[fk] = fv;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
out[k] = nextFonts;
|
|
1371
|
+
continue;
|
|
1372
|
+
}
|
|
1373
|
+
out[k] = rewrite(v, mapping, seen, k);
|
|
1374
|
+
}
|
|
1375
|
+
return out;
|
|
1376
|
+
}
|
|
1377
|
+
return node;
|
|
1378
|
+
}
|
|
1379
|
+
function maybeSwap(name, mapping, seen) {
|
|
1380
|
+
const trimmed = name.trim();
|
|
1381
|
+
if (trimmed.length === 0) return name;
|
|
1382
|
+
if (isSafeFont(trimmed)) return name;
|
|
1383
|
+
const lowered = trimmed.toLowerCase();
|
|
1384
|
+
for (const [from, to] of Object.entries(mapping)) {
|
|
1385
|
+
if (from.toLowerCase() === lowered) {
|
|
1386
|
+
seen.set(trimmed, to);
|
|
1387
|
+
return to;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
return name;
|
|
1391
|
+
}
|
|
1392
|
+
var EXPLICIT_OVERRIDES = {
|
|
1393
|
+
Inter: "Calibri",
|
|
1394
|
+
Roboto: "Calibri",
|
|
1395
|
+
"Open Sans": "Calibri",
|
|
1396
|
+
Lato: "Calibri",
|
|
1397
|
+
"Source Sans 3": "Calibri",
|
|
1398
|
+
"Source Sans Pro": "Calibri",
|
|
1399
|
+
"IBM Plex Sans": "Calibri",
|
|
1400
|
+
"Work Sans": "Calibri",
|
|
1401
|
+
Manrope: "Calibri",
|
|
1402
|
+
Nunito: "Calibri",
|
|
1403
|
+
"Nunito Sans": "Calibri",
|
|
1404
|
+
Poppins: "Calibri",
|
|
1405
|
+
Montserrat: "Calibri",
|
|
1406
|
+
"Playfair Display": "Georgia",
|
|
1407
|
+
Merriweather: "Georgia",
|
|
1408
|
+
"Source Serif 4": "Cambria",
|
|
1409
|
+
"Source Serif Pro": "Cambria",
|
|
1410
|
+
"IBM Plex Serif": "Cambria",
|
|
1411
|
+
"Crimson Pro": "Cambria",
|
|
1412
|
+
Lora: "Georgia",
|
|
1413
|
+
"PT Serif": "Georgia",
|
|
1414
|
+
"Cormorant Garamond": "Cambria",
|
|
1415
|
+
"JetBrains Mono": "Consolas",
|
|
1416
|
+
"Fira Code": "Consolas",
|
|
1417
|
+
"IBM Plex Mono": "Consolas",
|
|
1418
|
+
"Source Code Pro": "Consolas",
|
|
1419
|
+
"Roboto Mono": "Consolas"
|
|
1420
|
+
};
|
|
1421
|
+
var CATEGORY_FALLBACK = {
|
|
1422
|
+
sans: "Calibri",
|
|
1423
|
+
serif: "Georgia",
|
|
1424
|
+
mono: "Consolas",
|
|
1425
|
+
display: "Georgia",
|
|
1426
|
+
handwriting: "Segoe UI"
|
|
1427
|
+
};
|
|
1428
|
+
function defaultSubstituteFor(family) {
|
|
1429
|
+
const trimmed = family.trim();
|
|
1430
|
+
for (const [from, to] of Object.entries(EXPLICIT_OVERRIDES)) {
|
|
1431
|
+
if (from.toLowerCase() === trimmed.toLowerCase()) return to;
|
|
1432
|
+
}
|
|
1433
|
+
const catalog = POPULAR_GOOGLE_FONTS.find(
|
|
1434
|
+
(f) => f.family.toLowerCase() === trimmed.toLowerCase()
|
|
1435
|
+
);
|
|
1436
|
+
if (catalog) {
|
|
1437
|
+
const cat = CATEGORY_FALLBACK[catalog.category];
|
|
1438
|
+
if (cat) return cat;
|
|
1439
|
+
}
|
|
1440
|
+
return "Calibri";
|
|
1441
|
+
}
|
|
1442
|
+
function buildDefaultSubstitutionMap(referencedNames) {
|
|
1443
|
+
const out = {};
|
|
1444
|
+
for (const raw of referencedNames) {
|
|
1445
|
+
const name = raw.trim();
|
|
1446
|
+
if (name.length === 0) continue;
|
|
1447
|
+
if (isSafeFont(name)) continue;
|
|
1448
|
+
if (out[name]) continue;
|
|
1449
|
+
out[name] = defaultSubstituteFor(name);
|
|
1450
|
+
}
|
|
1451
|
+
return out;
|
|
1452
|
+
}
|
|
1453
|
+
function scopedThemeName(baseThemeName, fontMode) {
|
|
1454
|
+
return fontMode === "substitute" ? `${baseThemeName}#substitute` : baseThemeName;
|
|
1455
|
+
}
|
|
1456
|
+
function applyExportMode(input) {
|
|
1457
|
+
const mode = input.fonts?.mode ?? "custom";
|
|
1458
|
+
if (mode === "custom") {
|
|
1459
|
+
if (!input.fonts) {
|
|
1460
|
+
return { doc: input.doc, theme: input.theme, warnings: [] };
|
|
1461
|
+
}
|
|
1462
|
+
const referenced2 = /* @__PURE__ */ new Set([
|
|
1463
|
+
...collectFontNames(input.doc),
|
|
1464
|
+
...collectFontNames(input.theme)
|
|
1465
|
+
]);
|
|
1466
|
+
const nonSafe = [...referenced2].filter((name) => !isSafeFont(name.trim()));
|
|
1467
|
+
const warnings2 = nonSafe.length > 0 ? [
|
|
1468
|
+
{
|
|
1469
|
+
code: "FONT_MODE_CUSTOM",
|
|
1470
|
+
message: `Export mode "custom": non-safe font references (${nonSafe.join(", ")}) kept as-is. Recipients need these fonts installed locally; Word falls back to a generic substitute otherwise.`
|
|
1471
|
+
}
|
|
1472
|
+
] : [];
|
|
1473
|
+
return {
|
|
1474
|
+
doc: input.doc,
|
|
1475
|
+
theme: input.theme,
|
|
1476
|
+
warnings: warnings2
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
const referenced = /* @__PURE__ */ new Set([
|
|
1480
|
+
...collectFontNames(input.doc),
|
|
1481
|
+
...collectFontNames(input.theme)
|
|
1482
|
+
]);
|
|
1483
|
+
const defaults = buildDefaultSubstitutionMap(referenced);
|
|
1484
|
+
const mapping = {
|
|
1485
|
+
...defaults,
|
|
1486
|
+
...input.fonts?.substitution ?? {}
|
|
1487
|
+
};
|
|
1488
|
+
const docRewrite = applyFontSubstitution(input.doc, mapping);
|
|
1489
|
+
const themeRewrite = applyFontSubstitution(input.theme, mapping);
|
|
1490
|
+
const combined = /* @__PURE__ */ new Map();
|
|
1491
|
+
for (const s of docRewrite.substitutions) combined.set(s.from, s.to);
|
|
1492
|
+
for (const s of themeRewrite.substitutions) combined.set(s.from, s.to);
|
|
1493
|
+
const warnings = [];
|
|
1494
|
+
if (combined.size > 0) {
|
|
1495
|
+
const list = [...combined].map(([from, to]) => `${from} \u2192 ${to}`).join(", ");
|
|
1496
|
+
warnings.push({
|
|
1497
|
+
code: "FONT_MODE_SUBSTITUTED",
|
|
1498
|
+
message: `Export mode "substitute": rewrote non-safe families to safe equivalents \u2014 ${list}. No fonts embedded; document renders identically on every machine.`
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
return {
|
|
1502
|
+
doc: docRewrite.doc,
|
|
1503
|
+
theme: themeRewrite.doc,
|
|
1504
|
+
warnings
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// src/utils/deepMerge.ts
|
|
1509
|
+
function isObject(item) {
|
|
1510
|
+
return item !== null && typeof item === "object" && !Array.isArray(item);
|
|
1511
|
+
}
|
|
1512
|
+
function deepMerge(target, source) {
|
|
1513
|
+
const output = { ...target };
|
|
1514
|
+
if (isObject(target) && isObject(source)) {
|
|
1515
|
+
Object.keys(source).forEach((key) => {
|
|
1516
|
+
if (isObject(source[key])) {
|
|
1517
|
+
if (!(key in target)) {
|
|
1518
|
+
output[key] = source[key];
|
|
1519
|
+
} else {
|
|
1520
|
+
output[key] = deepMerge(target[key], source[key]);
|
|
1521
|
+
}
|
|
1522
|
+
} else {
|
|
1523
|
+
output[key] = source[key];
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
return output;
|
|
1528
|
+
}
|
|
1529
|
+
function mergeWithDefaults(userConfig, themeDefaults) {
|
|
1530
|
+
return deepMerge(themeDefaults, userConfig);
|
|
1531
|
+
}
|
|
44
1532
|
export {
|
|
45
1533
|
ComponentValidationError,
|
|
46
1534
|
DEFAULT_ERROR_CONFIG,
|
|
47
1535
|
DuplicateComponentError,
|
|
48
1536
|
ERROR_EMOJIS,
|
|
1537
|
+
FontFamilyNameSchema,
|
|
1538
|
+
FontRegistry,
|
|
1539
|
+
FontRegistryEntrySchema,
|
|
1540
|
+
FontRegistrySchema,
|
|
1541
|
+
FontSourceSchema,
|
|
1542
|
+
POPULAR_GOOGLE_FONTS,
|
|
1543
|
+
SAFE_FONTS,
|
|
1544
|
+
UPSTREAM_OVERRIDES,
|
|
1545
|
+
WEIGHT_LABELS,
|
|
1546
|
+
applyExportMode,
|
|
1547
|
+
applyFontSubstitution,
|
|
1548
|
+
buildDefaultSubstitutionMap,
|
|
49
1549
|
calculatePosition,
|
|
50
1550
|
clearComponentNamesCache,
|
|
1551
|
+
collectFontNamesFromDocx,
|
|
1552
|
+
collectFontNamesFromPptx,
|
|
51
1553
|
compareSemver,
|
|
52
1554
|
convertToJsonSchema,
|
|
53
1555
|
createComponent,
|
|
@@ -56,26 +1558,36 @@ export {
|
|
|
56
1558
|
createErrorConfig,
|
|
57
1559
|
createJsonParseError,
|
|
58
1560
|
createVersion,
|
|
1561
|
+
defaultSubstituteFor,
|
|
1562
|
+
detectFontFormat,
|
|
59
1563
|
exportSchemaToFile,
|
|
60
1564
|
extractStandardComponentNames,
|
|
1565
|
+
fetchGoogleFontSources,
|
|
61
1566
|
fixSchemaReferences,
|
|
62
1567
|
formatErrorMessage,
|
|
63
1568
|
formatErrorSummary,
|
|
64
1569
|
getLiteralValue,
|
|
65
1570
|
getObjectSchemaPropertyNames,
|
|
66
1571
|
getSchemaMetadata,
|
|
1572
|
+
getUpstreamOverride,
|
|
67
1573
|
getValidationSummary,
|
|
68
1574
|
groupErrorsByPath,
|
|
69
1575
|
isLiteralSchema,
|
|
70
1576
|
isObjectSchema,
|
|
1577
|
+
isSafeFont,
|
|
71
1578
|
isUnionSchema,
|
|
72
1579
|
isValidSemver,
|
|
73
1580
|
isValidationSuccess,
|
|
74
1581
|
latestVersion,
|
|
1582
|
+
mergeWithDefaults,
|
|
75
1583
|
parseSemver,
|
|
76
1584
|
resolveComponentVersion,
|
|
1585
|
+
rewriteFontFamilyName,
|
|
1586
|
+
scopedThemeName,
|
|
1587
|
+
synthesizeFamilyName,
|
|
77
1588
|
transformValueError,
|
|
78
1589
|
transformValueErrors,
|
|
79
|
-
validateCustomComponentProps
|
|
1590
|
+
validateCustomComponentProps,
|
|
1591
|
+
validateFontReferences
|
|
80
1592
|
};
|
|
81
1593
|
//# sourceMappingURL=index.js.map
|