@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.d.ts CHANGED
@@ -3,6 +3,8 @@ export { AddWarningFunction, GenerationWarning } from './types/warnings.js';
3
3
  export { DEFAULT_ERROR_CONFIG, ERROR_EMOJIS, ErrorFormatterConfig, calculatePosition, clearComponentNamesCache, createErrorConfig, createJsonParseError, extractStandardComponentNames, formatErrorMessage, formatErrorSummary, getLiteralValue, getObjectSchemaPropertyNames, getSchemaMetadata, groupErrorsByPath, isLiteralSchema, isObjectSchema, isUnionSchema, transformValueError, transformValueErrors } from './validation/unified/index.js';
4
4
  export { T as TransformedError, V as ValidationError, a as ValidationResult } from './types-BWFZ7OaO.js';
5
5
  export { ComponentValidationError, ComponentValidationResult, ComponentVersion, ComponentVersionMap, CustomComponent, DuplicateComponentError, PluginValidationOptions, PluginValidationResult, RenderContext, RenderFunction, createComponent, createVersion, getValidationSummary, isValidationSuccess, resolveComponentVersion, validateCustomComponentProps } from './plugin/index.js';
6
+ import { F as FontRegistryEntry, a as FontRuntimeOpts, R as ResolvedFontSource, b as ResolvedFont } from './types-CL0Hbw6x.js';
7
+ export { c as FontFamilyNameSchema, d as FontRegistryDefinition, e as FontRegistryEntrySchema, f as FontRegistrySchema, g as FontSource, h as FontSourceSchema, S as SAFE_FONTS, i as SafeFontName, j as isSafeFont } from './types-CL0Hbw6x.js';
6
8
  export { ParsedSemver, compareSemver, isValidSemver, latestVersion, parseSemver } from './utils/semver.js';
7
9
  import '@sinclair/typebox';
8
10
  import '@sinclair/typebox/value';
@@ -18,4 +20,452 @@ interface ServicesConfig {
18
20
  highcharts?: HighchartsServiceConfig;
19
21
  }
20
22
 
