@json-to-office/jto 0.8.3 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1487 -114
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/HomePage-CtePCdPD.js +99 -0
- package/dist/client/assets/HomePage-CtePCdPD.js.map +1 -0
- package/dist/client/assets/{JsonEditorPage-DCvwatOj.js → JsonEditorPage-Dk_qYbQR.js} +3 -3
- package/dist/client/assets/{JsonEditorPage-DCvwatOj.js.map → JsonEditorPage-Dk_qYbQR.js.map} +1 -1
- package/dist/client/assets/{MonacoPluginProvider-DrTR8ld3.js → MonacoPluginProvider-BmWwTUcW.js} +3 -3
- package/dist/client/assets/{MonacoPluginProvider-DrTR8ld3.js.map → MonacoPluginProvider-BmWwTUcW.js.map} +1 -1
- package/dist/client/assets/{button-D_d9zKq6.js → button-jTamq7gj.js} +2 -2
- package/dist/client/assets/{button-D_d9zKq6.js.map → button-jTamq7gj.js.map} +1 -1
- package/dist/client/assets/{editor-AIcSsY9W.js → editor-QOUTSCme.js} +2 -2
- package/dist/client/assets/{editor-AIcSsY9W.js.map → editor-QOUTSCme.js.map} +1 -1
- package/dist/client/assets/{editor-monaco-json-DHxCqVbk.js → editor-monaco-json-Bi4IKPau.js} +2 -2
- package/dist/client/assets/{editor-monaco-json-DHxCqVbk.js.map → editor-monaco-json-Bi4IKPau.js.map} +1 -1
- package/dist/client/assets/index-CLUTL9ST.js +5 -0
- package/dist/client/assets/index-CLUTL9ST.js.map +1 -0
- package/dist/client/assets/index-DKIIAAih.css +1 -0
- package/dist/client/assets/preview-CtIrY86t.js +3 -0
- package/dist/client/assets/preview-CtIrY86t.js.map +1 -0
- package/dist/client/assets/{radix-ui-BZ5iKMtq.js → radix-ui-BiXCNJNt.js} +2 -2
- package/dist/client/assets/{radix-ui-BZ5iKMtq.js.map → radix-ui-BiXCNJNt.js.map} +1 -1
- package/dist/client/assets/{state-vendor-BDrPu9qj.js → state-vendor-DTum9m7F.js} +2 -2
- package/dist/client/assets/{state-vendor-BDrPu9qj.js.map → state-vendor-DTum9m7F.js.map} +1 -1
- package/dist/client/assets/{ui-vendor-Dyg3GRT-.js → ui-vendor-D3QbouTA.js} +10 -5
- package/dist/client/assets/ui-vendor-D3QbouTA.js.map +1 -0
- package/dist/client/index.html +5 -4
- package/dist/client/templates/Wiseair 16-9.pptx.json +6254 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +12 -6
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
- package/dist/client/assets/HomePage-N11lGfcq.js +0 -99
- package/dist/client/assets/HomePage-N11lGfcq.js.map +0 -1
- package/dist/client/assets/index-B0s8Zyy_.css +0 -1
- package/dist/client/assets/index-DmdAtyxx.js +0 -3
- package/dist/client/assets/index-DmdAtyxx.js.map +0 -1
- package/dist/client/assets/preview-BrqBUZLp.js +0 -3
- package/dist/client/assets/preview-BrqBUZLp.js.map +0 -1
- package/dist/client/assets/ui-vendor-Dyg3GRT-.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1180,6 +1180,150 @@ var init_logger = __esm({
|
|
|
1180
1180
|
});
|
|
1181
1181
|
|
|
1182
1182
|
// src/server/services/generator.ts
|
|
1183
|
+
import {
|
|
1184
|
+
collectFontNamesFromDocx,
|
|
1185
|
+
collectFontNamesFromPptx,
|
|
1186
|
+
POPULAR_GOOGLE_FONTS,
|
|
1187
|
+
getUpstreamOverride,
|
|
1188
|
+
isSafeFont as isSafeFont2
|
|
1189
|
+
} from "@json-to-office/shared";
|
|
1190
|
+
function collectReferencedNames(config2, customThemes, adapterName) {
|
|
1191
|
+
const collect = adapterName === "docx" ? collectFontNamesFromDocx : collectFontNamesFromPptx;
|
|
1192
|
+
const names = /* @__PURE__ */ new Set();
|
|
1193
|
+
for (const n of collect(config2)) names.add(n);
|
|
1194
|
+
for (const theme of Object.values(customThemes ?? {})) {
|
|
1195
|
+
for (const n of collect(theme)) names.add(n);
|
|
1196
|
+
}
|
|
1197
|
+
return names;
|
|
1198
|
+
}
|
|
1199
|
+
function collectReferencedWeights(config2, customThemes) {
|
|
1200
|
+
const weights = /* @__PURE__ */ new Set();
|
|
1201
|
+
const visit = (node) => {
|
|
1202
|
+
if (node == null) return;
|
|
1203
|
+
if (Array.isArray(node)) {
|
|
1204
|
+
for (const item of node) visit(item);
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
if (typeof node === "object") {
|
|
1208
|
+
for (const [k, v] of Object.entries(node)) {
|
|
1209
|
+
if (k === "fontWeight" && typeof v === "number" && v >= 100 && v <= 900) {
|
|
1210
|
+
weights.add(v);
|
|
1211
|
+
} else {
|
|
1212
|
+
visit(v);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
visit(config2);
|
|
1218
|
+
for (const theme of Object.values(customThemes ?? {})) visit(theme);
|
|
1219
|
+
return weights;
|
|
1220
|
+
}
|
|
1221
|
+
function collectReferencedItalic(config2, customThemes) {
|
|
1222
|
+
let found = false;
|
|
1223
|
+
const visit = (node) => {
|
|
1224
|
+
if (found || node == null) return;
|
|
1225
|
+
if (Array.isArray(node)) {
|
|
1226
|
+
for (const item of node) visit(item);
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
if (typeof node === "object") {
|
|
1230
|
+
for (const [k, v] of Object.entries(node)) {
|
|
1231
|
+
if (k === "italic" && v === true) {
|
|
1232
|
+
found = true;
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
visit(v);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
};
|
|
1239
|
+
visit(config2);
|
|
1240
|
+
if (!found) {
|
|
1241
|
+
for (const theme of Object.values(customThemes ?? {})) {
|
|
1242
|
+
visit(theme);
|
|
1243
|
+
if (found) break;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
return found;
|
|
1247
|
+
}
|
|
1248
|
+
function autoGoogleFontEntries(names, skipFamilies, referencedWeights, referencedItalic, warnings) {
|
|
1249
|
+
const googleByLower = new Map(
|
|
1250
|
+
POPULAR_GOOGLE_FONTS.map((f) => [f.family.toLowerCase(), f])
|
|
1251
|
+
);
|
|
1252
|
+
const entries = [];
|
|
1253
|
+
for (const name of names) {
|
|
1254
|
+
if (isSafeFont2(name)) continue;
|
|
1255
|
+
if (skipFamilies.has(name.toLowerCase())) continue;
|
|
1256
|
+
const match = googleByLower.get(name.toLowerCase());
|
|
1257
|
+
if (!match) continue;
|
|
1258
|
+
const wanted = (() => {
|
|
1259
|
+
if (!referencedWeights || referencedWeights.size === 0) {
|
|
1260
|
+
const filtered2 = match.weights.filter((w) => w === 400 || w === 700);
|
|
1261
|
+
if (filtered2.length > 0) return filtered2;
|
|
1262
|
+
return match.weights.length > 0 ? [Math.min(...match.weights)] : [400];
|
|
1263
|
+
}
|
|
1264
|
+
const want = /* @__PURE__ */ new Set([400, ...referencedWeights]);
|
|
1265
|
+
const filtered = match.weights.filter((w) => want.has(w));
|
|
1266
|
+
return filtered.length > 0 ? filtered : [400];
|
|
1267
|
+
})();
|
|
1268
|
+
const override = getUpstreamOverride(match.family);
|
|
1269
|
+
if (override) {
|
|
1270
|
+
const overrideWantedSet = (() => {
|
|
1271
|
+
if (!referencedWeights || referencedWeights.size === 0) {
|
|
1272
|
+
return /* @__PURE__ */ new Set([400, 700]);
|
|
1273
|
+
}
|
|
1274
|
+
return /* @__PURE__ */ new Set([400, ...referencedWeights]);
|
|
1275
|
+
})();
|
|
1276
|
+
const keepItalic = referencedItalic !== false;
|
|
1277
|
+
const variants = override.variants.filter(
|
|
1278
|
+
(v) => overrideWantedSet.has(v.weight) && (keepItalic || !v.italic)
|
|
1279
|
+
);
|
|
1280
|
+
let selected = variants;
|
|
1281
|
+
if (selected.length === 0) {
|
|
1282
|
+
const missing = [...overrideWantedSet].filter((w) => !override.variants.some((v) => v.weight === w)).sort((a, b) => a - b);
|
|
1283
|
+
warnings?.push(
|
|
1284
|
+
`FONT_WEIGHT_NOT_IN_OVERRIDE: family "${match.family}" \u2014 referenced weight(s) ${missing.join(", ")} not in upstream override (has ${override.variants.map((v) => v.weight).filter((w, i, a) => a.indexOf(w) === i).sort((a, b) => a - b).join(", ")}). Fetching all override variants as a fallback.`
|
|
1285
|
+
);
|
|
1286
|
+
selected = override.variants;
|
|
1287
|
+
}
|
|
1288
|
+
entries.push({
|
|
1289
|
+
id: match.family,
|
|
1290
|
+
family: match.family,
|
|
1291
|
+
sources: selected.map(
|
|
1292
|
+
(v) => v.kind === "variable" ? {
|
|
1293
|
+
kind: "variable",
|
|
1294
|
+
url: v.url,
|
|
1295
|
+
weight: v.weight,
|
|
1296
|
+
italic: v.italic ?? false,
|
|
1297
|
+
...v.axes ? { axes: v.axes } : {}
|
|
1298
|
+
} : {
|
|
1299
|
+
kind: "url",
|
|
1300
|
+
url: v.url,
|
|
1301
|
+
weight: v.weight,
|
|
1302
|
+
italic: v.italic ?? false
|
|
1303
|
+
}
|
|
1304
|
+
)
|
|
1305
|
+
});
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
entries.push({
|
|
1309
|
+
id: match.family,
|
|
1310
|
+
family: match.family,
|
|
1311
|
+
sources: [
|
|
1312
|
+
{
|
|
1313
|
+
kind: "google",
|
|
1314
|
+
family: match.family,
|
|
1315
|
+
weights: wanted,
|
|
1316
|
+
// Only request italics when the catalog advertises them. Requesting
|
|
1317
|
+
// italics for a family without italic variants (e.g. Inter) makes
|
|
1318
|
+
// Google return 404s that surface as `FONT_WEIGHT_UNAVAILABLE`
|
|
1319
|
+
// warnings and confuse diagnostics.
|
|
1320
|
+
italics: match.hasItalic
|
|
1321
|
+
}
|
|
1322
|
+
]
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
return entries;
|
|
1326
|
+
}
|
|
1183
1327
|
var GeneratorService;
|
|
1184
1328
|
var init_generator = __esm({
|
|
1185
1329
|
"src/server/services/generator.ts"() {
|
|
@@ -1201,10 +1345,55 @@ var init_generator = __esm({
|
|
|
1201
1345
|
async generate(request) {
|
|
1202
1346
|
const { jsonDefinition, customThemes, options } = request;
|
|
1203
1347
|
const config2 = typeof jsonDefinition === "string" ? JSON.parse(jsonDefinition) : jsonDefinition;
|
|
1204
|
-
const
|
|
1348
|
+
const referencedNames = collectReferencedNames(
|
|
1349
|
+
config2,
|
|
1350
|
+
customThemes,
|
|
1351
|
+
this.adapter.name
|
|
1352
|
+
);
|
|
1353
|
+
const referencedWeights = collectReferencedWeights(config2, customThemes);
|
|
1354
|
+
const referencedItalic = collectReferencedItalic(config2, customThemes);
|
|
1355
|
+
const callerFonts = options?.fonts;
|
|
1356
|
+
const callerExtraEntriesRaw = callerFonts?.extraEntries;
|
|
1357
|
+
const callerExtraEntries = Array.isArray(
|
|
1358
|
+
callerExtraEntriesRaw
|
|
1359
|
+
) ? callerExtraEntriesRaw : [];
|
|
1360
|
+
const callerStrict = typeof callerFonts?.strict === "boolean" ? callerFonts.strict : void 0;
|
|
1361
|
+
const rawMode = callerFonts?.mode;
|
|
1362
|
+
const fontMode = rawMode === "substitute" || rawMode === "custom" ? rawMode : void 0;
|
|
1363
|
+
const rawSub = callerFonts?.substitution;
|
|
1364
|
+
const fontSubstitution = rawSub && typeof rawSub === "object" && !Array.isArray(rawSub) ? rawSub : void 0;
|
|
1365
|
+
const callerFamilies = new Set(
|
|
1366
|
+
callerExtraEntries.map((e) => e.family.toLowerCase())
|
|
1367
|
+
);
|
|
1368
|
+
const overrideWarnings = [];
|
|
1369
|
+
const autoEntries = fontMode === "substitute" ? [] : autoGoogleFontEntries(
|
|
1370
|
+
referencedNames,
|
|
1371
|
+
callerFamilies,
|
|
1372
|
+
referencedWeights,
|
|
1373
|
+
referencedItalic,
|
|
1374
|
+
overrideWarnings
|
|
1375
|
+
);
|
|
1376
|
+
const extraEntries = [...callerExtraEntries, ...autoEntries];
|
|
1377
|
+
if (callerExtraEntries.length > 0) {
|
|
1378
|
+
const googleFamiliesLower = new Set(
|
|
1379
|
+
POPULAR_GOOGLE_FONTS.map((f) => f.family.toLowerCase())
|
|
1380
|
+
);
|
|
1381
|
+
for (const e of callerExtraEntries) {
|
|
1382
|
+
const lower = e.family.toLowerCase();
|
|
1383
|
+
if (googleFamiliesLower.has(lower) && referencedNames.has(e.family)) {
|
|
1384
|
+
overrideWarnings.push(
|
|
1385
|
+
`[FONT_OVERRIDE_LOCAL] ${e.family}: caller-supplied source used; Google Fonts auto-fetch skipped for this family.`
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
const bypassCache = options?.bypassCache === true || extraEntries.length > 0;
|
|
1205
1391
|
const cacheKeyData = {
|
|
1206
1392
|
config: config2,
|
|
1207
|
-
customThemes: customThemes && Object.keys(customThemes).length > 0 ? customThemes : null
|
|
1393
|
+
customThemes: customThemes && Object.keys(customThemes).length > 0 ? customThemes : null,
|
|
1394
|
+
fontMode: fontMode ?? null,
|
|
1395
|
+
fontSubstitution: fontSubstitution ?? null,
|
|
1396
|
+
fontStrict: callerStrict ?? null
|
|
1208
1397
|
};
|
|
1209
1398
|
const cacheKey = this.cacheService.generateCacheKey(cacheKeyData);
|
|
1210
1399
|
const hasDynamicContent = this.cacheService.hasDynamicContent(config2);
|
|
@@ -1226,24 +1415,68 @@ var init_generator = __esm({
|
|
|
1226
1415
|
});
|
|
1227
1416
|
const registry = PluginRegistry.getInstance();
|
|
1228
1417
|
let buffer;
|
|
1418
|
+
const resolvedFonts = [];
|
|
1419
|
+
const needsFontOpts = extraEntries.length > 0 || fontMode !== void 0 || fontSubstitution !== void 0 || callerStrict !== void 0;
|
|
1420
|
+
const fontOpts = needsFontOpts ? {
|
|
1421
|
+
...extraEntries.length > 0 && { extraEntries },
|
|
1422
|
+
...fontMode && { mode: fontMode },
|
|
1423
|
+
...fontSubstitution && { substitution: fontSubstitution },
|
|
1424
|
+
...callerStrict !== void 0 && { strict: callerStrict },
|
|
1425
|
+
onResolved: (r) => {
|
|
1426
|
+
resolvedFonts.push(...r);
|
|
1427
|
+
}
|
|
1428
|
+
} : void 0;
|
|
1229
1429
|
if (registry.hasPlugins()) {
|
|
1230
1430
|
const plugins = registry.getPlugins();
|
|
1231
1431
|
const generator = await this.adapter.createGenerator(plugins, {
|
|
1232
|
-
customThemes
|
|
1432
|
+
customThemes,
|
|
1433
|
+
fonts: fontOpts
|
|
1233
1434
|
});
|
|
1234
1435
|
buffer = await generator.generateBuffer(config2);
|
|
1235
1436
|
} else {
|
|
1236
|
-
buffer = await this.adapter.generateBuffer(config2, {
|
|
1437
|
+
buffer = await this.adapter.generateBuffer(config2, {
|
|
1438
|
+
customThemes,
|
|
1439
|
+
fonts: fontOpts
|
|
1440
|
+
});
|
|
1237
1441
|
}
|
|
1238
1442
|
this.cacheService.set(cacheKey, buffer, config2, {
|
|
1239
1443
|
bypassCache: bypassCache || hasDynamicContent
|
|
1240
1444
|
});
|
|
1445
|
+
const CANONICAL_WEIGHTS = /* @__PURE__ */ new Set([
|
|
1446
|
+
100,
|
|
1447
|
+
200,
|
|
1448
|
+
300,
|
|
1449
|
+
400,
|
|
1450
|
+
500,
|
|
1451
|
+
600,
|
|
1452
|
+
700,
|
|
1453
|
+
800,
|
|
1454
|
+
900
|
|
1455
|
+
]);
|
|
1456
|
+
const nonCanonical = [...referencedWeights].filter(
|
|
1457
|
+
(w) => !CANONICAL_WEIGHTS.has(w)
|
|
1458
|
+
);
|
|
1459
|
+
const extraWarnings = overrideWarnings.map((message) => ({
|
|
1460
|
+
component: "fontRegistry",
|
|
1461
|
+
message,
|
|
1462
|
+
severity: "info",
|
|
1463
|
+
context: { code: "FONT_OVERRIDE_LOCAL" }
|
|
1464
|
+
}));
|
|
1465
|
+
for (const w of nonCanonical) {
|
|
1466
|
+
extraWarnings.push({
|
|
1467
|
+
component: "fontRegistry",
|
|
1468
|
+
message: `[FONT_NONCANONICAL_WEIGHT] fontWeight ${w} is not one of 100/200/.../900; render path rounds to Regular or Bold via bold-only fallback.`,
|
|
1469
|
+
severity: "info",
|
|
1470
|
+
context: { code: "FONT_NONCANONICAL_WEIGHT" }
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1241
1473
|
return {
|
|
1242
1474
|
filename: `${config2.metadata?.title || this.adapter.label}${this.adapter.extension}`,
|
|
1243
1475
|
fileId: Date.now().toString(),
|
|
1244
1476
|
buffer,
|
|
1245
1477
|
cached: false,
|
|
1246
|
-
warnings: null
|
|
1478
|
+
warnings: extraWarnings.length > 0 ? extraWarnings : null,
|
|
1479
|
+
resolvedFonts
|
|
1247
1480
|
};
|
|
1248
1481
|
}
|
|
1249
1482
|
async validate(jsonDefinition) {
|
|
@@ -1368,20 +1601,414 @@ var init_cache = __esm({
|
|
|
1368
1601
|
}
|
|
1369
1602
|
});
|
|
1370
1603
|
|
|
1604
|
+
// src/server/services/font-staging/noop-stager.ts
|
|
1605
|
+
var NoopFontStager;
|
|
1606
|
+
var init_noop_stager = __esm({
|
|
1607
|
+
"src/server/services/font-staging/noop-stager.ts"() {
|
|
1608
|
+
"use strict";
|
|
1609
|
+
init_esm_shims();
|
|
1610
|
+
NoopFontStager = class {
|
|
1611
|
+
async stage() {
|
|
1612
|
+
return {
|
|
1613
|
+
envOverrides: {},
|
|
1614
|
+
cleanup: async () => {
|
|
1615
|
+
}
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
// src/server/services/font-staging/types.ts
|
|
1623
|
+
function nextStagingId() {
|
|
1624
|
+
counter += 1;
|
|
1625
|
+
return `${process.pid}-${counter}`;
|
|
1626
|
+
}
|
|
1627
|
+
function safeFilenamePart(s) {
|
|
1628
|
+
return s.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 48);
|
|
1629
|
+
}
|
|
1630
|
+
var counter;
|
|
1631
|
+
var init_types = __esm({
|
|
1632
|
+
"src/server/services/font-staging/types.ts"() {
|
|
1633
|
+
"use strict";
|
|
1634
|
+
init_esm_shims();
|
|
1635
|
+
counter = 0;
|
|
1636
|
+
}
|
|
1637
|
+
});
|
|
1638
|
+
|
|
1639
|
+
// src/server/services/font-staging/fontconfig-stager.ts
|
|
1640
|
+
import { promises as fs10 } from "fs";
|
|
1641
|
+
import path13 from "path";
|
|
1642
|
+
import {
|
|
1643
|
+
synthesizeFamilyName,
|
|
1644
|
+
rewriteFontFamilyName
|
|
1645
|
+
} from "@json-to-office/shared";
|
|
1646
|
+
function escapeXml(s) {
|
|
1647
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1648
|
+
}
|
|
1649
|
+
var SYSTEM_FONTS_CONF_CANDIDATES, FontconfigStager;
|
|
1650
|
+
var init_fontconfig_stager = __esm({
|
|
1651
|
+
"src/server/services/font-staging/fontconfig-stager.ts"() {
|
|
1652
|
+
"use strict";
|
|
1653
|
+
init_esm_shims();
|
|
1654
|
+
init_types();
|
|
1655
|
+
SYSTEM_FONTS_CONF_CANDIDATES = [
|
|
1656
|
+
"/etc/fonts/fonts.conf",
|
|
1657
|
+
"/opt/homebrew/etc/fonts/fonts.conf",
|
|
1658
|
+
"/usr/local/etc/fonts/fonts.conf"
|
|
1659
|
+
];
|
|
1660
|
+
FontconfigStager = class {
|
|
1661
|
+
async stage(fonts, tempDir) {
|
|
1662
|
+
const id = nextStagingId();
|
|
1663
|
+
const fontsDir = path13.join(tempDir, "fonts");
|
|
1664
|
+
await fs10.mkdir(fontsDir, { recursive: true });
|
|
1665
|
+
let serial = 0;
|
|
1666
|
+
for (const r of fonts) {
|
|
1667
|
+
if (r.sources.length === 0) continue;
|
|
1668
|
+
for (const s of r.sources) {
|
|
1669
|
+
serial += 1;
|
|
1670
|
+
const suffix = s.italic ? "i" : "r";
|
|
1671
|
+
const synth = synthesizeFamilyName(r.family, s.weight, s.italic);
|
|
1672
|
+
const data = synth.family === r.family ? s.data : rewriteFontFamilyName(s.data, synth.family);
|
|
1673
|
+
const name = `${safeFilenamePart(synth.family)}-${s.weight}${suffix}-${id}-${serial}.ttf`;
|
|
1674
|
+
await fs10.writeFile(path13.join(fontsDir, name), data);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
await fs10.chmod(fontsDir, 365).catch(() => {
|
|
1678
|
+
});
|
|
1679
|
+
const includeLines = await this.pickSystemIncludes();
|
|
1680
|
+
const cacheDir = path13.join(tempDir, "fc-cache");
|
|
1681
|
+
await fs10.mkdir(cacheDir, { recursive: true });
|
|
1682
|
+
const configPath = path13.join(tempDir, "fontconfig.xml");
|
|
1683
|
+
const configXml = [
|
|
1684
|
+
'<?xml version="1.0"?>',
|
|
1685
|
+
'<!DOCTYPE fontconfig SYSTEM "fonts.dtd">',
|
|
1686
|
+
"<fontconfig>",
|
|
1687
|
+
` <dir>${escapeXml(fontsDir)}</dir>`,
|
|
1688
|
+
` <cachedir>${escapeXml(cacheDir)}</cachedir>`,
|
|
1689
|
+
...includeLines,
|
|
1690
|
+
"</fontconfig>",
|
|
1691
|
+
""
|
|
1692
|
+
].join("\n");
|
|
1693
|
+
await fs10.writeFile(configPath, configXml, "utf8");
|
|
1694
|
+
return {
|
|
1695
|
+
envOverrides: {
|
|
1696
|
+
FONTCONFIG_FILE: configPath,
|
|
1697
|
+
XDG_CACHE_HOME: cacheDir
|
|
1698
|
+
},
|
|
1699
|
+
cleanup: async () => {
|
|
1700
|
+
}
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
async pickSystemIncludes() {
|
|
1704
|
+
for (const candidate of SYSTEM_FONTS_CONF_CANDIDATES) {
|
|
1705
|
+
try {
|
|
1706
|
+
await fs10.access(candidate);
|
|
1707
|
+
return [
|
|
1708
|
+
` <include ignore_missing="yes">${escapeXml(candidate)}</include>`
|
|
1709
|
+
];
|
|
1710
|
+
} catch {
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
return [` <include ignore_missing="yes">/etc/fonts/fonts.conf</include>`];
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
});
|
|
1718
|
+
|
|
1719
|
+
// src/server/services/font-staging/windows-stager.ts
|
|
1720
|
+
import { promises as fs11 } from "fs";
|
|
1721
|
+
import path14 from "path";
|
|
1722
|
+
import {
|
|
1723
|
+
synthesizeFamilyName as synthesizeFamilyName2,
|
|
1724
|
+
rewriteFontFamilyName as rewriteFontFamilyName2
|
|
1725
|
+
} from "@json-to-office/shared";
|
|
1726
|
+
async function getGdiBindings() {
|
|
1727
|
+
if (cachedBindings) return cachedBindings;
|
|
1728
|
+
const koffi = await import("koffi");
|
|
1729
|
+
const mod = koffi.default ?? koffi;
|
|
1730
|
+
const gdi32 = mod.load("gdi32.dll");
|
|
1731
|
+
cachedBindings = {
|
|
1732
|
+
addFont: gdi32.func("int __stdcall AddFontResourceW(str16)"),
|
|
1733
|
+
removeFont: gdi32.func("bool __stdcall RemoveFontResourceW(str16)")
|
|
1734
|
+
};
|
|
1735
|
+
return cachedBindings;
|
|
1736
|
+
}
|
|
1737
|
+
var cachedBindings, WindowsFontStager;
|
|
1738
|
+
var init_windows_stager = __esm({
|
|
1739
|
+
"src/server/services/font-staging/windows-stager.ts"() {
|
|
1740
|
+
"use strict";
|
|
1741
|
+
init_esm_shims();
|
|
1742
|
+
init_types();
|
|
1743
|
+
cachedBindings = null;
|
|
1744
|
+
WindowsFontStager = class {
|
|
1745
|
+
async stage(fonts, tempDir) {
|
|
1746
|
+
const id = nextStagingId();
|
|
1747
|
+
const fontsDir = path14.join(tempDir, "fonts");
|
|
1748
|
+
await fs11.mkdir(fontsDir, { recursive: true });
|
|
1749
|
+
const stagedPaths = [];
|
|
1750
|
+
let serial = 0;
|
|
1751
|
+
for (const r of fonts) {
|
|
1752
|
+
if (r.sources.length === 0) continue;
|
|
1753
|
+
for (const s of r.sources) {
|
|
1754
|
+
serial += 1;
|
|
1755
|
+
const suffix = s.italic ? "i" : "r";
|
|
1756
|
+
const synth = synthesizeFamilyName2(r.family, s.weight, s.italic);
|
|
1757
|
+
const data = synth.family === r.family ? s.data : rewriteFontFamilyName2(s.data, synth.family);
|
|
1758
|
+
const name = `${safeFilenamePart(synth.family)}-${s.weight}${suffix}-${id}-${serial}.ttf`;
|
|
1759
|
+
const fullPath = path14.join(fontsDir, name);
|
|
1760
|
+
await fs11.writeFile(fullPath, data);
|
|
1761
|
+
stagedPaths.push(fullPath);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
if (stagedPaths.length === 0) {
|
|
1765
|
+
return { envOverrides: {}, cleanup: async () => {
|
|
1766
|
+
} };
|
|
1767
|
+
}
|
|
1768
|
+
const { addFont, removeFont } = await getGdiBindings();
|
|
1769
|
+
const registered = [];
|
|
1770
|
+
for (const p of stagedPaths) {
|
|
1771
|
+
const added = addFont(p);
|
|
1772
|
+
if (added > 0) registered.push(p);
|
|
1773
|
+
}
|
|
1774
|
+
let cleaned = false;
|
|
1775
|
+
return {
|
|
1776
|
+
envOverrides: {
|
|
1777
|
+
// Force GDI backend so the freshly-registered fonts are visible.
|
|
1778
|
+
// Skia on Windows uses DirectWrite which does not reliably see
|
|
1779
|
+
// fonts added via AddFontResourceW.
|
|
1780
|
+
SAL_DISABLE_SKIA: "1"
|
|
1781
|
+
},
|
|
1782
|
+
cleanup: async () => {
|
|
1783
|
+
if (cleaned) return;
|
|
1784
|
+
cleaned = true;
|
|
1785
|
+
for (const p of registered) {
|
|
1786
|
+
try {
|
|
1787
|
+
removeFont(p);
|
|
1788
|
+
} catch {
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
});
|
|
1797
|
+
|
|
1798
|
+
// src/server/services/font-staging/macos-stager.ts
|
|
1799
|
+
import { promises as fs12 } from "fs";
|
|
1800
|
+
import path15 from "path";
|
|
1801
|
+
import {
|
|
1802
|
+
synthesizeFamilyName as synthesizeFamilyName3,
|
|
1803
|
+
rewriteFontFamilyName as rewriteFontFamilyName3
|
|
1804
|
+
} from "@json-to-office/shared";
|
|
1805
|
+
var PYTHON_MACRO, REGISTRY_MOD_XCU, MacOSCoreTextStager;
|
|
1806
|
+
var init_macos_stager = __esm({
|
|
1807
|
+
"src/server/services/font-staging/macos-stager.ts"() {
|
|
1808
|
+
"use strict";
|
|
1809
|
+
init_esm_shims();
|
|
1810
|
+
init_types();
|
|
1811
|
+
PYTHON_MACRO = `# Auto-generated by @json-to-office/jto. Runs inside soffice on OnStartApp
|
|
1812
|
+
# to make staged fonts visible to LibreOffice's font enumeration. macOS 26
|
|
1813
|
+
# blocks Session/Persistent CT registration for unsigned callers, but
|
|
1814
|
+
# Process scope still works from inside the target process \u2014 which is
|
|
1815
|
+
# exactly where this macro runs.
|
|
1816
|
+
import os
|
|
1817
|
+
import sys
|
|
1818
|
+
import ctypes
|
|
1819
|
+
import ctypes.util
|
|
1820
|
+
|
|
1821
|
+
|
|
1822
|
+
def _log(msg):
|
|
1823
|
+
# soffice swallows Python stdout in headless mode; stderr surfaces to
|
|
1824
|
+
# the parent's pipe. The stager doesn't read this today but the
|
|
1825
|
+
# converter logs stderr on failure, which is how we'll debug.
|
|
1826
|
+
sys.stderr.write("[jto-font-register] " + msg + "\\n")
|
|
1827
|
+
|
|
1828
|
+
|
|
1829
|
+
def register(*_args):
|
|
1830
|
+
paths_env = os.environ.get("JTO_FONT_PATHS", "")
|
|
1831
|
+
if not paths_env:
|
|
1832
|
+
return
|
|
1833
|
+
try:
|
|
1834
|
+
cf = ctypes.CDLL(ctypes.util.find_library("CoreFoundation"))
|
|
1835
|
+
ct = ctypes.CDLL(ctypes.util.find_library("CoreText"))
|
|
1836
|
+
except Exception as e:
|
|
1837
|
+
_log("failed to load CoreFoundation/CoreText: " + repr(e))
|
|
1838
|
+
return
|
|
1839
|
+
cf.CFURLCreateFromFileSystemRepresentation.argtypes = [
|
|
1840
|
+
ctypes.c_void_p, ctypes.c_char_p, ctypes.c_long, ctypes.c_bool,
|
|
1841
|
+
]
|
|
1842
|
+
cf.CFURLCreateFromFileSystemRepresentation.restype = ctypes.c_void_p
|
|
1843
|
+
cf.CFRelease.argtypes = [ctypes.c_void_p]
|
|
1844
|
+
ct.CTFontManagerRegisterFontsForURL.argtypes = [
|
|
1845
|
+
ctypes.c_void_p, ctypes.c_uint32, ctypes.c_void_p,
|
|
1846
|
+
]
|
|
1847
|
+
ct.CTFontManagerRegisterFontsForURL.restype = ctypes.c_bool
|
|
1848
|
+
|
|
1849
|
+
kCTFontManagerScopeProcess = 1
|
|
1850
|
+
registered = 0
|
|
1851
|
+
for p in paths_env.split(os.pathsep):
|
|
1852
|
+
if not p:
|
|
1853
|
+
continue
|
|
1854
|
+
try:
|
|
1855
|
+
b = p.encode("utf-8")
|
|
1856
|
+
url = cf.CFURLCreateFromFileSystemRepresentation(
|
|
1857
|
+
None, b, len(b), False
|
|
1858
|
+
)
|
|
1859
|
+
if not url:
|
|
1860
|
+
_log("CFURL failed for " + p)
|
|
1861
|
+
continue
|
|
1862
|
+
ok = ct.CTFontManagerRegisterFontsForURL(
|
|
1863
|
+
url, kCTFontManagerScopeProcess, None
|
|
1864
|
+
)
|
|
1865
|
+
cf.CFRelease(url)
|
|
1866
|
+
if ok:
|
|
1867
|
+
registered += 1
|
|
1868
|
+
else:
|
|
1869
|
+
_log("CT register returned false for " + p)
|
|
1870
|
+
except Exception as e:
|
|
1871
|
+
_log("exception registering " + p + ": " + repr(e))
|
|
1872
|
+
_log("registered " + str(registered) + " font(s) at Process scope")
|
|
1873
|
+
|
|
1874
|
+
|
|
1875
|
+
# Expose under "register" (event-binding URL) and module-level run so
|
|
1876
|
+
# command-line vnd.sun.star.script invocation works either way.
|
|
1877
|
+
g_exportedScripts = (register,)
|
|
1878
|
+
`;
|
|
1879
|
+
REGISTRY_MOD_XCU = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1880
|
+
<oor:items xmlns:oor="http://openoffice.org/2001/registry" xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
1881
|
+
<item oor:path="/org.openoffice.Office.Events/ApplicationEvents/Bindings">
|
|
1882
|
+
<node oor:name="OnStartApp" oor:op="replace">
|
|
1883
|
+
<prop oor:name="BindingURL" oor:type="xs:string">
|
|
1884
|
+
<value>vnd.sun.star.script:JtoFontRegister.py$register?language=Python&location=user</value>
|
|
1885
|
+
</prop>
|
|
1886
|
+
</node>
|
|
1887
|
+
</item>
|
|
1888
|
+
<item oor:path="/org.openoffice.Office.Common/Security/Scripting">
|
|
1889
|
+
<prop oor:name="MacroSecurityLevel" oor:op="fuse">
|
|
1890
|
+
<value>0</value>
|
|
1891
|
+
</prop>
|
|
1892
|
+
</item>
|
|
1893
|
+
</oor:items>
|
|
1894
|
+
`;
|
|
1895
|
+
MacOSCoreTextStager = class {
|
|
1896
|
+
async stage(fonts, tempDir) {
|
|
1897
|
+
const embeddable = fonts.filter((r) => r.sources.length > 0);
|
|
1898
|
+
if (embeddable.length === 0) {
|
|
1899
|
+
return { envOverrides: {}, cleanup: async () => {
|
|
1900
|
+
} };
|
|
1901
|
+
}
|
|
1902
|
+
const fontsDir = path15.join(tempDir, "fonts");
|
|
1903
|
+
await fs12.mkdir(fontsDir, { recursive: true });
|
|
1904
|
+
const id = nextStagingId();
|
|
1905
|
+
const fontPaths = [];
|
|
1906
|
+
const staged = [];
|
|
1907
|
+
let serial = 0;
|
|
1908
|
+
for (const r of embeddable) {
|
|
1909
|
+
for (const s of r.sources) {
|
|
1910
|
+
serial += 1;
|
|
1911
|
+
const suffix = s.italic ? "i" : "r";
|
|
1912
|
+
const synth = synthesizeFamilyName3(r.family, s.weight, s.italic);
|
|
1913
|
+
const data = synth.family === r.family ? s.data : rewriteFontFamilyName3(s.data, synth.family);
|
|
1914
|
+
const name = `${safeFilenamePart(synth.family)}-${s.weight}${suffix}-${id}-${serial}.ttf`;
|
|
1915
|
+
const full = path15.join(fontsDir, name);
|
|
1916
|
+
await fs12.writeFile(full, data);
|
|
1917
|
+
fontPaths.push(full);
|
|
1918
|
+
staged.push({
|
|
1919
|
+
family: synth.family,
|
|
1920
|
+
weight: s.weight,
|
|
1921
|
+
italic: s.italic,
|
|
1922
|
+
path: full
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
if (process.env.JTO_DEBUG_FONTS === "1") {
|
|
1927
|
+
console.log(
|
|
1928
|
+
"[jto macos-stager] staged " + staged.length + " font(s) for CT Process-scope registration; JTO_FONT_PATHS has " + fontPaths.length + " entries\n" + staged.map(
|
|
1929
|
+
(s) => ` ${s.family} (w=${s.weight}${s.italic ? " italic" : ""}) \u2192 ${path15.basename(s.path)}`
|
|
1930
|
+
).join("\n")
|
|
1931
|
+
);
|
|
1932
|
+
}
|
|
1933
|
+
const profileUser = path15.join(tempDir, "user-profile", "user");
|
|
1934
|
+
const scriptsDir = path15.join(profileUser, "Scripts", "python");
|
|
1935
|
+
await fs12.mkdir(scriptsDir, { recursive: true });
|
|
1936
|
+
await fs12.writeFile(
|
|
1937
|
+
path15.join(scriptsDir, "JtoFontRegister.py"),
|
|
1938
|
+
PYTHON_MACRO
|
|
1939
|
+
);
|
|
1940
|
+
await fs12.writeFile(
|
|
1941
|
+
path15.join(profileUser, "registrymodifications.xcu"),
|
|
1942
|
+
REGISTRY_MOD_XCU
|
|
1943
|
+
);
|
|
1944
|
+
return {
|
|
1945
|
+
envOverrides: {
|
|
1946
|
+
// Colon-separated list of staged TTF paths (`:` matches Python's
|
|
1947
|
+
// `os.pathsep` on macOS). The macro reads this at OnStartApp.
|
|
1948
|
+
JTO_FONT_PATHS: fontPaths.join(":"),
|
|
1949
|
+
// Force LibreOffice's Core Graphics backend. Skia on macOS can
|
|
1950
|
+
// skip Core Text's freshly-registered fonts in some builds.
|
|
1951
|
+
SAL_DISABLE_SKIA: "1"
|
|
1952
|
+
},
|
|
1953
|
+
cleanup: async () => {
|
|
1954
|
+
}
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
});
|
|
1960
|
+
|
|
1961
|
+
// src/server/services/font-staging/index.ts
|
|
1962
|
+
function getFontStager(platform = process.platform) {
|
|
1963
|
+
const hit = cached.get(platform);
|
|
1964
|
+
if (hit) return hit;
|
|
1965
|
+
let stager;
|
|
1966
|
+
switch (platform) {
|
|
1967
|
+
case "win32":
|
|
1968
|
+
stager = new WindowsFontStager();
|
|
1969
|
+
break;
|
|
1970
|
+
case "darwin":
|
|
1971
|
+
stager = new MacOSCoreTextStager();
|
|
1972
|
+
break;
|
|
1973
|
+
case "linux":
|
|
1974
|
+
case "freebsd":
|
|
1975
|
+
case "openbsd":
|
|
1976
|
+
stager = new FontconfigStager();
|
|
1977
|
+
break;
|
|
1978
|
+
default:
|
|
1979
|
+
stager = new NoopFontStager();
|
|
1980
|
+
}
|
|
1981
|
+
cached.set(platform, stager);
|
|
1982
|
+
return stager;
|
|
1983
|
+
}
|
|
1984
|
+
var cached;
|
|
1985
|
+
var init_font_staging = __esm({
|
|
1986
|
+
"src/server/services/font-staging/index.ts"() {
|
|
1987
|
+
"use strict";
|
|
1988
|
+
init_esm_shims();
|
|
1989
|
+
init_noop_stager();
|
|
1990
|
+
init_fontconfig_stager();
|
|
1991
|
+
init_windows_stager();
|
|
1992
|
+
init_macos_stager();
|
|
1993
|
+
cached = /* @__PURE__ */ new Map();
|
|
1994
|
+
}
|
|
1995
|
+
});
|
|
1996
|
+
|
|
1371
1997
|
// src/server/services/libreoffice-converter.ts
|
|
1372
1998
|
import { execFile } from "child_process";
|
|
1373
|
-
import { promises as
|
|
1999
|
+
import { promises as fs13 } from "fs";
|
|
1374
2000
|
import os from "os";
|
|
1375
|
-
import
|
|
1376
|
-
function executeFile(binary, args, timeoutMs) {
|
|
1377
|
-
return new Promise((
|
|
2001
|
+
import path16 from "path";
|
|
2002
|
+
function executeFile(binary, args, timeoutMs, envOverrides) {
|
|
2003
|
+
return new Promise((resolve18, reject) => {
|
|
1378
2004
|
execFile(
|
|
1379
2005
|
binary,
|
|
1380
2006
|
args,
|
|
1381
2007
|
{
|
|
1382
2008
|
timeout: timeoutMs,
|
|
1383
2009
|
maxBuffer: MAX_EXEC_BUFFER_BYTES,
|
|
1384
|
-
windowsHide: true
|
|
2010
|
+
windowsHide: true,
|
|
2011
|
+
env: envOverrides ? { ...process.env, ...envOverrides } : process.env
|
|
1385
2012
|
},
|
|
1386
2013
|
(error, stdout, stderr) => {
|
|
1387
2014
|
if (error) {
|
|
@@ -1391,7 +2018,7 @@ function executeFile(binary, args, timeoutMs) {
|
|
|
1391
2018
|
reject(execError);
|
|
1392
2019
|
return;
|
|
1393
2020
|
}
|
|
1394
|
-
|
|
2021
|
+
resolve18({ stdout: stdout ?? "", stderr: stderr ?? "" });
|
|
1395
2022
|
}
|
|
1396
2023
|
);
|
|
1397
2024
|
});
|
|
@@ -1403,7 +2030,7 @@ function toErrorText(value) {
|
|
|
1403
2030
|
return String(value);
|
|
1404
2031
|
}
|
|
1405
2032
|
function sanitizeBaseName(originalName) {
|
|
1406
|
-
const parsed =
|
|
2033
|
+
const parsed = path16.parse(originalName || "document");
|
|
1407
2034
|
return (parsed.name || "document").replace(/[^a-zA-Z0-9._-]/g, "_") || "document";
|
|
1408
2035
|
}
|
|
1409
2036
|
var DEFAULT_CONVERSION_TIMEOUT_MS, BINARY_PROBE_TIMEOUT_MS, MAX_EXEC_BUFFER_BYTES, LibreOfficeError, LibreOfficeBinaryNotFoundError, LibreOfficeTimeoutError, LibreOfficeConversionError, LibreOfficeOutputNotFoundError, LibreOfficeConverterService;
|
|
@@ -1412,6 +2039,7 @@ var init_libreoffice_converter = __esm({
|
|
|
1412
2039
|
"use strict";
|
|
1413
2040
|
init_esm_shims();
|
|
1414
2041
|
init_config();
|
|
2042
|
+
init_font_staging();
|
|
1415
2043
|
DEFAULT_CONVERSION_TIMEOUT_MS = 3e4;
|
|
1416
2044
|
BINARY_PROBE_TIMEOUT_MS = 5e3;
|
|
1417
2045
|
MAX_EXEC_BUFFER_BYTES = 20 * 1024 * 1024;
|
|
@@ -1471,24 +2099,35 @@ var init_libreoffice_converter = __esm({
|
|
|
1471
2099
|
this.format = format;
|
|
1472
2100
|
this.timeoutMs = timeoutMs || config.LIBREOFFICE_TIMEOUT_MS || DEFAULT_CONVERSION_TIMEOUT_MS;
|
|
1473
2101
|
}
|
|
1474
|
-
async convertToPdf(input, originalName = "document") {
|
|
2102
|
+
async convertToPdf(input, originalName = "document", resolvedFonts) {
|
|
1475
2103
|
if (!input || input.length === 0) {
|
|
1476
2104
|
throw new LibreOfficeConversionError("Input file is empty");
|
|
1477
2105
|
}
|
|
1478
2106
|
const binaryPath = await this.resolveBinaryPath();
|
|
1479
|
-
const tempDir = await
|
|
1480
|
-
|
|
2107
|
+
const tempDir = await fs13.mkdtemp(
|
|
2108
|
+
path16.join(os.tmpdir(), "jto-libreoffice-")
|
|
1481
2109
|
);
|
|
1482
2110
|
const outputBaseName = sanitizeBaseName(originalName);
|
|
1483
2111
|
const ext = this.format === "pptx" ? ".pptx" : ".docx";
|
|
1484
|
-
const inputPath =
|
|
1485
|
-
const pdfPath =
|
|
2112
|
+
const inputPath = path16.join(tempDir, `${outputBaseName}${ext}`);
|
|
2113
|
+
const pdfPath = path16.join(tempDir, `${outputBaseName}.pdf`);
|
|
2114
|
+
const stager = getFontStager();
|
|
2115
|
+
const fontsToStage = (resolvedFonts ?? []).filter(
|
|
2116
|
+
(r) => r.sources.length > 0
|
|
2117
|
+
);
|
|
2118
|
+
const stageHandle = fontsToStage.length > 0 ? await stager.stage(fontsToStage, tempDir) : null;
|
|
1486
2119
|
try {
|
|
1487
|
-
await
|
|
2120
|
+
await fs13.writeFile(inputPath, input);
|
|
1488
2121
|
const filterName = this.format === "pptx" ? "impress_pdf_Export" : "writer_pdf_Export";
|
|
1489
|
-
await this.runConversion(
|
|
2122
|
+
await this.runConversion(
|
|
2123
|
+
binaryPath,
|
|
2124
|
+
inputPath,
|
|
2125
|
+
tempDir,
|
|
2126
|
+
filterName,
|
|
2127
|
+
stageHandle?.envOverrides
|
|
2128
|
+
);
|
|
1490
2129
|
try {
|
|
1491
|
-
return await
|
|
2130
|
+
return await fs13.readFile(pdfPath);
|
|
1492
2131
|
} catch (error) {
|
|
1493
2132
|
if (error.code === "ENOENT") {
|
|
1494
2133
|
throw new LibreOfficeOutputNotFoundError(pdfPath);
|
|
@@ -1496,7 +2135,11 @@ var init_libreoffice_converter = __esm({
|
|
|
1496
2135
|
throw error;
|
|
1497
2136
|
}
|
|
1498
2137
|
} finally {
|
|
1499
|
-
|
|
2138
|
+
if (stageHandle) {
|
|
2139
|
+
await stageHandle.cleanup().catch(() => {
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
await fs13.rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
1500
2143
|
});
|
|
1501
2144
|
}
|
|
1502
2145
|
}
|
|
@@ -1523,9 +2166,9 @@ var init_libreoffice_converter = __esm({
|
|
|
1523
2166
|
throw new LibreOfficeBinaryNotFoundError(candidates);
|
|
1524
2167
|
}
|
|
1525
2168
|
async isBinaryAvailable(binaryPath) {
|
|
1526
|
-
if (binaryPath.includes(
|
|
2169
|
+
if (binaryPath.includes(path16.sep)) {
|
|
1527
2170
|
try {
|
|
1528
|
-
await
|
|
2171
|
+
await fs13.access(binaryPath);
|
|
1529
2172
|
} catch {
|
|
1530
2173
|
return false;
|
|
1531
2174
|
}
|
|
@@ -1538,8 +2181,8 @@ var init_libreoffice_converter = __esm({
|
|
|
1538
2181
|
return code !== "ENOENT" && code !== "EACCES";
|
|
1539
2182
|
}
|
|
1540
2183
|
}
|
|
1541
|
-
async runConversion(binaryPath, inputPath, outputDir, filterName) {
|
|
1542
|
-
const userProfilePath =
|
|
2184
|
+
async runConversion(binaryPath, inputPath, outputDir, filterName, envOverrides) {
|
|
2185
|
+
const userProfilePath = path16.join(outputDir, "user-profile").replace(/\\/g, "/");
|
|
1543
2186
|
const userInstallation = `file:///${userProfilePath.replace(/^\//, "")}`;
|
|
1544
2187
|
const args = [
|
|
1545
2188
|
"--headless",
|
|
@@ -1553,8 +2196,31 @@ var init_libreoffice_converter = __esm({
|
|
|
1553
2196
|
outputDir,
|
|
1554
2197
|
inputPath
|
|
1555
2198
|
];
|
|
2199
|
+
if (process.env.JTO_DEBUG_FONTS === "1") {
|
|
2200
|
+
console.log(
|
|
2201
|
+
"[jto libreoffice-converter] spawning soffice with env overrides: " + JSON.stringify(
|
|
2202
|
+
envOverrides ? Object.fromEntries(
|
|
2203
|
+
Object.entries(envOverrides).map(([k, v]) => [
|
|
2204
|
+
k,
|
|
2205
|
+
k === "JTO_FONT_PATHS" ? `<${v.split(":").length} paths>` : v
|
|
2206
|
+
])
|
|
2207
|
+
) : {}
|
|
2208
|
+
)
|
|
2209
|
+
);
|
|
2210
|
+
}
|
|
1556
2211
|
try {
|
|
1557
|
-
await executeFile(
|
|
2212
|
+
const result = await executeFile(
|
|
2213
|
+
binaryPath,
|
|
2214
|
+
args,
|
|
2215
|
+
this.timeoutMs,
|
|
2216
|
+
envOverrides
|
|
2217
|
+
);
|
|
2218
|
+
if (process.env.JTO_DEBUG_FONTS === "1") {
|
|
2219
|
+
const stderr = result.stderr?.trim();
|
|
2220
|
+
console.log(
|
|
2221
|
+
"[jto libreoffice-converter] conversion ok; stderr len=" + (stderr?.length ?? 0) + (stderr ? "\n" + stderr : " (empty)")
|
|
2222
|
+
);
|
|
2223
|
+
}
|
|
1558
2224
|
} catch (error) {
|
|
1559
2225
|
const execError = error;
|
|
1560
2226
|
if (execError.code === "ETIMEDOUT")
|
|
@@ -1670,11 +2336,27 @@ var init_health = __esm({
|
|
|
1670
2336
|
|
|
1671
2337
|
// src/server/schemas/loose.ts
|
|
1672
2338
|
import { Type as Type2 } from "@sinclair/typebox";
|
|
1673
|
-
var LooseDocumentGenerationRequestSchema, LooseDocumentValidationRequestSchema;
|
|
2339
|
+
var FontOptionsSchema, LooseDocumentGenerationRequestSchema, LooseDocumentValidationRequestSchema;
|
|
1674
2340
|
var init_loose = __esm({
|
|
1675
2341
|
"src/server/schemas/loose.ts"() {
|
|
1676
2342
|
"use strict";
|
|
1677
2343
|
init_esm_shims();
|
|
2344
|
+
FontOptionsSchema = Type2.Object(
|
|
2345
|
+
{
|
|
2346
|
+
mode: Type2.Optional(
|
|
2347
|
+
Type2.Union([Type2.Literal("substitute"), Type2.Literal("custom")])
|
|
2348
|
+
),
|
|
2349
|
+
substitution: Type2.Optional(
|
|
2350
|
+
Type2.Record(
|
|
2351
|
+
Type2.String({ maxLength: 128 }),
|
|
2352
|
+
Type2.String({ maxLength: 128 }),
|
|
2353
|
+
{ maxProperties: 256 }
|
|
2354
|
+
)
|
|
2355
|
+
),
|
|
2356
|
+
strict: Type2.Optional(Type2.Boolean())
|
|
2357
|
+
},
|
|
2358
|
+
{ additionalProperties: false }
|
|
2359
|
+
);
|
|
1678
2360
|
LooseDocumentGenerationRequestSchema = Type2.Object(
|
|
1679
2361
|
{
|
|
1680
2362
|
jsonDefinition: Type2.Union([
|
|
@@ -1684,7 +2366,16 @@ var init_loose = __esm({
|
|
|
1684
2366
|
// Allow any object
|
|
1685
2367
|
]),
|
|
1686
2368
|
customThemes: Type2.Optional(Type2.Record(Type2.String(), Type2.Unknown())),
|
|
1687
|
-
options: Type2.Optional(
|
|
2369
|
+
options: Type2.Optional(
|
|
2370
|
+
Type2.Object(
|
|
2371
|
+
{
|
|
2372
|
+
bypassCache: Type2.Optional(Type2.Boolean()),
|
|
2373
|
+
returnUrl: Type2.Optional(Type2.Boolean()),
|
|
2374
|
+
fonts: Type2.Optional(FontOptionsSchema)
|
|
2375
|
+
},
|
|
2376
|
+
{ additionalProperties: true }
|
|
2377
|
+
)
|
|
2378
|
+
)
|
|
1688
2379
|
},
|
|
1689
2380
|
{ additionalProperties: true }
|
|
1690
2381
|
);
|
|
@@ -1812,6 +2503,7 @@ var init_rate_limit = __esm({
|
|
|
1812
2503
|
// src/server/routes/format.ts
|
|
1813
2504
|
import { Hono as Hono2 } from "hono";
|
|
1814
2505
|
import { HTTPException as HTTPException3 } from "hono/http-exception";
|
|
2506
|
+
import { bodyLimit } from "hono/body-limit";
|
|
1815
2507
|
function createFormatRouter(adapter) {
|
|
1816
2508
|
const router = new Hono2();
|
|
1817
2509
|
const contentTypeMw = async (c, next) => {
|
|
@@ -1838,10 +2530,22 @@ function createFormatRouter(adapter) {
|
|
|
1838
2530
|
const requestId = c.get("requestId");
|
|
1839
2531
|
try {
|
|
1840
2532
|
const bypassCache = c.req.header("X-Bypass-Cache") === "true" || c.req.query("bypass-cache") === "true" || options?.bypassCache === true;
|
|
2533
|
+
let sanitizedFonts;
|
|
2534
|
+
const rawFonts = options?.fonts;
|
|
2535
|
+
if (rawFonts && "strict" in rawFonts) {
|
|
2536
|
+
sanitizedFonts = { ...rawFonts };
|
|
2537
|
+
delete sanitizedFonts.strict;
|
|
2538
|
+
} else if (rawFonts) {
|
|
2539
|
+
sanitizedFonts = rawFonts;
|
|
2540
|
+
}
|
|
1841
2541
|
const result = await generatorService.generate({
|
|
1842
2542
|
jsonDefinition,
|
|
1843
2543
|
customThemes,
|
|
1844
|
-
options: {
|
|
2544
|
+
options: {
|
|
2545
|
+
...options,
|
|
2546
|
+
...sanitizedFonts !== void 0 && { fonts: sanitizedFonts },
|
|
2547
|
+
bypassCache
|
|
2548
|
+
}
|
|
1845
2549
|
});
|
|
1846
2550
|
const cacheService = getContainer().get("cacheService");
|
|
1847
2551
|
const cacheStats = cacheService.getStats();
|
|
@@ -1961,6 +2665,68 @@ function createFormatRouter(adapter) {
|
|
|
1961
2665
|
}
|
|
1962
2666
|
}
|
|
1963
2667
|
);
|
|
2668
|
+
router.post(
|
|
2669
|
+
"/preview/libreoffice-from-json",
|
|
2670
|
+
bodyLimit({
|
|
2671
|
+
// Doc JSON + custom themes. 2 MB covers large report fixtures; anything
|
|
2672
|
+
// bigger is almost certainly an attempt to OOM the worker.
|
|
2673
|
+
maxSize: 2 * 1024 * 1024,
|
|
2674
|
+
onError: () => {
|
|
2675
|
+
throw new HTTPException3(413, { message: "Request body too large" });
|
|
2676
|
+
}
|
|
2677
|
+
}),
|
|
2678
|
+
rateLimiter({
|
|
2679
|
+
limit: process.env.NODE_ENV === "production" ? 20 : 1e3,
|
|
2680
|
+
window: 15 * 60 * 1e3,
|
|
2681
|
+
keyGenerator: (c) => c.req.header("X-Real-IP") || c.req.header("X-Forwarded-For")?.split(",").pop()?.trim() || "anonymous"
|
|
2682
|
+
}),
|
|
2683
|
+
contentTypeMw,
|
|
2684
|
+
tbValidator(LooseDocumentGenerationRequestSchema),
|
|
2685
|
+
async (c) => {
|
|
2686
|
+
const requestId = c.get("requestId");
|
|
2687
|
+
const generatorService = getContainer().get("generatorService");
|
|
2688
|
+
const libreOfficeService = getContainer().get(
|
|
2689
|
+
"libreOfficeConverterService"
|
|
2690
|
+
);
|
|
2691
|
+
const { jsonDefinition, customThemes } = getValidated(c, "json");
|
|
2692
|
+
try {
|
|
2693
|
+
const generated = await generatorService.generate({
|
|
2694
|
+
jsonDefinition,
|
|
2695
|
+
customThemes,
|
|
2696
|
+
options: { bypassCache: true }
|
|
2697
|
+
});
|
|
2698
|
+
const pdfBuffer = await libreOfficeService.convertToPdf(
|
|
2699
|
+
generated.buffer,
|
|
2700
|
+
generated.filename,
|
|
2701
|
+
generated.resolvedFonts
|
|
2702
|
+
);
|
|
2703
|
+
const pdfName = generated.filename.replace(/\.[^.]+$/i, "") + ".pdf";
|
|
2704
|
+
c.header("Content-Type", "application/pdf");
|
|
2705
|
+
c.header("Content-Disposition", `inline; filename="${pdfName}"`);
|
|
2706
|
+
c.header("Content-Length", String(pdfBuffer.length));
|
|
2707
|
+
return c.body(pdfBuffer);
|
|
2708
|
+
} catch (error) {
|
|
2709
|
+
logger.error("LibreOffice (JSON) preview failed", {
|
|
2710
|
+
error,
|
|
2711
|
+
requestId
|
|
2712
|
+
});
|
|
2713
|
+
if (error instanceof HTTPException3) throw error;
|
|
2714
|
+
if (error instanceof LibreOfficeBinaryNotFoundError) {
|
|
2715
|
+
throw new HTTPException3(503, {
|
|
2716
|
+
message: "LibreOffice is not available. Install LibreOffice or set LIBREOFFICE_PATH."
|
|
2717
|
+
});
|
|
2718
|
+
}
|
|
2719
|
+
if (error instanceof LibreOfficeTimeoutError || error instanceof LibreOfficeConversionError || error instanceof LibreOfficeOutputNotFoundError) {
|
|
2720
|
+
throw new HTTPException3(500, {
|
|
2721
|
+
message: "LibreOffice preview conversion failed."
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
2724
|
+
throw new HTTPException3(500, {
|
|
2725
|
+
message: "Internal server error during preview conversion"
|
|
2726
|
+
});
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
);
|
|
1964
2730
|
router.post(
|
|
1965
2731
|
"/standard-components",
|
|
1966
2732
|
contentTypeMw,
|
|
@@ -2455,15 +3221,15 @@ var init_ai_schema = __esm({
|
|
|
2455
3221
|
|
|
2456
3222
|
// src/server/services/prompt-loader.ts
|
|
2457
3223
|
import { readFileSync as readFileSync5, existsSync as existsSync6 } from "fs";
|
|
2458
|
-
import { join as
|
|
3224
|
+
import { join as join8, dirname as dirname5 } from "path";
|
|
2459
3225
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2460
3226
|
function resolvePromptsDir() {
|
|
2461
3227
|
const candidates = [
|
|
2462
|
-
|
|
3228
|
+
join8(__dirname2, "..", "prompts"),
|
|
2463
3229
|
// dev: src/server/prompts
|
|
2464
|
-
|
|
3230
|
+
join8(__dirname2, "prompts"),
|
|
2465
3231
|
// bundled: dist/prompts
|
|
2466
|
-
|
|
3232
|
+
join8(__dirname2, "..", "src", "server", "prompts")
|
|
2467
3233
|
// prod from dist/
|
|
2468
3234
|
];
|
|
2469
3235
|
for (const dir of candidates) {
|
|
@@ -2473,7 +3239,7 @@ function resolvePromptsDir() {
|
|
|
2473
3239
|
}
|
|
2474
3240
|
function readPromptFile(name) {
|
|
2475
3241
|
if (cache.has(name)) return cache.get(name);
|
|
2476
|
-
const filePath =
|
|
3242
|
+
const filePath = join8(PROMPTS_DIR, name);
|
|
2477
3243
|
const content = readFileSync5(filePath, "utf-8");
|
|
2478
3244
|
cache.set(name, content);
|
|
2479
3245
|
return content;
|
|
@@ -2836,6 +3602,110 @@ var init_ai = __esm({
|
|
|
2836
3602
|
}
|
|
2837
3603
|
});
|
|
2838
3604
|
|
|
3605
|
+
// src/server/routes/fonts.ts
|
|
3606
|
+
import { createHash } from "crypto";
|
|
3607
|
+
import { Hono as Hono5 } from "hono";
|
|
3608
|
+
import { HTTPException as HTTPException4 } from "hono/http-exception";
|
|
3609
|
+
import { bodyLimit as bodyLimit2 } from "hono/body-limit";
|
|
3610
|
+
import {
|
|
3611
|
+
SAFE_FONTS,
|
|
3612
|
+
POPULAR_GOOGLE_FONTS as POPULAR_GOOGLE_FONTS2,
|
|
3613
|
+
fetchGoogleFontSources
|
|
3614
|
+
} from "@json-to-office/shared";
|
|
3615
|
+
var fontsRouter, MATERIALIZE_FAMILY_MAX, MATERIALIZE_WEIGHTS_MAX, CATALOG_BODY, CATALOG_ETAG;
|
|
3616
|
+
var init_fonts = __esm({
|
|
3617
|
+
"src/server/routes/fonts.ts"() {
|
|
3618
|
+
"use strict";
|
|
3619
|
+
init_esm_shims();
|
|
3620
|
+
init_rate_limit();
|
|
3621
|
+
fontsRouter = new Hono5();
|
|
3622
|
+
MATERIALIZE_FAMILY_MAX = 64;
|
|
3623
|
+
MATERIALIZE_WEIGHTS_MAX = 9;
|
|
3624
|
+
CATALOG_BODY = JSON.stringify({
|
|
3625
|
+
safe: SAFE_FONTS,
|
|
3626
|
+
google: POPULAR_GOOGLE_FONTS2
|
|
3627
|
+
});
|
|
3628
|
+
CATALOG_ETAG = `"${createHash("sha256").update(CATALOG_BODY).digest("base64").slice(0, 24)}"`;
|
|
3629
|
+
fontsRouter.get("/catalog", (c) => {
|
|
3630
|
+
const ifNoneMatch = c.req.header("If-None-Match");
|
|
3631
|
+
if (ifNoneMatch && ifNoneMatch === CATALOG_ETAG) {
|
|
3632
|
+
c.header("ETag", CATALOG_ETAG);
|
|
3633
|
+
c.header("Cache-Control", "public, max-age=86400");
|
|
3634
|
+
return c.body(null, 304);
|
|
3635
|
+
}
|
|
3636
|
+
c.header("Content-Type", "application/json");
|
|
3637
|
+
c.header("ETag", CATALOG_ETAG);
|
|
3638
|
+
c.header("Cache-Control", "public, max-age=86400");
|
|
3639
|
+
return c.body(CATALOG_BODY, 200);
|
|
3640
|
+
});
|
|
3641
|
+
fontsRouter.post(
|
|
3642
|
+
"/materialize",
|
|
3643
|
+
bodyLimit2({
|
|
3644
|
+
// Body is just {family, weights, italics} — 16 KB is generous.
|
|
3645
|
+
maxSize: 16 * 1024,
|
|
3646
|
+
onError: () => {
|
|
3647
|
+
throw new HTTPException4(413, { message: "Request body too large" });
|
|
3648
|
+
}
|
|
3649
|
+
}),
|
|
3650
|
+
rateLimiter({
|
|
3651
|
+
limit: process.env.NODE_ENV === "production" ? 20 : 1e3,
|
|
3652
|
+
window: 15 * 60 * 1e3,
|
|
3653
|
+
keyGenerator: (c) => c.req.header("X-Real-IP") || c.req.header("X-Forwarded-For")?.split(",").pop()?.trim() || "anonymous"
|
|
3654
|
+
}),
|
|
3655
|
+
async (c) => {
|
|
3656
|
+
let body;
|
|
3657
|
+
try {
|
|
3658
|
+
body = await c.req.json();
|
|
3659
|
+
} catch {
|
|
3660
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
3661
|
+
}
|
|
3662
|
+
const family = body.family?.trim();
|
|
3663
|
+
if (!family) {
|
|
3664
|
+
return c.json({ error: "Missing required field: family" }, 400);
|
|
3665
|
+
}
|
|
3666
|
+
if (family.length > MATERIALIZE_FAMILY_MAX) {
|
|
3667
|
+
return c.json(
|
|
3668
|
+
{ error: `family exceeds ${MATERIALIZE_FAMILY_MAX} characters` },
|
|
3669
|
+
400
|
|
3670
|
+
);
|
|
3671
|
+
}
|
|
3672
|
+
const weights = Array.isArray(body.weights) && body.weights.length > 0 ? Array.from(
|
|
3673
|
+
new Set(
|
|
3674
|
+
body.weights.filter(
|
|
3675
|
+
(w) => typeof w === "number" && w >= 100 && w <= 900
|
|
3676
|
+
)
|
|
3677
|
+
)
|
|
3678
|
+
).slice(0, MATERIALIZE_WEIGHTS_MAX) : [400, 700];
|
|
3679
|
+
const italics = Boolean(body.italics);
|
|
3680
|
+
try {
|
|
3681
|
+
const { sources, warnings } = await fetchGoogleFontSources({
|
|
3682
|
+
family,
|
|
3683
|
+
weights,
|
|
3684
|
+
italics
|
|
3685
|
+
});
|
|
3686
|
+
return c.json({
|
|
3687
|
+
family,
|
|
3688
|
+
sources: sources.map((s) => ({
|
|
3689
|
+
weight: s.weight,
|
|
3690
|
+
italic: s.italic,
|
|
3691
|
+
format: s.format,
|
|
3692
|
+
data: s.data.toString("base64")
|
|
3693
|
+
})),
|
|
3694
|
+
warnings
|
|
3695
|
+
});
|
|
3696
|
+
} catch (err) {
|
|
3697
|
+
return c.json(
|
|
3698
|
+
{
|
|
3699
|
+
error: `Google Fonts materialize failed: ${err.message}`
|
|
3700
|
+
},
|
|
3701
|
+
502
|
|
3702
|
+
);
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
);
|
|
3706
|
+
}
|
|
3707
|
+
});
|
|
3708
|
+
|
|
2839
3709
|
// src/server/middleware/hono/request-id.ts
|
|
2840
3710
|
import { randomUUID } from "crypto";
|
|
2841
3711
|
var requestIdMiddleware;
|
|
@@ -2890,7 +3760,7 @@ var init_auth = __esm({
|
|
|
2890
3760
|
});
|
|
2891
3761
|
|
|
2892
3762
|
// src/server/middleware/hono/error-handler.ts
|
|
2893
|
-
import { HTTPException as
|
|
3763
|
+
import { HTTPException as HTTPException5 } from "hono/http-exception";
|
|
2894
3764
|
var errorHandler;
|
|
2895
3765
|
var init_error_handler = __esm({
|
|
2896
3766
|
"src/server/middleware/hono/error-handler.ts"() {
|
|
@@ -2900,7 +3770,7 @@ var init_error_handler = __esm({
|
|
|
2900
3770
|
init_config();
|
|
2901
3771
|
errorHandler = (err, c) => {
|
|
2902
3772
|
const requestId = c.get("requestId") || "unknown";
|
|
2903
|
-
if (err instanceof
|
|
3773
|
+
if (err instanceof HTTPException5) {
|
|
2904
3774
|
return c.json(
|
|
2905
3775
|
{
|
|
2906
3776
|
success: false,
|
|
@@ -3068,14 +3938,14 @@ var init_request_logger = __esm({
|
|
|
3068
3938
|
});
|
|
3069
3939
|
|
|
3070
3940
|
// src/server/app.ts
|
|
3071
|
-
import { Hono as
|
|
3941
|
+
import { Hono as Hono6 } from "hono";
|
|
3072
3942
|
import { cors } from "hono/cors";
|
|
3073
3943
|
import { secureHeaders } from "hono/secure-headers";
|
|
3074
3944
|
import { logger as honoLogger } from "hono/logger";
|
|
3075
3945
|
import { timing } from "hono/timing";
|
|
3076
3946
|
function createAPIApp(adapter) {
|
|
3077
3947
|
Container.initialize(adapter);
|
|
3078
|
-
const honoApp = new
|
|
3948
|
+
const honoApp = new Hono6();
|
|
3079
3949
|
honoApp.use("*", timing());
|
|
3080
3950
|
honoApp.use("*", honoLogger());
|
|
3081
3951
|
honoApp.use(
|
|
@@ -3103,6 +3973,7 @@ function createAPIApp(adapter) {
|
|
|
3103
3973
|
const legacyPath = adapter.name === "docx" ? "/api/documents" : "/api/presentations";
|
|
3104
3974
|
honoApp.route(legacyPath, formatRouter);
|
|
3105
3975
|
honoApp.route("/api/discovery", discoveryRouter);
|
|
3976
|
+
honoApp.route("/api/fonts", fontsRouter);
|
|
3106
3977
|
if (process.env.AI_ENABLED !== "false") {
|
|
3107
3978
|
honoApp.route("/api/ai", createAiRouter());
|
|
3108
3979
|
}
|
|
@@ -3139,6 +4010,7 @@ var init_app = __esm({
|
|
|
3139
4010
|
init_format();
|
|
3140
4011
|
init_discovery();
|
|
3141
4012
|
init_ai();
|
|
4013
|
+
init_fonts();
|
|
3142
4014
|
init_request_id();
|
|
3143
4015
|
init_auth();
|
|
3144
4016
|
init_error_handler();
|
|
@@ -3154,11 +4026,11 @@ var unified_server_exports = {};
|
|
|
3154
4026
|
__export(unified_server_exports, {
|
|
3155
4027
|
UnifiedServer: () => UnifiedServer
|
|
3156
4028
|
});
|
|
3157
|
-
import { Hono as
|
|
4029
|
+
import { Hono as Hono7 } from "hono";
|
|
3158
4030
|
import { serve } from "@hono/node-server";
|
|
3159
4031
|
import { logger as logger2 } from "hono/logger";
|
|
3160
4032
|
import { compress } from "hono/compress";
|
|
3161
|
-
import { resolve as
|
|
4033
|
+
import { resolve as resolve16, dirname as dirname6 } from "path";
|
|
3162
4034
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
3163
4035
|
var __filename2, __dirname3, UnifiedServer;
|
|
3164
4036
|
var init_unified_server = __esm({
|
|
@@ -3178,7 +4050,7 @@ var init_unified_server = __esm({
|
|
|
3178
4050
|
constructor(adapter, config2) {
|
|
3179
4051
|
this.adapter = adapter;
|
|
3180
4052
|
this.config = config2;
|
|
3181
|
-
this.app = new
|
|
4053
|
+
this.app = new Hono7();
|
|
3182
4054
|
}
|
|
3183
4055
|
async initialize() {
|
|
3184
4056
|
await this.setupMiddleware();
|
|
@@ -3204,9 +4076,9 @@ var init_unified_server = __esm({
|
|
|
3204
4076
|
return await apiApp.fetch(c.req.raw);
|
|
3205
4077
|
});
|
|
3206
4078
|
this.app.use("*", async (c, next) => {
|
|
3207
|
-
const
|
|
3208
|
-
if (
|
|
3209
|
-
if (/\.(ts|tsx|js|jsx|css|map)$/.test(
|
|
4079
|
+
const path17 = c.req.path;
|
|
4080
|
+
if (path17.startsWith("/api")) {
|
|
4081
|
+
if (/\.(ts|tsx|js|jsx|css|map)$/.test(path17)) {
|
|
3210
4082
|
return next();
|
|
3211
4083
|
}
|
|
3212
4084
|
const response = await apiApp.fetch(c.req.raw);
|
|
@@ -3239,15 +4111,15 @@ var init_unified_server = __esm({
|
|
|
3239
4111
|
let clientPath = null;
|
|
3240
4112
|
if (process.env.JTO_CLIENT_PATH) {
|
|
3241
4113
|
const envClientPath = process.env.JTO_CLIENT_PATH;
|
|
3242
|
-
if (existsSync7(envClientPath) && existsSync7(
|
|
4114
|
+
if (existsSync7(envClientPath) && existsSync7(resolve16(envClientPath, "index.html"))) {
|
|
3243
4115
|
clientPath = envClientPath;
|
|
3244
4116
|
logger.debug("[Dev Server] Using client from JTO_CLIENT_PATH", {
|
|
3245
4117
|
path: clientPath
|
|
3246
4118
|
});
|
|
3247
4119
|
}
|
|
3248
4120
|
} else if (isBundled) {
|
|
3249
|
-
const bundledClientPath =
|
|
3250
|
-
if (existsSync7(bundledClientPath) && existsSync7(
|
|
4121
|
+
const bundledClientPath = resolve16(__dirname3, "client");
|
|
4122
|
+
if (existsSync7(bundledClientPath) && existsSync7(resolve16(bundledClientPath, "index.html"))) {
|
|
3251
4123
|
clientPath = bundledClientPath;
|
|
3252
4124
|
logger.debug("[Dev Server] Using bundled client at", {
|
|
3253
4125
|
path: clientPath
|
|
@@ -3255,8 +4127,8 @@ var init_unified_server = __esm({
|
|
|
3255
4127
|
}
|
|
3256
4128
|
} else {
|
|
3257
4129
|
const possiblePaths = [
|
|
3258
|
-
|
|
3259
|
-
|
|
4130
|
+
resolve16(__dirname3, "../client"),
|
|
4131
|
+
resolve16(__dirname3, "../../dist/client")
|
|
3260
4132
|
];
|
|
3261
4133
|
for (const p of possiblePaths) {
|
|
3262
4134
|
if (existsSync7(p)) {
|
|
@@ -3278,7 +4150,7 @@ var init_unified_server = __esm({
|
|
|
3278
4150
|
logger.warn("[Dev Server] Vite not available for dev mode");
|
|
3279
4151
|
}
|
|
3280
4152
|
break;
|
|
3281
|
-
} else if (existsSync7(
|
|
4153
|
+
} else if (existsSync7(resolve16(p, "index.html"))) {
|
|
3282
4154
|
clientPath = p;
|
|
3283
4155
|
logger.debug("[Dev Server] Using pre-built client at", {
|
|
3284
4156
|
path: clientPath
|
|
@@ -3292,7 +4164,7 @@ var init_unified_server = __esm({
|
|
|
3292
4164
|
return this.setupBuiltClient(clientPath);
|
|
3293
4165
|
}
|
|
3294
4166
|
if (!this.viteServer && !isBundled) {
|
|
3295
|
-
const sourceClientPath =
|
|
4167
|
+
const sourceClientPath = resolve16(__dirname3, "../client");
|
|
3296
4168
|
try {
|
|
3297
4169
|
const { createServer: createViteServer } = await import("vite");
|
|
3298
4170
|
this.viteServer = await createViteServer({
|
|
@@ -3309,17 +4181,17 @@ var init_unified_server = __esm({
|
|
|
3309
4181
|
}
|
|
3310
4182
|
if (this.viteServer) {
|
|
3311
4183
|
this.app.use("*", async (c, next) => {
|
|
3312
|
-
const
|
|
3313
|
-
if ((
|
|
4184
|
+
const path17 = c.req.path;
|
|
4185
|
+
if ((path17.startsWith("/api") || path17 === "/health") && !/\.(ts|tsx|js|jsx|css|map)$/.test(path17)) {
|
|
3314
4186
|
return next();
|
|
3315
4187
|
}
|
|
3316
4188
|
const env = c.env;
|
|
3317
4189
|
const req = env.incoming || env.req;
|
|
3318
4190
|
const res = env.outgoing || env.res;
|
|
3319
4191
|
if (req && res && this.viteServer) {
|
|
3320
|
-
return new Promise((
|
|
4192
|
+
return new Promise((resolve18) => {
|
|
3321
4193
|
this.viteServer.middlewares.handle(req, res, () => {
|
|
3322
|
-
|
|
4194
|
+
resolve18(next());
|
|
3323
4195
|
});
|
|
3324
4196
|
});
|
|
3325
4197
|
}
|
|
@@ -3329,26 +4201,26 @@ var init_unified_server = __esm({
|
|
|
3329
4201
|
}
|
|
3330
4202
|
async setupBuiltClient(clientPath) {
|
|
3331
4203
|
const { serveStatic } = await import("@hono/node-server/serve-static");
|
|
3332
|
-
const
|
|
3333
|
-
const { extname:
|
|
4204
|
+
const fs14 = await import("fs");
|
|
4205
|
+
const { extname: extname4 } = await import("path");
|
|
3334
4206
|
const mime = await import("mime-types");
|
|
3335
4207
|
const format = this.adapter.name.replace(/[^a-zA-Z0-9]/g, "");
|
|
3336
4208
|
this.app.use(
|
|
3337
4209
|
"/assets/*",
|
|
3338
4210
|
serveStatic({
|
|
3339
4211
|
root: clientPath,
|
|
3340
|
-
rewriteRequestPath: (
|
|
4212
|
+
rewriteRequestPath: (path17) => path17.replace(/^\/assets/, "/assets")
|
|
3341
4213
|
})
|
|
3342
4214
|
);
|
|
3343
4215
|
this.app.use("/*", async (c, next) => {
|
|
3344
4216
|
const reqPath = c.req.path;
|
|
3345
4217
|
if (reqPath.startsWith("/api") || reqPath === "/health") return next();
|
|
3346
|
-
const ext =
|
|
4218
|
+
const ext = extname4(reqPath);
|
|
3347
4219
|
if (ext) {
|
|
3348
|
-
const filePath =
|
|
3349
|
-
if (
|
|
4220
|
+
const filePath = resolve16(clientPath, reqPath.slice(1));
|
|
4221
|
+
if (fs14.existsSync(filePath)) {
|
|
3350
4222
|
const mimeType = mime.lookup(filePath) || "application/octet-stream";
|
|
3351
|
-
const content =
|
|
4223
|
+
const content = fs14.readFileSync(filePath);
|
|
3352
4224
|
c.header("Content-Type", mimeType);
|
|
3353
4225
|
return c.body(content);
|
|
3354
4226
|
}
|
|
@@ -3359,9 +4231,9 @@ var init_unified_server = __esm({
|
|
|
3359
4231
|
const reqPath = c.req.path;
|
|
3360
4232
|
if (reqPath.startsWith("/api") || reqPath === "/health")
|
|
3361
4233
|
return c.notFound();
|
|
3362
|
-
const indexPath =
|
|
3363
|
-
if (
|
|
3364
|
-
let html =
|
|
4234
|
+
const indexPath = resolve16(clientPath, "index.html");
|
|
4235
|
+
if (fs14.existsSync(indexPath)) {
|
|
4236
|
+
let html = fs14.readFileSync(indexPath, "utf-8");
|
|
3365
4237
|
html = html.replace(
|
|
3366
4238
|
"</head>",
|
|
3367
4239
|
`<script>window.__JTO_FORMAT__ = '${format}';</script>
|
|
@@ -3373,12 +4245,12 @@ var init_unified_server = __esm({
|
|
|
3373
4245
|
});
|
|
3374
4246
|
}
|
|
3375
4247
|
async setupProdClient() {
|
|
3376
|
-
const clientPath =
|
|
4248
|
+
const clientPath = resolve16(__dirname3, "client");
|
|
3377
4249
|
return this.setupBuiltClient(clientPath);
|
|
3378
4250
|
}
|
|
3379
4251
|
async start() {
|
|
3380
4252
|
await this.initialize();
|
|
3381
|
-
return new Promise((
|
|
4253
|
+
return new Promise((resolve18) => {
|
|
3382
4254
|
this.server = serve(
|
|
3383
4255
|
{
|
|
3384
4256
|
fetch: this.app.fetch,
|
|
@@ -3386,7 +4258,7 @@ var init_unified_server = __esm({
|
|
|
3386
4258
|
hostname: this.config.server.host
|
|
3387
4259
|
},
|
|
3388
4260
|
() => {
|
|
3389
|
-
|
|
4261
|
+
resolve18();
|
|
3390
4262
|
}
|
|
3391
4263
|
);
|
|
3392
4264
|
});
|
|
@@ -3394,8 +4266,8 @@ var init_unified_server = __esm({
|
|
|
3394
4266
|
async stop() {
|
|
3395
4267
|
if (this.viteServer) await this.viteServer.close();
|
|
3396
4268
|
if (this.server) {
|
|
3397
|
-
return new Promise((
|
|
3398
|
-
this.server?.close(() =>
|
|
4269
|
+
return new Promise((resolve18) => {
|
|
4270
|
+
this.server?.close(() => resolve18());
|
|
3399
4271
|
});
|
|
3400
4272
|
}
|
|
3401
4273
|
}
|
|
@@ -3405,8 +4277,8 @@ var init_unified_server = __esm({
|
|
|
3405
4277
|
|
|
3406
4278
|
// src/cli.ts
|
|
3407
4279
|
init_esm_shims();
|
|
3408
|
-
import { Command as
|
|
3409
|
-
import
|
|
4280
|
+
import { Command as Command8 } from "commander";
|
|
4281
|
+
import chalk10 from "chalk";
|
|
3410
4282
|
|
|
3411
4283
|
// src/format-adapter.ts
|
|
3412
4284
|
init_esm_shims();
|
|
@@ -3440,7 +4312,8 @@ var DocxFormatAdapter = class {
|
|
|
3440
4312
|
const services = buildServicesFromEnv();
|
|
3441
4313
|
return await core.generateBufferFromJson(docDefinition, {
|
|
3442
4314
|
customThemes,
|
|
3443
|
-
services
|
|
4315
|
+
services,
|
|
4316
|
+
fonts: options.fonts
|
|
3444
4317
|
});
|
|
3445
4318
|
}
|
|
3446
4319
|
async createGenerator(plugins, options) {
|
|
@@ -3455,7 +4328,8 @@ var DocxFormatAdapter = class {
|
|
|
3455
4328
|
const customThemes2 = await this.loadCustomThemes(options);
|
|
3456
4329
|
return await core.generateBufferFromJson(docDefinition, {
|
|
3457
4330
|
customThemes: customThemes2,
|
|
3458
|
-
services
|
|
4331
|
+
services,
|
|
4332
|
+
fonts: options.fonts
|
|
3459
4333
|
});
|
|
3460
4334
|
},
|
|
3461
4335
|
hasPlugins: false,
|
|
@@ -3468,7 +4342,8 @@ var DocxFormatAdapter = class {
|
|
|
3468
4342
|
theme,
|
|
3469
4343
|
customThemes,
|
|
3470
4344
|
debug: process.env.DEBUG === "true",
|
|
3471
|
-
services
|
|
4345
|
+
services,
|
|
4346
|
+
fonts: options.fonts
|
|
3472
4347
|
});
|
|
3473
4348
|
for (const plugin of plugins) {
|
|
3474
4349
|
generator = generator.addComponent(plugin);
|
|
@@ -3660,7 +4535,8 @@ var PptxFormatAdapter = class {
|
|
|
3660
4535
|
const services = buildServicesFromEnv();
|
|
3661
4536
|
return await core.generateBufferFromJson(docDefinition, {
|
|
3662
4537
|
customThemes,
|
|
3663
|
-
services
|
|
4538
|
+
services,
|
|
4539
|
+
fonts: options.fonts
|
|
3664
4540
|
});
|
|
3665
4541
|
}
|
|
3666
4542
|
async createGenerator(plugins, options) {
|
|
@@ -3675,7 +4551,8 @@ var PptxFormatAdapter = class {
|
|
|
3675
4551
|
const customThemes2 = await this.loadCustomThemes(options);
|
|
3676
4552
|
return await core.generateBufferFromJson(docDefinition, {
|
|
3677
4553
|
customThemes: customThemes2,
|
|
3678
|
-
services
|
|
4554
|
+
services,
|
|
4555
|
+
fonts: options.fonts
|
|
3679
4556
|
});
|
|
3680
4557
|
},
|
|
3681
4558
|
hasPlugins: false,
|
|
@@ -3688,7 +4565,8 @@ var PptxFormatAdapter = class {
|
|
|
3688
4565
|
theme,
|
|
3689
4566
|
customThemes,
|
|
3690
4567
|
debug: process.env.DEBUG === "true",
|
|
3691
|
-
services
|
|
4568
|
+
services,
|
|
4569
|
+
fonts: options.fonts
|
|
3692
4570
|
});
|
|
3693
4571
|
for (const plugin of plugins) {
|
|
3694
4572
|
generator = generator.addComponent(plugin);
|
|
@@ -3809,7 +4687,7 @@ init_esm_shims();
|
|
|
3809
4687
|
init_plugin_registry();
|
|
3810
4688
|
import { Command } from "commander";
|
|
3811
4689
|
import { readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
3812
|
-
import { resolve as
|
|
4690
|
+
import { resolve as resolve10, basename as basename5 } from "path";
|
|
3813
4691
|
import ora from "ora";
|
|
3814
4692
|
import chalk2 from "chalk";
|
|
3815
4693
|
import boxen from "boxen";
|
|
@@ -4243,7 +5121,125 @@ function formatError(error) {
|
|
|
4243
5121
|
}
|
|
4244
5122
|
}
|
|
4245
5123
|
|
|
5124
|
+
// src/commands/font-flags.ts
|
|
5125
|
+
init_esm_shims();
|
|
5126
|
+
import { readdirSync, statSync as statSync2 } from "fs";
|
|
5127
|
+
import { basename as basename4, extname, join as join4, resolve as resolve9 } from "path";
|
|
5128
|
+
var WEIGHT_NAMES = {
|
|
5129
|
+
thin: 100,
|
|
5130
|
+
hairline: 100,
|
|
5131
|
+
extralight: 200,
|
|
5132
|
+
ultralight: 200,
|
|
5133
|
+
light: 300,
|
|
5134
|
+
regular: 400,
|
|
5135
|
+
normal: 400,
|
|
5136
|
+
book: 400,
|
|
5137
|
+
medium: 500,
|
|
5138
|
+
semibold: 600,
|
|
5139
|
+
demibold: 600,
|
|
5140
|
+
bold: 700,
|
|
5141
|
+
extrabold: 800,
|
|
5142
|
+
ultrabold: 800,
|
|
5143
|
+
black: 900,
|
|
5144
|
+
heavy: 900
|
|
5145
|
+
};
|
|
5146
|
+
var SUPPORTED_EXT = /* @__PURE__ */ new Set([".ttf", ".otf"]);
|
|
5147
|
+
function parseFontFilename(filename) {
|
|
5148
|
+
const stem = basename4(filename, extname(filename));
|
|
5149
|
+
const parts = stem.split(/[-_\s]+/).filter(Boolean);
|
|
5150
|
+
if (parts.length <= 1) {
|
|
5151
|
+
return { family: stem, weight: 400, italic: false };
|
|
5152
|
+
}
|
|
5153
|
+
const tail = parts[parts.length - 1].toLowerCase();
|
|
5154
|
+
const italic = /italic|oblique/.test(tail);
|
|
5155
|
+
const withoutItalic = tail.replace(/italic|oblique/g, "");
|
|
5156
|
+
let weight = null;
|
|
5157
|
+
if (withoutItalic !== "") {
|
|
5158
|
+
const numeric = parseInt(withoutItalic, 10);
|
|
5159
|
+
if (!Number.isNaN(numeric) && String(numeric) === withoutItalic && numeric >= 100 && numeric <= 900) {
|
|
5160
|
+
weight = numeric;
|
|
5161
|
+
} else if (withoutItalic in WEIGHT_NAMES) {
|
|
5162
|
+
weight = WEIGHT_NAMES[withoutItalic];
|
|
5163
|
+
}
|
|
5164
|
+
} else if (italic) {
|
|
5165
|
+
weight = 400;
|
|
5166
|
+
}
|
|
5167
|
+
if (weight === null) {
|
|
5168
|
+
return { family: stem, weight: 400, italic: false };
|
|
5169
|
+
}
|
|
5170
|
+
const family = parts.slice(0, -1).join(" ");
|
|
5171
|
+
return { family, weight, italic };
|
|
5172
|
+
}
|
|
5173
|
+
function parseFontFlag(spec, cwd = process.cwd()) {
|
|
5174
|
+
const eq = spec.indexOf("=");
|
|
5175
|
+
if (eq < 0) {
|
|
5176
|
+
throw new Error(
|
|
5177
|
+
`--font expects <name>=<path>, got "${spec}". Example: --font Inter=./fonts/Inter-Regular.ttf`
|
|
5178
|
+
);
|
|
5179
|
+
}
|
|
5180
|
+
const family = spec.slice(0, eq).trim();
|
|
5181
|
+
const path17 = spec.slice(eq + 1).trim();
|
|
5182
|
+
if (!family || !path17) {
|
|
5183
|
+
throw new Error(`--font has empty name or path: "${spec}"`);
|
|
5184
|
+
}
|
|
5185
|
+
const absolutePath = resolve9(cwd, path17);
|
|
5186
|
+
const variant = parseFontFilename(path17);
|
|
5187
|
+
return {
|
|
5188
|
+
id: family,
|
|
5189
|
+
family,
|
|
5190
|
+
sources: [
|
|
5191
|
+
{
|
|
5192
|
+
kind: "file",
|
|
5193
|
+
path: absolutePath,
|
|
5194
|
+
weight: variant.weight,
|
|
5195
|
+
italic: variant.italic
|
|
5196
|
+
}
|
|
5197
|
+
]
|
|
5198
|
+
};
|
|
5199
|
+
}
|
|
5200
|
+
function parseFontsDir(dir, cwd = process.cwd()) {
|
|
5201
|
+
const absoluteDir = resolve9(cwd, dir);
|
|
5202
|
+
let entries;
|
|
5203
|
+
try {
|
|
5204
|
+
entries = readdirSync(absoluteDir);
|
|
5205
|
+
} catch (err) {
|
|
5206
|
+
throw new Error(
|
|
5207
|
+
`--fonts-dir "${dir}" could not be read: ${err.message}`
|
|
5208
|
+
);
|
|
5209
|
+
}
|
|
5210
|
+
const byFamily = /* @__PURE__ */ new Map();
|
|
5211
|
+
for (const name of entries) {
|
|
5212
|
+
const ext = extname(name).toLowerCase();
|
|
5213
|
+
if (!SUPPORTED_EXT.has(ext)) continue;
|
|
5214
|
+
const fullPath = join4(absoluteDir, name);
|
|
5215
|
+
try {
|
|
5216
|
+
if (!statSync2(fullPath).isFile()) continue;
|
|
5217
|
+
} catch {
|
|
5218
|
+
continue;
|
|
5219
|
+
}
|
|
5220
|
+
const variant = parseFontFilename(name);
|
|
5221
|
+
const source = {
|
|
5222
|
+
kind: "file",
|
|
5223
|
+
path: fullPath,
|
|
5224
|
+
weight: variant.weight,
|
|
5225
|
+
italic: variant.italic
|
|
5226
|
+
};
|
|
5227
|
+
const existing = byFamily.get(variant.family);
|
|
5228
|
+
if (existing) {
|
|
5229
|
+
existing.sources.push(source);
|
|
5230
|
+
} else {
|
|
5231
|
+
byFamily.set(variant.family, {
|
|
5232
|
+
id: variant.family,
|
|
5233
|
+
family: variant.family,
|
|
5234
|
+
sources: [source]
|
|
5235
|
+
});
|
|
5236
|
+
}
|
|
5237
|
+
}
|
|
5238
|
+
return [...byFamily.values()];
|
|
5239
|
+
}
|
|
5240
|
+
|
|
4246
5241
|
// src/commands/generate.ts
|
|
5242
|
+
import { isSafeFont } from "@json-to-office/shared";
|
|
4247
5243
|
function createGenerateCommand(adapter) {
|
|
4248
5244
|
return new Command("generate").description(`Generate ${adapter.label} from JSON`).argument("<input>", "Input JSON file path").option("-o, --output <path>", "Output file path").option("-t, --template <name>", "Template to use").option(
|
|
4249
5245
|
"--plugins [names-or-paths]",
|
|
@@ -4251,7 +5247,40 @@ function createGenerateCommand(adapter) {
|
|
|
4251
5247
|
).option("--plugin-dir <dir>", "Directory to search for plugins").option("--theme <name-or-path>", "Theme name or path to theme file").option(
|
|
4252
5248
|
"--theme-path <path>",
|
|
4253
5249
|
"Path to theme file (alternative to --theme)"
|
|
4254
|
-
).option("--strict", "Enable strict validation").option(
|
|
5250
|
+
).option("--strict", "Enable strict validation").option(
|
|
5251
|
+
"--strict-fonts",
|
|
5252
|
+
"Fail generation on unresolved fontRegistry references"
|
|
5253
|
+
).option(
|
|
5254
|
+
"--no-google-fonts",
|
|
5255
|
+
"Disable Google Fonts HTTP fetching (offline/CI builds)"
|
|
5256
|
+
).option(
|
|
5257
|
+
"--font-cache-dir <path>",
|
|
5258
|
+
"Directory to cache fetched Google Fonts TTFs"
|
|
5259
|
+
).option(
|
|
5260
|
+
"--font <name=path>",
|
|
5261
|
+
"Register a font file (repeatable): <family>=<path to .ttf/.otf>",
|
|
5262
|
+
(value, previous = []) => [...previous, value],
|
|
5263
|
+
[]
|
|
5264
|
+
).option(
|
|
5265
|
+
"--fonts-dir <path>",
|
|
5266
|
+
"Scan directory for .ttf/.otf files and auto-register by filename"
|
|
5267
|
+
).option(
|
|
5268
|
+
"--font-mode <mode>",
|
|
5269
|
+
'How to handle non-safe fonts: "custom" (default; keep refs as-is \u2014 recipient needs fonts installed) or "substitute" (rewrite to safe fonts)',
|
|
5270
|
+
(value) => {
|
|
5271
|
+
if (value !== "substitute" && value !== "custom") {
|
|
5272
|
+
throw new Error(
|
|
5273
|
+
`--font-mode must be one of: substitute, custom (got "${value}")`
|
|
5274
|
+
);
|
|
5275
|
+
}
|
|
5276
|
+
return value;
|
|
5277
|
+
}
|
|
5278
|
+
).option(
|
|
5279
|
+
"--font-substitute <family=safe>",
|
|
5280
|
+
"When --font-mode substitute: map a non-safe family to a specific safe font (repeatable). Falls back to category defaults when omitted.",
|
|
5281
|
+
(value, previous = []) => [...previous, value],
|
|
5282
|
+
[]
|
|
5283
|
+
).option("--dry-run", "Preview without writing files").action(async (input, options) => {
|
|
4255
5284
|
const spinner = ora("Initializing...").start();
|
|
4256
5285
|
const startTime = performance.now();
|
|
4257
5286
|
try {
|
|
@@ -4274,13 +5303,13 @@ function createGenerateCommand(adapter) {
|
|
|
4274
5303
|
adapter.name
|
|
4275
5304
|
);
|
|
4276
5305
|
spinner.text = "Reading input file...";
|
|
4277
|
-
const inputPath =
|
|
5306
|
+
const inputPath = resolve10(process.cwd(), input);
|
|
4278
5307
|
const jsonContent = readFileSync3(inputPath, "utf-8");
|
|
4279
5308
|
const documentDefinition = JSON.parse(jsonContent);
|
|
4280
5309
|
const factory = new GeneratorFactory(adapter);
|
|
4281
|
-
const outputPath = options.output ?
|
|
5310
|
+
const outputPath = options.output ? resolve10(process.cwd(), options.output) : resolve10(
|
|
4282
5311
|
process.cwd(),
|
|
4283
|
-
|
|
5312
|
+
basename5(input, ".json") + adapter.extension
|
|
4284
5313
|
);
|
|
4285
5314
|
const pluginInfo = factory.getPluginInfo();
|
|
4286
5315
|
if (options.dryRun) {
|
|
@@ -4310,11 +5339,52 @@ function createGenerateCommand(adapter) {
|
|
|
4310
5339
|
PluginRegistry.getInstance().clear();
|
|
4311
5340
|
return;
|
|
4312
5341
|
}
|
|
5342
|
+
const extraEntries = [];
|
|
5343
|
+
for (const spec of options.font ?? []) {
|
|
5344
|
+
extraEntries.push(parseFontFlag(spec));
|
|
5345
|
+
}
|
|
5346
|
+
if (options.fontsDir) {
|
|
5347
|
+
extraEntries.push(...parseFontsDir(options.fontsDir));
|
|
5348
|
+
}
|
|
5349
|
+
const substitution = {};
|
|
5350
|
+
for (const spec of options.fontSubstitute ?? []) {
|
|
5351
|
+
const eq = spec.indexOf("=");
|
|
5352
|
+
if (eq < 0) {
|
|
5353
|
+
throw new Error(
|
|
5354
|
+
`--font-substitute expects <family>=<safe-font>, got "${spec}"`
|
|
5355
|
+
);
|
|
5356
|
+
}
|
|
5357
|
+
const from = spec.slice(0, eq).trim();
|
|
5358
|
+
const to = spec.slice(eq + 1).trim();
|
|
5359
|
+
if (!from || !to) {
|
|
5360
|
+
throw new Error(
|
|
5361
|
+
`--font-substitute expects non-empty family and safe-font, got "${spec}"`
|
|
5362
|
+
);
|
|
5363
|
+
}
|
|
5364
|
+
if (!isSafeFont(to)) {
|
|
5365
|
+
throw new Error(
|
|
5366
|
+
`--font-substitute target "${to}" is not in SAFE_FONTS (got "${spec}")`
|
|
5367
|
+
);
|
|
5368
|
+
}
|
|
5369
|
+
substitution[from] = to;
|
|
5370
|
+
}
|
|
4313
5371
|
spinner.text = `Generating ${adapter.label}...`;
|
|
4314
5372
|
const buffer = await factory.generate(documentDefinition, {
|
|
4315
5373
|
theme: mergedConfig.theme,
|
|
4316
5374
|
themePath: mergedConfig.themePath,
|
|
4317
|
-
validation: mergedConfig.validation
|
|
5375
|
+
validation: mergedConfig.validation,
|
|
5376
|
+
fonts: {
|
|
5377
|
+
strict: options.strictFonts,
|
|
5378
|
+
...extraEntries.length > 0 && { extraEntries },
|
|
5379
|
+
...options.fontMode && { mode: options.fontMode },
|
|
5380
|
+
...Object.keys(substitution).length > 0 && { substitution },
|
|
5381
|
+
googleFonts: {
|
|
5382
|
+
...options.noGoogleFonts === true && { enabled: false },
|
|
5383
|
+
...options.fontCacheDir && {
|
|
5384
|
+
cacheDir: resolve10(process.cwd(), options.fontCacheDir)
|
|
5385
|
+
}
|
|
5386
|
+
}
|
|
5387
|
+
}
|
|
4318
5388
|
});
|
|
4319
5389
|
spinner.text = "Writing output file...";
|
|
4320
5390
|
writeFileSync(outputPath, Buffer.from(buffer));
|
|
@@ -4359,6 +5429,11 @@ ${chalk2.gray("Examples:")}
|
|
|
4359
5429
|
$ jto ${adapter.name} generate doc.json --theme minimal ${chalk2.dim("# Use built-in theme")}
|
|
4360
5430
|
$ jto ${adapter.name} generate doc.json --theme-path ./theme.json ${chalk2.dim("# Use custom theme")}
|
|
4361
5431
|
$ jto ${adapter.name} generate doc.json --dry-run ${chalk2.dim("# Preview without writing")}
|
|
5432
|
+
$ jto ${adapter.name} generate doc.json --strict-fonts ${chalk2.dim("# Fail on unresolved fonts")}
|
|
5433
|
+
$ jto ${adapter.name} generate doc.json --font-cache-dir .fontcache ${chalk2.dim("# Cache Google Fonts TTFs")}
|
|
5434
|
+
$ jto ${adapter.name} generate doc.json --no-google-fonts ${chalk2.dim("# Offline build, skip network fetches")}
|
|
5435
|
+
$ jto ${adapter.name} generate doc.json --font Inter=./fonts/Inter-Regular.ttf ${chalk2.dim("# Register one TTF")}
|
|
5436
|
+
$ jto ${adapter.name} generate doc.json --fonts-dir ./fonts ${chalk2.dim("# Auto-register every .ttf/.otf in ./fonts")}
|
|
4362
5437
|
`
|
|
4363
5438
|
);
|
|
4364
5439
|
}
|
|
@@ -4368,13 +5443,13 @@ init_esm_shims();
|
|
|
4368
5443
|
import { Command as Command2 } from "commander";
|
|
4369
5444
|
import chalk3 from "chalk";
|
|
4370
5445
|
import ora2 from "ora";
|
|
4371
|
-
import { resolve as
|
|
5446
|
+
import { resolve as resolve12, relative as relative4 } from "path";
|
|
4372
5447
|
import { existsSync as existsSync3 } from "fs";
|
|
4373
5448
|
|
|
4374
5449
|
// src/services/json-validator.ts
|
|
4375
5450
|
init_esm_shims();
|
|
4376
|
-
import { readFileSync as readFileSync4, statSync as
|
|
4377
|
-
import { resolve as
|
|
5451
|
+
import { readFileSync as readFileSync4, statSync as statSync3, readdirSync as readdirSync2 } from "fs";
|
|
5452
|
+
import { resolve as resolve11, join as join5, extname as extname2 } from "path";
|
|
4378
5453
|
import { glob as glob2 } from "glob";
|
|
4379
5454
|
var JsonValidator = class {
|
|
4380
5455
|
format;
|
|
@@ -4391,7 +5466,7 @@ var JsonValidator = class {
|
|
|
4391
5466
|
return results;
|
|
4392
5467
|
}
|
|
4393
5468
|
async validateFile(filePath, options = {}) {
|
|
4394
|
-
const absolutePath =
|
|
5469
|
+
const absolutePath = resolve11(filePath);
|
|
4395
5470
|
try {
|
|
4396
5471
|
const content = readFileSync4(absolutePath, "utf-8");
|
|
4397
5472
|
let jsonData;
|
|
@@ -4513,7 +5588,7 @@ var JsonValidator = class {
|
|
|
4513
5588
|
}
|
|
4514
5589
|
async validateWithCustomSchema(filePath, jsonData, schemaPath, strict) {
|
|
4515
5590
|
try {
|
|
4516
|
-
const schemaContent = readFileSync4(
|
|
5591
|
+
const schemaContent = readFileSync4(resolve11(schemaPath), "utf-8");
|
|
4517
5592
|
const schema = JSON.parse(schemaContent);
|
|
4518
5593
|
const Ajv = (await import("ajv")).default;
|
|
4519
5594
|
const addFormats = (await import("ajv-formats")).default;
|
|
@@ -4562,20 +5637,20 @@ var JsonValidator = class {
|
|
|
4562
5637
|
return null;
|
|
4563
5638
|
}
|
|
4564
5639
|
async getFilesToValidate(pathOrPattern, options) {
|
|
4565
|
-
const resolvedPath =
|
|
5640
|
+
const resolvedPath = resolve11(pathOrPattern);
|
|
4566
5641
|
try {
|
|
4567
|
-
const stats =
|
|
5642
|
+
const stats = statSync3(resolvedPath);
|
|
4568
5643
|
if (stats.isFile()) {
|
|
4569
5644
|
return [resolvedPath];
|
|
4570
5645
|
} else if (stats.isDirectory()) {
|
|
4571
5646
|
if (options.recursive) {
|
|
4572
|
-
const pattern =
|
|
5647
|
+
const pattern = join5(resolvedPath, "**/*.json");
|
|
4573
5648
|
return glob2(pattern, {
|
|
4574
5649
|
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**"]
|
|
4575
5650
|
});
|
|
4576
5651
|
} else {
|
|
4577
|
-
const files =
|
|
4578
|
-
return files.filter((file) =>
|
|
5652
|
+
const files = readdirSync2(resolvedPath);
|
|
5653
|
+
return files.filter((file) => extname2(file).toLowerCase() === ".json").map((file) => join5(resolvedPath, file));
|
|
4579
5654
|
}
|
|
4580
5655
|
}
|
|
4581
5656
|
} catch {
|
|
@@ -4626,7 +5701,7 @@ function createValidateCommand(adapter) {
|
|
|
4626
5701
|
const spinner = showSpinner ? ora2("Validating...").start() : null;
|
|
4627
5702
|
const startTime = performance.now();
|
|
4628
5703
|
try {
|
|
4629
|
-
if (options.schema && !existsSync3(
|
|
5704
|
+
if (options.schema && !existsSync3(resolve12(options.schema))) {
|
|
4630
5705
|
throw new Error(`Schema file not found: ${options.schema}`);
|
|
4631
5706
|
}
|
|
4632
5707
|
const results = await validator.validate(fileOrDirectory, {
|
|
@@ -5483,7 +6558,7 @@ function groupByLocation(items) {
|
|
|
5483
6558
|
init_esm_shims();
|
|
5484
6559
|
import { Command as Command5 } from "commander";
|
|
5485
6560
|
import { mkdirSync, writeFileSync as writeFileSync2, existsSync as existsSync4 } from "fs";
|
|
5486
|
-
import { resolve as
|
|
6561
|
+
import { resolve as resolve14, join as join7 } from "path";
|
|
5487
6562
|
import { execSync } from "child_process";
|
|
5488
6563
|
import ora5 from "ora";
|
|
5489
6564
|
import chalk7 from "chalk";
|
|
@@ -5509,7 +6584,7 @@ function createInitCommand(adapter) {
|
|
|
5509
6584
|
process.exit(EXIT_CODES.FAIL);
|
|
5510
6585
|
}
|
|
5511
6586
|
}
|
|
5512
|
-
const projectPath =
|
|
6587
|
+
const projectPath = resolve14(process.cwd(), name);
|
|
5513
6588
|
if (existsSync4(projectPath)) {
|
|
5514
6589
|
console.error(chalk7.red(`Directory ${name} already exists`));
|
|
5515
6590
|
process.exit(EXIT_CODES.FAIL);
|
|
@@ -5535,7 +6610,7 @@ function createInitCommand(adapter) {
|
|
|
5535
6610
|
}
|
|
5536
6611
|
};
|
|
5537
6612
|
writeFileSync2(
|
|
5538
|
-
|
|
6613
|
+
join7(projectPath, "package.json"),
|
|
5539
6614
|
JSON.stringify(packageJson, null, 2)
|
|
5540
6615
|
);
|
|
5541
6616
|
const exampleDocument = adapter.name === "docx" ? {
|
|
@@ -5583,7 +6658,7 @@ function createInitCommand(adapter) {
|
|
|
5583
6658
|
]
|
|
5584
6659
|
};
|
|
5585
6660
|
writeFileSync2(
|
|
5586
|
-
|
|
6661
|
+
join7(projectPath, "example.json"),
|
|
5587
6662
|
JSON.stringify(exampleDocument, null, 2)
|
|
5588
6663
|
);
|
|
5589
6664
|
const gitignore = `node_modules
|
|
@@ -5593,7 +6668,7 @@ dist
|
|
|
5593
6668
|
.env
|
|
5594
6669
|
.env.local
|
|
5595
6670
|
`;
|
|
5596
|
-
writeFileSync2(
|
|
6671
|
+
writeFileSync2(join7(projectPath, ".gitignore"), gitignore);
|
|
5597
6672
|
console.log(chalk7.green(`
|
|
5598
6673
|
Created project at ${projectPath}
|
|
5599
6674
|
`));
|
|
@@ -5651,7 +6726,7 @@ import boxen3 from "boxen";
|
|
|
5651
6726
|
init_esm_shims();
|
|
5652
6727
|
import { existsSync as existsSync5 } from "fs";
|
|
5653
6728
|
import { readFile as readFile4 } from "fs/promises";
|
|
5654
|
-
import { resolve as
|
|
6729
|
+
import { resolve as resolve15 } from "path";
|
|
5655
6730
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
5656
6731
|
|
|
5657
6732
|
// src/config/schema.ts
|
|
@@ -5796,7 +6871,7 @@ async function loadConfig(configPath) {
|
|
|
5796
6871
|
async function findConfigFile() {
|
|
5797
6872
|
const cwd = process.cwd();
|
|
5798
6873
|
for (const filename of CONFIG_FILES) {
|
|
5799
|
-
const filepath =
|
|
6874
|
+
const filepath = resolve15(cwd, filename);
|
|
5800
6875
|
if (existsSync5(filepath)) {
|
|
5801
6876
|
return filepath;
|
|
5802
6877
|
}
|
|
@@ -5896,8 +6971,304 @@ ${chalk8.cyan("Health:")} ${url}/health
|
|
|
5896
6971
|
});
|
|
5897
6972
|
}
|
|
5898
6973
|
|
|
6974
|
+
// src/commands/fonts.ts
|
|
6975
|
+
init_esm_shims();
|
|
6976
|
+
import { Command as Command7 } from "commander";
|
|
6977
|
+
import chalk9 from "chalk";
|
|
6978
|
+
import {
|
|
6979
|
+
readFileSync as readFileSync6,
|
|
6980
|
+
statSync as statSync4,
|
|
6981
|
+
mkdirSync as mkdirSync2,
|
|
6982
|
+
writeFileSync as writeFileSync3,
|
|
6983
|
+
renameSync,
|
|
6984
|
+
unlinkSync
|
|
6985
|
+
} from "fs";
|
|
6986
|
+
import { resolve as resolve17, basename as basename6, extname as extname3 } from "path";
|
|
6987
|
+
import {
|
|
6988
|
+
SAFE_FONTS as SAFE_FONTS2,
|
|
6989
|
+
collectFontNamesFromDocx as collectFontNamesFromDocx2,
|
|
6990
|
+
collectFontNamesFromPptx as collectFontNamesFromPptx2,
|
|
6991
|
+
detectFontFormat,
|
|
6992
|
+
fetchGoogleFontSources as fetchGoogleFontSources2,
|
|
6993
|
+
isSafeFont as isSafeFont3,
|
|
6994
|
+
POPULAR_GOOGLE_FONTS as POPULAR_GOOGLE_FONTS3
|
|
6995
|
+
} from "@json-to-office/shared";
|
|
6996
|
+
function formatBytes(bytes) {
|
|
6997
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
6998
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
6999
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
7000
|
+
}
|
|
7001
|
+
function weightLabel(weight, italic) {
|
|
7002
|
+
return `${weight}${italic ? " italic" : ""}`;
|
|
7003
|
+
}
|
|
7004
|
+
function listSafeFonts() {
|
|
7005
|
+
console.log(
|
|
7006
|
+
chalk9.bold("\nSafe fonts (bundled with Office, no embedding needed):")
|
|
7007
|
+
);
|
|
7008
|
+
for (const name of SAFE_FONTS2) {
|
|
7009
|
+
console.log(` ${chalk9.cyan(name)}`);
|
|
7010
|
+
}
|
|
7011
|
+
}
|
|
7012
|
+
function listLocalFonts(dir) {
|
|
7013
|
+
let entries;
|
|
7014
|
+
try {
|
|
7015
|
+
entries = parseFontsDir(dir);
|
|
7016
|
+
} catch (err) {
|
|
7017
|
+
console.log(
|
|
7018
|
+
chalk9.yellow(
|
|
7019
|
+
`
|
|
7020
|
+
(--fonts-dir ${dir} not readable: ${err.message})`
|
|
7021
|
+
)
|
|
7022
|
+
);
|
|
7023
|
+
return;
|
|
7024
|
+
}
|
|
7025
|
+
if (entries.length === 0) {
|
|
7026
|
+
console.log(chalk9.dim(`
|
|
7027
|
+
Local fonts in ${dir}: (none)`));
|
|
7028
|
+
return;
|
|
7029
|
+
}
|
|
7030
|
+
console.log(chalk9.bold(`
|
|
7031
|
+
Local fonts in ${dir}:`));
|
|
7032
|
+
for (const e of entries) {
|
|
7033
|
+
const variants = e.sources.map((s) => {
|
|
7034
|
+
const weight = "weight" in s ? s.weight ?? 400 : 400;
|
|
7035
|
+
const italic = "italic" in s ? Boolean(s.italic) : false;
|
|
7036
|
+
return weightLabel(weight, italic);
|
|
7037
|
+
}).join(", ");
|
|
7038
|
+
console.log(` ${chalk9.cyan(e.family)} ${chalk9.dim(`(${variants})`)}`);
|
|
7039
|
+
}
|
|
7040
|
+
}
|
|
7041
|
+
function listReferencedFonts(docPath, adapterName) {
|
|
7042
|
+
const absolutePath = resolve17(process.cwd(), docPath);
|
|
7043
|
+
let raw;
|
|
7044
|
+
try {
|
|
7045
|
+
raw = readFileSync6(absolutePath, "utf-8");
|
|
7046
|
+
} catch (err) {
|
|
7047
|
+
console.log(
|
|
7048
|
+
chalk9.yellow(`
|
|
7049
|
+
(Could not read ${docPath}: ${err.message})`)
|
|
7050
|
+
);
|
|
7051
|
+
return;
|
|
7052
|
+
}
|
|
7053
|
+
let parsed;
|
|
7054
|
+
try {
|
|
7055
|
+
parsed = JSON.parse(raw);
|
|
7056
|
+
} catch (err) {
|
|
7057
|
+
console.log(
|
|
7058
|
+
chalk9.yellow(
|
|
7059
|
+
`
|
|
7060
|
+
(${docPath} is not valid JSON: ${err.message})`
|
|
7061
|
+
)
|
|
7062
|
+
);
|
|
7063
|
+
return;
|
|
7064
|
+
}
|
|
7065
|
+
const names = adapterName === "docx" ? collectFontNamesFromDocx2(parsed) : collectFontNamesFromPptx2(parsed);
|
|
7066
|
+
if (names.size === 0) {
|
|
7067
|
+
console.log(chalk9.dim(`
|
|
7068
|
+
Fonts referenced in ${docPath}: (none)`));
|
|
7069
|
+
return;
|
|
7070
|
+
}
|
|
7071
|
+
console.log(chalk9.bold(`
|
|
7072
|
+
Fonts referenced in ${docPath}:`));
|
|
7073
|
+
for (const name of [...names].sort()) {
|
|
7074
|
+
const status = isSafeFont3(name) ? chalk9.green("[safe]") : POPULAR_GOOGLE_FONTS3.some(
|
|
7075
|
+
(g) => g.family.toLowerCase() === name.toLowerCase()
|
|
7076
|
+
) ? chalk9.blue("[google]") : chalk9.yellow("[unresolved]");
|
|
7077
|
+
console.log(` ${chalk9.cyan(name)} ${status}`);
|
|
7078
|
+
}
|
|
7079
|
+
}
|
|
7080
|
+
function createListCommand(adapter) {
|
|
7081
|
+
return new Command7("list").description(
|
|
7082
|
+
"List safe fonts, fonts in ./fonts, and fonts referenced in a document"
|
|
7083
|
+
).argument(
|
|
7084
|
+
"[document]",
|
|
7085
|
+
"Optional JSON document to scan for font references"
|
|
7086
|
+
).option(
|
|
7087
|
+
"--fonts-dir <path>",
|
|
7088
|
+
"Directory of local .ttf/.otf files to list (default: ./fonts)",
|
|
7089
|
+
"./fonts"
|
|
7090
|
+
).action(async (document, options) => {
|
|
7091
|
+
listSafeFonts();
|
|
7092
|
+
if (options.fontsDir) listLocalFonts(options.fontsDir);
|
|
7093
|
+
if (document) {
|
|
7094
|
+
listReferencedFonts(document, adapter.name);
|
|
7095
|
+
}
|
|
7096
|
+
console.log();
|
|
7097
|
+
});
|
|
7098
|
+
}
|
|
7099
|
+
function createInspectCommand() {
|
|
7100
|
+
return new Command7("inspect").description("Print family/weight/italic/format/size for a .ttf/.otf file").argument("<file>", "Path to a font file").action(async (file) => {
|
|
7101
|
+
const absolute = resolve17(process.cwd(), file);
|
|
7102
|
+
let buf;
|
|
7103
|
+
let size;
|
|
7104
|
+
try {
|
|
7105
|
+
buf = readFileSync6(absolute);
|
|
7106
|
+
size = statSync4(absolute).size;
|
|
7107
|
+
} catch (err) {
|
|
7108
|
+
formatError(err);
|
|
7109
|
+
process.exit(EXIT_CODES.FAIL);
|
|
7110
|
+
}
|
|
7111
|
+
const format = detectFontFormat(buf);
|
|
7112
|
+
const parsed = parseFontFilename(basename6(file));
|
|
7113
|
+
const ext = extname3(file).toLowerCase();
|
|
7114
|
+
console.log();
|
|
7115
|
+
console.log(chalk9.bold("Font:"), chalk9.cyan(parsed.family));
|
|
7116
|
+
console.log(chalk9.bold("File:"), file);
|
|
7117
|
+
console.log(
|
|
7118
|
+
chalk9.bold("Format:"),
|
|
7119
|
+
format === "unknown" ? chalk9.yellow(format) : format
|
|
7120
|
+
);
|
|
7121
|
+
console.log(chalk9.bold("Weight:"), parsed.weight);
|
|
7122
|
+
console.log(chalk9.bold("Italic:"), parsed.italic ? "yes" : "no");
|
|
7123
|
+
console.log(chalk9.bold("Size:"), formatBytes(size));
|
|
7124
|
+
if (format !== "ttf" && format !== "otf") {
|
|
7125
|
+
console.log(
|
|
7126
|
+
chalk9.yellow(
|
|
7127
|
+
"\nWarning: embedding requires TTF or OTF. WOFF/WOFF2 won't embed in .docx."
|
|
7128
|
+
)
|
|
7129
|
+
);
|
|
7130
|
+
}
|
|
7131
|
+
if (parsed.family === basename6(file, ext)) {
|
|
7132
|
+
console.log(
|
|
7133
|
+
chalk9.dim(
|
|
7134
|
+
"\n(Family was not parsed from a recognized weight/style suffix \u2014 treating full filename as family.)"
|
|
7135
|
+
)
|
|
7136
|
+
);
|
|
7137
|
+
}
|
|
7138
|
+
console.log();
|
|
7139
|
+
});
|
|
7140
|
+
}
|
|
7141
|
+
function parseWeightsOption(raw) {
|
|
7142
|
+
if (!raw) return [400, 700];
|
|
7143
|
+
const out = [];
|
|
7144
|
+
for (const token of raw.split(/[,\s]+/).filter(Boolean)) {
|
|
7145
|
+
const n = parseInt(token, 10);
|
|
7146
|
+
if (Number.isNaN(n) || n < 100 || n > 900 || n % 100 !== 0) {
|
|
7147
|
+
throw new Error(
|
|
7148
|
+
`--weights must be 100-step integers between 100 and 900, got "${token}"`
|
|
7149
|
+
);
|
|
7150
|
+
}
|
|
7151
|
+
out.push(n);
|
|
7152
|
+
}
|
|
7153
|
+
return [...new Set(out)].sort((a, b) => a - b);
|
|
7154
|
+
}
|
|
7155
|
+
var WEIGHT_NAMES2 = {
|
|
7156
|
+
100: "Thin",
|
|
7157
|
+
200: "ExtraLight",
|
|
7158
|
+
300: "Light",
|
|
7159
|
+
400: "Regular",
|
|
7160
|
+
500: "Medium",
|
|
7161
|
+
600: "SemiBold",
|
|
7162
|
+
700: "Bold",
|
|
7163
|
+
800: "ExtraBold",
|
|
7164
|
+
900: "Black"
|
|
7165
|
+
};
|
|
7166
|
+
function filenameFor(family, weight, italic) {
|
|
7167
|
+
const safeFamily = family.replace(/\s+/g, "");
|
|
7168
|
+
const suffix = `${WEIGHT_NAMES2[weight] ?? weight}${italic ? "Italic" : ""}`;
|
|
7169
|
+
return `${safeFamily}-${suffix}.ttf`;
|
|
7170
|
+
}
|
|
7171
|
+
function createInstallCommand() {
|
|
7172
|
+
return new Command7("install").description(
|
|
7173
|
+
"Download a Google Fonts family into ./fonts/ (filenames compatible with --fonts-dir)"
|
|
7174
|
+
).argument("<family>", 'Google Fonts family name (e.g. "Inter")').option(
|
|
7175
|
+
"-w, --weights <list>",
|
|
7176
|
+
'Comma-separated weights (100..900, step 100). Default: "400,700"'
|
|
7177
|
+
).option("--italics", "Also download italic variants").option(
|
|
7178
|
+
"-d, --dir <path>",
|
|
7179
|
+
"Output directory (default: ./fonts)",
|
|
7180
|
+
"./fonts"
|
|
7181
|
+
).action(async (family, options) => {
|
|
7182
|
+
const weights = parseWeightsOption(options.weights);
|
|
7183
|
+
const italics = Boolean(options.italics);
|
|
7184
|
+
const outDir = resolve17(process.cwd(), options.dir ?? "./fonts");
|
|
7185
|
+
console.log(
|
|
7186
|
+
chalk9.dim(
|
|
7187
|
+
`Fetching ${family} [${weights.join(", ")}]${italics ? " + italics" : ""} from Google Fonts...`
|
|
7188
|
+
)
|
|
7189
|
+
);
|
|
7190
|
+
const { sources, warnings } = await fetchGoogleFontSources2({
|
|
7191
|
+
family,
|
|
7192
|
+
weights,
|
|
7193
|
+
italics
|
|
7194
|
+
});
|
|
7195
|
+
if (warnings.length > 0) {
|
|
7196
|
+
for (const w of warnings) console.warn(chalk9.yellow(` ${w}`));
|
|
7197
|
+
}
|
|
7198
|
+
if (sources.length === 0) {
|
|
7199
|
+
console.error(chalk9.red(`
|
|
7200
|
+
No variants downloaded for "${family}".`));
|
|
7201
|
+
process.exit(EXIT_CODES.FAIL);
|
|
7202
|
+
}
|
|
7203
|
+
mkdirSync2(outDir, { recursive: true });
|
|
7204
|
+
const written = [];
|
|
7205
|
+
const failures = [];
|
|
7206
|
+
for (const src of sources) {
|
|
7207
|
+
const name = filenameFor(family, src.weight, src.italic);
|
|
7208
|
+
const full = resolve17(outDir, name);
|
|
7209
|
+
const tmp = `${full}.tmp`;
|
|
7210
|
+
try {
|
|
7211
|
+
writeFileSync3(tmp, src.data);
|
|
7212
|
+
renameSync(tmp, full);
|
|
7213
|
+
written.push(name);
|
|
7214
|
+
} catch (err) {
|
|
7215
|
+
try {
|
|
7216
|
+
unlinkSync(tmp);
|
|
7217
|
+
} catch {
|
|
7218
|
+
}
|
|
7219
|
+
failures.push({ name, err: err.message });
|
|
7220
|
+
}
|
|
7221
|
+
}
|
|
7222
|
+
if (failures.length > 0) {
|
|
7223
|
+
console.error(
|
|
7224
|
+
chalk9.red(`
|
|
7225
|
+
${failures.length} file(s) failed to install:`)
|
|
7226
|
+
);
|
|
7227
|
+
for (const f of failures) {
|
|
7228
|
+
console.error(` ${chalk9.red(f.name)}: ${f.err}`);
|
|
7229
|
+
}
|
|
7230
|
+
}
|
|
7231
|
+
if (written.length > 0) {
|
|
7232
|
+
console.log(
|
|
7233
|
+
chalk9.green(`
|
|
7234
|
+
Installed ${written.length} file(s) into ${outDir}:`)
|
|
7235
|
+
);
|
|
7236
|
+
for (const name of written) console.log(` ${chalk9.cyan(name)}`);
|
|
7237
|
+
console.log(
|
|
7238
|
+
chalk9.dim(
|
|
7239
|
+
`
|
|
7240
|
+
Use them with: jto <format> generate doc.json --fonts-dir ${options.dir ?? "./fonts"}`
|
|
7241
|
+
)
|
|
7242
|
+
);
|
|
7243
|
+
}
|
|
7244
|
+
if (failures.length > 0) {
|
|
7245
|
+
process.exit(EXIT_CODES.FAIL);
|
|
7246
|
+
}
|
|
7247
|
+
});
|
|
7248
|
+
}
|
|
7249
|
+
function createFontsCommand(adapter) {
|
|
7250
|
+
const cmd = new Command7("fonts").description(
|
|
7251
|
+
`Font introspection and Google Fonts install for ${adapter.label}s`
|
|
7252
|
+
);
|
|
7253
|
+
cmd.addCommand(createListCommand(adapter));
|
|
7254
|
+
cmd.addCommand(createInspectCommand());
|
|
7255
|
+
cmd.addCommand(createInstallCommand());
|
|
7256
|
+
cmd.addHelpText(
|
|
7257
|
+
"after",
|
|
7258
|
+
`
|
|
7259
|
+
${chalk9.gray("Examples:")}
|
|
7260
|
+
$ jto ${adapter.name} fonts list ${chalk9.dim("# Safe + ./fonts")}
|
|
7261
|
+
$ jto ${adapter.name} fonts list doc.json ${chalk9.dim("# + fonts referenced in doc")}
|
|
7262
|
+
$ jto ${adapter.name} fonts inspect ./fonts/Inter-Bold.ttf
|
|
7263
|
+
$ jto ${adapter.name} fonts install Inter ${chalk9.dim("# \u2192 ./fonts/Inter-Regular.ttf + Inter-Bold.ttf")}
|
|
7264
|
+
$ jto ${adapter.name} fonts install "Playfair Display" --weights 400,700 --italics
|
|
7265
|
+
`
|
|
7266
|
+
);
|
|
7267
|
+
return cmd;
|
|
7268
|
+
}
|
|
7269
|
+
|
|
5899
7270
|
// src/cli.ts
|
|
5900
|
-
var PACKAGE_VERSION = true ? "0.
|
|
7271
|
+
var PACKAGE_VERSION = true ? "0.9.0" : "dev-mode";
|
|
5901
7272
|
function registerFormatCommands(parent, adapter) {
|
|
5902
7273
|
parent.addCommand(createGenerateCommand(adapter));
|
|
5903
7274
|
parent.addCommand(createValidateCommand(adapter));
|
|
@@ -5905,25 +7276,27 @@ function registerFormatCommands(parent, adapter) {
|
|
|
5905
7276
|
parent.addCommand(createDiscoverCommand(adapter));
|
|
5906
7277
|
parent.addCommand(createInitCommand(adapter));
|
|
5907
7278
|
parent.addCommand(createDevCommand(adapter));
|
|
7279
|
+
parent.addCommand(createFontsCommand(adapter));
|
|
5908
7280
|
}
|
|
5909
|
-
var program = new
|
|
7281
|
+
var program = new Command8();
|
|
5910
7282
|
program.name("jto").description("JSON to Office CLI - Generate .docx and .pptx from JSON").version(PACKAGE_VERSION);
|
|
5911
|
-
var docx = new
|
|
7283
|
+
var docx = new Command8("docx").description("DOCX document commands");
|
|
5912
7284
|
registerFormatCommands(docx, new DocxFormatAdapter());
|
|
5913
|
-
var pptx = new
|
|
7285
|
+
var pptx = new Command8("pptx").description("PPTX presentation commands");
|
|
5914
7286
|
registerFormatCommands(pptx, new PptxFormatAdapter());
|
|
5915
7287
|
program.addCommand(docx);
|
|
5916
7288
|
program.addCommand(pptx);
|
|
5917
7289
|
program.addHelpText(
|
|
5918
7290
|
"after",
|
|
5919
7291
|
`
|
|
5920
|
-
${
|
|
5921
|
-
$ jto docx generate doc.json ${
|
|
5922
|
-
$ jto pptx generate slides.json ${
|
|
5923
|
-
$ jto docx dev ${
|
|
5924
|
-
$ jto pptx validate slides.json ${
|
|
5925
|
-
$ jto docx schemas ${
|
|
5926
|
-
$ jto pptx discover ${
|
|
7292
|
+
${chalk10.gray("Examples:")}
|
|
7293
|
+
$ jto docx generate doc.json ${chalk10.dim("# Generate DOCX from JSON")}
|
|
7294
|
+
$ jto pptx generate slides.json ${chalk10.dim("# Generate PPTX from JSON")}
|
|
7295
|
+
$ jto docx dev ${chalk10.dim("# Start DOCX dev server")}
|
|
7296
|
+
$ jto pptx validate slides.json ${chalk10.dim("# Validate PPTX JSON")}
|
|
7297
|
+
$ jto docx schemas ${chalk10.dim("# Export DOCX JSON schemas")}
|
|
7298
|
+
$ jto pptx discover ${chalk10.dim("# Discover PPTX plugins")}
|
|
7299
|
+
$ jto docx fonts install Inter ${chalk10.dim("# Download a Google Font into ./fonts")}
|
|
5927
7300
|
`
|
|
5928
7301
|
);
|
|
5929
7302
|
program.exitOverride();
|
|
@@ -5937,7 +7310,7 @@ program.exitOverride();
|
|
|
5937
7310
|
if (error.code === "commander.executeSubCommandAsync") {
|
|
5938
7311
|
process.exit(error.exitCode);
|
|
5939
7312
|
}
|
|
5940
|
-
console.error(
|
|
7313
|
+
console.error(chalk10.red("Error:"), error.message);
|
|
5941
7314
|
process.exit(1);
|
|
5942
7315
|
}
|
|
5943
7316
|
})();
|