@json-to-office/shared 2.5.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-6TNT7DGC.js → chunk-LFHU4EOV.js} +5 -2
- package/dist/chunk-LFHU4EOV.js.map +1 -0
- package/dist/fonts/node.d.ts +9 -1
- package/dist/fonts/node.js +23 -0
- package/dist/fonts/node.js.map +1 -1
- package/dist/index.d.ts +411 -6
- package/dist/index.js +2021 -25
- package/dist/index.js.map +1 -1
- package/dist/{schema-utils-C5Qdzsgy.d.ts → schema-utils-B2i75lAT.d.ts} +7 -0
- package/dist/schemas/schema-utils.d.ts +1 -1
- package/dist/schemas/schema-utils.js +1 -1
- package/dist/schemas/slide-content.d.ts +16 -0
- package/dist/schemas/slide-content.js +22 -0
- package/dist/schemas/slide-content.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-6TNT7DGC.js.map +0 -1
|
@@ -59,6 +59,7 @@ function restructureNameDiscriminatedUnions(schema) {
|
|
|
59
59
|
obj.required = ["name"];
|
|
60
60
|
obj.properties = {
|
|
61
61
|
name: {
|
|
62
|
+
description: "Component or registered plugin to render.",
|
|
62
63
|
anyOf: [...groups.entries()].map(
|
|
63
64
|
([name, group]) => nameEntry(name, group)
|
|
64
65
|
)
|
|
@@ -168,7 +169,8 @@ function convertToJsonSchema(schema, options = {}) {
|
|
|
168
169
|
$id,
|
|
169
170
|
title,
|
|
170
171
|
description,
|
|
171
|
-
definitions = {}
|
|
172
|
+
definitions = {},
|
|
173
|
+
enrich
|
|
172
174
|
} = options;
|
|
173
175
|
const schemaJson = JSON.parse(JSON.stringify(schema));
|
|
174
176
|
if (schemaJson.$id && typeof schemaJson.$id === "string" && /^T\d+$/.test(schemaJson.$id)) {
|
|
@@ -214,6 +216,7 @@ function convertToJsonSchema(schema, options = {}) {
|
|
|
214
216
|
jsonSchema.definitions = extractedDefinitions;
|
|
215
217
|
}
|
|
216
218
|
fixSchemaReferences(jsonSchema);
|
|
219
|
+
enrich?.(jsonSchema);
|
|
217
220
|
restructureNameDiscriminatedUnions(jsonSchema);
|
|
218
221
|
return jsonSchema;
|
|
219
222
|
}
|
|
@@ -294,4 +297,4 @@ export {
|
|
|
294
297
|
exportSchemaToFile,
|
|
295
298
|
createComponentSchemaObject
|
|
296
299
|
};
|
|
297
|
-
//# sourceMappingURL=chunk-
|
|
300
|
+
//# sourceMappingURL=chunk-LFHU4EOV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/schemas/schema-utils.ts","../src/schemas/discriminated-unions.ts"],"sourcesContent":["import { Type, TSchema } from '@sinclair/typebox';\nimport { restructureNameDiscriminatedUnions } from './discriminated-unions';\nimport type { ComponentDefinition } from '../types/components';\n\nexport interface ComponentSchemaConfig {\n schema: TSchema;\n title: string;\n description: string;\n requiresName?: boolean;\n enhanceForRichContent?: boolean;\n}\n\nfunction replaceRefs(\n obj: Record<string, unknown>,\n target: string,\n replacement: string\n): void {\n if (typeof obj !== 'object' || obj === null) return;\n if (Array.isArray(obj)) {\n obj.forEach((item) => {\n if (typeof item === 'object' && item !== null) {\n replaceRefs(item as Record<string, unknown>, target, replacement);\n }\n });\n return;\n }\n if (obj.$ref === target) {\n obj.$ref = replacement;\n }\n for (const value of Object.values(obj)) {\n if (typeof value === 'object' && value !== null) {\n replaceRefs(value as Record<string, unknown>, target, replacement);\n }\n }\n}\n\n/**\n * Resolve the bare `$ref` values TypeBox leaves behind into JSON Pointers.\n *\n * `rootDefinitionName` is only the fallback for a reference that names nothing\n * hoisted. A schema may carry several recursive definitions — the DOCX\n * document schema carries one per renderer — so a bare reference that matches\n * an actual definition resolves to *that* one, which is what keeps the two\n * renderer views from collapsing into whichever was walked last.\n */\nexport function fixSchemaReferences(\n schema: Record<string, unknown>,\n rootDefinitionName = 'ComponentDefinition'\n): void {\n const definitionNames = new Set(\n Object.keys(\n (schema.definitions as Record<string, unknown> | undefined) ?? {}\n )\n );\n // Undefined when nothing by either name was hoisted. Substituting the root\n // name regardless is how a reference to a definition that does not exist got\n // written, and Ajv refuses to compile a schema containing one — so an\n // unresolved reference is dropped rather than invented.\n const definitionRef = (name: string): string | undefined => {\n const target = definitionNames.has(name) ? name : rootDefinitionName;\n return definitionNames.has(target) ? `#/definitions/${target}` : undefined;\n };\n const isBareDefinitionRef = (value: unknown): value is string =>\n typeof value === 'string' &&\n (/^T\\d+$/.test(value) ||\n value === rootDefinitionName ||\n definitionNames.has(value));\n\n function traverse(obj: Record<string, unknown>, path = ''): void {\n if (typeof obj !== 'object' || obj === null) return;\n\n for (const [key, value] of Object.entries(obj)) {\n const currentPath = path ? `${path}.${key}` : key;\n\n if (value && typeof value === 'object') {\n const schemaValue = value as Record<string, unknown>;\n\n // Only when there is something to point at. A schema that holds\n // components without embedding their definition (the theme schema\n // holds `componentDefaults` but cannot carry a renderer's component\n // union) keeps its untyped item: a `$ref` to a definition that was\n // never hoisted is unresolvable, and Ajv refuses to compile the whole\n // schema over it.\n if (\n schemaValue.type === 'array' &&\n schemaValue.items &&\n Object.keys(schemaValue.items).length === 0 &&\n definitionNames.has(rootDefinitionName)\n ) {\n schemaValue.items = {\n $ref: `#/definitions/${rootDefinitionName}`,\n };\n }\n\n if (\n schemaValue.type === 'array' &&\n schemaValue.items &&\n typeof schemaValue.items === 'object' &&\n '$ref' in schemaValue.items &&\n isBareDefinitionRef(\n (schemaValue.items as Record<string, unknown>).$ref\n )\n ) {\n const name = (schemaValue.items as Record<string, unknown>)\n .$ref as string;\n const ref = definitionRef(name);\n // Untyped rather than dangling when the target was never hoisted.\n schemaValue.items = ref ? { $ref: ref } : {};\n }\n\n if (isBareDefinitionRef(schemaValue.$ref)) {\n const ref = definitionRef(schemaValue.$ref as string);\n if (ref) schemaValue.$ref = ref;\n else delete schemaValue.$ref;\n }\n\n if (\n key === '$id' &&\n isBareDefinitionRef(value) &&\n currentPath !== `definitions.${value}.$id`\n ) {\n delete obj[key];\n continue;\n }\n\n traverse(value as Record<string, unknown>, currentPath);\n }\n }\n }\n\n traverse(schema);\n}\n\nexport function convertToJsonSchema(\n schema: TSchema,\n options: {\n $schema?: string;\n $id?: string;\n title?: string;\n description?: string;\n definitions?: Record<string, unknown>;\n /**\n * Format-specific enrichment applied once references resolve and before\n * the name-discriminated unions are restructured — the point at which a\n * component union is still a flat `anyOf` a derivation can filter and\n * copy. The PPTX block-body authoring schemas are added here.\n */\n enrich?: (schema: Record<string, unknown>) => void;\n } = {}\n): Record<string, unknown> {\n const {\n $schema = 'http://json-schema.org/draft-07/schema#',\n $id,\n title,\n description,\n definitions = {},\n enrich,\n } = options;\n\n const schemaJson = JSON.parse(JSON.stringify(schema));\n\n if (\n schemaJson.$id &&\n typeof schemaJson.$id === 'string' &&\n /^T\\d+$/.test(schemaJson.$id)\n ) {\n const recursiveId = schemaJson.$id;\n delete schemaJson.$id;\n replaceRefs(schemaJson, recursiveId, '#');\n }\n\n const extractedDefinitions: Record<string, unknown> = { ...definitions };\n\n function extractRecursiveSchemas(\n obj: Record<string, unknown>,\n path = ''\n ): void {\n if (typeof obj !== 'object' || obj === null) return;\n\n for (const [key, value] of Object.entries(obj)) {\n if (value && typeof value === 'object') {\n const schemaValue = value as Record<string, unknown>;\n\n if (schemaValue.$id && typeof schemaValue.$id === 'string') {\n const definitionName = schemaValue.$id;\n\n if (path !== `definitions.${definitionName}`) {\n const { $id: _id, ...schemaWithoutId } = schemaValue; // eslint-disable-line @typescript-eslint/no-unused-vars\n extractedDefinitions[definitionName] = schemaWithoutId;\n obj[key] = { $ref: `#/definitions/${definitionName}` };\n extractRecursiveSchemas(\n schemaWithoutId,\n `definitions.${definitionName}`\n );\n continue;\n }\n }\n\n extractRecursiveSchemas(\n value as Record<string, unknown>,\n path ? `${path}.${key}` : key\n );\n }\n }\n }\n\n extractRecursiveSchemas(schemaJson);\n\n const jsonSchema: Record<string, unknown> = { $schema };\n\n if ($id) jsonSchema.$id = $id;\n\n Object.assign(jsonSchema, schemaJson);\n\n jsonSchema.$schema = $schema;\n if ($id) jsonSchema.$id = $id;\n if (title !== undefined) jsonSchema.title = title;\n if (description !== undefined) jsonSchema.description = description;\n\n if (Object.keys(extractedDefinitions).length > 0) {\n jsonSchema.definitions = extractedDefinitions;\n }\n\n fixSchemaReferences(jsonSchema);\n enrich?.(jsonSchema);\n restructureNameDiscriminatedUnions(jsonSchema);\n\n return jsonSchema;\n}\n\nexport function createComponentSchema(\n name: string,\n config: ComponentSchemaConfig,\n containerNames: string[],\n componentDefinitionSchema?: TSchema,\n // The definition the recursive children reference. Callers whose component\n // union is renderer-specific pass that renderer's name so the embedded\n // definition and the `$ref` pointing at it agree.\n rootDefinitionName = 'ComponentDefinition'\n): Record<string, unknown> {\n const componentStructure: Record<string, unknown> = {\n $schema: 'http://json-schema.org/draft-07/schema#',\n $id: `${name}.schema.json`,\n title: config.title,\n description: config.description,\n type: 'object',\n required: ['name', 'props'],\n properties: {\n name: {\n type: 'string',\n const: name,\n description: `Component name identifier (must be \"${name}\")`,\n },\n id: {\n type: 'string',\n description: 'Optional unique identifier for the component',\n },\n props: JSON.parse(JSON.stringify(config.schema)),\n },\n };\n\n if (containerNames.includes(name)) {\n (componentStructure.properties as Record<string, unknown>).children = {\n type: 'array',\n description: 'Children within this container',\n items: {\n $ref: `#/definitions/${rootDefinitionName}`,\n },\n };\n\n if (componentDefinitionSchema) {\n componentStructure.definitions = {\n [rootDefinitionName]: JSON.parse(\n JSON.stringify(componentDefinitionSchema)\n ),\n };\n }\n }\n\n fixSchemaReferences(componentStructure, rootDefinitionName);\n componentStructure.additionalProperties = false;\n\n return componentStructure;\n}\n\nexport async function exportSchemaToFile(\n schema: Record<string, unknown>,\n outputPath: string,\n options: { prettyPrint?: boolean } = {}\n): Promise<void> {\n const { prettyPrint = true } = options;\n const jsonSchema = prettyPrint\n ? JSON.stringify(schema, null, 2)\n : JSON.stringify(schema);\n const fs = await import('fs/promises');\n await fs.writeFile(outputPath, jsonSchema, 'utf-8');\n}\n\n/**\n * Create a TypeBox schema object for any component definition.\n * Works for both docx and pptx components.\n */\nexport function createComponentSchemaObject(\n component: ComponentDefinition,\n recursiveRef?: TSchema\n): TSchema {\n const schema: Record<string, TSchema> = {\n name: Type.Literal(component.name),\n id: Type.Optional(Type.String()),\n enabled: Type.Optional(\n Type.Boolean({\n default: true,\n description:\n 'When false, this component is filtered out and not rendered. Defaults to true.',\n })\n ),\n };\n\n if (component.special?.hasSchemaField) {\n schema.$schema = Type.Optional(Type.String({ format: 'uri' }));\n }\n\n schema.props = component.propsSchema;\n\n if (component.hasChildren && recursiveRef) {\n schema.children = Type.Optional(Type.Array(recursiveRef));\n }\n\n return Type.Object(schema, { additionalProperties: false });\n}\n","/**\n * Canonical `if/then` restructuring for name-discriminated component unions.\n *\n * The generators export component unions as a flat `anyOf`. Schema-driven\n * editors (Monaco, VS Code — vscode-json-languageservice) resolve a partially\n * typed node against an `anyOf` by picking the single best-matching branch:\n * while typing `{ \"name\": | }`, every branch requiring `props` fails\n * validation, so its name const never reached autocomplete, and diagnostics\n * reported one arbitrary branch's complaints (\"Value must be \\\"heading\\\"\",\n * \"Missing property \\\"props\\\"\") instead of the real problem.\n *\n * This transform rewrites each such union — at JSON-Schema export time only,\n * the runtime TypeBox validators are untouched — into the standard\n * discriminated-union dispatch:\n *\n * {\n * type: \"object\",\n * required: [\"name\"],\n * properties: { name: { anyOf: [{ const, description }, …] } },\n * allOf: [\n * { if: { properties: { name: { const } }, required: [\"name\"] },\n * then: <branch> },\n * …\n * ]\n * }\n *\n * The accepted set of documents is exactly the same — `properties.name` is\n * the enum the branches already imply, and each `then` is the original\n * branch — but editors now behave deterministically:\n * - completing `name` offers every component, with its description\n * - an empty object reports only `Missing property \"name\"`\n * - a wrong name reports only `Value is not accepted. Valid values: …`\n * - a valid name activates exactly its branch for keys, props and errors\n *\n * Standard draft-07 keywords only, so ajv and every schema-aware editor\n * agree. Versioned plugin branches share a name; they stay grouped in a\n * small `anyOf` inside their `then`, containing best-match ambiguity to the\n * component's own versions.\n */\n\ninterface SchemaNode {\n [key: string]: unknown;\n}\n\ninterface NameConstEntry {\n const: string;\n type: 'string';\n description?: string;\n}\n\n/** A union branch shaped `{ properties: { name: { const: \"...\" } } }`. */\nfunction branchNameConst(branch: unknown): string | undefined {\n if (typeof branch !== 'object' || branch === null || Array.isArray(branch))\n return undefined;\n const name = ((branch as SchemaNode).properties as SchemaNode | undefined)\n ?.name as SchemaNode | undefined;\n return typeof name?.const === 'string' ? name.const : undefined;\n}\n\n/** True when the branch also discriminates on a `version` const (plugins). */\nfunction isVersionedBranch(branch: SchemaNode): boolean {\n const version = (branch.properties as SchemaNode | undefined)?.version as\n | SchemaNode\n | undefined;\n return typeof version?.const === 'string';\n}\n\nfunction branchRequiresName(branch: SchemaNode): boolean {\n return Array.isArray(branch.required) && branch.required.includes('name');\n}\n\n/** Group branches by their name const, preserving union order. */\nfunction groupByName(branches: SchemaNode[]): Map<string, SchemaNode[]> {\n const groups = new Map<string, SchemaNode[]>();\n for (const branch of branches) {\n const name = branchNameConst(branch)!;\n const group = groups.get(name);\n if (group) group.push(branch);\n else groups.set(name, [branch]);\n }\n return groups;\n}\n\nfunction nameEntry(name: string, group: SchemaNode[]): NameConstEntry {\n // Versioned plugins repeat the same name across version branches; the\n // un-versioned fallback carries the cleanest component description.\n const source =\n group.find(\n (b) => !isVersionedBranch(b) && typeof b.description === 'string'\n ) ?? group.find((b) => typeof b.description === 'string');\n return {\n const: name,\n type: 'string',\n ...(source ? { description: source.description as string } : {}),\n };\n}\n\n/**\n * Walk a JSON Schema and restructure every `anyOf` union whose branches are\n * all name-discriminated objects into the `if/then` dispatch shape above.\n *\n * Mutates in place. Conservative by design — a union is only restructured\n * when the rewrite is provably equivalent:\n * - every branch is an object with a `name` const that lists `name` as\n * required (unions containing `$ref` or free-form branches are left alone;\n * a `$ref`'s target union is restructured where it is defined)\n * - the node declares no `properties`, `allOf`, `if`, `required`,\n * `additionalProperties` or `type` of its own that the rewrite would have\n * to merge with\n * - at least two distinct names; single-name unions (a versioned plugin's\n * variants) validate and complete fine as a plain anyOf\n */\nexport function restructureNameDiscriminatedUnions(schema: unknown): void {\n const visited = new WeakSet<object>();\n\n function walk(node: unknown): void {\n if (typeof node !== 'object' || node === null) return;\n if (visited.has(node)) return;\n visited.add(node);\n\n if (Array.isArray(node)) {\n node.forEach(walk);\n return;\n }\n\n const obj = node as SchemaNode;\n const anyOf = obj.anyOf;\n const isCandidate =\n Array.isArray(anyOf) &&\n anyOf.length >= 2 &&\n anyOf.every(\n (b) => branchNameConst(b) !== undefined && branchRequiresName(b)\n ) &&\n obj.properties === undefined &&\n obj.allOf === undefined &&\n obj.if === undefined &&\n obj.required === undefined &&\n // A sibling `additionalProperties` evaluates against the node's own\n // (absent) `properties`; declaring `name` here would change what it\n // rejects, so such unions are left alone.\n obj.additionalProperties === undefined &&\n (obj.type === undefined || obj.type === 'object');\n // Dispatch needs at least two distinct names. Same-name groups (a\n // versioned plugin's variants) stay a plain anyOf — restructuring them\n // would recurse forever on the group it just created.\n const groups = isCandidate ? groupByName(anyOf as SchemaNode[]) : undefined;\n if (groups && groups.size >= 2) {\n obj.type = 'object';\n obj.required = ['name'];\n obj.properties = {\n name: {\n description: 'Component or registered plugin to render.',\n anyOf: [...groups.entries()].map(([name, group]) =>\n nameEntry(name, group)\n ),\n },\n };\n obj.allOf = [...groups.entries()].map(([name, group]) => ({\n if: {\n properties: { name: { const: name } },\n required: ['name'],\n },\n then: group.length === 1 ? group[0] : { anyOf: group },\n }));\n delete obj.anyOf;\n }\n\n for (const value of Object.values(obj)) walk(value);\n }\n\n walk(schema);\n}\n\n/**\n * Iterate the component branches of an exported union, whichever shape it is\n * in — the flat `anyOf` the generators emit, or the `if/then` dispatch this\n * module rewrites it into. For consumers that post-process branch objects\n * (description enhancement, theme-name injection, …).\n */\nexport function unionBranches(schema: unknown): SchemaNode[] {\n if (typeof schema !== 'object' || schema === null) return [];\n const obj = schema as SchemaNode;\n if (Array.isArray(obj.anyOf)) {\n return obj.anyOf.filter(\n (b): b is SchemaNode => typeof b === 'object' && b !== null\n );\n }\n if (Array.isArray(obj.allOf)) {\n return obj.allOf.flatMap((entry): SchemaNode[] => {\n const then = (entry as SchemaNode | null)?.then;\n if (typeof then !== 'object' || then === null) return [];\n const inner = (then as SchemaNode).anyOf;\n return Array.isArray(inner)\n ? inner.filter(\n (b): b is SchemaNode => typeof b === 'object' && b !== null\n )\n : [then as SchemaNode];\n });\n }\n return [];\n}\n"],"mappings":"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","names":[]}
|
package/dist/fonts/node.d.ts
CHANGED
|
@@ -123,5 +123,13 @@ declare function toRasterizeFontFaces(fonts: readonly ResolvedFont[], warnings?:
|
|
|
123
123
|
* matching how the registry keys resolved fonts.
|
|
124
124
|
*/
|
|
125
125
|
declare function fromRasterizeFontFaces(faces: readonly RasterizeFontFace[]): ResolvedFont[];
|
|
126
|
+
/**
|
|
127
|
+
* Flatten resolved fonts into the faces a Highcharts export server can be
|
|
128
|
+
* handed as inline `@font-face` rules. Wider than `toRasterizeFontFaces`:
|
|
129
|
+
* the chart is drawn by Chromium, which reads WOFF and WOFF2 as readily as
|
|
130
|
+
* an sfnt, so only formats no browser loads are dropped. Safe-only fonts
|
|
131
|
+
* carry no bytes and are skipped; the server's own host faces cover them.
|
|
132
|
+
*/
|
|
133
|
+
declare function toChartFontFaces(fonts: readonly ResolvedFont[]): RasterizeFontFace[];
|
|
126
134
|
|
|
127
|
-
export { FontDiskCache, type VariableFetchOptions, fetchVariableFontSource, fromRasterizeFontFaces, loadFileFontSource, toRasterizeFontFaces };
|
|
135
|
+
export { FontDiskCache, type VariableFetchOptions, fetchVariableFontSource, fromRasterizeFontFaces, loadFileFontSource, toChartFontFaces, toRasterizeFontFaces };
|
package/dist/fonts/node.js
CHANGED
|
@@ -279,11 +279,34 @@ function fromRasterizeFontFaces(faces) {
|
|
|
279
279
|
}
|
|
280
280
|
return [...byFamily.values()];
|
|
281
281
|
}
|
|
282
|
+
var BROWSER_FORMATS = /* @__PURE__ */ new Set([
|
|
283
|
+
"ttf",
|
|
284
|
+
"otf",
|
|
285
|
+
"woff",
|
|
286
|
+
"woff2"
|
|
287
|
+
]);
|
|
288
|
+
function toChartFontFaces(fonts) {
|
|
289
|
+
const faces = [];
|
|
290
|
+
for (const font of fonts) {
|
|
291
|
+
for (const source of font.sources) {
|
|
292
|
+
if (!BROWSER_FORMATS.has(source.format)) continue;
|
|
293
|
+
faces.push({
|
|
294
|
+
family: font.family,
|
|
295
|
+
weight: source.weight,
|
|
296
|
+
italic: source.italic,
|
|
297
|
+
data: source.data.toString("base64"),
|
|
298
|
+
format: source.format
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return faces;
|
|
303
|
+
}
|
|
282
304
|
export {
|
|
283
305
|
FontDiskCache,
|
|
284
306
|
fetchVariableFontSource,
|
|
285
307
|
fromRasterizeFontFaces,
|
|
286
308
|
loadFileFontSource,
|
|
309
|
+
toChartFontFaces,
|
|
287
310
|
toRasterizeFontFaces
|
|
288
311
|
};
|
|
289
312
|
//# 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","../../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 font once (disk-cached; TTF,\n * OTF, or WOFF/WOFF2 — fontverter converts compressed containers before\n * instancing), then pins its `wght` axis (plus any additional axes) to\n * produce a clean static TTF per requested weight. Uses harfbuzz via\n * `subset-font` — pure JS + WASM, 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 font cached at key `varsrc|<url>` — one download per URL\n * per process (+ optional disk layer).\n * 2. Instanced static TTF cached at `variable2|<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 { rewriteFontSubfamilyNames } from './ttf-name';\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 // `variable2`: v2 stamps standard subfamily names into the instanced\n // output — bumped so persistent disk caches drop pre-stamp instances.\n return `variable2|${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 // WOFF/WOFF2 sources are fine: subset-font funnels every input through\n // fontverter (sfnt/woff/woff2 → truetype) before harfbuzz sees it, and\n // the instanced output is always plain sfnt. Needed in practice —\n // rsms/inter publishes its italic variable master only as woff2.\n if (\n format !== 'ttf' &&\n format !== 'otf' &&\n format !== 'woff' &&\n format !== 'woff2'\n ) {\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 name rewrites (`rewriteFontFamilyName`,\n // `rewriteFontSubfamilyNames`) depend 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 // harfbuzz preserves the source's name records verbatim, so the instanced\n // static still carries the variable font's default-instance subfamily\n // (typically \"Regular\") in nameID 2/17 — which would trip\n // validateFontMetadata's FONT_METADATA_DEFECT warning for every non-\n // Regular weight. Stamp the standard subfamily for the pinned pair.\n instanced = rewriteFontSubfamilyNames(instanced, opts.weight, opts.italic);\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"]}
|
|
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 font once (disk-cached; TTF,\n * OTF, or WOFF/WOFF2 — fontverter converts compressed containers before\n * instancing), then pins its `wght` axis (plus any additional axes) to\n * produce a clean static TTF per requested weight. Uses harfbuzz via\n * `subset-font` — pure JS + WASM, 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 font cached at key `varsrc|<url>` — one download per URL\n * per process (+ optional disk layer).\n * 2. Instanced static TTF cached at `variable2|<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 { rewriteFontSubfamilyNames } from './ttf-name';\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 // `variable2`: v2 stamps standard subfamily names into the instanced\n // output — bumped so persistent disk caches drop pre-stamp instances.\n return `variable2|${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 // WOFF/WOFF2 sources are fine: subset-font funnels every input through\n // fontverter (sfnt/woff/woff2 → truetype) before harfbuzz sees it, and\n // the instanced output is always plain sfnt. Needed in practice —\n // rsms/inter publishes its italic variable master only as woff2.\n if (\n format !== 'ttf' &&\n format !== 'otf' &&\n format !== 'woff' &&\n format !== 'woff2'\n ) {\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 name rewrites (`rewriteFontFamilyName`,\n // `rewriteFontSubfamilyNames`) depend 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 // harfbuzz preserves the source's name records verbatim, so the instanced\n // static still carries the variable font's default-instance subfamily\n // (typically \"Regular\") in nameID 2/17 — which would trip\n // validateFontMetadata's FONT_METADATA_DEFECT warning for every non-\n // Regular weight. Stamp the standard subfamily for the pinned pair.\n instanced = rewriteFontSubfamilyNames(instanced, opts.weight, opts.italic);\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\n/** Formats a browser's `@font-face` can load; a chart is drawn by one. */\nconst BROWSER_FORMATS = new Set<ResolvedFontSource['format']>([\n 'ttf',\n 'otf',\n 'woff',\n 'woff2',\n]);\n\n/**\n * Flatten resolved fonts into the faces a Highcharts export server can be\n * handed as inline `@font-face` rules. Wider than `toRasterizeFontFaces`:\n * the chart is drawn by Chromium, which reads WOFF and WOFF2 as readily as\n * an sfnt, so only formats no browser loads are dropped. Safe-only fonts\n * carry no bytes and are skipped; the server's own host faces cover them.\n */\nexport function toChartFontFaces(\n fonts: readonly ResolvedFont[]\n): RasterizeFontFace[] {\n const faces: RasterizeFontFace[] = [];\n for (const font of fonts) {\n for (const source of font.sources) {\n if (!BROWSER_FORMATS.has(source.format)) continue;\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"],"mappings":";;;;;;;AAKA,SAAS,gBAAgB;AACzB,SAAS,YAAY,WAAW,mBAAmB;AAYnD,eAAsB,mBACpB,OAC6B;AAC7B,QAAM,WAAW,WAAW,MAAM,IAAI,IAClC,MAAM,OACN,YAAY,MAAM,WAAW,QAAQ,IAAI,GAAG,MAAM,IAAI;AAC1D,QAAM,OAAO,MAAM,SAAS,QAAQ;AACpC,QAAM,SAAS,iBAAiB,IAAI;AACpC,MAAI,WAAW,WAAW;AACxB,UAAM,IAAI;AAAA,MACR,iBAAiB,QAAQ;AAAA,IAC3B;AAAA,EACF;AAKA,SAAO;AAAA,IACL;AAAA,IACA,QAAQ,MAAM,UAAU;AAAA,IACxB,QAAQ,MAAM,UAAU;AAAA,IACxB;AAAA,EACF;AACF;;;ACpCA,SAAS,kBAAkB;AAC3B,SAAS,OAAO,YAAAA,WAAU,iBAAiB;AAC3C,SAAS,YAAY;AAEd,IAAM,gBAAN,MAAoB;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMT,gBAAsC;AAAA,EAE9C,YAAY,KAAa;AACvB,SAAK,MAAM;AAAA,EACb;AAAA,EAEQ,YAA2B;AACjC,QAAI,CAAC,KAAK,eAAe;AACvB,WAAK,gBAAgB,MAAM,KAAK,KAAK,EAAE,WAAW,KAAK,CAAC,EAAE;AAAA,QACxD,MAAM;AAAA,MACR;AAAA,IACF;AACA,WAAO,KAAK;AAAA,EACd;AAAA,EAEQ,QAAQ,KAAqB;AACnC,UAAM,OAAO,WAAW,QAAQ,EAAE,OAAO,GAAG,EAAE,OAAO,KAAK,EAAE,MAAM,GAAG,EAAE;AACvE,WAAO,KAAK,KAAK,KAAK,GAAG,IAAI,MAAM;AAAA,EACrC;AAAA,EAEA,MAAM,IAAI,KAA0C;AAClD,QAAI;AACF,aAAO,MAAMA,UAAS,KAAK,QAAQ,GAAG,CAAC;AAAA,IACzC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,IAAI,KAAa,OAA8B;AACnD,UAAM,KAAK,UAAU;AACrB,UAAM,UAAU,KAAK,QAAQ,GAAG,GAAG,KAAK;AAAA,EAC1C;AACF;;;ACVA,IAAI,oBACF;AACF,SAAS,iBAAgE;AACvE,MAAI,CAAC,mBAAmB;AACtB,wBAAoB,OAAO,aAAa,EAAE,KAAK,CAAC,MAAM,EAAE,OAAO;AAAA,EACjE;AACA,SAAO;AACT;AAuBA,SAAS,YAAY,KAAqB;AACxC,SAAO,UAAU,GAAG;AACtB;AAEA,SAAS,iBACP,KACA,QACA,QACA,MACQ;AAGR,QAAM,WAAW,OACb,MACA,OAAO,QAAQ,IAAI,EAChB,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,EACrC,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,EAAE,EAC3B,KAAK,GAAG,IACX;AAGJ,SAAO,aAAa,GAAG,IAAI,MAAM,IAAI,SAAS,MAAM,GAAG,GAAG,QAAQ;AACpE;AASA,IAAI,mBAAkC;AACtC,SAAS,aAAqB;AAC5B,MAAI,iBAAkB,QAAO;AAC7B,MAAI,IAAI;AACR,WAAS,KAAK,IAAM,MAAM,OAAQ,MAAM;AAGtC,QAAI,MAAM,SAAU,MAAM,MAAQ;AAClC,SAAK,OAAO,cAAc,EAAE;AAAA,EAC9B;AACA,qBAAmB;AACnB,SAAO;AACT;AAIA,eAAe,oBACb,MACsB;AACtB,MAAI,CAAC,iBAAiB,KAAK,GAAG,GAAG;AAC/B,WAAO,EAAE,OAAO,qCAAqC;AAAA,EACvD;AACA,QAAM,MAAM,YAAY,KAAK,GAAG;AAChC,QAAM,MAAM,KAAK,aAAa,IAAI,GAAG;AACrC,MAAI,IAAK,QAAO,EAAE,KAAK,IAAI;AAC3B,QAAM,OAAO,MAAM,KAAK,WAAW,IAAI,GAAG;AAC1C,MAAI,MAAM;AACR,SAAK,aAAa,IAAI,KAAK,IAAI;AAC/B,WAAO,EAAE,KAAK,KAAK;AAAA,EACrB;AACA,QAAM,OAAO,IAAI,gBAAgB;AACjC,QAAM,QAAQ,WAAW,MAAM,KAAK,MAAM,GAAG,KAAK,kBAAkB,GAAK;AACzE,MAAI;AACF,UAAM,IAAI,KAAK,WAAW;AAE1B,QAAI,MAAM,MAAM,EAAE,KAAK,KAAK,EAAE,QAAQ,KAAK,QAAQ,UAAU,SAAS,CAAC;AACvE,QAAI,OAAO;AACX,WAAO,IAAI,UAAU,OAAO,IAAI,SAAS,OAAO,IAAI,WAAW,KAAK;AAClE,YAAM,OAAO,IAAI,QAAQ,IAAI,UAAU;AACvC,UAAI,CAAC,KAAM,QAAO,EAAE,OAAO,GAAG,IAAI,MAAM,oBAAoB;AAC5D,YAAM,WAAW,IAAI,IAAI,MAAM,KAAK,GAAG,EAAE,SAAS;AAClD,UAAI,CAAC,iBAAiB,QAAQ,GAAG;AAC/B,eAAO,EAAE,OAAO,gCAAgC,QAAQ,GAAG;AAAA,MAC7D;AACA,UAAI,EAAE,OAAO,EAAG,QAAO,EAAE,OAAO,qBAAqB;AACrD,YAAM,MAAM,EAAE,UAAU,EAAE,QAAQ,KAAK,QAAQ,UAAU,SAAS,CAAC;AAAA,IACrE;AACA,QAAI,CAAC,IAAI,GAAI,QAAO,EAAE,OAAO,QAAQ,IAAI,MAAM,IAAI,IAAI,UAAU,GAAG;AACpE,UAAM,KAAK,MAAM,IAAI,YAAY;AACjC,UAAM,MAAM,OAAO,KAAK,EAAE;AAI1B,QAAI,IAAI,SAAS;AACf,aAAO,EAAE,OAAO,uBAAuB,IAAI,MAAM,KAAK;AACxD,UAAM,SAAS,iBAAiB,GAAG;AAKnC,QACE,WAAW,SACX,WAAW,SACX,WAAW,UACX,WAAW,SACX;AACA,aAAO,EAAE,OAAO,2BAA2B,MAAM,GAAG;AAAA,IACtD;AACA,SAAK,aAAa,IAAI,KAAK,GAAG;AAC9B,UAAM,KAAK,WAAW,IAAI,KAAK,GAAG;AAClC,WAAO,EAAE,IAAI;AAAA,EACf,SAAS,KAAK;AACZ,WAAO,EAAE,OAAQ,IAAc,QAAQ;AAAA,EACzC,UAAE;AACA,iBAAa,KAAK;AAAA,EACpB;AACF;AAEA,eAAsB,wBACpB,MAC+D;AAC/D,QAAM,MAAM,iBAAiB,KAAK,KAAK,KAAK,QAAQ,KAAK,QAAQ,KAAK,IAAI;AAC1E,QAAM,MAAM,KAAK,aAAa,IAAI,GAAG;AACrC,MAAI,KAAK;AACP,WAAO;AAAA,MACL,QAAQ;AAAA,QACN,MAAM;AAAA,QACN,QAAQ,KAAK;AAAA,QACb,QAAQ,KAAK;AAAA,QACb,QAAQ,iBAAiB,GAAG;AAAA,MAC9B;AAAA,MACA,UAAU,CAAC;AAAA,IACb;AAAA,EACF;AACA,QAAM,OAAO,MAAM,KAAK,WAAW,IAAI,GAAG;AAC1C,MAAI,MAAM;AACR,SAAK,aAAa,IAAI,KAAK,IAAI;AAC/B,WAAO;AAAA,MACL,QAAQ;AAAA,QACN,MAAM;AAAA,QACN,QAAQ,KAAK;AAAA,QACb,QAAQ,KAAK;AAAA,QACb,QAAQ,iBAAiB,IAAI;AAAA,MAC/B;AAAA,MACA,UAAU,CAAC;AAAA,IACb;AAAA,EACF;AAEA,QAAM,UAAU,MAAM,oBAAoB,IAAI;AAC9C,MAAI,WAAW,SAAS;AACtB,WAAO;AAAA,MACL,UAAU;AAAA,QACR,wBAAwB,KAAK,GAAG,UAAU,KAAK,eAAe,KAAK,GAAG,YAAY,KAAK,MAAM,KAAK,QAAQ,KAAK;AAAA,MACjH;AAAA,IACF;AAAA,EACF;AACA,QAAM,MAAM,QAAQ;AAYpB,QAAM,gBAAwC;AAAA,IAC5C,MAAM,KAAK;AAAA,IACX,GAAI,KAAK,QAAQ,CAAC;AAAA,EACpB;AAEA,MAAI;AACJ,MAAI;AACF,UAAM,aAAa,MAAM,eAAe;AACxC,gBAAY,MAAM,WAAW,KAAK,WAAW,GAAG;AAAA,MAC9C,cAAc;AAAA,MACd;AAAA;AAAA;AAAA,MAGA,iBAAiB;AAAA,QACf;AAAA,QAAG;AAAA,QAAG;AAAA,QAAG;AAAA,QAAG;AAAA,QAAG;AAAA,QAAG;AAAA,QAAG;AAAA,QAAG;AAAA,QAAG;AAAA,QAAG;AAAA,QAAI;AAAA,QAAI;AAAA,QAAI;AAAA,QAAI;AAAA,QAAI;AAAA,QAAI;AAAA,QAAI;AAAA,QAAI;AAAA,QAAI;AAAA,QAClE;AAAA,QAAI;AAAA,QAAI;AAAA,QAAI;AAAA,QAAI;AAAA,QAAI;AAAA,MACtB;AAAA,IACF,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,WAAO;AAAA,MACL,UAAU;AAAA,QACR,iCAAiC,KAAK,eAAe,KAAK,GAAG,YAAY,KAAK,MAAM,KAAM,IAAc,OAAO;AAAA,MACjH;AAAA,IACF;AAAA,EACF;AAOA,cAAY,0BAA0B,WAAW,KAAK,QAAQ,KAAK,MAAM;AAEzE,OAAK,aAAa,IAAI,KAAK,SAAS;AACpC,QAAM,KAAK,WAAW,IAAI,KAAK,SAAS;AACxC,SAAO;AAAA,IACL,QAAQ;AAAA,MACN,MAAM;AAAA,MACN,QAAQ,KAAK;AAAA,MACb,QAAQ,KAAK;AAAA,MACb,QAAQ,iBAAiB,SAAS;AAAA,IACpC;AAAA,IACA,UAAU,CAAC;AAAA,EACb;AACF;;;ACzOA,IAAM,oBAAoB,oBAAI,IAAkC,CAAC,OAAO,KAAK,CAAC;AAcvE,SAAS,qBACd,OACA,UACqB;AACrB,QAAM,QAA6B,CAAC;AACpC,aAAW,QAAQ,OAAO;AACxB,QAAI,KAAK,QAAQ,WAAW,EAAG;AAC/B,eAAW,UAAU,KAAK,SAAS;AACjC,UAAI,CAAC,kBAAkB,IAAI,OAAO,MAAM,GAAG;AACzC,kBAAU,KAAK;AAAA,UACb,WAAW;AAAA,UACX,UAAU;AAAA,UACV,SAAS,EAAE,MAAM,+BAA+B;AAAA,UAChD,SACE,IAAI,KAAK,MAAM,YAAY,OAAO,MAAM,GACrC,OAAO,SAAS,YAAY,EAAE,OAAO,OAAO,MAAM;AAAA,QAGzD,CAAC;AACD;AAAA,MACF;AACA,YAAM,KAAK;AAAA,QACT,QAAQ,KAAK;AAAA,QACb,QAAQ,OAAO;AAAA,QACf,QAAQ,OAAO;AAAA,QACf,MAAM,OAAO,KAAK,SAAS,QAAQ;AAAA,QACnC,QAAQ,OAAO;AAAA,MACjB,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO;AACT;AAQO,SAAS,uBACd,OACgB;AAChB,QAAM,WAAW,oBAAI,IAA0B;AAC/C,aAAW,QAAQ,OAAO;AACxB,QAAI,OAAO,SAAS,IAAI,KAAK,MAAM;AACnC,QAAI,CAAC,MAAM;AACT,aAAO,EAAE,QAAQ,KAAK,QAAQ,SAAS,CAAC,GAAG,UAAU,CAAC,EAAE;AACxD,eAAS,IAAI,KAAK,QAAQ,IAAI;AAAA,IAChC;AACA,UAAM,SAA6B;AAAA,MACjC,MAAM,OAAO,KAAK,KAAK,MAAM,QAAQ;AAAA,MACrC,QAAQ,KAAK;AAAA,MACb,QAAQ,KAAK;AAAA,MACb,QAAQ,KAAK,UAAU;AAAA,IACzB;AACA,SAAK,QAAQ,KAAK,MAAM;AAAA,EAC1B;AACA,SAAO,CAAC,GAAG,SAAS,OAAO,CAAC;AAC9B;AAGA,IAAM,kBAAkB,oBAAI,IAAkC;AAAA,EAC5D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AASM,SAAS,iBACd,OACqB;AACrB,QAAM,QAA6B,CAAC;AACpC,aAAW,QAAQ,OAAO;AACxB,eAAW,UAAU,KAAK,SAAS;AACjC,UAAI,CAAC,gBAAgB,IAAI,OAAO,MAAM,EAAG;AACzC,YAAM,KAAK;AAAA,QACT,QAAQ,KAAK;AAAA,QACb,QAAQ,OAAO;AAAA,QACf,QAAQ,OAAO;AAAA,QACf,MAAM,OAAO,KAAK,SAAS,QAAQ;AAAA,QACnC,QAAQ,OAAO;AAAA,MACjB,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO;AACT;","names":["readFile"]}
|