21
- export type { HighchartsServiceConfig, ServicesConfig };
23
+ /** Scan an arbitrary doc tree (DOCX or PPTX) for every font family referenced. */
24
+ declare function collectFontNames(doc: unknown): Set<string>;
25
+ /** Scan a DOCX document tree for every font family name referenced. */
26
+ declare const collectFontNamesFromDocx: typeof collectFontNames;
27
+ /** Scan a PPTX presentation tree for every font family name referenced. */
28
+ declare const collectFontNamesFromPptx: typeof collectFontNames;
29
+
30
+ /**
31
+ * Validate that every font name referenced in a document is either
32
+ * in SAFE_FONTS or present in the document's fontRegistry / runtime overrides.
33
+ *
34
+ * Used at generate-start; emits warnings for unresolved names so pipelines
35
+ * can surface them via their existing warning channels.
36
+ */
37
+
38
+ /**
39
+ * Warning codes for font resolution + rendering.
40
+ *
41
+ * - `FONT_UNRESOLVED` — family not in SAFE_FONTS and not registered.
42
+ * - `FONT_MODE_SUBSTITUTED` — non-safe families rewritten to safe equivalents.
43
+ * - `FONT_MODE_CUSTOM` — export mode "custom" — refs kept as-is.
44
+ */
45
+ type FontIssueCode = 'FONT_UNRESOLVED' | 'FONT_MODE_SUBSTITUTED' | 'FONT_MODE_CUSTOM';
46
+ interface FontResolutionIssue {
47
+ code: FontIssueCode;
48
+ family: string;
49
+ message: string;
50
+ }
51
+ interface FontValidationResult {
52
+ /** Names that resolved via SAFE_FONTS or the registry. */
53
+ resolved: string[];
54
+ /** Names with no resolution path. */
55
+ unresolved: string[];
56
+ /** One warning per unresolved name. */
57
+ warnings: FontResolutionIssue[];
58
+ }
59
+ interface FontValidationInput {
60
+ /** Font names referenced in the document (from collectFontNamesFromDocx / FromPptx). */
61
+ referencedNames: Iterable<string>;
62
+ /** Runtime-registered entries (e.g. from FontRuntimeOpts.extraEntries). */
63
+ registeredEntries?: FontRegistryEntry[];
64
+ }
65
+ /**
66
+ * Validate referenced font names against SAFE_FONTS + runtime-registered entries.
67
+ * Does not perform network fetches — this runs purely off schema + opts content.
68
+ */
69
+ declare function validateFontReferences(input: FontValidationInput): FontValidationResult;
70
+
71
+ /**
72
+ * Map a (family, weight, italic) tuple to the pair of
73
+ * `(familyName, { bold, italic })` the renderer should actually use.
74
+ *
75
+ * OOXML runs can only carry a bold/italic toggle, not a numeric weight.
76
+ * For weights outside the RIBBI quad (400/700 × roman/italic), Word
77
+ * resolves intermediate weights via **separate sub-family faces** whose
78
+ * internal family name is the canonical Google-Fonts-style subfamily,
79
+ * e.g. `Inter Light`, `Inter ExtraBold Italic`. Rewriting the run's
80
+ * `family` to that synthetic name lets Word pick the right face when the
81
+ * recipient has the full family installed, and lets the LibreOffice
82
+ * preview resolve the matching staged TTF by its internal name.
83
+ *
84
+ * No embedding involved — this is purely a name transform applied at
85
+ * render time. Safe fonts and unrecognised weights fall back to the
86
+ * bold-only heuristic (`weight >= 600 → bold`).
87
+ */
88
+ /** Human-readable labels for the canonical font-weight numbers. */
89
+ declare const WEIGHT_LABELS: Record<number, string>;
90
+ interface SynthesizedFamily {
91
+ /** The family name to emit in `rFonts`/`fontFace`. */
92
+ family: string;
93
+ /** Whether to also set the run's bold toggle. */
94
+ bold: boolean;
95
+ /** Whether to also set the run's italic toggle. */
96
+ italic: boolean;
97
+ /**
98
+ * `true` when the input `weight` was not one of the canonical
99
+ * 100/200/.../900 labels. The canonical family name is returned with
100
+ * a `weight >= 600 → bold` fallback, but the run will not match a
101
+ * dedicated sub-family face — callers should surface this so authors
102
+ * know the weight was effectively rounded to Regular or Bold.
103
+ */
104
+ nonCanonicalWeight: boolean;
105
+ }
106
+ /**
107
+ * Translate `(family, weight, italic)` into the rendering-time family name
108
+ * plus the bold/italic toggles to emit on the run.
109
+ *
110
+ * - RIBBI (weights 400 + 700, roman + italic) stays on the canonical family
111
+ * name and uses native bold/italic toggles.
112
+ * - Other canonical weights become `"<Family> <Weight>"` (e.g.
113
+ * `"Inter Light"`) with bold/italic toggles cleared; any italic flag is
114
+ * folded into the name (`"Inter Light Italic"`).
115
+ * - Non-canonical weights (floating or out-of-range) fall back to
116
+ * `bold = weight >= 600` and leave the family name untouched.
117
+ */
118
+ declare function synthesizeFamilyName(family: string, weight: number | undefined, italic: boolean): SynthesizedFamily;
119
+
120
+ /**
121
+ * Rewrite a TTF/OTF's `name` table so `nameID` 1 / 4 / 6 / 16 carry the
122
+ * supplied synthetic family name. Used by the preview-side font stagers
123
+ * so that running-text references like `"Inter Light"` resolve to the
124
+ * correct face when the stager registers it with Core Text / fontconfig
125
+ * / GDI (all of which index by the font's internal `name` table rather
126
+ * than the filename).
127
+ *
128
+ * The transform rebuilds the whole font: new `name` table bytes, new
129
+ * table directory with shifted offsets, recomputed per-table checksums,
130
+ * and the magic `head.checkSumAdjustment` recomputed against the whole
131
+ * output buffer. Nothing else is touched.
132
+ *
133
+ * OTF (CFF-flavoured) and TTF (glyf-flavoured) share the sfnt outer
134
+ * structure, so the same code handles both.
135
+ */
136
+ /**
137
+ * Return a copy of `input` whose name table has `nameID` 1/4/6/16 rewritten
138
+ * to `newFamily`. Returns the original buffer unchanged if the font has no
139
+ * `name` table or the sfnt header is invalid.
140
+ */
141
+ declare function rewriteFontFamilyName(input: Buffer, newFamily: string): Buffer;
142
+
143
+ /**
144
+ * FontRegistry — merges catalog + document registry + runtime entries
145
+ * and materializes referenced fonts into ResolvedFont records.
146
+ *
147
+ * Resolution rules, per referenced name:
148
+ * 1. Registry match (by family or id, case-insensitive). Runtime entries win
149
+ * on collision with document entries. Materialize each source.
150
+ * 2. SAFE_FONTS membership → empty sources.
151
+ * 3. Otherwise → empty sources with FONT_UNRESOLVED warning.
152
+ */
153
+
154
+ /**
155
+ * Minimal interface the registry needs from a disk cache. The concrete
156
+ * implementation ships in `./cache/disk-cache` but is Node-only (uses fs/crypto).
157
+ * Callers on Node inject an instance; browser callers pass nothing.
158
+ */
159
+ interface FontDiskCacheLike$1 {
160
+ get(key: string): Promise<Buffer | undefined>;
161
+ set(key: string, value: Buffer): Promise<void>;
162
+ }
163
+ /**
164
+ * Minimal interface for a file-loader. Same reasoning as FontDiskCacheLike:
165
+ * concrete impl is Node-only, callers inject when on Node.
166
+ */
167
+ type FontFileLoader = (input: {
168
+ path: string;
169
+ weight?: number;
170
+ italic?: boolean;
171
+ baseDir?: string;
172
+ }) => Promise<ResolvedFontSource>;
173
+ /**
174
+ * Minimal interface for the variable-font fetcher. `subset-font` (the
175
+ * harfbuzz-wasm wrapper we use for axis pinning) reaches for `fs` at
176
+ * init time, which crashes in the browser. Injection keeps that import
177
+ * behind the Node-only subpath; browser bundles never pull it in, and
178
+ * browser callers simply won't see `kind: 'variable'` fonts resolved
179
+ * (the registry warns and skips instead).
180
+ */
181
+ type FontVariableLoader = (input: {
182
+ url: string;
183
+ weight: number;
184
+ italic: boolean;
185
+ axes?: Record<string, number>;
186
+ fetchTimeoutMs?: number;
187
+ memoryCache?: {
188
+ get(key: string): Buffer | undefined;
189
+ set(key: string, value: Buffer): void;
190
+ };
191
+ diskCache?: {
192
+ get(key: string): Promise<Buffer | undefined>;
193
+ set(key: string, value: Buffer): Promise<void>;
194
+ };
195
+ }) => Promise<{
196
+ source?: ResolvedFontSource;
197
+ warnings?: string[];
198
+ }>;
199
+ interface FontRegistryInput {
200
+ /** Runtime options — entries come from opts.extraEntries. */
201
+ opts?: FontRuntimeOpts;
202
+ /** Optional disk cache (Node only). Pass an instance of FontDiskCache. */
203
+ diskCache?: FontDiskCacheLike$1;
204
+ /**
205
+ * Optional `kind: "file"` loader (Node only). Inject `loadFileFontSource`
206
+ * from `@json-to-office/shared/fonts/sources/file-loader` on Node. Browser
207
+ * callers pass nothing; `kind: "file"` sources then warn and skip.
208
+ */
209
+ fileLoader?: FontFileLoader;
210
+ /**
211
+ * Optional `kind: "variable"` loader (Node only). Inject
212
+ * `fetchVariableFontSource` from `@json-to-office/shared/fonts/node` on
213
+ * Node. Browser callers pass nothing; `kind: "variable"` sources then
214
+ * warn and skip. Keeping this injected avoids dragging subset-font (and
215
+ * its `fs.promises.readFile` bootstrap) into client bundles.
216
+ */
217
+ variableLoader?: FontVariableLoader;
218
+ }
219
+ declare class FontRegistry {
220
+ private readonly index;
221
+ private readonly cache;
222
+ private readonly opts;
223
+ private readonly memoryCache;
224
+ private readonly diskCache;
225
+ private readonly fileLoader;
226
+ private readonly variableLoader;
227
+ constructor(input?: FontRegistryInput);
228
+ private addEntry;
229
+ /** Resolve every referenced name in one pass. Order preserved. */
230
+ resolveMany(names: Iterable<string>): Promise<ResolvedFont[]>;
231
+ resolve(name: string): Promise<ResolvedFont>;
232
+ private materializeEntry;
233
+ private materializeSource;
234
+ }
235
+
236
+ /**
237
+ * Font format detection from magic bytes.
238
+ * Source: OpenType spec + WOFF1/WOFF2 W3C specs.
239
+ */
240
+
241
+ declare function detectFontFormat(buf: Buffer): ResolvedFontSource['format'];
242
+
243
+ /**
244
+ * Curated list of popular Google Fonts for picker autocomplete.
245
+ *
246
+ * Not exhaustive — the full Google Fonts library has ~1500 families.
247
+ * This is ~30 names known to cover most real-world use cases.
248
+ */
249
+ interface PopularGoogleFont {
250
+ family: string;
251
+ category: 'sans' | 'serif' | 'mono' | 'display' | 'handwriting';
252
+ /** Weights available on Google Fonts for this family. */
253
+ weights: number[];
254
+ /** Whether italic variants exist. */
255
+ hasItalic: boolean;
256
+ }
257
+ declare const POPULAR_GOOGLE_FONTS: readonly PopularGoogleFont[];
258
+
259
+ /**
260
+ * Per-family upstream overrides for popular Google Fonts whose
261
+ * redistribution on fonts.google.com has known defects we can't fix via
262
+ * metadata patching alone.
263
+ *
264
+ * When `autoGoogleFontEntries` hits a family present in this table, it
265
+ * builds override sources instead of issuing `kind: "google"` CSS requests.
266
+ * Each entry is either:
267
+ *
268
+ * - `{ kind: "url", url, weight, italic? }` — a direct HTTPS TTF/OTF.
269
+ * Use when a clean per-weight static exists on a stable CDN.
270
+ *
271
+ * - `{ kind: "variable", url, weight, italic? }` — points at a variable
272
+ * TTF with an `fvar` table. The registry downloads the variable font
273
+ * once and harfbuzz-pins the `wght` axis to the specified weight,
274
+ * producing a clean static TTF. Use when the upstream ships a variable
275
+ * font but no per-weight statics on a CDN (rsms/inter is the
276
+ * canonical example — variable font on jsDelivr, per-weight statics
277
+ * only in GitHub release zips).
278
+ *
279
+ * Pick `variable` over `url` when both are available: the instancer
280
+ * produces per-weight glyph outlines that diverge correctly at every
281
+ * axis value. Google's static redistributions collapse adjacent weights
282
+ * onto the same instance — Inter Thin and ExtraLight both source at
283
+ * ~wght=250 in Google's pipeline, so their static TTFs have 98% identical
284
+ * glyph outlines. Instancing the upstream variable font at exactly wght=100
285
+ * vs wght=200 gives properly distinct geometry.
286
+ *
287
+ * Validate new entries with a HEAD request before adding — the fetchers
288
+ * reject non-TTF responses, but a failed override silently falls back to
289
+ * the Google path, defeating the purpose.
290
+ */
291
+ /** One upstream variant source. Type matches the FontSource schema so we
292
+ * can pass the entry directly into `FontRegistry`'s materialize pipeline. */
293
+ type UpstreamVariant = {
294
+ kind: 'url';
295
+ url: string;
296
+ weight: number;
297
+ italic?: boolean;
298
+ } | {
299
+ kind: 'variable';
300
+ url: string;
301
+ weight: number;
302
+ italic?: boolean;
303
+ /** Extra axis pins merged on top of the derived `wght` pin. */
304
+ axes?: Record<string, number>;
305
+ };
306
+ interface UpstreamOverride {
307
+ /** Human-readable for logs/diagnostics only. */
308
+ reason: string;
309
+ variants: UpstreamVariant[];
310
+ }
311
+ declare const UPSTREAM_OVERRIDES: Record<string, UpstreamOverride>;
312
+ /** Case-insensitive lookup. Returns undefined when the family has no override. */
313
+ declare function getUpstreamOverride(family: string): UpstreamOverride | undefined;
314
+
315
+ /**
316
+ * In-process LRU cache for resolved font buffers.
317
+ * Scoped to a single process — do not share across requests on a server.
318
+ */
319
+ interface MemoryCacheOptions {
320
+ /** Approximate soft cap in bytes. LRU-evict when exceeded. */
321
+ maxBytes?: number;
322
+ }
323
+ declare class FontMemoryCache {
324
+ private readonly store;
325
+ private bytes;
326
+ private readonly maxBytes;
327
+ constructor(opts?: MemoryCacheOptions);
328
+ get(key: string): Buffer | undefined;
329
+ set(key: string, value: Buffer): void;
330
+ size(): number;
331
+ }
332
+
333
+ /**
334
+ * Google Fonts fetcher.
335
+ *
336
+ * Hits the CSS API v2 with an older User-Agent that returns TTF (default UA
337
+ * gets WOFF2, which Office cannot embed as-is). Parses the `src: url(...)` line
338
+ * and downloads the binary.
339
+ *
340
+ * Uses memory + optional disk cache keyed by `${family}|${weight}|${italic}`.
341
+ */
342
+
343
+ interface FontDiskCacheLike {
344
+ get(key: string): Promise<Buffer | undefined>;
345
+ set(key: string, value: Buffer): Promise<void>;
346
+ }
347
+ interface GoogleFetchOptions {
348
+ family: string;
349
+ weights: number[];
350
+ italics?: boolean;
351
+ memoryCache?: FontMemoryCache;
352
+ diskCache?: FontDiskCacheLike;
353
+ fetchTimeoutMs?: number;
354
+ /** Override for tests. */
355
+ fetcher?: typeof fetch;
356
+ }
357
+ interface GoogleFetchResult {
358
+ sources: ResolvedFontSource[];
359
+ warnings: string[];
360
+ }
361
+ declare function fetchGoogleFontSources(opts: GoogleFetchOptions): Promise<GoogleFetchResult>;
362
+
363
+ /**
364
+ * Font family substitution: rewrite every non-safe family reference in
365
+ * the doc tree + theme to a SAFE_FONTS equivalent. Used by the
366
+ * `'substitute'` export mode (`FontRuntimeOpts.mode`) so that non-safe
367
+ * fonts (Playfair Display, Inter, …) ship as Georgia/Calibri and the
368
+ * document renders identically on every recipient machine — no embed
369
+ * bytes, no Word-for-Mac intermediate-weight surprises.
370
+ *
371
+ * The walker mirrors the shape used by `collectFontNamesFromDocx/Pptx`
372
+ * so the two stay in sync: whatever `collect` scans, `rewrite` will
373
+ * rewrite. Future component-schema additions that introduce new font
374
+ * keys go in `FONT_NAME_KEYS` / `THEME_FONT_KEYS` once, both sides pick
375
+ * them up.
376
+ */
377
+
378
+ /** One swap recorded during a rewrite. */
379
+ interface FontSubstitution {
380
+ from: string;
381
+ to: string;
382
+ }
383
+ interface ApplyFontSubstitutionResult<T> {
384
+ doc: T;
385
+ substitutions: FontSubstitution[];
386
+ }
387
+ /**
388
+ * Walk a doc tree + swap every non-safe family reference per `mapping`.
389
+ * Returns a new tree (structural clone) plus the list of `(from, to)`
390
+ * swaps made, deduped by source name.
391
+ *
392
+ * Families already in SAFE_FONTS are never rewritten (even if a mapping
393
+ * entry targets them as a key — safe fonts don't need substitution).
394
+ * Families with no mapping entry are left untouched — callers should
395
+ * feed the result of `buildDefaultSubstitutionMap` to ensure every
396
+ * non-safe reference gets a fallback.
397
+ */
398
+ declare function applyFontSubstitution<T>(doc: T, mapping: Record<string, string>): ApplyFontSubstitutionResult<T>;
399
+ /**
400
+ * Pick the safe-font fallback for a single non-safe family. Precedence:
401
+ * 1. Explicit override in `EXPLICIT_OVERRIDES`.
402
+ * 2. Category lookup in `POPULAR_GOOGLE_FONTS`.
403
+ * 3. Final default (`Calibri`).
404
+ *
405
+ * Exposed for the playground dialog so it can pre-populate the per-family
406
+ * picker with the same defaults the CLI would apply.
407
+ */
408
+ declare function defaultSubstituteFor(family: string): string;
409
+ /**
410
+ * Build a substitution map for every non-safe family in `referencedNames`.
411
+ * Safe fonts are omitted from the result since they don't need swapping.
412
+ * Caller can override individual entries before passing to
413
+ * `applyFontSubstitution`.
414
+ */
415
+ declare function buildDefaultSubstitutionMap(referencedNames: Iterable<string>): Record<string, string>;
416
+
417
+ /**
418
+ * Cache-key suffix used to scope generator outputs by export mode. When
419
+ * `fonts.mode === 'substitute'` the doc tree is rewritten pre-render, so
420
+ * a substitute-mode buffer and a custom-mode buffer for the same base
421
+ * theme must not collide in the byte cache. Keep this as a single
422
+ * helper so a typo in one caller can't silently alias one mode onto the
423
+ * other's cache slot.
424
+ */
425
+ declare function scopedThemeName(baseThemeName: string, fontMode: string | undefined): string;
426
+
427
+ interface ApplyExportModeInput<D, T> {
428
+ doc: D;
429
+ theme: T;
430
+ fonts?: FontRuntimeOpts;
431
+ }
432
+ interface ApplyExportModeWarning {
433
+ code: 'FONT_MODE_CUSTOM' | 'FONT_MODE_SUBSTITUTED';
434
+ message: string;
435
+ }
436
+ interface ApplyExportModeResult<D, T> {
437
+ doc: D;
438
+ theme: T;
439
+ warnings: ApplyExportModeWarning[];
440
+ }
441
+ /**
442
+ * Inspect `fonts.mode` and apply the pre-resolution rewrite for the
443
+ * requested mode.
444
+ *
445
+ * - `'custom'` (default) — no rewrite. Font references stay as authored;
446
+ * recipients need the font installed or Word falls back. The
447
+ * LibreOffice preview stager registers resolved bytes so preview
448
+ * fidelity matches the recipient-side experience when the font is
449
+ * installed.
450
+ * - `'substitute'` — rewrite every non-safe family in doc + theme to its
451
+ * mapped safe equivalent. Fills in defaults via
452
+ * `buildDefaultSubstitutionMap` for any non-safe reference not present
453
+ * in `fonts.substitution`. Emits one `FONT_MODE_SUBSTITUTED` warning
454
+ * listing every swap.
455
+ */
456
+ declare function applyExportMode<D, T>(input: ApplyExportModeInput<D, T>): ApplyExportModeResult<D, T>;
457
+
458
+ /**
459
+ * Deep Merge Utilities
460
+ * Generic deep-merge helpers used by both docx and pptx
461
+ * componentDefaults resolution systems.
462
+ */
463
+ /**
464
+ * Merge theme defaults with user-provided configuration.
465
+ * User config takes precedence over theme defaults.
466
+ * Uses deep merge to preserve nested objects.
467
+ * Arrays are replaced wholesale, not merged per-element.
468
+ */
469
+ declare function mergeWithDefaults<T>(userConfig: T, themeDefaults: Partial<T>): T;
470
+
471
+ export { type FontIssueCode, FontRegistry, FontRegistryEntry, type FontRegistryInput, type FontResolutionIssue, FontRuntimeOpts, type FontSubstitution, type FontValidationInput, type FontValidationResult, type HighchartsServiceConfig, POPULAR_GOOGLE_FONTS, type PopularGoogleFont, ResolvedFont, ResolvedFontSource, type ServicesConfig, type SynthesizedFamily, UPSTREAM_OVERRIDES, type UpstreamOverride, type UpstreamVariant, WEIGHT_LABELS, applyExportMode, applyFontSubstitution, buildDefaultSubstitutionMap, collectFontNamesFromDocx, collectFontNamesFromPptx, defaultSubstituteFor, detectFontFormat, fetchGoogleFontSources, getUpstreamOverride, mergeWithDefaults, rewriteFontFamilyName, scopedThemeName, synthesizeFamilyName, validateFontReferences };