@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/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