@json-to-office/shared 0.33.0 → 1.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-CP2I5NPP.js → chunk-FDSJYZ5W.js} +21 -20
- package/dist/chunk-FDSJYZ5W.js.map +1 -0
- package/dist/chunk-JM5KTMNL.js +240 -0
- package/dist/chunk-JM5KTMNL.js.map +1 -0
- package/dist/{chunk-KLWNDWC4.js → chunk-SJ2YYRCT.js} +10 -3
- package/dist/chunk-SJ2YYRCT.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 +52 -148
- package/dist/index.js +118 -23
- package/dist/index.js.map +1 -1
- package/dist/rendering/index.d.ts +210 -0
- package/dist/rendering/index.js +25 -0
- package/dist/rendering/index.js.map +1 -0
- package/dist/schemas/schema-utils.js +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 +5 -5
- package/dist/cache/index.d.ts +0 -464
- package/dist/cache/index.js +0 -741
- package/dist/cache/index.js.map +0 -1
- package/dist/chunk-CP2I5NPP.js.map +0 -1
- package/dist/chunk-KLWNDWC4.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":[]}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// src/rendering/types.ts
|
|
2
|
+
function assertNever(value, context) {
|
|
3
|
+
const described = describeUnhandled(value);
|
|
4
|
+
throw new Error(
|
|
5
|
+
context ? `Unhandled variant in ${context}: ${described}` : `Unhandled variant: ${described}`
|
|
6
|
+
);
|
|
7
|
+
}
|
|
8
|
+
function describeUnhandled(value) {
|
|
9
|
+
if (value === null || typeof value !== "object") {
|
|
10
|
+
return String(value);
|
|
11
|
+
}
|
|
12
|
+
const kind = value.kind;
|
|
13
|
+
const type = value.type;
|
|
14
|
+
if (typeof kind === "string") return `kind="${kind}"`;
|
|
15
|
+
if (typeof type === "string") return `type="${type}"`;
|
|
16
|
+
try {
|
|
17
|
+
return JSON.stringify(value);
|
|
18
|
+
} catch {
|
|
19
|
+
return Object.prototype.toString.call(value);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/rendering/diagnostics.ts
|
|
24
|
+
var UnsupportedRendererFeatureError = class _UnsupportedRendererFeatureError extends Error {
|
|
25
|
+
code = "UNSUPPORTED_RENDERER_FEATURE";
|
|
26
|
+
format;
|
|
27
|
+
rendererId;
|
|
28
|
+
/** Distinct unsupported features, in first-seen order. */
|
|
29
|
+
features;
|
|
30
|
+
/** Distinct IR paths that required them, in first-seen order. */
|
|
31
|
+
paths;
|
|
32
|
+
/** Every error-severity diagnostic that produced this failure. */
|
|
33
|
+
diagnostics;
|
|
34
|
+
constructor(init) {
|
|
35
|
+
const { format, rendererId, diagnostics } = init;
|
|
36
|
+
const features = distinct(diagnostics.map((d) => d.feature));
|
|
37
|
+
const paths = distinct(diagnostics.map((d) => d.path));
|
|
38
|
+
super(formatMessage(format, rendererId, diagnostics, features));
|
|
39
|
+
this.name = "UnsupportedRendererFeatureError";
|
|
40
|
+
this.format = format;
|
|
41
|
+
this.rendererId = rendererId;
|
|
42
|
+
this.features = features;
|
|
43
|
+
this.paths = paths;
|
|
44
|
+
this.diagnostics = [...diagnostics];
|
|
45
|
+
if (Error.captureStackTrace) {
|
|
46
|
+
Error.captureStackTrace(this, _UnsupportedRendererFeatureError);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
var UnknownRendererError = class _UnknownRendererError extends Error {
|
|
51
|
+
code = "UNKNOWN_RENDERER";
|
|
52
|
+
format;
|
|
53
|
+
/** What the caller asked for. */
|
|
54
|
+
rendererId;
|
|
55
|
+
/** Every id registered for this format, in registration order. */
|
|
56
|
+
availableIds;
|
|
57
|
+
constructor(format, rendererId, availableIds) {
|
|
58
|
+
const known = availableIds.map((id) => `"${id}"`).join(", ");
|
|
59
|
+
super(
|
|
60
|
+
`Unknown ${format} renderer "${rendererId}". Available renderers: ${known}.`
|
|
61
|
+
);
|
|
62
|
+
this.name = "UnknownRendererError";
|
|
63
|
+
this.format = format;
|
|
64
|
+
this.rendererId = rendererId;
|
|
65
|
+
this.availableIds = [...availableIds];
|
|
66
|
+
if (Error.captureStackTrace) {
|
|
67
|
+
Error.captureStackTrace(this, _UnknownRendererError);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
function distinct(values) {
|
|
72
|
+
return [...new Set(values)];
|
|
73
|
+
}
|
|
74
|
+
function formatMessage(format, rendererId, diagnostics, features) {
|
|
75
|
+
const featureList = features.map((f) => `"${f}"`).join(", ");
|
|
76
|
+
const lines = diagnostics.map(
|
|
77
|
+
(d) => ` - ${d.feature} at ${d.path}: ${d.message}`
|
|
78
|
+
);
|
|
79
|
+
return `The "${rendererId}" ${format} renderer does not support ${features.length} required feature(s): ${featureList}.
|
|
80
|
+
${lines.join("\n")}`;
|
|
81
|
+
}
|
|
82
|
+
function rendererError(feature, path, message) {
|
|
83
|
+
return { feature, path, severity: "error", message };
|
|
84
|
+
}
|
|
85
|
+
function rendererWarning(feature, path, message) {
|
|
86
|
+
return { feature, path, severity: "warning", message };
|
|
87
|
+
}
|
|
88
|
+
function partitionDiagnostics(diagnostics) {
|
|
89
|
+
const errors = [];
|
|
90
|
+
const warnings = [];
|
|
91
|
+
for (const diagnostic of diagnostics) {
|
|
92
|
+
if (diagnostic.severity === "error") errors.push(diagnostic);
|
|
93
|
+
else warnings.push(diagnostic);
|
|
94
|
+
}
|
|
95
|
+
return { errors, warnings };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/rendering/capabilities.ts
|
|
99
|
+
var FeatureRequirementCollector = class {
|
|
100
|
+
requirements = [];
|
|
101
|
+
seen = /* @__PURE__ */ new Set();
|
|
102
|
+
/**
|
|
103
|
+
* Record that `feature` is needed at `path`.
|
|
104
|
+
*
|
|
105
|
+
* Duplicate (feature, path) pairs collapse, so a compiler can call this
|
|
106
|
+
* unconditionally inside a loop without inflating the diagnostics.
|
|
107
|
+
*/
|
|
108
|
+
require(feature, path, detail) {
|
|
109
|
+
const key = `${feature}\0${path}`;
|
|
110
|
+
if (this.seen.has(key)) return;
|
|
111
|
+
this.seen.add(key);
|
|
112
|
+
this.requirements.push(
|
|
113
|
+
detail === void 0 ? { feature, path } : { feature, path, detail }
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
/** Every recorded requirement, in first-seen order. */
|
|
117
|
+
list() {
|
|
118
|
+
return this.requirements;
|
|
119
|
+
}
|
|
120
|
+
/** Distinct required features, in first-seen order. */
|
|
121
|
+
features() {
|
|
122
|
+
return [...new Set(this.requirements.map((r) => r.feature))];
|
|
123
|
+
}
|
|
124
|
+
/** True when nothing has been required yet. */
|
|
125
|
+
isEmpty() {
|
|
126
|
+
return this.requirements.length === 0;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
function diagnoseUnsupportedFeatures(required, capabilities, rendererId) {
|
|
130
|
+
const diagnostics = [];
|
|
131
|
+
for (const requirement of required) {
|
|
132
|
+
if (capabilities.has(requirement.feature)) continue;
|
|
133
|
+
diagnostics.push(
|
|
134
|
+
rendererError(
|
|
135
|
+
requirement.feature,
|
|
136
|
+
requirement.path,
|
|
137
|
+
buildMessage(requirement, rendererId)
|
|
138
|
+
)
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return diagnostics;
|
|
142
|
+
}
|
|
143
|
+
function buildMessage(requirement, rendererId) {
|
|
144
|
+
const base = `the "${rendererId}" renderer cannot express "${requirement.feature}"`;
|
|
145
|
+
return requirement.detail ? `${base} (${requirement.detail})` : base;
|
|
146
|
+
}
|
|
147
|
+
function assertRendererSupports(required, renderer) {
|
|
148
|
+
const diagnostics = diagnoseUnsupportedFeatures(
|
|
149
|
+
required,
|
|
150
|
+
renderer.capabilities,
|
|
151
|
+
renderer.id
|
|
152
|
+
);
|
|
153
|
+
if (diagnostics.length === 0) return;
|
|
154
|
+
throw new UnsupportedRendererFeatureError({
|
|
155
|
+
format: renderer.format,
|
|
156
|
+
rendererId: renderer.id,
|
|
157
|
+
diagnostics
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
var RendererRegistry = class {
|
|
161
|
+
constructor(format, defaultId) {
|
|
162
|
+
this.format = format;
|
|
163
|
+
this.defaultId = defaultId;
|
|
164
|
+
}
|
|
165
|
+
renderers = /* @__PURE__ */ new Map();
|
|
166
|
+
/**
|
|
167
|
+
* Register a lazily-constructed renderer.
|
|
168
|
+
*
|
|
169
|
+
* The factory is async and only invoked on selection, so an adapter whose
|
|
170
|
+
* backend is an optional dependency is never imported unless it is chosen.
|
|
171
|
+
*/
|
|
172
|
+
register(id, factory) {
|
|
173
|
+
this.renderers.set(id, factory);
|
|
174
|
+
}
|
|
175
|
+
/** Renderer ids registered for this format, in registration order. */
|
|
176
|
+
ids() {
|
|
177
|
+
return [...this.renderers.keys()];
|
|
178
|
+
}
|
|
179
|
+
/** The id used when a caller does not pass one. */
|
|
180
|
+
getDefaultId() {
|
|
181
|
+
return this.defaultId;
|
|
182
|
+
}
|
|
183
|
+
has(id) {
|
|
184
|
+
return this.renderers.has(id);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Resolve a renderer, defaulting when `id` is omitted.
|
|
188
|
+
*
|
|
189
|
+
* An unknown id is `UnknownRendererError`, which carries the id asked for and
|
|
190
|
+
* the ones that exist, so a caller boundary can answer "bad request" rather
|
|
191
|
+
* than "the server broke". A missing optional dependency is re-thrown with an
|
|
192
|
+
* actionable install hint.
|
|
193
|
+
*/
|
|
194
|
+
async resolve(id) {
|
|
195
|
+
const selected = id ?? this.defaultId;
|
|
196
|
+
const factory = this.renderers.get(selected);
|
|
197
|
+
if (!factory) {
|
|
198
|
+
throw new UnknownRendererError(this.format, selected, this.ids());
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
return await factory();
|
|
202
|
+
} catch (error) {
|
|
203
|
+
throw enrichLoadFailure(error, this.format, selected);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
function enrichLoadFailure(error, format, rendererId) {
|
|
208
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
209
|
+
const isMissingModule = /Cannot find (?:module|package)|ERR_MODULE_NOT_FOUND|Failed to resolve/i.test(
|
|
210
|
+
message
|
|
211
|
+
);
|
|
212
|
+
if (!isMissingModule) {
|
|
213
|
+
return error instanceof Error ? error : new Error(message);
|
|
214
|
+
}
|
|
215
|
+
const pkg = missingPackageName(message) ?? `the "${rendererId}" backend`;
|
|
216
|
+
const enriched = new Error(
|
|
217
|
+
`The "${rendererId}" ${format} renderer requires ${pkg}, which is not installed. Install it with: pnpm add ${pkg}
|
|
218
|
+
Original error: ${message}`
|
|
219
|
+
);
|
|
220
|
+
enriched.name = "RendererDependencyMissingError";
|
|
221
|
+
return enriched;
|
|
222
|
+
}
|
|
223
|
+
function missingPackageName(message) {
|
|
224
|
+
const match = /Cannot find (?:module|package) ['"]([^'"]+)['"]/.exec(message) ?? /Failed to resolve (?:module|import)[: ]+['"]?([^'"\s]+)/.exec(message);
|
|
225
|
+
return match?.[1];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export {
|
|
229
|
+
assertNever,
|
|
230
|
+
UnsupportedRendererFeatureError,
|
|
231
|
+
UnknownRendererError,
|
|
232
|
+
rendererError,
|
|
233
|
+
rendererWarning,
|
|
234
|
+
partitionDiagnostics,
|
|
235
|
+
FeatureRequirementCollector,
|
|
236
|
+
diagnoseUnsupportedFeatures,
|
|
237
|
+
assertRendererSupports,
|
|
238
|
+
RendererRegistry
|
|
239
|
+
};
|
|
240
|
+
//# sourceMappingURL=chunk-JM5KTMNL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/rendering/types.ts","../src/rendering/diagnostics.ts","../src/rendering/capabilities.ts"],"sourcesContent":["/**\n * Format-independent renderer contracts.\n *\n * This module deliberately knows nothing about DOCX or PPTX semantics. Each\n * format owns its own intermediate representation (`DocxIR`, `PptxIR`) and its\n * own feature union; the only thing shared between them is the shape of the\n * contract a backend adapter must satisfy.\n *\n * Do not add format-specific feature names, IR nodes or units here.\n */\n\n/** The Office formats this repository can produce. */\nexport type OfficeFormat = 'docx' | 'pptx';\n\n/**\n * Options every renderer accepts.\n *\n * `deterministic` asks the adapter (and the packaging step after it) to make\n * output byte-stable across runs: fixed zip entry timestamps, fixed core\n * metadata timestamps, no random identifiers.\n *\n * `generatedAt` pins the timestamp written into package metadata. Callers that\n * want reproducible bytes pass both.\n */\nexport interface RenderOptions {\n deterministic?: boolean;\n generatedAt?: Date;\n}\n\n/**\n * A backend that turns a format-specific IR into package bytes.\n *\n * @typeParam TIR - the format's intermediate representation (plain data)\n * @typeParam TFeature - the format's feature union (see `capabilities.ts`)\n * @typeParam TId - the string-literal union of renderer ids for the format\n */\nexport interface OfficeRenderer<\n TIR,\n TFeature extends string,\n TId extends string,\n> {\n readonly id: TId;\n readonly format: OfficeFormat;\n readonly capabilities: ReadonlySet<TFeature>;\n\n render(document: TIR, options?: RenderOptions): Promise<Uint8Array>;\n}\n\n/**\n * Exhaustiveness guard for discriminated-union switches.\n *\n * Reaching this at runtime means an IR node kind was added without a matching\n * `case`, so it throws rather than silently dropping content.\n */\nexport function assertNever(value: never, context?: string): never {\n const described = describeUnhandled(value);\n throw new Error(\n context\n ? `Unhandled variant in ${context}: ${described}`\n : `Unhandled variant: ${described}`\n );\n}\n\nfunction describeUnhandled(value: unknown): string {\n if (value === null || typeof value !== 'object') {\n return String(value);\n }\n const kind = (value as { kind?: unknown }).kind;\n const type = (value as { type?: unknown }).type;\n if (typeof kind === 'string') return `kind=\"${kind}\"`;\n if (typeof type === 'string') return `type=\"${type}\"`;\n try {\n return JSON.stringify(value);\n } catch {\n return Object.prototype.toString.call(value);\n }\n}\n","import type { OfficeFormat } from './types';\n\n/**\n * Diagnostics raised when an IR asks a renderer for something it cannot do.\n *\n * These are distinct from `GenerationWarning` (see `../types/warnings`), which\n * describes authoring problems found while building the document. A renderer\n * diagnostic describes a *backend* limitation: the document is fine, this\n * particular adapter just cannot express part of it.\n */\n\nexport type RendererDiagnosticSeverity = 'error' | 'warning';\n\n/**\n * One unsupported (or degraded) feature at one place in the IR.\n *\n * `path` is an IR path such as `slides[2].elements[0].fill` — not an author-JSON\n * path — because the check runs against compiled IR. Compilers record the\n * authoring path alongside where it is useful for the message text.\n */\nexport interface RendererDiagnostic<TFeature extends string = string> {\n feature: TFeature;\n path: string;\n severity: RendererDiagnosticSeverity;\n message: string;\n}\n\nexport interface UnsupportedRendererFeatureErrorInit<\n TFeature extends string = string,\n> {\n format: OfficeFormat;\n rendererId: string;\n diagnostics: readonly RendererDiagnostic<TFeature>[];\n}\n\n/**\n * Aggregated failure thrown *before* rendering starts.\n *\n * One error carries every unsupported feature found in the IR so a caller sees\n * the whole gap at once instead of fixing them one render at a time.\n */\nexport class UnsupportedRendererFeatureError<\n TFeature extends string = string,\n> extends Error {\n public readonly code = 'UNSUPPORTED_RENDERER_FEATURE';\n public readonly format: OfficeFormat;\n public readonly rendererId: string;\n /** Distinct unsupported features, in first-seen order. */\n public readonly features: readonly TFeature[];\n /** Distinct IR paths that required them, in first-seen order. */\n public readonly paths: readonly string[];\n /** Every error-severity diagnostic that produced this failure. */\n public readonly diagnostics: readonly RendererDiagnostic<TFeature>[];\n\n constructor(init: UnsupportedRendererFeatureErrorInit<TFeature>) {\n const { format, rendererId, diagnostics } = init;\n const features = distinct(diagnostics.map((d) => d.feature));\n const paths = distinct(diagnostics.map((d) => d.path));\n\n super(formatMessage(format, rendererId, diagnostics, features));\n\n this.name = 'UnsupportedRendererFeatureError';\n this.format = format;\n this.rendererId = rendererId;\n this.features = features;\n this.paths = paths;\n this.diagnostics = [...diagnostics];\n\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, UnsupportedRendererFeatureError);\n }\n }\n}\n\n/**\n * A renderer id that is not registered for the format asked for.\n *\n * Caller input, not an infrastructure failure — which is the whole reason it is\n * a class with a `code` rather than a bare `Error`. A server matching on the\n * message text could only answer `500`, so an unknown id looked like the\n * service falling over, and a retry looked worth attempting (#263).\n */\nexport class UnknownRendererError extends Error {\n public readonly code = 'UNKNOWN_RENDERER';\n public readonly format: OfficeFormat;\n /** What the caller asked for. */\n public readonly rendererId: string;\n /** Every id registered for this format, in registration order. */\n public readonly availableIds: readonly string[];\n\n constructor(\n format: OfficeFormat,\n rendererId: string,\n availableIds: readonly string[]\n ) {\n const known = availableIds.map((id) => `\"${id}\"`).join(', ');\n super(\n `Unknown ${format} renderer \"${rendererId}\". Available renderers: ${known}.`\n );\n\n this.name = 'UnknownRendererError';\n this.format = format;\n this.rendererId = rendererId;\n this.availableIds = [...availableIds];\n\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, UnknownRendererError);\n }\n }\n}\n\nfunction distinct<T>(values: readonly T[]): T[] {\n return [...new Set(values)];\n}\n\nfunction formatMessage<TFeature extends string>(\n format: OfficeFormat,\n rendererId: string,\n diagnostics: readonly RendererDiagnostic<TFeature>[],\n features: readonly TFeature[]\n): string {\n const featureList = features.map((f) => `\"${f}\"`).join(', ');\n const lines = diagnostics.map(\n (d) => ` - ${d.feature} at ${d.path}: ${d.message}`\n );\n return (\n `The \"${rendererId}\" ${format} renderer does not support ${features.length} ` +\n `required feature(s): ${featureList}.\\n${lines.join('\\n')}`\n );\n}\n\n/** Build a `RendererDiagnostic` with `severity: 'error'`. */\nexport function rendererError<TFeature extends string>(\n feature: TFeature,\n path: string,\n message: string\n): RendererDiagnostic<TFeature> {\n return { feature, path, severity: 'error', message };\n}\n\n/** Build a `RendererDiagnostic` with `severity: 'warning'`. */\nexport function rendererWarning<TFeature extends string>(\n feature: TFeature,\n path: string,\n message: string\n): RendererDiagnostic<TFeature> {\n return { feature, path, severity: 'warning', message };\n}\n\n/** Split diagnostics into blocking errors and non-blocking warnings. */\nexport function partitionDiagnostics<TFeature extends string>(\n diagnostics: readonly RendererDiagnostic<TFeature>[]\n): {\n errors: RendererDiagnostic<TFeature>[];\n warnings: RendererDiagnostic<TFeature>[];\n} {\n const errors: RendererDiagnostic<TFeature>[] = [];\n const warnings: RendererDiagnostic<TFeature>[] = [];\n for (const diagnostic of diagnostics) {\n if (diagnostic.severity === 'error') errors.push(diagnostic);\n else warnings.push(diagnostic);\n }\n return { errors, warnings };\n}\n","import {\n UnknownRendererError,\n UnsupportedRendererFeatureError,\n rendererError,\n type RendererDiagnostic,\n} from './diagnostics';\nimport type { OfficeFormat, OfficeRenderer } from './types';\n\n/**\n * Capability checking: what an IR *requires* versus what an adapter *provides*.\n *\n * A compiler records one `FeatureRequirement` each time it emits an IR node that\n * needs a backend capability. Before rendering, `assertRendererSupports` diffs\n * those requirements against the adapter's `capabilities` set and throws a\n * single aggregated `UnsupportedRendererFeatureError` if anything is missing.\n *\n * The point is that nothing is dropped silently: a feature either appears in the\n * adapter's capability set and is rendered, or it fails loudly before bytes are\n * produced.\n */\n\n/** One capability an IR node needs, and where in the IR it was needed. */\nexport interface FeatureRequirement<TFeature extends string = string> {\n feature: TFeature;\n /** IR path, e.g. `sections[0].children[3].image`. */\n path: string;\n /** Optional detail folded into the failure message. */\n detail?: string;\n}\n\n/**\n * Accumulates feature requirements during compilation.\n *\n * Deliberately per-compilation (never module-global) so concurrent generations\n * never share state.\n */\nexport class FeatureRequirementCollector<TFeature extends string> {\n private readonly requirements: FeatureRequirement<TFeature>[] = [];\n private readonly seen = new Set<string>();\n\n /**\n * Record that `feature` is needed at `path`.\n *\n * Duplicate (feature, path) pairs collapse, so a compiler can call this\n * unconditionally inside a loop without inflating the diagnostics.\n */\n require(feature: TFeature, path: string, detail?: string): void {\n const key = `${feature}\\u0000${path}`;\n if (this.seen.has(key)) return;\n this.seen.add(key);\n this.requirements.push(\n detail === undefined ? { feature, path } : { feature, path, detail }\n );\n }\n\n /** Every recorded requirement, in first-seen order. */\n list(): readonly FeatureRequirement<TFeature>[] {\n return this.requirements;\n }\n\n /** Distinct required features, in first-seen order. */\n features(): readonly TFeature[] {\n return [...new Set(this.requirements.map((r) => r.feature))];\n }\n\n /** True when nothing has been required yet. */\n isEmpty(): boolean {\n return this.requirements.length === 0;\n }\n}\n\n/**\n * Diff required features against a capability set.\n *\n * Returns one error-severity diagnostic per unsupported requirement. An empty\n * array means the renderer can render the IR.\n */\nexport function diagnoseUnsupportedFeatures<TFeature extends string>(\n required: readonly FeatureRequirement<TFeature>[],\n capabilities: ReadonlySet<TFeature>,\n rendererId: string\n): RendererDiagnostic<TFeature>[] {\n const diagnostics: RendererDiagnostic<TFeature>[] = [];\n for (const requirement of required) {\n if (capabilities.has(requirement.feature)) continue;\n diagnostics.push(\n rendererError(\n requirement.feature,\n requirement.path,\n buildMessage(requirement, rendererId)\n )\n );\n }\n return diagnostics;\n}\n\nfunction buildMessage<TFeature extends string>(\n requirement: FeatureRequirement<TFeature>,\n rendererId: string\n): string {\n const base = `the \"${rendererId}\" renderer cannot express \"${requirement.feature}\"`;\n return requirement.detail ? `${base} (${requirement.detail})` : base;\n}\n\n/**\n * Throw one aggregated error if the renderer is missing any required feature.\n *\n * Call this after compiling to IR and before handing the IR to an adapter.\n */\nexport function assertRendererSupports<TFeature extends string>(\n required: readonly FeatureRequirement<TFeature>[],\n renderer: Pick<\n OfficeRenderer<unknown, TFeature, string>,\n 'id' | 'format' | 'capabilities'\n >\n): void {\n const diagnostics = diagnoseUnsupportedFeatures(\n required,\n renderer.capabilities,\n renderer.id\n );\n if (diagnostics.length === 0) return;\n throw new UnsupportedRendererFeatureError<TFeature>({\n format: renderer.format,\n rendererId: renderer.id,\n diagnostics,\n });\n}\n\n/**\n * A registry of renderers for a single format.\n *\n * Instances are created per format module, not per generation, and hold only\n * immutable adapter descriptors — never per-document state.\n */\nexport class RendererRegistry<\n TIR,\n TFeature extends string,\n TId extends string,\n> {\n private readonly renderers = new Map<\n TId,\n () => Promise<OfficeRenderer<TIR, TFeature, TId>>\n >();\n\n constructor(\n private readonly format: OfficeFormat,\n private readonly defaultId: TId\n ) {}\n\n /**\n * Register a lazily-constructed renderer.\n *\n * The factory is async and only invoked on selection, so an adapter whose\n * backend is an optional dependency is never imported unless it is chosen.\n */\n register(\n id: TId,\n factory: () => Promise<OfficeRenderer<TIR, TFeature, TId>>\n ): void {\n this.renderers.set(id, factory);\n }\n\n /** Renderer ids registered for this format, in registration order. */\n ids(): readonly TId[] {\n return [...this.renderers.keys()];\n }\n\n /** The id used when a caller does not pass one. */\n getDefaultId(): TId {\n return this.defaultId;\n }\n\n has(id: string): id is TId {\n return this.renderers.has(id as TId);\n }\n\n /**\n * Resolve a renderer, defaulting when `id` is omitted.\n *\n * An unknown id is `UnknownRendererError`, which carries the id asked for and\n * the ones that exist, so a caller boundary can answer \"bad request\" rather\n * than \"the server broke\". A missing optional dependency is re-thrown with an\n * actionable install hint.\n */\n async resolve(id?: TId): Promise<OfficeRenderer<TIR, TFeature, TId>> {\n const selected = id ?? this.defaultId;\n const factory = this.renderers.get(selected);\n if (!factory) {\n throw new UnknownRendererError(this.format, selected, this.ids());\n }\n try {\n return await factory();\n } catch (error) {\n throw enrichLoadFailure(error, this.format, selected);\n }\n }\n}\n\n/**\n * Turn a bare module-resolution failure into something a user can act on.\n *\n * Optional backends are not installed by default, so the common failure here is\n * a missing package rather than a bug.\n */\nfunction enrichLoadFailure(\n error: unknown,\n format: OfficeFormat,\n rendererId: string\n): Error {\n const message = error instanceof Error ? error.message : String(error);\n const isMissingModule =\n /Cannot find (?:module|package)|ERR_MODULE_NOT_FOUND|Failed to resolve/i.test(\n message\n );\n if (!isMissingModule) {\n return error instanceof Error ? error : new Error(message);\n }\n const pkg = missingPackageName(message) ?? `the \"${rendererId}\" backend`;\n const enriched = new Error(\n `The \"${rendererId}\" ${format} renderer requires ${pkg}, which is not installed. ` +\n `Install it with: pnpm add ${pkg}\\nOriginal error: ${message}`\n );\n enriched.name = 'RendererDependencyMissingError';\n return enriched;\n}\n\nfunction missingPackageName(message: string): string | undefined {\n const match =\n /Cannot find (?:module|package) ['\"]([^'\"]+)['\"]/.exec(message) ??\n /Failed to resolve (?:module|import)[: ]+['\"]?([^'\"\\s]+)/.exec(message);\n return match?.[1];\n}\n"],"mappings":";AAsDO,SAAS,YAAY,OAAc,SAAyB;AACjE,QAAM,YAAY,kBAAkB,KAAK;AACzC,QAAM,IAAI;AAAA,IACR,UACI,wBAAwB,OAAO,KAAK,SAAS,KAC7C,sBAAsB,SAAS;AAAA,EACrC;AACF;AAEA,SAAS,kBAAkB,OAAwB;AACjD,MAAI,UAAU,QAAQ,OAAO,UAAU,UAAU;AAC/C,WAAO,OAAO,KAAK;AAAA,EACrB;AACA,QAAM,OAAQ,MAA6B;AAC3C,QAAM,OAAQ,MAA6B;AAC3C,MAAI,OAAO,SAAS,SAAU,QAAO,SAAS,IAAI;AAClD,MAAI,OAAO,SAAS,SAAU,QAAO,SAAS,IAAI;AAClD,MAAI;AACF,WAAO,KAAK,UAAU,KAAK;AAAA,EAC7B,QAAQ;AACN,WAAO,OAAO,UAAU,SAAS,KAAK,KAAK;AAAA,EAC7C;AACF;;;ACnCO,IAAM,kCAAN,MAAM,yCAEH,MAAM;AAAA,EACE,OAAO;AAAA,EACP;AAAA,EACA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA,EAEhB,YAAY,MAAqD;AAC/D,UAAM,EAAE,QAAQ,YAAY,YAAY,IAAI;AAC5C,UAAM,WAAW,SAAS,YAAY,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC;AAC3D,UAAM,QAAQ,SAAS,YAAY,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC;AAErD,UAAM,cAAc,QAAQ,YAAY,aAAa,QAAQ,CAAC;AAE9D,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,aAAa;AAClB,SAAK,WAAW;AAChB,SAAK,QAAQ;AACb,SAAK,cAAc,CAAC,GAAG,WAAW;AAElC,QAAI,MAAM,mBAAmB;AAC3B,YAAM,kBAAkB,MAAM,gCAA+B;AAAA,IAC/D;AAAA,EACF;AACF;AAUO,IAAM,uBAAN,MAAM,8BAA6B,MAAM;AAAA,EAC9B,OAAO;AAAA,EACP;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA,EAEhB,YACE,QACA,YACA,cACA;AACA,UAAM,QAAQ,aAAa,IAAI,CAAC,OAAO,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI;AAC3D;AAAA,MACE,WAAW,MAAM,cAAc,UAAU,2BAA2B,KAAK;AAAA,IAC3E;AAEA,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,aAAa;AAClB,SAAK,eAAe,CAAC,GAAG,YAAY;AAEpC,QAAI,MAAM,mBAAmB;AAC3B,YAAM,kBAAkB,MAAM,qBAAoB;AAAA,IACpD;AAAA,EACF;AACF;AAEA,SAAS,SAAY,QAA2B;AAC9C,SAAO,CAAC,GAAG,IAAI,IAAI,MAAM,CAAC;AAC5B;AAEA,SAAS,cACP,QACA,YACA,aACA,UACQ;AACR,QAAM,cAAc,SAAS,IAAI,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,KAAK,IAAI;AAC3D,QAAM,QAAQ,YAAY;AAAA,IACxB,CAAC,MAAM,OAAO,EAAE,OAAO,OAAO,EAAE,IAAI,KAAK,EAAE,OAAO;AAAA,EACpD;AACA,SACE,QAAQ,UAAU,KAAK,MAAM,8BAA8B,SAAS,MAAM,yBAClD,WAAW;AAAA,EAAM,MAAM,KAAK,IAAI,CAAC;AAE7D;AAGO,SAAS,cACd,SACA,MACA,SAC8B;AAC9B,SAAO,EAAE,SAAS,MAAM,UAAU,SAAS,QAAQ;AACrD;AAGO,SAAS,gBACd,SACA,MACA,SAC8B;AAC9B,SAAO,EAAE,SAAS,MAAM,UAAU,WAAW,QAAQ;AACvD;AAGO,SAAS,qBACd,aAIA;AACA,QAAM,SAAyC,CAAC;AAChD,QAAM,WAA2C,CAAC;AAClD,aAAW,cAAc,aAAa;AACpC,QAAI,WAAW,aAAa,QAAS,QAAO,KAAK,UAAU;AAAA,QACtD,UAAS,KAAK,UAAU;AAAA,EAC/B;AACA,SAAO,EAAE,QAAQ,SAAS;AAC5B;;;AC/HO,IAAM,8BAAN,MAA2D;AAAA,EAC/C,eAA+C,CAAC;AAAA,EAChD,OAAO,oBAAI,IAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQxC,QAAQ,SAAmB,MAAc,QAAuB;AAC9D,UAAM,MAAM,GAAG,OAAO,KAAS,IAAI;AACnC,QAAI,KAAK,KAAK,IAAI,GAAG,EAAG;AACxB,SAAK,KAAK,IAAI,GAAG;AACjB,SAAK,aAAa;AAAA,MAChB,WAAW,SAAY,EAAE,SAAS,KAAK,IAAI,EAAE,SAAS,MAAM,OAAO;AAAA,IACrE;AAAA,EACF;AAAA;AAAA,EAGA,OAAgD;AAC9C,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,WAAgC;AAC9B,WAAO,CAAC,GAAG,IAAI,IAAI,KAAK,aAAa,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAAA,EAC7D;AAAA;AAAA,EAGA,UAAmB;AACjB,WAAO,KAAK,aAAa,WAAW;AAAA,EACtC;AACF;AAQO,SAAS,4BACd,UACA,cACA,YACgC;AAChC,QAAM,cAA8C,CAAC;AACrD,aAAW,eAAe,UAAU;AAClC,QAAI,aAAa,IAAI,YAAY,OAAO,EAAG;AAC3C,gBAAY;AAAA,MACV;AAAA,QACE,YAAY;AAAA,QACZ,YAAY;AAAA,QACZ,aAAa,aAAa,UAAU;AAAA,MACtC;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,aACP,aACA,YACQ;AACR,QAAM,OAAO,QAAQ,UAAU,8BAA8B,YAAY,OAAO;AAChF,SAAO,YAAY,SAAS,GAAG,IAAI,KAAK,YAAY,MAAM,MAAM;AAClE;AAOO,SAAS,uBACd,UACA,UAIM;AACN,QAAM,cAAc;AAAA,IAClB;AAAA,IACA,SAAS;AAAA,IACT,SAAS;AAAA,EACX;AACA,MAAI,YAAY,WAAW,EAAG;AAC9B,QAAM,IAAI,gCAA0C;AAAA,IAClD,QAAQ,SAAS;AAAA,IACjB,YAAY,SAAS;AAAA,IACrB;AAAA,EACF,CAAC;AACH;AAQO,IAAM,mBAAN,MAIL;AAAA,EAMA,YACmB,QACA,WACjB;AAFiB;AACA;AAAA,EAChB;AAAA,EARc,YAAY,oBAAI,IAG/B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaF,SACE,IACA,SACM;AACN,SAAK,UAAU,IAAI,IAAI,OAAO;AAAA,EAChC;AAAA;AAAA,EAGA,MAAsB;AACpB,WAAO,CAAC,GAAG,KAAK,UAAU,KAAK,CAAC;AAAA,EAClC;AAAA;AAAA,EAGA,eAAoB;AAClB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,IAAuB;AACzB,WAAO,KAAK,UAAU,IAAI,EAAS;AAAA,EACrC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,QAAQ,IAAuD;AACnE,UAAM,WAAW,MAAM,KAAK;AAC5B,UAAM,UAAU,KAAK,UAAU,IAAI,QAAQ;AAC3C,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,qBAAqB,KAAK,QAAQ,UAAU,KAAK,IAAI,CAAC;AAAA,IAClE;AACA,QAAI;AACF,aAAO,MAAM,QAAQ;AAAA,IACvB,SAAS,OAAO;AACd,YAAM,kBAAkB,OAAO,KAAK,QAAQ,QAAQ;AAAA,IACtD;AAAA,EACF;AACF;AAQA,SAAS,kBACP,OACA,QACA,YACO;AACP,QAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,QAAM,kBACJ,yEAAyE;AAAA,IACvE;AAAA,EACF;AACF,MAAI,CAAC,iBAAiB;AACpB,WAAO,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO;AAAA,EAC3D;AACA,QAAM,MAAM,mBAAmB,OAAO,KAAK,QAAQ,UAAU;AAC7D,QAAM,WAAW,IAAI;AAAA,IACnB,QAAQ,UAAU,KAAK,MAAM,sBAAsB,GAAG,uDACvB,GAAG;AAAA,kBAAqB,OAAO;AAAA,EAChE;AACA,WAAS,OAAO;AAChB,SAAO;AACT;AAEA,SAAS,mBAAmB,SAAqC;AAC/D,QAAM,QACJ,kDAAkD,KAAK,OAAO,KAC9D,0DAA0D,KAAK,OAAO;AACxE,SAAO,QAAQ,CAAC;AAClB;","names":[]}
|
|
@@ -119,6 +119,12 @@ function replaceRefs(obj, target, replacement) {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
function fixSchemaReferences(schema, rootDefinitionName = "ComponentDefinition") {
|
|
122
|
+
const definitionNames = new Set(
|
|
123
|
+
Object.keys(
|
|
124
|
+
schema.definitions ?? {}
|
|
125
|
+
)
|
|
126
|
+
);
|
|
127
|
+
const definitionRef = (name) => `#/definitions/${definitionNames.has(name) ? name : rootDefinitionName}`;
|
|
122
128
|
function traverse(obj, path = "") {
|
|
123
129
|
if (typeof obj !== "object" || obj === null) return;
|
|
124
130
|
for (const [key, value] of Object.entries(obj)) {
|
|
@@ -133,12 +139,13 @@ function fixSchemaReferences(schema, rootDefinitionName = "ComponentDefinition")
|
|
|
133
139
|
if (schemaValue.type === "array" && schemaValue.items && typeof schemaValue.items === "object" && "$ref" in schemaValue.items && typeof schemaValue.items.$ref === "string" && /^T\d+$/.test(
|
|
134
140
|
schemaValue.items.$ref
|
|
135
141
|
)) {
|
|
142
|
+
const name = schemaValue.items.$ref;
|
|
136
143
|
schemaValue.items = {
|
|
137
|
-
$ref:
|
|
144
|
+
$ref: definitionRef(name)
|
|
138
145
|
};
|
|
139
146
|
}
|
|
140
147
|
if (typeof schemaValue.$ref === "string" && (/^T\d+$/.test(schemaValue.$ref) || schemaValue.$ref === rootDefinitionName)) {
|
|
141
|
-
schemaValue.$ref =
|
|
148
|
+
schemaValue.$ref = definitionRef(schemaValue.$ref);
|
|
142
149
|
}
|
|
143
150
|
if (key === "$id" && typeof value === "string" && (/^T\d+$/.test(value) || value === rootDefinitionName) && currentPath !== `definitions.${rootDefinitionName}.$id`) {
|
|
144
151
|
delete obj[key];
|
|
@@ -282,4 +289,4 @@ export {
|
|
|
282
289
|
exportSchemaToFile,
|
|
283
290
|
createComponentSchemaObject
|
|
284
291
|
};
|
|
285
|
-
//# sourceMappingURL=chunk-
|
|
292
|
+
//# sourceMappingURL=chunk-SJ2YYRCT.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\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 const definitionRef = (name: string): string =>\n `#/definitions/${definitionNames.has(name) ? name : rootDefinitionName}`;\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 if (\n schemaValue.type === 'array' &&\n schemaValue.items &&\n Object.keys(schemaValue.items).length === 0\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 typeof (schemaValue.items as Record<string, unknown>).$ref ===\n 'string' &&\n /^T\\d+$/.test(\n (schemaValue.items as Record<string, unknown>).$ref as string\n )\n ) {\n const name = (schemaValue.items as Record<string, unknown>)\n .$ref as string;\n schemaValue.items = {\n $ref: definitionRef(name),\n };\n }\n\n if (\n typeof schemaValue.$ref === 'string' &&\n (/^T\\d+$/.test(schemaValue.$ref as string) ||\n schemaValue.$ref === rootDefinitionName)\n ) {\n schemaValue.$ref = definitionRef(schemaValue.$ref as string);\n }\n\n if (\n key === '$id' &&\n typeof value === 'string' &&\n (/^T\\d+$/.test(value) || value === rootDefinitionName) &&\n currentPath !== `definitions.${rootDefinitionName}.$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): Record<string, unknown> {\n const {\n $schema = 'https://json-schema.org/draft-07/schema#',\n $id,\n title,\n description,\n definitions = {},\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 restructureNameDiscriminatedUnions(jsonSchema);\n\n return jsonSchema;\n}\n\nexport function createComponentSchema(\n name: string,\n config: ComponentSchemaConfig,\n containerNames: string[],\n componentDefinitionSchema?: TSchema\n): Record<string, unknown> {\n const componentStructure: Record<string, unknown> = {\n $schema: 'https://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/ComponentDefinition',\n },\n };\n\n if (componentDefinitionSchema) {\n componentStructure.definitions = {\n ComponentDefinition: JSON.parse(\n JSON.stringify(componentDefinitionSchema)\n ),\n };\n }\n }\n\n fixSchemaReferences(componentStructure);\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 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
|
@@ -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
|