@json-to-office/shared 0.33.0 → 0.35.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 → chunk-FDSJYZ5W.js} +21 -20
- package/dist/chunk-FDSJYZ5W.js.map +1 -0
- package/dist/fonts/node.d.ts +42 -2
- package/dist/fonts/node.js +50 -2
- package/dist/fonts/node.js.map +1 -1
- package/dist/index.d.ts +51 -138
- package/dist/index.js +81 -2
- package/dist/index.js.map +1 -1
- package/dist/schemas/slide-content.d.ts +18 -0
- package/dist/schemas/slide-content.js +42 -0
- package/dist/schemas/slide-content.js.map +1 -1
- package/dist/{types-CL0Hbw6x.d.ts → types-kcQwhOlf.d.ts} +175 -1
- package/package.json +1 -1
- package/dist/chunk-CP2I5NPP.js.map +0 -1
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
// src/fonts/sources/url-allowlist.ts
|
|
2
|
+
var FONT_URL_ALLOWLIST = [
|
|
3
|
+
"fonts.gstatic.com",
|
|
4
|
+
"fonts.googleapis.com",
|
|
5
|
+
"cdn.jsdelivr.net"
|
|
6
|
+
];
|
|
7
|
+
function isAllowedFontUrl(url) {
|
|
8
|
+
let parsed;
|
|
9
|
+
try {
|
|
10
|
+
parsed = new URL(url);
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (parsed.protocol !== "https:") return false;
|
|
15
|
+
return FONT_URL_ALLOWLIST.includes(parsed.hostname.toLowerCase());
|
|
16
|
+
}
|
|
17
|
+
|
|
1
18
|
// src/fonts/sources/format.ts
|
|
2
19
|
function detectFontFormat(buf) {
|
|
3
20
|
if (buf.length < 4) return "unknown";
|
|
@@ -16,25 +33,9 @@ function detectFontFormat(buf) {
|
|
|
16
33
|
return "unknown";
|
|
17
34
|
}
|
|
18
35
|
|
|
19
|
-
// src/fonts/sources/url-allowlist.ts
|
|
20
|
-
var FONT_URL_ALLOWLIST = [
|
|
21
|
-
"fonts.gstatic.com",
|
|
22
|
-
"fonts.googleapis.com",
|
|
23
|
-
"cdn.jsdelivr.net"
|
|
24
|
-
];
|
|
25
|
-
function isAllowedFontUrl(url) {
|
|
26
|
-
let parsed;
|
|
27
|
-
try {
|
|
28
|
-
parsed = new URL(url);
|
|
29
|
-
} catch {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
if (parsed.protocol !== "https:") return false;
|
|
33
|
-
return FONT_URL_ALLOWLIST.includes(parsed.hostname.toLowerCase());
|
|
34
|
-
}
|
|
35
|
-
|
|
36
36
|
export {
|
|
37
|
-
|
|
38
|
-
isAllowedFontUrl
|
|
37
|
+
FONT_URL_ALLOWLIST,
|
|
38
|
+
isAllowedFontUrl,
|
|
39
|
+
detectFontFormat
|
|
39
40
|
};
|
|
40
|
-
//# sourceMappingURL=chunk-
|
|
41
|
+
//# sourceMappingURL=chunk-FDSJYZ5W.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/fonts/sources/url-allowlist.ts","../src/fonts/sources/format.ts"],"sourcesContent":["/**\n * Hostname allowlist for font fetchers.\n *\n * `url-fetcher` and `variable-fetcher` can be handed arbitrary URLs via\n * `FontRegistryEntry.sources`, which may originate from document JSON. Without\n * a guard, a malicious doc could point fetchers at internal hosts (SSRF), the\n * filesystem (`file://`), or the IMDS endpoint. Limit downloads to the hosts\n * our catalog + UPSTREAM_OVERRIDES actually target.\n *\n * Keep the list small and HTTPS-only. Expansions should be deliberate code\n * reviews, not config-driven — the cost of a new domain is the code change.\n */\n\nexport const FONT_URL_ALLOWLIST: readonly string[] = [\n 'fonts.gstatic.com',\n 'fonts.googleapis.com',\n 'cdn.jsdelivr.net',\n];\n\nexport function isAllowedFontUrl(url: string): boolean {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n return false;\n }\n if (parsed.protocol !== 'https:') return false;\n return FONT_URL_ALLOWLIST.includes(parsed.hostname.toLowerCase());\n}\n","/**\n * Font format detection from magic bytes.\n * Source: OpenType spec + WOFF1/WOFF2 W3C specs.\n */\n\nimport type { ResolvedFontSource } from '../types';\n\nexport function detectFontFormat(buf: Buffer): ResolvedFontSource['format'] {\n if (buf.length < 4) return 'unknown';\n\n const b0 = buf[0],\n b1 = buf[1],\n b2 = buf[2],\n b3 = buf[3];\n\n // TTF: 0x00010000 (SFNT) or 'true' (0x74727565) or 'typ1' (0x74797031)\n if (\n (b0 === 0x00 && b1 === 0x01 && b2 === 0x00 && b3 === 0x00) ||\n (b0 === 0x74 && b1 === 0x72 && b2 === 0x75 && b3 === 0x65) ||\n (b0 === 0x74 && b1 === 0x79 && b2 === 0x70 && b3 === 0x31)\n ) {\n return 'ttf';\n }\n // OTF: 'OTTO'\n if (b0 === 0x4f && b1 === 0x54 && b2 === 0x54 && b3 === 0x4f) return 'otf';\n // WOFF: 'wOFF'\n if (b0 === 0x77 && b1 === 0x4f && b2 === 0x46 && b3 === 0x46) return 'woff';\n // WOFF2: 'wOF2'\n if (b0 === 0x77 && b1 === 0x4f && b2 === 0x46 && b3 === 0x32) return 'woff2';\n // EOT: version bytes at offset 8-11 — rougher signature\n if (buf.length >= 36 && buf[34] === 0x4c && buf[35] === 0x50) return 'eot';\n // PostScript Type 1 (.pfb) — binary container marker byte 0x80 followed by\n // segment type 0x01 (ASCII). Also match the text-form ASCII header\n // \"%!PS-AdobeFont\". Note: .pfm (metric files) have no reliable magic and\n // stay in 'unknown' — same treatment (rejection at the loader).\n if (b0 === 0x80 && b1 === 0x01) return 'pfb';\n if (\n buf.length >= 14 &&\n buf.slice(0, 14).toString('ascii') === '%!PS-AdobeFont'\n ) {\n return 'pfb';\n }\n\n return 'unknown';\n}\n\n/**\n * Formats we detect but cannot legally embed in an OOXML document:\n * WOFF/WOFF2 are web-only containers; PostScript (.pfb) is explicitly\n * disallowed by Microsoft's embedding guidance.\n */\nexport const UNEMBEDDABLE_FORMATS = new Set<ResolvedFontSource['format']>([\n 'woff',\n 'woff2',\n 'pfb',\n]);\n"],"mappings":";AAaO,IAAM,qBAAwC;AAAA,EACnD;AAAA,EACA;AAAA,EACA;AACF;AAEO,SAAS,iBAAiB,KAAsB;AACrD,MAAI;AACJ,MAAI;AACF,aAAS,IAAI,IAAI,GAAG;AAAA,EACtB,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,OAAO,aAAa,SAAU,QAAO;AACzC,SAAO,mBAAmB,SAAS,OAAO,SAAS,YAAY,CAAC;AAClE;;;ACrBO,SAAS,iBAAiB,KAA2C;AAC1E,MAAI,IAAI,SAAS,EAAG,QAAO;AAE3B,QAAM,KAAK,IAAI,CAAC,GACd,KAAK,IAAI,CAAC,GACV,KAAK,IAAI,CAAC,GACV,KAAK,IAAI,CAAC;AAGZ,MACG,OAAO,KAAQ,OAAO,KAAQ,OAAO,KAAQ,OAAO,KACpD,OAAO,OAAQ,OAAO,OAAQ,OAAO,OAAQ,OAAO,OACpD,OAAO,OAAQ,OAAO,OAAQ,OAAO,OAAQ,OAAO,IACrD;AACA,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,MAAQ,OAAO,MAAQ,OAAO,MAAQ,OAAO,GAAM,QAAO;AAErE,MAAI,OAAO,OAAQ,OAAO,MAAQ,OAAO,MAAQ,OAAO,GAAM,QAAO;AAErE,MAAI,OAAO,OAAQ,OAAO,MAAQ,OAAO,MAAQ,OAAO,GAAM,QAAO;AAErE,MAAI,IAAI,UAAU,MAAM,IAAI,EAAE,MAAM,MAAQ,IAAI,EAAE,MAAM,GAAM,QAAO;AAKrE,MAAI,OAAO,OAAQ,OAAO,EAAM,QAAO;AACvC,MACE,IAAI,UAAU,MACd,IAAI,MAAM,GAAG,EAAE,EAAE,SAAS,OAAO,MAAM,kBACvC;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AACT;","names":[]}
|
package/dist/fonts/node.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { R as ResolvedFontSource } from '../types-
|
|
1
|
+
import { R as ResolvedFontSource, a as RasterizeFontFace, b as ResolvedFont } from '../types-kcQwhOlf.js';
|
|
2
|
+
import { GenerationWarning } from '../types/warnings.js';
|
|
2
3
|
import '@sinclair/typebox';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -83,4 +84,43 @@ declare function fetchVariableFontSource(opts: VariableFetchOptions): Promise<{
|
|
|
83
84
|
warnings?: string[];
|
|
84
85
|
}>;
|
|
85
86
|
|
|
86
|
-
|
|
87
|
+
/**
|
|
88
|
+
* `ResolvedFont[]` ⇄ `RasterizeFontFace[]` — the one encoder/decoder pair for
|
|
89
|
+
* shipping font bytes to the pptx rasterizer.
|
|
90
|
+
*
|
|
91
|
+
* The docx side encodes (core-docx, from `resolveDocumentFonts`) and the
|
|
92
|
+
* rasterizer side decodes (jto-cli, before handing the faces to a
|
|
93
|
+
* `FontStager`). Keeping both halves here means the two cannot drift on
|
|
94
|
+
* base64 handling or on the family-name convention.
|
|
95
|
+
*
|
|
96
|
+
* FAMILY NAMES STAY UNSYNTHESIZED. The wire carries the catalog family
|
|
97
|
+
* ("Inter"); the stager applies `synthesizeFamilyName` +
|
|
98
|
+
* `rewriteFontFamilyName` to produce the sub-family the presentation
|
|
99
|
+
* actually references ("Inter Light"). Encoding a pre-synthesized name here
|
|
100
|
+
* would make the stager apply the suffix twice.
|
|
101
|
+
*
|
|
102
|
+
* Buffer-dependent → Node-only. Exported from `@json-to-office/shared/fonts/node`.
|
|
103
|
+
*/
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Flatten resolved fonts into the serializable wire faces (one face per
|
|
107
|
+
* source variant). Entries with no sources — safe-only fonts, which the
|
|
108
|
+
* renderer resolves against system faces — carry no bytes and are skipped,
|
|
109
|
+
* as are sources in a format no stager can register.
|
|
110
|
+
*
|
|
111
|
+
* @param warnings - sink for one warning per dropped source, shaped like every
|
|
112
|
+
* other generation warning so a caller can hand in the same array it already
|
|
113
|
+
* collects. Both docx entry paths do: a dropped face renders as a fallback,
|
|
114
|
+
* which is precisely the silent substitution this pipeline exists to make
|
|
115
|
+
* visible, so it must not be discoverable only by reading the code.
|
|
116
|
+
*/
|
|
117
|
+
declare function toRasterizeFontFaces(fonts: readonly ResolvedFont[], warnings?: GenerationWarning[]): RasterizeFontFace[];
|
|
118
|
+
/**
|
|
119
|
+
* Inverse of {@link toRasterizeFontFaces}: regroup wire faces back into
|
|
120
|
+
* `ResolvedFont[]` so the existing `FontStager.stage(ResolvedFont[], …)`
|
|
121
|
+
* signature needs no change. Grouping is by exact (case-sensitive) family,
|
|
122
|
+
* matching how the registry keys resolved fonts.
|
|
123
|
+
*/
|
|
124
|
+
declare function fromRasterizeFontFaces(faces: readonly RasterizeFontFace[]): ResolvedFont[];
|
|
125
|
+
|
|
126
|
+
export { FontDiskCache, type VariableFetchOptions, fetchVariableFontSource, fromRasterizeFontFaces, loadFileFontSource, toRasterizeFontFaces };
|
package/dist/fonts/node.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
detectFontFormat,
|
|
3
3
|
isAllowedFontUrl
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-FDSJYZ5W.js";
|
|
5
5
|
|
|
6
6
|
// src/fonts/sources/file-loader.ts
|
|
7
7
|
import { readFile } from "fs/promises";
|
|
@@ -231,9 +231,57 @@ async function fetchVariableFontSource(opts) {
|
|
|
231
231
|
warnings: []
|
|
232
232
|
};
|
|
233
233
|
}
|
|
234
|
+
|
|
235
|
+
// src/fonts/rasterize-faces.ts
|
|
236
|
+
var STAGEABLE_FORMATS = /* @__PURE__ */ new Set(["ttf", "otf"]);
|
|
237
|
+
function toRasterizeFontFaces(fonts, warnings) {
|
|
238
|
+
const faces = [];
|
|
239
|
+
for (const font of fonts) {
|
|
240
|
+
if (font.sources.length === 0) continue;
|
|
241
|
+
for (const source of font.sources) {
|
|
242
|
+
if (!STAGEABLE_FORMATS.has(source.format)) {
|
|
243
|
+
warnings?.push({
|
|
244
|
+
component: "fontRegistry",
|
|
245
|
+
severity: "warning",
|
|
246
|
+
context: { code: "FONT_FORMAT_NOT_RASTERIZABLE" },
|
|
247
|
+
message: `"${font.family}" weight ${source.weight}${source.italic ? " italic" : ""} is ${source.format}; the rasterizer's font stagers only register TTF/OTF, so this face is omitted and the visual renders with a fallback face.`
|
|
248
|
+
});
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
faces.push({
|
|
252
|
+
family: font.family,
|
|
253
|
+
weight: source.weight,
|
|
254
|
+
italic: source.italic,
|
|
255
|
+
data: source.data.toString("base64"),
|
|
256
|
+
format: source.format
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return faces;
|
|
261
|
+
}
|
|
262
|
+
function fromRasterizeFontFaces(faces) {
|
|
263
|
+
const byFamily = /* @__PURE__ */ new Map();
|
|
264
|
+
for (const face of faces) {
|
|
265
|
+
let font = byFamily.get(face.family);
|
|
266
|
+
if (!font) {
|
|
267
|
+
font = { family: face.family, sources: [], warnings: [] };
|
|
268
|
+
byFamily.set(face.family, font);
|
|
269
|
+
}
|
|
270
|
+
const source = {
|
|
271
|
+
data: Buffer.from(face.data, "base64"),
|
|
272
|
+
weight: face.weight,
|
|
273
|
+
italic: face.italic,
|
|
274
|
+
format: face.format ?? "ttf"
|
|
275
|
+
};
|
|
276
|
+
font.sources.push(source);
|
|
277
|
+
}
|
|
278
|
+
return [...byFamily.values()];
|
|
279
|
+
}
|
|
234
280
|
export {
|
|
235
281
|
FontDiskCache,
|
|
236
282
|
fetchVariableFontSource,
|
|
237
|
-
|
|
283
|
+
fromRasterizeFontFaces,
|
|
284
|
+
loadFileFontSource,
|
|
285
|
+
toRasterizeFontFaces
|
|
238
286
|
};
|
|
239
287
|
//# sourceMappingURL=node.js.map
|
package/dist/fonts/node.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/fonts/sources/file-loader.ts","../../src/fonts/cache/disk-cache.ts","../../src/fonts/sources/variable-fetcher.ts"],"sourcesContent":["/**\n * Load a .ttf/.otf file from disk.\n * Node-only — called from the render pipeline.\n */\n\nimport { readFile } from 'fs/promises';\nimport { isAbsolute, resolve as resolvePath } from 'path';\nimport type { ResolvedFontSource } from '../types';\nimport { detectFontFormat } from './format';\n\nexport interface FileSourceInput {\n path: string;\n weight?: number;\n italic?: boolean;\n baseDir?: string;\n}\n\n/** Read a font file and wrap as a ResolvedFontSource. */\nexport async function loadFileFontSource(\n input: FileSourceInput\n): Promise<ResolvedFontSource> {\n const fullPath = isAbsolute(input.path)\n ? input.path\n : resolvePath(input.baseDir ?? process.cwd(), input.path);\n const data = await readFile(fullPath);\n const format = detectFontFormat(data);\n if (format === 'unknown') {\n throw new Error(\n `Font file at \"${fullPath}\" is not a recognized font file (expected TTF/OTF/WOFF/WOFF2)`\n );\n }\n // No format rejection here: bytes flow to the LibreOffice preview\n // stager, which handles WOFF/WOFF2 natively via fontconfig on\n // Linux/macOS. Office output never embeds these bytes — substitute/\n // custom modes rely on recipient-side fonts.\n return {\n data,\n weight: input.weight ?? 400,\n italic: input.italic ?? false,\n format,\n };\n}\n","/**\n * On-disk cache for fetched Google Fonts TTFs.\n * Optional — only active when a cacheDir is provided. Node-only.\n */\n\nimport { createHash } from 'crypto';\nimport { mkdir, readFile, writeFile } from 'fs/promises';\nimport { join } from 'path';\n\nexport class FontDiskCache {\n private readonly dir: string;\n // In-flight promise dedupes the first-write mkdir across concurrent set()\n // calls. Without it, two simultaneous cold-cache writes could both see\n // `ensured=false`, both issue mkdir, and both flip the flag afterwards —\n // harmless today (recursive mkdir is idempotent) but the pattern is\n // right and leaves room to add per-directory locks if we ever need to.\n private ensurePromise: Promise<void> | null = null;\n\n constructor(dir: string) {\n this.dir = dir;\n }\n\n private ensureDir(): Promise<void> {\n if (!this.ensurePromise) {\n this.ensurePromise = mkdir(this.dir, { recursive: true }).then(\n () => undefined\n );\n }\n return this.ensurePromise;\n }\n\n private pathFor(key: string): string {\n const hash = createHash('sha256').update(key).digest('hex').slice(0, 24);\n return join(this.dir, `${hash}.bin`);\n }\n\n async get(key: string): Promise<Buffer | undefined> {\n try {\n return await readFile(this.pathFor(key));\n } catch {\n return undefined;\n }\n }\n\n async set(key: string, value: Buffer): Promise<void> {\n await this.ensureDir();\n await writeFile(this.pathFor(key), value);\n }\n}\n","/**\n * Variable-font instancer. Fetches a variable TTF once (disk-cached), then\n * pins its `wght` axis (plus any additional axes) to produce a clean static\n * TTF per requested weight. Uses harfbuzz via `subset-font` — pure JS + WASM,\n * no native toolchain.\n *\n * Why this exists. Google Fonts serves pre-instanced static TTFs for many\n * families, but the instancing step is lossy: Inter Thin (100) and\n * ExtraLight (200) both ship with `OS/2.usWeightClass=250` and near-\n * identical glyph outlines (xAvgCharWidth differs by 1.8%, glyf table\n * differs by 83 bytes out of 135 KB). Pinning the upstream variable TTF's\n * `wght` axis at exactly 100 vs 200 produces properly distinct instances.\n *\n * Cache strategy:\n * 1. Raw variable TTF cached at key `varsrc|<url>` — one download per URL\n * per process (+ optional disk layer).\n * 2. Instanced static TTF cached at `variable|<url>|<weight>|<italic>` —\n * avoids re-running harfbuzz for weights we've already produced.\n *\n * Full-glyph retention. subset-font's `text` parameter drives which\n * codepoints' glyphs survive. We pass every BMP codepoint so the output\n * is effectively a full-glyph static (not a subset) for any Latin /\n * Cyrillic / Greek / Vietnamese-covering family — which includes every\n * entry in our POPULAR_GOOGLE_FONTS catalog. Supplementary-plane glyphs\n * (emoji) would be dropped, but those aren't in the variable families we\n * target. `preserveNameIds` keeps the human-readable name records our\n * downstream normalization expects.\n */\n\nimport type { ResolvedFontSource } from '../types';\nimport { detectFontFormat } from './format';\nimport { isAllowedFontUrl } from './url-allowlist';\n\n// `subset-font` carries a harfbuzz WASM payload and is Node-only. Lazy-load\n// so a browser bundler that chases the generic `sources/` tree doesn't pull\n// it in. Cached across calls so the WASM heap is created once per process.\nlet subsetFontPromise: Promise<typeof import('subset-font').default> | null =\n null;\nfunction loadSubsetFont(): Promise<typeof import('subset-font').default> {\n if (!subsetFontPromise) {\n subsetFontPromise = import('subset-font').then((m) => m.default);\n }\n return subsetFontPromise;\n}\n\nexport interface VariableFetchOptions {\n url: string;\n weight: number;\n italic: boolean;\n /** Extra axis pins merged on top of the derived `wght` pin (e.g. `ital`,\n * `opsz`, `slnt`). Rare — the `weight`/`italic` pair is usually enough. */\n axes?: Record<string, number>;\n /** Family label used in error messages and diagnostics. */\n familyLabel?: string;\n fetchTimeoutMs?: number;\n fetcher?: typeof fetch;\n memoryCache?: {\n get(key: string): Buffer | undefined;\n set(key: string, value: Buffer): void;\n };\n diskCache?: {\n get(key: string): Promise<Buffer | undefined>;\n set(key: string, value: Buffer): Promise<void>;\n };\n}\n\nfunction rawCacheKey(url: string): string {\n return `varsrc|${url}`;\n}\n\nfunction instanceCacheKey(\n url: string,\n weight: number,\n italic: boolean,\n axes?: Record<string, number>\n): string {\n // Axes go into the key deterministically so different axis pins don't\n // collide. Sorted so `{a:1,b:2}` and `{b:2,a:1}` hash the same.\n const axisPart = axes\n ? '|' +\n Object.entries(axes)\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([k, v]) => `${k}=${v}`)\n .join(',')\n : '';\n return `variable|${url}|${weight}|${italic ? 'i' : 'r'}${axisPart}`;\n}\n\n/**\n * String covering every assigned BMP codepoint (0x20-0xFFFF minus surrogate\n * range). Built lazily on first use — ~127 KiB of UTF-16 memory (0xFFFF\n * codepoints × 2 bytes per UTF-16 code unit, minus the surrogate range)\n * held for the lifetime of the process, which is negligible next to the\n * WASM heap harfbuzz already carries.\n */\nlet cachedBmpCharset: string | null = null;\nfunction bmpCharset(): string {\n if (cachedBmpCharset) return cachedBmpCharset;\n let s = '';\n for (let cp = 0x20; cp <= 0xffff; cp++) {\n // Surrogate range is structurally invalid as standalone codepoints —\n // harfbuzz rejects them. Skip.\n if (cp >= 0xd800 && cp <= 0xdfff) continue;\n s += String.fromCodePoint(cp);\n }\n cachedBmpCharset = s;\n return s;\n}\n\ntype FetchResult = { buf: Buffer } | { error: string };\n\nasync function fetchVariableSource(\n opts: VariableFetchOptions\n): Promise<FetchResult> {\n if (!isAllowedFontUrl(opts.url)) {\n return { error: 'host not in allowlist or non-HTTPS' };\n }\n const key = rawCacheKey(opts.url);\n const mem = opts.memoryCache?.get(key);\n if (mem) return { buf: mem };\n const disk = await opts.diskCache?.get(key);\n if (disk) {\n opts.memoryCache?.set(key, disk);\n return { buf: disk };\n }\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), opts.fetchTimeoutMs ?? 10000);\n try {\n const f = opts.fetcher ?? fetch;\n // redirect: 'manual' so the allowlist can't be bypassed via Location.\n let res = await f(opts.url, { signal: ctrl.signal, redirect: 'manual' });\n let hops = 0;\n while (res.status >= 300 && res.status < 400 && res.status !== 304) {\n const next = res.headers.get('location');\n if (!next) return { error: `${res.status} with no Location` };\n const resolved = new URL(next, opts.url).toString();\n if (!isAllowedFontUrl(resolved)) {\n return { error: `redirect to disallowed host: ${resolved}` };\n }\n if (++hops > 3) return { error: 'too many redirects' };\n res = await f(resolved, { signal: ctrl.signal, redirect: 'manual' });\n }\n if (!res.ok) return { error: `HTTP ${res.status} ${res.statusText}` };\n const ab = await res.arrayBuffer();\n const buf = Buffer.from(ab);\n // Sanity-check: reject sub-1KB or non-TTF responses up front. The\n // instancer would fail loudly on garbage, but a clear \"wrong URL\"\n // signal here shortens the debug cycle.\n if (buf.length < 1024)\n return { error: `response too small (${buf.length}B)` };\n const format = detectFontFormat(buf);\n if (format !== 'ttf' && format !== 'otf') {\n return { error: `unexpected font format: ${format}` };\n }\n opts.memoryCache?.set(key, buf);\n await opts.diskCache?.set(key, buf);\n return { buf };\n } catch (err) {\n return { error: (err as Error).message };\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport async function fetchVariableFontSource(\n opts: VariableFetchOptions\n): Promise<{ source?: ResolvedFontSource; warnings?: string[] }> {\n const key = instanceCacheKey(opts.url, opts.weight, opts.italic, opts.axes);\n const mem = opts.memoryCache?.get(key);\n if (mem) {\n return {\n source: {\n data: mem,\n weight: opts.weight,\n italic: opts.italic,\n format: detectFontFormat(mem),\n },\n warnings: [],\n };\n }\n const disk = await opts.diskCache?.get(key);\n if (disk) {\n opts.memoryCache?.set(key, disk);\n return {\n source: {\n data: disk,\n weight: opts.weight,\n italic: opts.italic,\n format: detectFontFormat(disk),\n },\n warnings: [],\n };\n }\n\n const fetched = await fetchVariableSource(opts);\n if ('error' in fetched) {\n return {\n warnings: [\n `Variable font fetch \"${opts.url}\" for \"${opts.familyLabel ?? opts.url}\" weight ${opts.weight}: ${fetched.error}; falling back to host defaults.`,\n ],\n };\n }\n const raw = fetched.buf;\n\n // Harfbuzz refuses to emit WOFF2 for subset-font's default SFNT target,\n // but we need plain SFNT anyway — Office embeds TTFs, not compressed\n // formats. Pin the weight (and any extra axes) and preserve the name\n // records that our downstream `normalizeNameTable` depends on.\n //\n // Note: italic is encoded by URL (separate italic master), not by axis pin.\n // The `ital` axis exists on some fonts but not others (Inter ships a\n // separate InterVariable-Italic.ttf instead). Callers that want to force\n // an axis pin can pass `axes: { ital: 1 }` explicitly.\n const variationAxes: Record<string, number> = {\n wght: opts.weight,\n ...(opts.axes ?? {}),\n };\n\n let instanced: Buffer;\n try {\n const subsetFont = await loadSubsetFont();\n instanced = await subsetFont(raw, bmpCharset(), {\n targetFormat: 'sfnt',\n variationAxes,\n // Keep every common name record. harfbuzz drops the ones not in\n // this list; our downstream rewrites need 1/2/4/6/16/17 intact.\n preserveNameIds: [\n 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,\n 20, 21, 22, 23, 24, 25,\n ],\n });\n } catch (err) {\n return {\n warnings: [\n `Variable font instancing for \"${opts.familyLabel ?? opts.url}\" weight ${opts.weight}: ${(err as Error).message}`,\n ],\n };\n }\n\n opts.memoryCache?.set(key, instanced);\n await opts.diskCache?.set(key, instanced);\n return {\n source: {\n data: instanced,\n weight: opts.weight,\n italic: opts.italic,\n format: detectFontFormat(instanced),\n },\n warnings: [],\n };\n}\n"],"mappings":"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","names":["readFile"]}
|
|
1
|
+
{"version":3,"sources":["../../src/fonts/sources/file-loader.ts","../../src/fonts/cache/disk-cache.ts","../../src/fonts/sources/variable-fetcher.ts","../../src/fonts/rasterize-faces.ts"],"sourcesContent":["/**\n * Load a .ttf/.otf file from disk.\n * Node-only — called from the render pipeline.\n */\n\nimport { readFile } from 'fs/promises';\nimport { isAbsolute, resolve as resolvePath } from 'path';\nimport type { ResolvedFontSource } from '../types';\nimport { detectFontFormat } from './format';\n\nexport interface FileSourceInput {\n path: string;\n weight?: number;\n italic?: boolean;\n baseDir?: string;\n}\n\n/** Read a font file and wrap as a ResolvedFontSource. */\nexport async function loadFileFontSource(\n input: FileSourceInput\n): Promise<ResolvedFontSource> {\n const fullPath = isAbsolute(input.path)\n ? input.path\n : resolvePath(input.baseDir ?? process.cwd(), input.path);\n const data = await readFile(fullPath);\n const format = detectFontFormat(data);\n if (format === 'unknown') {\n throw new Error(\n `Font file at \"${fullPath}\" is not a recognized font file (expected TTF/OTF/WOFF/WOFF2)`\n );\n }\n // No format rejection here: bytes flow to the LibreOffice preview\n // stager, which handles WOFF/WOFF2 natively via fontconfig on\n // Linux/macOS. Office output never embeds these bytes — substitute/\n // custom modes rely on recipient-side fonts.\n return {\n data,\n weight: input.weight ?? 400,\n italic: input.italic ?? false,\n format,\n };\n}\n","/**\n * On-disk cache for fetched Google Fonts TTFs.\n * Optional — only active when a cacheDir is provided. Node-only.\n */\n\nimport { createHash } from 'crypto';\nimport { mkdir, readFile, writeFile } from 'fs/promises';\nimport { join } from 'path';\n\nexport class FontDiskCache {\n private readonly dir: string;\n // In-flight promise dedupes the first-write mkdir across concurrent set()\n // calls. Without it, two simultaneous cold-cache writes could both see\n // `ensured=false`, both issue mkdir, and both flip the flag afterwards —\n // harmless today (recursive mkdir is idempotent) but the pattern is\n // right and leaves room to add per-directory locks if we ever need to.\n private ensurePromise: Promise<void> | null = null;\n\n constructor(dir: string) {\n this.dir = dir;\n }\n\n private ensureDir(): Promise<void> {\n if (!this.ensurePromise) {\n this.ensurePromise = mkdir(this.dir, { recursive: true }).then(\n () => undefined\n );\n }\n return this.ensurePromise;\n }\n\n private pathFor(key: string): string {\n const hash = createHash('sha256').update(key).digest('hex').slice(0, 24);\n return join(this.dir, `${hash}.bin`);\n }\n\n async get(key: string): Promise<Buffer | undefined> {\n try {\n return await readFile(this.pathFor(key));\n } catch {\n return undefined;\n }\n }\n\n async set(key: string, value: Buffer): Promise<void> {\n await this.ensureDir();\n await writeFile(this.pathFor(key), value);\n }\n}\n","/**\n * Variable-font instancer. Fetches a variable TTF once (disk-cached), then\n * pins its `wght` axis (plus any additional axes) to produce a clean static\n * TTF per requested weight. Uses harfbuzz via `subset-font` — pure JS + WASM,\n * no native toolchain.\n *\n * Why this exists. Google Fonts serves pre-instanced static TTFs for many\n * families, but the instancing step is lossy: Inter Thin (100) and\n * ExtraLight (200) both ship with `OS/2.usWeightClass=250` and near-\n * identical glyph outlines (xAvgCharWidth differs by 1.8%, glyf table\n * differs by 83 bytes out of 135 KB). Pinning the upstream variable TTF's\n * `wght` axis at exactly 100 vs 200 produces properly distinct instances.\n *\n * Cache strategy:\n * 1. Raw variable TTF cached at key `varsrc|<url>` — one download per URL\n * per process (+ optional disk layer).\n * 2. Instanced static TTF cached at `variable|<url>|<weight>|<italic>` —\n * avoids re-running harfbuzz for weights we've already produced.\n *\n * Full-glyph retention. subset-font's `text` parameter drives which\n * codepoints' glyphs survive. We pass every BMP codepoint so the output\n * is effectively a full-glyph static (not a subset) for any Latin /\n * Cyrillic / Greek / Vietnamese-covering family — which includes every\n * entry in our POPULAR_GOOGLE_FONTS catalog. Supplementary-plane glyphs\n * (emoji) would be dropped, but those aren't in the variable families we\n * target. `preserveNameIds` keeps the human-readable name records our\n * downstream normalization expects.\n */\n\nimport type { ResolvedFontSource } from '../types';\nimport { detectFontFormat } from './format';\nimport { isAllowedFontUrl } from './url-allowlist';\n\n// `subset-font` carries a harfbuzz WASM payload and is Node-only. Lazy-load\n// so a browser bundler that chases the generic `sources/` tree doesn't pull\n// it in. Cached across calls so the WASM heap is created once per process.\nlet subsetFontPromise: Promise<typeof import('subset-font').default> | null =\n null;\nfunction loadSubsetFont(): Promise<typeof import('subset-font').default> {\n if (!subsetFontPromise) {\n subsetFontPromise = import('subset-font').then((m) => m.default);\n }\n return subsetFontPromise;\n}\n\nexport interface VariableFetchOptions {\n url: string;\n weight: number;\n italic: boolean;\n /** Extra axis pins merged on top of the derived `wght` pin (e.g. `ital`,\n * `opsz`, `slnt`). Rare — the `weight`/`italic` pair is usually enough. */\n axes?: Record<string, number>;\n /** Family label used in error messages and diagnostics. */\n familyLabel?: string;\n fetchTimeoutMs?: number;\n fetcher?: typeof fetch;\n memoryCache?: {\n get(key: string): Buffer | undefined;\n set(key: string, value: Buffer): void;\n };\n diskCache?: {\n get(key: string): Promise<Buffer | undefined>;\n set(key: string, value: Buffer): Promise<void>;\n };\n}\n\nfunction rawCacheKey(url: string): string {\n return `varsrc|${url}`;\n}\n\nfunction instanceCacheKey(\n url: string,\n weight: number,\n italic: boolean,\n axes?: Record<string, number>\n): string {\n // Axes go into the key deterministically so different axis pins don't\n // collide. Sorted so `{a:1,b:2}` and `{b:2,a:1}` hash the same.\n const axisPart = axes\n ? '|' +\n Object.entries(axes)\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([k, v]) => `${k}=${v}`)\n .join(',')\n : '';\n return `variable|${url}|${weight}|${italic ? 'i' : 'r'}${axisPart}`;\n}\n\n/**\n * String covering every assigned BMP codepoint (0x20-0xFFFF minus surrogate\n * range). Built lazily on first use — ~127 KiB of UTF-16 memory (0xFFFF\n * codepoints × 2 bytes per UTF-16 code unit, minus the surrogate range)\n * held for the lifetime of the process, which is negligible next to the\n * WASM heap harfbuzz already carries.\n */\nlet cachedBmpCharset: string | null = null;\nfunction bmpCharset(): string {\n if (cachedBmpCharset) return cachedBmpCharset;\n let s = '';\n for (let cp = 0x20; cp <= 0xffff; cp++) {\n // Surrogate range is structurally invalid as standalone codepoints —\n // harfbuzz rejects them. Skip.\n if (cp >= 0xd800 && cp <= 0xdfff) continue;\n s += String.fromCodePoint(cp);\n }\n cachedBmpCharset = s;\n return s;\n}\n\ntype FetchResult = { buf: Buffer } | { error: string };\n\nasync function fetchVariableSource(\n opts: VariableFetchOptions\n): Promise<FetchResult> {\n if (!isAllowedFontUrl(opts.url)) {\n return { error: 'host not in allowlist or non-HTTPS' };\n }\n const key = rawCacheKey(opts.url);\n const mem = opts.memoryCache?.get(key);\n if (mem) return { buf: mem };\n const disk = await opts.diskCache?.get(key);\n if (disk) {\n opts.memoryCache?.set(key, disk);\n return { buf: disk };\n }\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), opts.fetchTimeoutMs ?? 10000);\n try {\n const f = opts.fetcher ?? fetch;\n // redirect: 'manual' so the allowlist can't be bypassed via Location.\n let res = await f(opts.url, { signal: ctrl.signal, redirect: 'manual' });\n let hops = 0;\n while (res.status >= 300 && res.status < 400 && res.status !== 304) {\n const next = res.headers.get('location');\n if (!next) return { error: `${res.status} with no Location` };\n const resolved = new URL(next, opts.url).toString();\n if (!isAllowedFontUrl(resolved)) {\n return { error: `redirect to disallowed host: ${resolved}` };\n }\n if (++hops > 3) return { error: 'too many redirects' };\n res = await f(resolved, { signal: ctrl.signal, redirect: 'manual' });\n }\n if (!res.ok) return { error: `HTTP ${res.status} ${res.statusText}` };\n const ab = await res.arrayBuffer();\n const buf = Buffer.from(ab);\n // Sanity-check: reject sub-1KB or non-TTF responses up front. The\n // instancer would fail loudly on garbage, but a clear \"wrong URL\"\n // signal here shortens the debug cycle.\n if (buf.length < 1024)\n return { error: `response too small (${buf.length}B)` };\n const format = detectFontFormat(buf);\n if (format !== 'ttf' && format !== 'otf') {\n return { error: `unexpected font format: ${format}` };\n }\n opts.memoryCache?.set(key, buf);\n await opts.diskCache?.set(key, buf);\n return { buf };\n } catch (err) {\n return { error: (err as Error).message };\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport async function fetchVariableFontSource(\n opts: VariableFetchOptions\n): Promise<{ source?: ResolvedFontSource; warnings?: string[] }> {\n const key = instanceCacheKey(opts.url, opts.weight, opts.italic, opts.axes);\n const mem = opts.memoryCache?.get(key);\n if (mem) {\n return {\n source: {\n data: mem,\n weight: opts.weight,\n italic: opts.italic,\n format: detectFontFormat(mem),\n },\n warnings: [],\n };\n }\n const disk = await opts.diskCache?.get(key);\n if (disk) {\n opts.memoryCache?.set(key, disk);\n return {\n source: {\n data: disk,\n weight: opts.weight,\n italic: opts.italic,\n format: detectFontFormat(disk),\n },\n warnings: [],\n };\n }\n\n const fetched = await fetchVariableSource(opts);\n if ('error' in fetched) {\n return {\n warnings: [\n `Variable font fetch \"${opts.url}\" for \"${opts.familyLabel ?? opts.url}\" weight ${opts.weight}: ${fetched.error}; falling back to host defaults.`,\n ],\n };\n }\n const raw = fetched.buf;\n\n // Harfbuzz refuses to emit WOFF2 for subset-font's default SFNT target,\n // but we need plain SFNT anyway — Office embeds TTFs, not compressed\n // formats. Pin the weight (and any extra axes) and preserve the name\n // records that our downstream `normalizeNameTable` depends on.\n //\n // Note: italic is encoded by URL (separate italic master), not by axis pin.\n // The `ital` axis exists on some fonts but not others (Inter ships a\n // separate InterVariable-Italic.ttf instead). Callers that want to force\n // an axis pin can pass `axes: { ital: 1 }` explicitly.\n const variationAxes: Record<string, number> = {\n wght: opts.weight,\n ...(opts.axes ?? {}),\n };\n\n let instanced: Buffer;\n try {\n const subsetFont = await loadSubsetFont();\n instanced = await subsetFont(raw, bmpCharset(), {\n targetFormat: 'sfnt',\n variationAxes,\n // Keep every common name record. harfbuzz drops the ones not in\n // this list; our downstream rewrites need 1/2/4/6/16/17 intact.\n preserveNameIds: [\n 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,\n 20, 21, 22, 23, 24, 25,\n ],\n });\n } catch (err) {\n return {\n warnings: [\n `Variable font instancing for \"${opts.familyLabel ?? opts.url}\" weight ${opts.weight}: ${(err as Error).message}`,\n ],\n };\n }\n\n opts.memoryCache?.set(key, instanced);\n await opts.diskCache?.set(key, instanced);\n return {\n source: {\n data: instanced,\n weight: opts.weight,\n italic: opts.italic,\n format: detectFontFormat(instanced),\n },\n warnings: [],\n };\n}\n","/**\n * `ResolvedFont[]` ⇄ `RasterizeFontFace[]` — the one encoder/decoder pair for\n * shipping font bytes to the pptx rasterizer.\n *\n * The docx side encodes (core-docx, from `resolveDocumentFonts`) and the\n * rasterizer side decodes (jto-cli, before handing the faces to a\n * `FontStager`). Keeping both halves here means the two cannot drift on\n * base64 handling or on the family-name convention.\n *\n * FAMILY NAMES STAY UNSYNTHESIZED. The wire carries the catalog family\n * (\"Inter\"); the stager applies `synthesizeFamilyName` +\n * `rewriteFontFamilyName` to produce the sub-family the presentation\n * actually references (\"Inter Light\"). Encoding a pre-synthesized name here\n * would make the stager apply the suffix twice.\n *\n * Buffer-dependent → Node-only. Exported from `@json-to-office/shared/fonts/node`.\n */\n\nimport type { ResolvedFont, ResolvedFontSource } from './types';\nimport type { RasterizeFontFace } from '../types/services';\nimport type { GenerationWarning } from '../types/warnings';\n\n/**\n * Formats the rasterizer's native stagers can actually register.\n *\n * All three stagers (fontconfig, macOS Core Text, Windows GDI) write every\n * staged source as a `.ttf` and register it as a raw sfnt, and they rename\n * the face through `rewriteFontFamilyName`, which returns the buffer\n * UNCHANGED for anything without an sfnt header. So a WOFF/WOFF2 (or EOT, or\n * PostScript) source is staged as bytes no font system parses, under the\n * catalog family rather than the synthesized sub-family the presentation\n * references — it renders as fallback text, silently.\n *\n * Shipping those bytes anyway costs wire size, disk writes, and a distinct\n * rasterizer cache key for a render that is identical to the fontless one.\n * An allowlist (rather than a WOFF denylist) keeps any format added to\n * `ResolvedFontSource['format']` later excluded until a stager can handle it.\n */\nconst STAGEABLE_FORMATS = new Set<ResolvedFontSource['format']>(['ttf', 'otf']);\n\n/**\n * Flatten resolved fonts into the serializable wire faces (one face per\n * source variant). Entries with no sources — safe-only fonts, which the\n * renderer resolves against system faces — carry no bytes and are skipped,\n * as are sources in a format no stager can register.\n *\n * @param warnings - sink for one warning per dropped source, shaped like every\n * other generation warning so a caller can hand in the same array it already\n * collects. Both docx entry paths do: a dropped face renders as a fallback,\n * which is precisely the silent substitution this pipeline exists to make\n * visible, so it must not be discoverable only by reading the code.\n */\nexport function toRasterizeFontFaces(\n fonts: readonly ResolvedFont[],\n warnings?: GenerationWarning[]\n): RasterizeFontFace[] {\n const faces: RasterizeFontFace[] = [];\n for (const font of fonts) {\n if (font.sources.length === 0) continue;\n for (const source of font.sources) {\n if (!STAGEABLE_FORMATS.has(source.format)) {\n warnings?.push({\n component: 'fontRegistry',\n severity: 'warning',\n context: { code: 'FONT_FORMAT_NOT_RASTERIZABLE' },\n message:\n `\"${font.family}\" weight ${source.weight}` +\n `${source.italic ? ' italic' : ''} is ${source.format}; the rasterizer's ` +\n `font stagers only register TTF/OTF, so this face is omitted and the ` +\n `visual renders with a fallback face.`,\n });\n continue;\n }\n faces.push({\n family: font.family,\n weight: source.weight,\n italic: source.italic,\n data: source.data.toString('base64'),\n format: source.format as RasterizeFontFace['format'],\n });\n }\n }\n return faces;\n}\n\n/**\n * Inverse of {@link toRasterizeFontFaces}: regroup wire faces back into\n * `ResolvedFont[]` so the existing `FontStager.stage(ResolvedFont[], …)`\n * signature needs no change. Grouping is by exact (case-sensitive) family,\n * matching how the registry keys resolved fonts.\n */\nexport function fromRasterizeFontFaces(\n faces: readonly RasterizeFontFace[]\n): ResolvedFont[] {\n const byFamily = new Map<string, ResolvedFont>();\n for (const face of faces) {\n let font = byFamily.get(face.family);\n if (!font) {\n font = { family: face.family, sources: [], warnings: [] };\n byFamily.set(face.family, font);\n }\n const source: ResolvedFontSource = {\n data: Buffer.from(face.data, 'base64'),\n weight: face.weight,\n italic: face.italic,\n format: face.format ?? 'ttf',\n };\n font.sources.push(source);\n }\n return [...byFamily.values()];\n}\n"],"mappings":"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","names":["readFile"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,148 +1,14 @@
|
|
|
1
1
|
export { C as ComponentDefinition, a as ComponentSchemaConfig, c as convertToJsonSchema, b as createComponentSchema, d as createComponentSchemaObject, e as exportSchemaToFile, f as fixSchemaReferences } from './schema-utils-CbCi6dEk.js';
|
|
2
2
|
export { AddWarningFunction, GenerationWarning } from './types/warnings.js';
|
|
3
|
+
import { F as FontRegistryEntry, c as FontRuntimeOpts, R as ResolvedFontSource, b as ResolvedFont } from './types-kcQwhOlf.js';
|
|
4
|
+
export { D as DEFAULT_VISUAL_DPI, d as FontFamilyNameSchema, e as FontRegistryDefinition, f as FontRegistryEntrySchema, g as FontRegistrySchema, h as FontSource, i as FontSourceSchema, H as HighchartsHeaders, j as HighchartsHeadersResolver, k as HighchartsServiceConfig, M as MAX_RASTERIZE_BATCH_SLIDES, l as MAX_RASTERIZE_FONTS, m as MAX_RASTERIZE_FONT_BYTES, n as MAX_VISUAL_DPI, o as MIN_VISUAL_DPI, P as PptxBatchRasterizer, p as PptxRasterizeBatchRequest, q as PptxRasterizeBatchResult, r as PptxRasterizeBatchSlide, s as PptxRasterizeBatchSlideResult, t as PptxRasterizeFailureStage, u as PptxRasterizeRequest, v as PptxRasterizeResult, w as PptxRasterizer, x as PptxServiceConfig, y as PptxServiceHeaders, z as PptxServiceHeadersResolver, a as RasterizeFontFace, S as SAFE_FONTS, A as SafeFontName, B as ServicesConfig, C as clampVisualDpi, E as isSafeFont } from './types-kcQwhOlf.js';
|
|
3
5
|
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
6
|
export { T as TransformedError, V as ValidationError, a as ValidationResult } from './types-BWFZ7OaO.js';
|
|
5
7
|
export { ComponentValidationError, ComponentValidationResult, ComponentVersion, ComponentVersionMap, CustomComponent, DuplicateComponentError, PluginValidationOptions, PluginValidationResult, RenderContext, RenderFunction, UnknownPreservedComponentError, 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';
|
|
8
8
|
export { ParsedSemver, compareSemver, isValidSemver, latestVersion, parseSemver } from './utils/semver.js';
|
|
9
9
|
import '@sinclair/typebox';
|
|
10
10
|
import '@sinclair/typebox/value';
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
* Service configuration types for external integrations (e.g. Highcharts export server)
|
|
14
|
-
*/
|
|
15
|
-
/** Default raster resolution when a `visual` does not specify one. */
|
|
16
|
-
declare const DEFAULT_VISUAL_DPI = 200;
|
|
17
|
-
/** Minimum accepted raster resolution. */
|
|
18
|
-
declare const MIN_VISUAL_DPI = 36;
|
|
19
|
-
/** Maximum accepted raster resolution (bounds bitmap size / DoS surface). */
|
|
20
|
-
declare const MAX_VISUAL_DPI = 600;
|
|
21
|
-
/** Clamp an arbitrary dpi to [MIN_VISUAL_DPI, MAX_VISUAL_DPI]; non-finite → default. */
|
|
22
|
-
declare function clampVisualDpi(dpi: unknown): number;
|
|
23
|
-
type HighchartsHeaders = Record<string, string>;
|
|
24
|
-
type HighchartsHeadersResolver = (body: unknown) => HighchartsHeaders | Promise<HighchartsHeaders>;
|
|
25
|
-
interface HighchartsServiceConfig {
|
|
26
|
-
serverUrl?: string;
|
|
27
|
-
headers?: HighchartsHeaders | HighchartsHeadersResolver;
|
|
28
|
-
}
|
|
29
|
-
type PptxServiceHeaders = Record<string, string>;
|
|
30
|
-
type PptxServiceHeadersResolver = (body: unknown) => PptxServiceHeaders | Promise<PptxServiceHeaders>;
|
|
31
|
-
/**
|
|
32
|
-
* Request handed to a pptx rasterizer: a single-slide pptx presentation
|
|
33
|
-
* component definition plus the target resolution.
|
|
34
|
-
*/
|
|
35
|
-
interface PptxRasterizeRequest {
|
|
36
|
-
/** A pptx presentation component definition ({ name: 'pptx', ... }) with one slide */
|
|
37
|
-
presentation: unknown;
|
|
38
|
-
/** Target raster resolution in dots-per-inch */
|
|
39
|
-
dpi: number;
|
|
40
|
-
/**
|
|
41
|
-
* Directory that relative asset paths inside the presentation resolve
|
|
42
|
-
* against — the originating document's own directory. Absent → the
|
|
43
|
-
* rasterizer's cwd, the legacy behavior (#142).
|
|
44
|
-
*/
|
|
45
|
-
baseDir?: string;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Result returned by a pptx rasterizer.
|
|
49
|
-
*/
|
|
50
|
-
interface PptxRasterizeResult {
|
|
51
|
-
/** Rendered PNG as a base64 data URI (data:image/png;base64,...) */
|
|
52
|
-
base64DataUri: string;
|
|
53
|
-
/** Natural pixel width of the rendered image */
|
|
54
|
-
width: number;
|
|
55
|
-
/** Natural pixel height of the rendered image */
|
|
56
|
-
height: number;
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* In-process rasterizer callback. Implementations build the .pptx from the
|
|
60
|
-
* presentation JSON and rasterize it to a PNG (e.g. via LibreOffice + poppler).
|
|
61
|
-
*/
|
|
62
|
-
type PptxRasterizer = (request: PptxRasterizeRequest) => Promise<PptxRasterizeResult>;
|
|
63
|
-
/**
|
|
64
|
-
* Maximum slides accepted in one batch request. Shared by the HTTP surface
|
|
65
|
-
* (request validation) and clients (chunk size) so the two cannot drift.
|
|
66
|
-
* Bounds per-request work and response size the same way rate limits bound
|
|
67
|
-
* request counts.
|
|
68
|
-
*/
|
|
69
|
-
declare const MAX_RASTERIZE_BATCH_SLIDES = 32;
|
|
70
|
-
/** One slide in a batch: a single-slide presentation plus its resolution. */
|
|
71
|
-
interface PptxRasterizeBatchSlide {
|
|
72
|
-
/** A pptx presentation component definition ({ name: 'pptx', ... }) with one slide */
|
|
73
|
-
presentation: unknown;
|
|
74
|
-
/** Target raster resolution in dots-per-inch (absent → service default) */
|
|
75
|
-
dpi?: number;
|
|
76
|
-
}
|
|
77
|
-
/** Request handed to a batch pptx rasterizer. */
|
|
78
|
-
interface PptxRasterizeBatchRequest {
|
|
79
|
-
/** Slides to rasterize; results come back index-aligned with this array. */
|
|
80
|
-
slides: PptxRasterizeBatchSlide[];
|
|
81
|
-
/** Base directory for relative asset paths, shared by every slide (#142). */
|
|
82
|
-
baseDir?: string;
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Pipeline stage a slide failed in. `build` failures are caused by the
|
|
86
|
-
* slide's own JSON (safe to surface verbatim to callers); `convert` and
|
|
87
|
-
* `rasterize` failures are environment/tooling errors whose raw messages may
|
|
88
|
-
* carry host paths — HTTP surfaces sanitize those.
|
|
89
|
-
*/
|
|
90
|
-
type PptxRasterizeFailureStage = 'build' | 'convert' | 'rasterize';
|
|
91
|
-
/**
|
|
92
|
-
* Per-slide outcome. A batch response is 200-with-item-errors rather than
|
|
93
|
-
* all-or-nothing: one bad visual must not discard its siblings' pixels.
|
|
94
|
-
*/
|
|
95
|
-
type PptxRasterizeBatchSlideResult = ({
|
|
96
|
-
ok: true;
|
|
97
|
-
} & PptxRasterizeResult) | {
|
|
98
|
-
ok: false;
|
|
99
|
-
error: string;
|
|
100
|
-
stage?: PptxRasterizeFailureStage;
|
|
101
|
-
};
|
|
102
|
-
/** Result returned by a batch pptx rasterizer. */
|
|
103
|
-
interface PptxRasterizeBatchResult {
|
|
104
|
-
/** Index-aligned with the request's `slides` (same length, same order). */
|
|
105
|
-
results: PptxRasterizeBatchSlideResult[];
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* In-process batch rasterizer callback. Batch-level failures (missing
|
|
109
|
-
* binaries, bad request) throw; per-slide failures land in `results`.
|
|
110
|
-
*/
|
|
111
|
-
type PptxBatchRasterizer = (request: PptxRasterizeBatchRequest) => Promise<PptxRasterizeBatchResult>;
|
|
112
|
-
/**
|
|
113
|
-
* Configuration for the pptx rasterization service backing `visual` components.
|
|
114
|
-
*
|
|
115
|
-
* Mirrors {@link HighchartsServiceConfig}: the published packages depend on this
|
|
116
|
-
* interface, never on a binary. A host injects either an in-process `render`
|
|
117
|
-
* callback or an HTTP `serverUrl`.
|
|
118
|
-
*/
|
|
119
|
-
interface PptxServiceConfig {
|
|
120
|
-
/**
|
|
121
|
-
* In-process rasterizer. Takes precedence over `serverUrl` when provided.
|
|
122
|
-
* Ideal for tests (no binaries) and single-process hosts.
|
|
123
|
-
*/
|
|
124
|
-
render?: PptxRasterizer;
|
|
125
|
-
/**
|
|
126
|
-
* In-process batch rasterizer. When provided, the docx renderer coalesces a
|
|
127
|
-
* document's visuals into batch calls (#153) instead of one `render` call
|
|
128
|
-
* per visual. Like `render`, takes precedence over `serverUrl`.
|
|
129
|
-
*/
|
|
130
|
-
renderBatch?: PptxBatchRasterizer;
|
|
131
|
-
/**
|
|
132
|
-
* HTTP rasterization service URL. The service receives
|
|
133
|
-
* `{ presentation, dpi }` and returns a {@link PptxRasterizeResult}.
|
|
134
|
-
*/
|
|
135
|
-
serverUrl?: string;
|
|
136
|
-
/** Optional headers (or async resolver) for the HTTP service. */
|
|
137
|
-
headers?: PptxServiceHeaders | PptxServiceHeadersResolver;
|
|
138
|
-
/** Default DPI applied when a `visual` does not specify one. */
|
|
139
|
-
dpi?: number;
|
|
140
|
-
}
|
|
141
|
-
interface ServicesConfig {
|
|
142
|
-
highcharts?: HighchartsServiceConfig;
|
|
143
|
-
pptx?: PptxServiceConfig;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
12
|
/**
|
|
147
13
|
* Canonical `if/then` restructuring for name-discriminated component unions.
|
|
148
14
|
*
|
|
@@ -216,6 +82,34 @@ declare const collectFontNamesFromDocx: typeof collectFontNames;
|
|
|
216
82
|
/** Scan a PPTX presentation tree for every font family name referenced. */
|
|
217
83
|
declare const collectFontNamesFromPptx: typeof collectFontNames;
|
|
218
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Read the document-scoped and theme-scoped font registries and merge them
|
|
87
|
+
* with runtime `fonts.extraEntries`.
|
|
88
|
+
*
|
|
89
|
+
* Precedence (last wins, matching registry.ts's documented resolution rules
|
|
90
|
+
* and FontRuntimeOpts.extraEntries's "merged over the document's
|
|
91
|
+
* fontRegistry"):
|
|
92
|
+
*
|
|
93
|
+
* theme.fontRegistry < document.props.fontRegistry < fonts.extraEntries
|
|
94
|
+
*
|
|
95
|
+
* Merging happens here rather than inside FontRegistry because
|
|
96
|
+
* `validateFontReferences` needs the same merged list, and two merge sites
|
|
97
|
+
* would eventually disagree — which would show up as a font that validates
|
|
98
|
+
* but never renders, or vice versa.
|
|
99
|
+
*/
|
|
100
|
+
|
|
101
|
+
/** `document.props.fontRegistry`, defensively (props may be absent/null). */
|
|
102
|
+
declare function documentFontRegistry(document: unknown): FontRegistryEntry[];
|
|
103
|
+
/** `theme.fontRegistry`, defensively. */
|
|
104
|
+
declare function themeFontRegistry(theme: unknown): FontRegistryEntry[];
|
|
105
|
+
/**
|
|
106
|
+
* Merge entry groups in precedence order — later groups win on a collision of
|
|
107
|
+
* `family` OR `id`, case-insensitively, which are the same two keys
|
|
108
|
+
* `FontRegistry.addEntry` indexes on. Returns a flat list safe to hand to both
|
|
109
|
+
* `validateFontReferences` and `new FontRegistry({ opts: { extraEntries } })`.
|
|
110
|
+
*/
|
|
111
|
+
declare function mergeFontRegistries(...groups: (FontRegistryEntry[] | undefined)[]): FontRegistryEntry[];
|
|
112
|
+
|
|
219
113
|
/**
|
|
220
114
|
* Validate that every font name referenced in a document is either
|
|
221
115
|
* in SAFE_FONTS or present in the document's fontRegistry / runtime overrides.
|
|
@@ -329,6 +223,21 @@ declare function synthesizeFamilyName(family: string, weight: number | undefined
|
|
|
329
223
|
*/
|
|
330
224
|
declare function rewriteFontFamilyName(input: Buffer, newFamily: string): Buffer;
|
|
331
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Hostname allowlist for font fetchers.
|
|
228
|
+
*
|
|
229
|
+
* `url-fetcher` and `variable-fetcher` can be handed arbitrary URLs via
|
|
230
|
+
* `FontRegistryEntry.sources`, which may originate from document JSON. Without
|
|
231
|
+
* a guard, a malicious doc could point fetchers at internal hosts (SSRF), the
|
|
232
|
+
* filesystem (`file://`), or the IMDS endpoint. Limit downloads to the hosts
|
|
233
|
+
* our catalog + UPSTREAM_OVERRIDES actually target.
|
|
234
|
+
*
|
|
235
|
+
* Keep the list small and HTTPS-only. Expansions should be deliberate code
|
|
236
|
+
* reviews, not config-driven — the cost of a new domain is the code change.
|
|
237
|
+
*/
|
|
238
|
+
declare const FONT_URL_ALLOWLIST: readonly string[];
|
|
239
|
+
declare function isAllowedFontUrl(url: string): boolean;
|
|
240
|
+
|
|
332
241
|
/**
|
|
333
242
|
* FontRegistry — merges catalog + document registry + runtime entries
|
|
334
243
|
* and materializes referenced fonts into ResolvedFont records.
|
|
@@ -433,7 +342,7 @@ declare function detectFontFormat(buf: Buffer): ResolvedFontSource['format'];
|
|
|
433
342
|
* Curated list of popular Google Fonts for picker autocomplete.
|
|
434
343
|
*
|
|
435
344
|
* Not exhaustive — the full Google Fonts library has ~1500 families.
|
|
436
|
-
* This is ~
|
|
345
|
+
* This is ~37 names known to cover most real-world use cases.
|
|
437
346
|
*/
|
|
438
347
|
interface PopularGoogleFont {
|
|
439
348
|
family: string;
|
|
@@ -578,6 +487,10 @@ interface ApplyFontSubstitutionResult<T> {
|
|
|
578
487
|
* Returns a new tree (structural clone) plus the list of `(from, to)`
|
|
579
488
|
* swaps made, deduped by source name.
|
|
580
489
|
*
|
|
490
|
+
* One deliberate exception to the clone: `fontRegistry` subtrees are carried
|
|
491
|
+
* through by reference, since they declare fonts rather than reference them
|
|
492
|
+
* and nothing downstream mutates them.
|
|
493
|
+
*
|
|
581
494
|
* Families already in SAFE_FONTS are never rewritten (even if a mapping
|
|
582
495
|
* entry targets them as a key — safe fonts don't need substitution).
|
|
583
496
|
* Families with no mapping entry are left untouched — callers should
|
|
@@ -694,4 +607,4 @@ declare const DEFAULT_CHART_THEME_COLORS: string[];
|
|
|
694
607
|
*/
|
|
695
608
|
declare function mergeWithDefaults<T>(userConfig: T, themeDefaults: Partial<T>): T;
|
|
696
609
|
|
|
697
|
-
export { DEFAULT_CHART_THEME_COLORS,
|
|
610
|
+
export { DEFAULT_CHART_THEME_COLORS, FONT_URL_ALLOWLIST, type FontIssueCode, FontRegistry, FontRegistryEntry, type FontRegistryInput, type FontResolutionIssue, FontRuntimeOpts, type FontSubstitution, type FontValidationInput, type FontValidationResult, POPULAR_GOOGLE_FONTS, type PopularGoogleFont, ResolvedFont, ResolvedFontSource, type SynthesizedFamily, UPSTREAM_OVERRIDES, type UpstreamOverride, type UpstreamVariant, WEIGHT_LABELS, applyExportMode, applyFontSubstitution, buildDefaultSubstitutionMap, collectFontNamesFromDocx, collectFontNamesFromPptx, defaultSubstituteFor, detectFontFormat, documentFontRegistry, fetchGoogleFontSources, getUpstreamOverride, isAllowedFontUrl, mergeFontRegistries, mergeWithDefaults, restructureNameDiscriminatedUnions, rewriteFontFamilyName, scopedThemeName, synthesizeFamilyName, themeFontRegistry, unionBranches, validateFontReferences };
|