@json-to-office/jto 0.33.2 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +214 -516
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/HomePage-DgVVosCp.js +106 -0
- package/dist/client/assets/HomePage-DgVVosCp.js.map +1 -0
- package/dist/client/assets/{JsonEditorPage-GZV7oJD2.js → JsonEditorPage-13oxEHfY.js} +3 -3
- package/dist/client/assets/{JsonEditorPage-GZV7oJD2.js.map → JsonEditorPage-13oxEHfY.js.map} +1 -1
- package/dist/client/assets/{MonacoPluginProvider-btNzJ41o.js → MonacoPluginProvider-DiUyU9px.js} +3 -3
- package/dist/client/assets/{MonacoPluginProvider-btNzJ41o.js.map → MonacoPluginProvider-DiUyU9px.js.map} +1 -1
- package/dist/client/assets/editor-monaco-json-CTrPZKuA.js +24 -0
- package/dist/client/assets/{editor-monaco-json-rhvIaBDI.js.map → editor-monaco-json-CTrPZKuA.js.map} +1 -1
- package/dist/client/assets/{editor-Cugq4RzH.js → editor-ouhvdGgK.js} +2 -2
- package/dist/client/assets/{editor-Cugq4RzH.js.map → editor-ouhvdGgK.js.map} +1 -1
- package/dist/client/assets/{editor-refs-store-DeJZ8FB9.js → editor-refs-store-DNQzN0_z.js} +2 -2
- package/dist/client/assets/{editor-refs-store-DeJZ8FB9.js.map → editor-refs-store-DNQzN0_z.js.map} +1 -1
- package/dist/client/assets/index-CRuLxurM.js +5 -0
- package/dist/client/assets/index-CRuLxurM.js.map +1 -0
- package/dist/client/assets/index-CnUTzeor.css +1 -0
- package/dist/client/assets/preview-D3QYaLPp.js +4 -0
- package/dist/client/assets/preview-D3QYaLPp.js.map +1 -0
- package/dist/client/assets/{ui-vendor-Cs3OXpi7.js → ui-vendor-DyQLnWB8.js} +16 -11
- package/dist/client/assets/ui-vendor-DyQLnWB8.js.map +1 -0
- package/dist/client/index.html +3 -3
- package/dist/client/templates/Alternative deck 16_9.pptx.json +91 -49
- package/dist/client/templates/Company deck 16_9.pptx.json +134 -70
- package/dist/client/templates/Company deck 4_3.pptx.json +84 -44
- package/dist/client/templates/data-report-presentation.pptx.json +100 -52
- package/dist/client/templates/modern-annual-report-1.docx.json +207 -116
- package/dist/client/templates/modern-annual-report-2.docx.json +52 -37
- package/dist/client/templates/modern-annual-report-3.docx.json +40 -20
- package/dist/client/templates/tech-report.docx.json +96 -48
- package/dist/render-server.js +168 -106
- package/dist/render-server.js.map +1 -1
- package/package.json +7 -8
- package/dist/client/assets/HomePage-Cq6woGMG.js +0 -106
- package/dist/client/assets/HomePage-Cq6woGMG.js.map +0 -1
- package/dist/client/assets/editor-monaco-json-rhvIaBDI.js +0 -24
- package/dist/client/assets/index-C9ply7vX.css +0 -1
- package/dist/client/assets/index-C9uVroFQ.js +0 -5
- package/dist/client/assets/index-C9uVroFQ.js.map +0 -1
- package/dist/client/assets/preview-B_B09gpA.js +0 -3
- package/dist/client/assets/preview-B_B09gpA.js.map +0 -1
- package/dist/client/assets/ui-vendor-Cs3OXpi7.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -221,7 +221,9 @@ import {
|
|
|
221
221
|
collectFontNamesFromPptx,
|
|
222
222
|
POPULAR_GOOGLE_FONTS,
|
|
223
223
|
getUpstreamOverride,
|
|
224
|
-
isSafeFont
|
|
224
|
+
isSafeFont,
|
|
225
|
+
documentFontRegistry,
|
|
226
|
+
themeFontRegistry
|
|
225
227
|
} from "@json-to-office/shared";
|
|
226
228
|
function collectReferencedNames(config2, customThemes, adapterName) {
|
|
227
229
|
const collect = adapterName === "docx" ? collectFontNamesFromDocx : collectFontNamesFromPptx;
|
|
@@ -281,6 +283,14 @@ function collectReferencedItalic(config2, customThemes) {
|
|
|
281
283
|
}
|
|
282
284
|
return found;
|
|
283
285
|
}
|
|
286
|
+
function toSupplementalWarnings(warnings) {
|
|
287
|
+
return warnings.map(({ code, message }) => ({
|
|
288
|
+
component: "fontRegistry",
|
|
289
|
+
message,
|
|
290
|
+
severity: "info",
|
|
291
|
+
context: { code }
|
|
292
|
+
}));
|
|
293
|
+
}
|
|
284
294
|
function autoGoogleFontEntries(names, skipFamilies, referencedWeights, referencedItalic, warnings) {
|
|
285
295
|
const googleByLower = new Map(
|
|
286
296
|
POPULAR_GOOGLE_FONTS.map((f) => [f.family.toLowerCase(), f])
|
|
@@ -316,9 +326,10 @@ function autoGoogleFontEntries(names, skipFamilies, referencedWeights, reference
|
|
|
316
326
|
let selected = variants;
|
|
317
327
|
if (selected.length === 0) {
|
|
318
328
|
const missing = [...overrideWantedSet].filter((w) => !override.variants.some((v) => v.weight === w)).sort((a, b) => a - b);
|
|
319
|
-
warnings?.push(
|
|
320
|
-
|
|
321
|
-
|
|
329
|
+
warnings?.push({
|
|
330
|
+
code: "FONT_WEIGHT_NOT_IN_OVERRIDE",
|
|
331
|
+
message: `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.`
|
|
332
|
+
});
|
|
322
333
|
selected = override.variants;
|
|
323
334
|
}
|
|
324
335
|
entries.push({
|
|
@@ -396,8 +407,13 @@ var init_generator = __esm({
|
|
|
396
407
|
const fontMode = rawMode === "substitute" || rawMode === "custom" ? rawMode : void 0;
|
|
397
408
|
const rawSub = callerFonts?.substitution;
|
|
398
409
|
const fontSubstitution = rawSub && typeof rawSub === "object" && !Array.isArray(rawSub) ? rawSub : void 0;
|
|
410
|
+
const declaredFamilies = [
|
|
411
|
+
...callerExtraEntries,
|
|
412
|
+
...documentFontRegistry(config2),
|
|
413
|
+
...Object.values(customThemes ?? {}).flatMap((t) => themeFontRegistry(t))
|
|
414
|
+
];
|
|
399
415
|
const callerFamilies = new Set(
|
|
400
|
-
|
|
416
|
+
declaredFamilies.map((e) => e.family.toLowerCase())
|
|
401
417
|
);
|
|
402
418
|
const overrideWarnings = [];
|
|
403
419
|
const autoEntries = fontMode === "substitute" ? [] : autoGoogleFontEntries(
|
|
@@ -415,13 +431,17 @@ var init_generator = __esm({
|
|
|
415
431
|
for (const e of callerExtraEntries) {
|
|
416
432
|
const lower = e.family.toLowerCase();
|
|
417
433
|
if (googleFamiliesLower.has(lower) && referencedNames.has(e.family)) {
|
|
418
|
-
overrideWarnings.push(
|
|
419
|
-
|
|
420
|
-
|
|
434
|
+
overrideWarnings.push({
|
|
435
|
+
code: "FONT_OVERRIDE_LOCAL",
|
|
436
|
+
message: `[FONT_OVERRIDE_LOCAL] ${e.family}: caller-supplied source used; Google Fonts auto-fetch skipped for this family.`
|
|
437
|
+
});
|
|
421
438
|
}
|
|
422
439
|
}
|
|
423
440
|
}
|
|
424
|
-
const
|
|
441
|
+
const declaresRegistry = documentFontRegistry(config2).length > 0 || Object.values(customThemes ?? {}).some(
|
|
442
|
+
(t) => themeFontRegistry(t).length > 0
|
|
443
|
+
);
|
|
444
|
+
const bypassCache = options?.bypassCache === true || extraEntries.length > 0 || declaresRegistry;
|
|
425
445
|
const baseDir = typeof options?.baseDir === "string" ? options.baseDir : void 0;
|
|
426
446
|
const cacheKeyData = {
|
|
427
447
|
config: config2,
|
|
@@ -435,15 +455,17 @@ var init_generator = __esm({
|
|
|
435
455
|
const cacheKey = this.cacheService.generateCacheKey(cacheKeyData);
|
|
436
456
|
const hasDynamicContent = this.cacheService.hasDynamicContent(config2);
|
|
437
457
|
if (!bypassCache && !hasDynamicContent) {
|
|
438
|
-
const
|
|
439
|
-
if (
|
|
458
|
+
const cached = this.cacheService.get(cacheKey);
|
|
459
|
+
if (cached) {
|
|
440
460
|
logger.info("Served from cache", { title: config2.metadata?.title });
|
|
441
461
|
return {
|
|
442
462
|
filename: `${config2.metadata?.title || this.adapter.label}${this.adapter.extension}`,
|
|
443
463
|
fileId: Date.now().toString(),
|
|
444
|
-
buffer:
|
|
464
|
+
buffer: cached.buffer,
|
|
465
|
+
// Warnings ride along with the bytes: a cache hit describes the same
|
|
466
|
+
// document, so it must report the same font problems.
|
|
445
467
|
cached: true,
|
|
446
|
-
warnings:
|
|
468
|
+
warnings: cached.warnings
|
|
447
469
|
};
|
|
448
470
|
}
|
|
449
471
|
}
|
|
@@ -453,7 +475,7 @@ var init_generator = __esm({
|
|
|
453
475
|
const registry = PluginRegistry.getInstance();
|
|
454
476
|
let buffer;
|
|
455
477
|
const resolvedFonts = [];
|
|
456
|
-
const needsFontOpts = extraEntries.length > 0 || fontMode !== void 0 || fontSubstitution !== void 0 || callerStrict !== void 0;
|
|
478
|
+
const needsFontOpts = extraEntries.length > 0 || declaresRegistry || fontMode !== void 0 || fontSubstitution !== void 0 || callerStrict !== void 0;
|
|
457
479
|
const fontOpts = needsFontOpts ? {
|
|
458
480
|
...extraEntries.length > 0 && { extraEntries },
|
|
459
481
|
...fontMode && { mode: fontMode },
|
|
@@ -463,24 +485,24 @@ var init_generator = __esm({
|
|
|
463
485
|
resolvedFonts.push(...r);
|
|
464
486
|
}
|
|
465
487
|
} : void 0;
|
|
488
|
+
const coreWarnings = [];
|
|
466
489
|
if (registry.hasPlugins()) {
|
|
467
490
|
const plugins = registry.getPlugins();
|
|
468
491
|
const generator = await this.adapter.createGenerator(plugins, {
|
|
469
492
|
customThemes,
|
|
470
493
|
fonts: fontOpts,
|
|
471
|
-
baseDir
|
|
494
|
+
baseDir,
|
|
495
|
+
warnings: coreWarnings
|
|
472
496
|
});
|
|
473
497
|
buffer = await generator.generateBuffer(config2);
|
|
474
498
|
} else {
|
|
475
499
|
buffer = await this.adapter.generateBuffer(config2, {
|
|
476
500
|
customThemes,
|
|
477
501
|
fonts: fontOpts,
|
|
478
|
-
baseDir
|
|
502
|
+
baseDir,
|
|
503
|
+
warnings: coreWarnings
|
|
479
504
|
});
|
|
480
505
|
}
|
|
481
|
-
this.cacheService.set(cacheKey, buffer, config2, {
|
|
482
|
-
bypassCache: bypassCache || hasDynamicContent
|
|
483
|
-
});
|
|
484
506
|
const CANONICAL_WEIGHTS = /* @__PURE__ */ new Set([
|
|
485
507
|
100,
|
|
486
508
|
200,
|
|
@@ -495,12 +517,7 @@ var init_generator = __esm({
|
|
|
495
517
|
const nonCanonical = [...referencedWeights].filter(
|
|
496
518
|
(w) => !CANONICAL_WEIGHTS.has(w)
|
|
497
519
|
);
|
|
498
|
-
const extraWarnings = overrideWarnings
|
|
499
|
-
component: "fontRegistry",
|
|
500
|
-
message,
|
|
501
|
-
severity: "info",
|
|
502
|
-
context: { code: "FONT_OVERRIDE_LOCAL" }
|
|
503
|
-
}));
|
|
520
|
+
const extraWarnings = toSupplementalWarnings(overrideWarnings);
|
|
504
521
|
for (const w of nonCanonical) {
|
|
505
522
|
extraWarnings.push({
|
|
506
523
|
component: "fontRegistry",
|
|
@@ -509,12 +526,19 @@ var init_generator = __esm({
|
|
|
509
526
|
context: { code: "FONT_NONCANONICAL_WEIGHT" }
|
|
510
527
|
});
|
|
511
528
|
}
|
|
529
|
+
const allWarnings = [...coreWarnings, ...extraWarnings];
|
|
530
|
+
this.cacheService.set(
|
|
531
|
+
cacheKey,
|
|
532
|
+
{ buffer, warnings: allWarnings.length > 0 ? allWarnings : null },
|
|
533
|
+
config2,
|
|
534
|
+
{ bypassCache: bypassCache || hasDynamicContent }
|
|
535
|
+
);
|
|
512
536
|
return {
|
|
513
537
|
filename: `${config2.metadata?.title || this.adapter.label}${this.adapter.extension}`,
|
|
514
538
|
fileId: Date.now().toString(),
|
|
515
539
|
buffer,
|
|
516
540
|
cached: false,
|
|
517
|
-
warnings:
|
|
541
|
+
warnings: allWarnings.length > 0 ? allWarnings : null,
|
|
518
542
|
resolvedFonts
|
|
519
543
|
};
|
|
520
544
|
}
|
|
@@ -559,7 +583,9 @@ var init_cache = __esm({
|
|
|
559
583
|
this.cache = new LRUCache({
|
|
560
584
|
max: config.cache.maxItems,
|
|
561
585
|
maxSize: maxSizeBytes,
|
|
562
|
-
|
|
586
|
+
// Must stay the buffer's byte length: lru-cache rejects a zero or
|
|
587
|
+
// undefined size when `maxSize` is set.
|
|
588
|
+
sizeCalculation: (value) => value.buffer.length,
|
|
563
589
|
ttl: config.cache.ttlSeconds * 1e3,
|
|
564
590
|
dispose: (_value, _key, reason) => {
|
|
565
591
|
if (reason === "evict" || reason === "delete") {
|
|
@@ -641,404 +667,12 @@ var init_cache = __esm({
|
|
|
641
667
|
}
|
|
642
668
|
});
|
|
643
669
|
|
|
644
|
-
// src/server/services/font-staging/noop-stager.ts
|
|
645
|
-
var NoopFontStager;
|
|
646
|
-
var init_noop_stager = __esm({
|
|
647
|
-
"src/server/services/font-staging/noop-stager.ts"() {
|
|
648
|
-
"use strict";
|
|
649
|
-
init_esm_shims();
|
|
650
|
-
NoopFontStager = class {
|
|
651
|
-
async stage() {
|
|
652
|
-
return {
|
|
653
|
-
envOverrides: {},
|
|
654
|
-
cleanup: async () => {
|
|
655
|
-
}
|
|
656
|
-
};
|
|
657
|
-
}
|
|
658
|
-
};
|
|
659
|
-
}
|
|
660
|
-
});
|
|
661
|
-
|
|
662
|
-
// src/server/services/font-staging/types.ts
|
|
663
|
-
function nextStagingId() {
|
|
664
|
-
counter += 1;
|
|
665
|
-
return `${process.pid}-${counter}`;
|
|
666
|
-
}
|
|
667
|
-
function safeFilenamePart(s) {
|
|
668
|
-
return s.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 48);
|
|
669
|
-
}
|
|
670
|
-
var counter;
|
|
671
|
-
var init_types = __esm({
|
|
672
|
-
"src/server/services/font-staging/types.ts"() {
|
|
673
|
-
"use strict";
|
|
674
|
-
init_esm_shims();
|
|
675
|
-
counter = 0;
|
|
676
|
-
}
|
|
677
|
-
});
|
|
678
|
-
|
|
679
|
-
// src/server/services/font-staging/fontconfig-stager.ts
|
|
680
|
-
import { promises as fs } from "fs";
|
|
681
|
-
import path2 from "path";
|
|
682
|
-
import {
|
|
683
|
-
synthesizeFamilyName,
|
|
684
|
-
rewriteFontFamilyName
|
|
685
|
-
} from "@json-to-office/shared";
|
|
686
|
-
function escapeXml(s) {
|
|
687
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
688
|
-
}
|
|
689
|
-
var SYSTEM_FONTS_CONF_CANDIDATES, FontconfigStager;
|
|
690
|
-
var init_fontconfig_stager = __esm({
|
|
691
|
-
"src/server/services/font-staging/fontconfig-stager.ts"() {
|
|
692
|
-
"use strict";
|
|
693
|
-
init_esm_shims();
|
|
694
|
-
init_types();
|
|
695
|
-
SYSTEM_FONTS_CONF_CANDIDATES = [
|
|
696
|
-
"/etc/fonts/fonts.conf",
|
|
697
|
-
"/opt/homebrew/etc/fonts/fonts.conf",
|
|
698
|
-
"/usr/local/etc/fonts/fonts.conf"
|
|
699
|
-
];
|
|
700
|
-
FontconfigStager = class {
|
|
701
|
-
async stage(fonts, tempDir) {
|
|
702
|
-
const id = nextStagingId();
|
|
703
|
-
const fontsDir = path2.join(tempDir, "fonts");
|
|
704
|
-
await fs.mkdir(fontsDir, { recursive: true });
|
|
705
|
-
let serial = 0;
|
|
706
|
-
for (const r of fonts) {
|
|
707
|
-
if (r.sources.length === 0) continue;
|
|
708
|
-
for (const s of r.sources) {
|
|
709
|
-
serial += 1;
|
|
710
|
-
const suffix = s.italic ? "i" : "r";
|
|
711
|
-
const synth = synthesizeFamilyName(r.family, s.weight, s.italic);
|
|
712
|
-
const data = synth.family === r.family ? s.data : rewriteFontFamilyName(s.data, synth.family);
|
|
713
|
-
const name = `${safeFilenamePart(synth.family)}-${s.weight}${suffix}-${id}-${serial}.ttf`;
|
|
714
|
-
await fs.writeFile(path2.join(fontsDir, name), data);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
await fs.chmod(fontsDir, 365).catch(() => {
|
|
718
|
-
});
|
|
719
|
-
const includeLines = await this.pickSystemIncludes();
|
|
720
|
-
const cacheDir = path2.join(tempDir, "fc-cache");
|
|
721
|
-
await fs.mkdir(cacheDir, { recursive: true });
|
|
722
|
-
const configPath = path2.join(tempDir, "fontconfig.xml");
|
|
723
|
-
const configXml = [
|
|
724
|
-
'<?xml version="1.0"?>',
|
|
725
|
-
'<!DOCTYPE fontconfig SYSTEM "fonts.dtd">',
|
|
726
|
-
"<fontconfig>",
|
|
727
|
-
` <dir>${escapeXml(fontsDir)}</dir>`,
|
|
728
|
-
` <cachedir>${escapeXml(cacheDir)}</cachedir>`,
|
|
729
|
-
...includeLines,
|
|
730
|
-
"</fontconfig>",
|
|
731
|
-
""
|
|
732
|
-
].join("\n");
|
|
733
|
-
await fs.writeFile(configPath, configXml, "utf8");
|
|
734
|
-
return {
|
|
735
|
-
envOverrides: {
|
|
736
|
-
FONTCONFIG_FILE: configPath,
|
|
737
|
-
XDG_CACHE_HOME: cacheDir
|
|
738
|
-
},
|
|
739
|
-
cleanup: async () => {
|
|
740
|
-
}
|
|
741
|
-
};
|
|
742
|
-
}
|
|
743
|
-
async pickSystemIncludes() {
|
|
744
|
-
for (const candidate of SYSTEM_FONTS_CONF_CANDIDATES) {
|
|
745
|
-
try {
|
|
746
|
-
await fs.access(candidate);
|
|
747
|
-
return [
|
|
748
|
-
` <include ignore_missing="yes">${escapeXml(candidate)}</include>`
|
|
749
|
-
];
|
|
750
|
-
} catch {
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
return [` <include ignore_missing="yes">/etc/fonts/fonts.conf</include>`];
|
|
754
|
-
}
|
|
755
|
-
};
|
|
756
|
-
}
|
|
757
|
-
});
|
|
758
|
-
|
|
759
|
-
// src/server/services/font-staging/windows-stager.ts
|
|
760
|
-
import { promises as fs2 } from "fs";
|
|
761
|
-
import path3 from "path";
|
|
762
|
-
import {
|
|
763
|
-
synthesizeFamilyName as synthesizeFamilyName2,
|
|
764
|
-
rewriteFontFamilyName as rewriteFontFamilyName2
|
|
765
|
-
} from "@json-to-office/shared";
|
|
766
|
-
async function getGdiBindings() {
|
|
767
|
-
if (cachedBindings) return cachedBindings;
|
|
768
|
-
const koffi = await import("koffi");
|
|
769
|
-
const mod = koffi.default ?? koffi;
|
|
770
|
-
const gdi32 = mod.load("gdi32.dll");
|
|
771
|
-
cachedBindings = {
|
|
772
|
-
addFont: gdi32.func("int __stdcall AddFontResourceW(str16)"),
|
|
773
|
-
removeFont: gdi32.func("bool __stdcall RemoveFontResourceW(str16)")
|
|
774
|
-
};
|
|
775
|
-
return cachedBindings;
|
|
776
|
-
}
|
|
777
|
-
var cachedBindings, WindowsFontStager;
|
|
778
|
-
var init_windows_stager = __esm({
|
|
779
|
-
"src/server/services/font-staging/windows-stager.ts"() {
|
|
780
|
-
"use strict";
|
|
781
|
-
init_esm_shims();
|
|
782
|
-
init_types();
|
|
783
|
-
cachedBindings = null;
|
|
784
|
-
WindowsFontStager = class {
|
|
785
|
-
async stage(fonts, tempDir) {
|
|
786
|
-
const id = nextStagingId();
|
|
787
|
-
const fontsDir = path3.join(tempDir, "fonts");
|
|
788
|
-
await fs2.mkdir(fontsDir, { recursive: true });
|
|
789
|
-
const stagedPaths = [];
|
|
790
|
-
let serial = 0;
|
|
791
|
-
for (const r of fonts) {
|
|
792
|
-
if (r.sources.length === 0) continue;
|
|
793
|
-
for (const s of r.sources) {
|
|
794
|
-
serial += 1;
|
|
795
|
-
const suffix = s.italic ? "i" : "r";
|
|
796
|
-
const synth = synthesizeFamilyName2(r.family, s.weight, s.italic);
|
|
797
|
-
const data = synth.family === r.family ? s.data : rewriteFontFamilyName2(s.data, synth.family);
|
|
798
|
-
const name = `${safeFilenamePart(synth.family)}-${s.weight}${suffix}-${id}-${serial}.ttf`;
|
|
799
|
-
const fullPath = path3.join(fontsDir, name);
|
|
800
|
-
await fs2.writeFile(fullPath, data);
|
|
801
|
-
stagedPaths.push(fullPath);
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
if (stagedPaths.length === 0) {
|
|
805
|
-
return { envOverrides: {}, cleanup: async () => {
|
|
806
|
-
} };
|
|
807
|
-
}
|
|
808
|
-
const { addFont, removeFont } = await getGdiBindings();
|
|
809
|
-
const registered = [];
|
|
810
|
-
for (const p of stagedPaths) {
|
|
811
|
-
const added = addFont(p);
|
|
812
|
-
if (added > 0) registered.push(p);
|
|
813
|
-
}
|
|
814
|
-
let cleaned = false;
|
|
815
|
-
return {
|
|
816
|
-
envOverrides: {
|
|
817
|
-
// Force GDI backend so the freshly-registered fonts are visible.
|
|
818
|
-
// Skia on Windows uses DirectWrite which does not reliably see
|
|
819
|
-
// fonts added via AddFontResourceW.
|
|
820
|
-
SAL_DISABLE_SKIA: "1"
|
|
821
|
-
},
|
|
822
|
-
cleanup: async () => {
|
|
823
|
-
if (cleaned) return;
|
|
824
|
-
cleaned = true;
|
|
825
|
-
for (const p of registered) {
|
|
826
|
-
try {
|
|
827
|
-
removeFont(p);
|
|
828
|
-
} catch {
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
};
|
|
833
|
-
}
|
|
834
|
-
};
|
|
835
|
-
}
|
|
836
|
-
});
|
|
837
|
-
|
|
838
|
-
// src/server/services/font-staging/macos-stager.ts
|
|
839
|
-
import { promises as fs3 } from "fs";
|
|
840
|
-
import path4 from "path";
|
|
841
|
-
import {
|
|
842
|
-
synthesizeFamilyName as synthesizeFamilyName3,
|
|
843
|
-
rewriteFontFamilyName as rewriteFontFamilyName3
|
|
844
|
-
} from "@json-to-office/shared";
|
|
845
|
-
var PYTHON_MACRO, REGISTRY_MOD_XCU, MacOSCoreTextStager;
|
|
846
|
-
var init_macos_stager = __esm({
|
|
847
|
-
"src/server/services/font-staging/macos-stager.ts"() {
|
|
848
|
-
"use strict";
|
|
849
|
-
init_esm_shims();
|
|
850
|
-
init_types();
|
|
851
|
-
PYTHON_MACRO = `# Auto-generated by @json-to-office/jto. Runs inside soffice on OnStartApp
|
|
852
|
-
# to make staged fonts visible to LibreOffice's font enumeration. macOS 26
|
|
853
|
-
# blocks Session/Persistent CT registration for unsigned callers, but
|
|
854
|
-
# Process scope still works from inside the target process \u2014 which is
|
|
855
|
-
# exactly where this macro runs.
|
|
856
|
-
import os
|
|
857
|
-
import sys
|
|
858
|
-
import ctypes
|
|
859
|
-
import ctypes.util
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
def _log(msg):
|
|
863
|
-
# soffice swallows Python stdout in headless mode; stderr surfaces to
|
|
864
|
-
# the parent's pipe. The stager doesn't read this today but the
|
|
865
|
-
# converter logs stderr on failure, which is how we'll debug.
|
|
866
|
-
sys.stderr.write("[jto-font-register] " + msg + "\\n")
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
def register(*_args):
|
|
870
|
-
paths_env = os.environ.get("JTO_FONT_PATHS", "")
|
|
871
|
-
if not paths_env:
|
|
872
|
-
return
|
|
873
|
-
try:
|
|
874
|
-
cf = ctypes.CDLL(ctypes.util.find_library("CoreFoundation"))
|
|
875
|
-
ct = ctypes.CDLL(ctypes.util.find_library("CoreText"))
|
|
876
|
-
except Exception as e:
|
|
877
|
-
_log("failed to load CoreFoundation/CoreText: " + repr(e))
|
|
878
|
-
return
|
|
879
|
-
cf.CFURLCreateFromFileSystemRepresentation.argtypes = [
|
|
880
|
-
ctypes.c_void_p, ctypes.c_char_p, ctypes.c_long, ctypes.c_bool,
|
|
881
|
-
]
|
|
882
|
-
cf.CFURLCreateFromFileSystemRepresentation.restype = ctypes.c_void_p
|
|
883
|
-
cf.CFRelease.argtypes = [ctypes.c_void_p]
|
|
884
|
-
ct.CTFontManagerRegisterFontsForURL.argtypes = [
|
|
885
|
-
ctypes.c_void_p, ctypes.c_uint32, ctypes.c_void_p,
|
|
886
|
-
]
|
|
887
|
-
ct.CTFontManagerRegisterFontsForURL.restype = ctypes.c_bool
|
|
888
|
-
|
|
889
|
-
kCTFontManagerScopeProcess = 1
|
|
890
|
-
registered = 0
|
|
891
|
-
for p in paths_env.split(os.pathsep):
|
|
892
|
-
if not p:
|
|
893
|
-
continue
|
|
894
|
-
try:
|
|
895
|
-
b = p.encode("utf-8")
|
|
896
|
-
url = cf.CFURLCreateFromFileSystemRepresentation(
|
|
897
|
-
None, b, len(b), False
|
|
898
|
-
)
|
|
899
|
-
if not url:
|
|
900
|
-
_log("CFURL failed for " + p)
|
|
901
|
-
continue
|
|
902
|
-
ok = ct.CTFontManagerRegisterFontsForURL(
|
|
903
|
-
url, kCTFontManagerScopeProcess, None
|
|
904
|
-
)
|
|
905
|
-
cf.CFRelease(url)
|
|
906
|
-
if ok:
|
|
907
|
-
registered += 1
|
|
908
|
-
else:
|
|
909
|
-
_log("CT register returned false for " + p)
|
|
910
|
-
except Exception as e:
|
|
911
|
-
_log("exception registering " + p + ": " + repr(e))
|
|
912
|
-
_log("registered " + str(registered) + " font(s) at Process scope")
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
# Expose under "register" (event-binding URL) and module-level run so
|
|
916
|
-
# command-line vnd.sun.star.script invocation works either way.
|
|
917
|
-
g_exportedScripts = (register,)
|
|
918
|
-
`;
|
|
919
|
-
REGISTRY_MOD_XCU = `<?xml version="1.0" encoding="UTF-8"?>
|
|
920
|
-
<oor:items xmlns:oor="http://openoffice.org/2001/registry" xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
921
|
-
<item oor:path="/org.openoffice.Office.Events/ApplicationEvents/Bindings">
|
|
922
|
-
<node oor:name="OnStartApp" oor:op="replace">
|
|
923
|
-
<prop oor:name="BindingURL" oor:type="xs:string">
|
|
924
|
-
<value>vnd.sun.star.script:JtoFontRegister.py$register?language=Python&location=user</value>
|
|
925
|
-
</prop>
|
|
926
|
-
</node>
|
|
927
|
-
</item>
|
|
928
|
-
<item oor:path="/org.openoffice.Office.Common/Security/Scripting">
|
|
929
|
-
<prop oor:name="MacroSecurityLevel" oor:op="fuse">
|
|
930
|
-
<value>0</value>
|
|
931
|
-
</prop>
|
|
932
|
-
</item>
|
|
933
|
-
</oor:items>
|
|
934
|
-
`;
|
|
935
|
-
MacOSCoreTextStager = class {
|
|
936
|
-
async stage(fonts, tempDir) {
|
|
937
|
-
const embeddable = fonts.filter((r) => r.sources.length > 0);
|
|
938
|
-
if (embeddable.length === 0) {
|
|
939
|
-
return { envOverrides: {}, cleanup: async () => {
|
|
940
|
-
} };
|
|
941
|
-
}
|
|
942
|
-
const fontsDir = path4.join(tempDir, "fonts");
|
|
943
|
-
await fs3.mkdir(fontsDir, { recursive: true });
|
|
944
|
-
const id = nextStagingId();
|
|
945
|
-
const fontPaths = [];
|
|
946
|
-
const staged = [];
|
|
947
|
-
let serial = 0;
|
|
948
|
-
for (const r of embeddable) {
|
|
949
|
-
for (const s of r.sources) {
|
|
950
|
-
serial += 1;
|
|
951
|
-
const suffix = s.italic ? "i" : "r";
|
|
952
|
-
const synth = synthesizeFamilyName3(r.family, s.weight, s.italic);
|
|
953
|
-
const data = synth.family === r.family ? s.data : rewriteFontFamilyName3(s.data, synth.family);
|
|
954
|
-
const name = `${safeFilenamePart(synth.family)}-${s.weight}${suffix}-${id}-${serial}.ttf`;
|
|
955
|
-
const full = path4.join(fontsDir, name);
|
|
956
|
-
await fs3.writeFile(full, data);
|
|
957
|
-
fontPaths.push(full);
|
|
958
|
-
staged.push({
|
|
959
|
-
family: synth.family,
|
|
960
|
-
weight: s.weight,
|
|
961
|
-
italic: s.italic,
|
|
962
|
-
path: full
|
|
963
|
-
});
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
if (process.env.JTO_DEBUG_FONTS === "1") {
|
|
967
|
-
console.log(
|
|
968
|
-
"[jto macos-stager] staged " + staged.length + " font(s) for CT Process-scope registration; JTO_FONT_PATHS has " + fontPaths.length + " entries\n" + staged.map(
|
|
969
|
-
(s) => ` ${s.family} (w=${s.weight}${s.italic ? " italic" : ""}) \u2192 ${path4.basename(s.path)}`
|
|
970
|
-
).join("\n")
|
|
971
|
-
);
|
|
972
|
-
}
|
|
973
|
-
const profileUser = path4.join(tempDir, "user-profile", "user");
|
|
974
|
-
const scriptsDir = path4.join(profileUser, "Scripts", "python");
|
|
975
|
-
await fs3.mkdir(scriptsDir, { recursive: true });
|
|
976
|
-
await fs3.writeFile(
|
|
977
|
-
path4.join(scriptsDir, "JtoFontRegister.py"),
|
|
978
|
-
PYTHON_MACRO
|
|
979
|
-
);
|
|
980
|
-
await fs3.writeFile(
|
|
981
|
-
path4.join(profileUser, "registrymodifications.xcu"),
|
|
982
|
-
REGISTRY_MOD_XCU
|
|
983
|
-
);
|
|
984
|
-
return {
|
|
985
|
-
envOverrides: {
|
|
986
|
-
// Colon-separated list of staged TTF paths (`:` matches Python's
|
|
987
|
-
// `os.pathsep` on macOS). The macro reads this at OnStartApp.
|
|
988
|
-
JTO_FONT_PATHS: fontPaths.join(":"),
|
|
989
|
-
// Force LibreOffice's Core Graphics backend. Skia on macOS can
|
|
990
|
-
// skip Core Text's freshly-registered fonts in some builds.
|
|
991
|
-
SAL_DISABLE_SKIA: "1"
|
|
992
|
-
},
|
|
993
|
-
cleanup: async () => {
|
|
994
|
-
}
|
|
995
|
-
};
|
|
996
|
-
}
|
|
997
|
-
};
|
|
998
|
-
}
|
|
999
|
-
});
|
|
1000
|
-
|
|
1001
|
-
// src/server/services/font-staging/index.ts
|
|
1002
|
-
function getFontStager(platform = process.platform) {
|
|
1003
|
-
const hit = cached.get(platform);
|
|
1004
|
-
if (hit) return hit;
|
|
1005
|
-
let stager;
|
|
1006
|
-
switch (platform) {
|
|
1007
|
-
case "win32":
|
|
1008
|
-
stager = new WindowsFontStager();
|
|
1009
|
-
break;
|
|
1010
|
-
case "darwin":
|
|
1011
|
-
stager = new MacOSCoreTextStager();
|
|
1012
|
-
break;
|
|
1013
|
-
case "linux":
|
|
1014
|
-
case "freebsd":
|
|
1015
|
-
case "openbsd":
|
|
1016
|
-
stager = new FontconfigStager();
|
|
1017
|
-
break;
|
|
1018
|
-
default:
|
|
1019
|
-
stager = new NoopFontStager();
|
|
1020
|
-
}
|
|
1021
|
-
cached.set(platform, stager);
|
|
1022
|
-
return stager;
|
|
1023
|
-
}
|
|
1024
|
-
var cached;
|
|
1025
|
-
var init_font_staging = __esm({
|
|
1026
|
-
"src/server/services/font-staging/index.ts"() {
|
|
1027
|
-
"use strict";
|
|
1028
|
-
init_esm_shims();
|
|
1029
|
-
init_noop_stager();
|
|
1030
|
-
init_fontconfig_stager();
|
|
1031
|
-
init_windows_stager();
|
|
1032
|
-
init_macos_stager();
|
|
1033
|
-
cached = /* @__PURE__ */ new Map();
|
|
1034
|
-
}
|
|
1035
|
-
});
|
|
1036
|
-
|
|
1037
670
|
// src/server/services/libreoffice-converter.ts
|
|
1038
671
|
import { execFile } from "child_process";
|
|
1039
|
-
import { promises as
|
|
672
|
+
import { promises as fs } from "fs";
|
|
1040
673
|
import os from "os";
|
|
1041
|
-
import
|
|
674
|
+
import path2 from "path";
|
|
675
|
+
import { getFontStager } from "@json-to-office/jto-cli";
|
|
1042
676
|
function executeFile(binary, args, timeoutMs, envOverrides) {
|
|
1043
677
|
return new Promise((resolve2, reject) => {
|
|
1044
678
|
execFile(
|
|
@@ -1070,7 +704,7 @@ function toErrorText(value) {
|
|
|
1070
704
|
return String(value);
|
|
1071
705
|
}
|
|
1072
706
|
function sanitizeBaseName(originalName) {
|
|
1073
|
-
const parsed =
|
|
707
|
+
const parsed = path2.parse(originalName || "document");
|
|
1074
708
|
return (parsed.name || "document").replace(/[^a-zA-Z0-9._-]/g, "_") || "document";
|
|
1075
709
|
}
|
|
1076
710
|
var DEFAULT_CONVERSION_TIMEOUT_MS, BINARY_PROBE_TIMEOUT_MS, MAX_EXEC_BUFFER_BYTES, LibreOfficeError, LibreOfficeBinaryNotFoundError, LibreOfficeTimeoutError, LibreOfficeConversionError, LibreOfficeOutputNotFoundError, LibreOfficeConverterService;
|
|
@@ -1079,7 +713,6 @@ var init_libreoffice_converter = __esm({
|
|
|
1079
713
|
"use strict";
|
|
1080
714
|
init_esm_shims();
|
|
1081
715
|
init_config();
|
|
1082
|
-
init_font_staging();
|
|
1083
716
|
DEFAULT_CONVERSION_TIMEOUT_MS = 3e4;
|
|
1084
717
|
BINARY_PROBE_TIMEOUT_MS = 5e3;
|
|
1085
718
|
MAX_EXEC_BUFFER_BYTES = 20 * 1024 * 1024;
|
|
@@ -1144,20 +777,20 @@ var init_libreoffice_converter = __esm({
|
|
|
1144
777
|
throw new LibreOfficeConversionError("Input file is empty");
|
|
1145
778
|
}
|
|
1146
779
|
const binaryPath = await this.resolveBinaryPath();
|
|
1147
|
-
const tempDir = await
|
|
1148
|
-
|
|
780
|
+
const tempDir = await fs.mkdtemp(
|
|
781
|
+
path2.join(os.tmpdir(), "jto-libreoffice-")
|
|
1149
782
|
);
|
|
1150
783
|
const outputBaseName = sanitizeBaseName(originalName);
|
|
1151
784
|
const ext = this.format === "pptx" ? ".pptx" : ".docx";
|
|
1152
|
-
const inputPath =
|
|
1153
|
-
const pdfPath =
|
|
785
|
+
const inputPath = path2.join(tempDir, `${outputBaseName}${ext}`);
|
|
786
|
+
const pdfPath = path2.join(tempDir, `${outputBaseName}.pdf`);
|
|
1154
787
|
const stager = getFontStager();
|
|
1155
788
|
const fontsToStage = (resolvedFonts ?? []).filter(
|
|
1156
789
|
(r) => r.sources.length > 0
|
|
1157
790
|
);
|
|
1158
791
|
const stageHandle = fontsToStage.length > 0 ? await stager.stage(fontsToStage, tempDir) : null;
|
|
1159
792
|
try {
|
|
1160
|
-
await
|
|
793
|
+
await fs.writeFile(inputPath, input);
|
|
1161
794
|
const filterName = this.format === "pptx" ? "impress_pdf_Export" : "writer_pdf_Export";
|
|
1162
795
|
await this.runConversion(
|
|
1163
796
|
binaryPath,
|
|
@@ -1167,7 +800,7 @@ var init_libreoffice_converter = __esm({
|
|
|
1167
800
|
stageHandle?.envOverrides
|
|
1168
801
|
);
|
|
1169
802
|
try {
|
|
1170
|
-
return await
|
|
803
|
+
return await fs.readFile(pdfPath);
|
|
1171
804
|
} catch (error) {
|
|
1172
805
|
if (error.code === "ENOENT") {
|
|
1173
806
|
throw new LibreOfficeOutputNotFoundError(pdfPath);
|
|
@@ -1179,7 +812,7 @@ var init_libreoffice_converter = __esm({
|
|
|
1179
812
|
await stageHandle.cleanup().catch(() => {
|
|
1180
813
|
});
|
|
1181
814
|
}
|
|
1182
|
-
await
|
|
815
|
+
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
1183
816
|
});
|
|
1184
817
|
}
|
|
1185
818
|
}
|
|
@@ -1206,9 +839,9 @@ var init_libreoffice_converter = __esm({
|
|
|
1206
839
|
throw new LibreOfficeBinaryNotFoundError(candidates);
|
|
1207
840
|
}
|
|
1208
841
|
async isBinaryAvailable(binaryPath) {
|
|
1209
|
-
if (binaryPath.includes(
|
|
842
|
+
if (binaryPath.includes(path2.sep)) {
|
|
1210
843
|
try {
|
|
1211
|
-
await
|
|
844
|
+
await fs.access(binaryPath);
|
|
1212
845
|
} catch {
|
|
1213
846
|
return false;
|
|
1214
847
|
}
|
|
@@ -1222,7 +855,7 @@ var init_libreoffice_converter = __esm({
|
|
|
1222
855
|
}
|
|
1223
856
|
}
|
|
1224
857
|
async runConversion(binaryPath, inputPath, outputDir, filterName, envOverrides) {
|
|
1225
|
-
const userProfilePath =
|
|
858
|
+
const userProfilePath = path2.join(outputDir, "user-profile").replace(/\\/g, "/");
|
|
1226
859
|
const userInstallation = `file:///${userProfilePath.replace(/^\//, "")}`;
|
|
1227
860
|
const args = [
|
|
1228
861
|
"--headless",
|
|
@@ -1640,36 +1273,36 @@ function isPrivateHost(host) {
|
|
|
1640
1273
|
const firstIpv6Group = Number.parseInt(normalized.split(":")[0] || "0", 16);
|
|
1641
1274
|
return firstIpv6Group >= 64512 && firstIpv6Group <= 65023 || firstIpv6Group >= 65152 && firstIpv6Group <= 65215;
|
|
1642
1275
|
}
|
|
1643
|
-
function assertAllowedUrl(source,
|
|
1276
|
+
function assertAllowedUrl(source, path5, allowedHosts) {
|
|
1644
1277
|
if (source.startsWith("data:")) return;
|
|
1645
1278
|
let url;
|
|
1646
1279
|
try {
|
|
1647
1280
|
url = new URL(source);
|
|
1648
1281
|
} catch {
|
|
1649
1282
|
throw new UnsafeOutboundSourceError(
|
|
1650
|
-
|
|
1283
|
+
path5,
|
|
1651
1284
|
"local and relative file paths are disabled for HTTP requests"
|
|
1652
1285
|
);
|
|
1653
1286
|
}
|
|
1654
1287
|
if (url.protocol !== "https:") {
|
|
1655
|
-
throw new UnsafeOutboundSourceError(
|
|
1288
|
+
throw new UnsafeOutboundSourceError(path5, "only HTTPS URLs are allowed");
|
|
1656
1289
|
}
|
|
1657
1290
|
if (url.username || url.password) {
|
|
1658
1291
|
throw new UnsafeOutboundSourceError(
|
|
1659
|
-
|
|
1292
|
+
path5,
|
|
1660
1293
|
"URLs containing credentials are not allowed"
|
|
1661
1294
|
);
|
|
1662
1295
|
}
|
|
1663
1296
|
const host = url.hostname.toLowerCase().replace(/\.$/, "");
|
|
1664
1297
|
if (isPrivateHost(host)) {
|
|
1665
1298
|
throw new UnsafeOutboundSourceError(
|
|
1666
|
-
|
|
1299
|
+
path5,
|
|
1667
1300
|
`private or local host "${host}" is not allowed`
|
|
1668
1301
|
);
|
|
1669
1302
|
}
|
|
1670
1303
|
if (!allowedHosts.some((pattern) => hostMatches(host, pattern))) {
|
|
1671
1304
|
throw new UnsafeOutboundSourceError(
|
|
1672
|
-
|
|
1305
|
+
path5,
|
|
1673
1306
|
`host "${host}" is not in OUTBOUND_HOST_ALLOWLIST`
|
|
1674
1307
|
);
|
|
1675
1308
|
}
|
|
@@ -1687,13 +1320,13 @@ function extractCssReferences(input) {
|
|
|
1687
1320
|
function extractAbsoluteUrls(input) {
|
|
1688
1321
|
return input.match(/(?:https?|file):\/\/[^\s'"<>)}\]]+/gi) ?? [];
|
|
1689
1322
|
}
|
|
1690
|
-
function assertSafeSvg(svg,
|
|
1323
|
+
function assertSafeSvg(svg, path5, allowedHosts) {
|
|
1691
1324
|
if (/<\s*script\b/i.test(svg) || /<\s*(?:foreignObject|iframe|object|embed)\b/i.test(svg) || /\bon[a-z]+\s*=/i.test(svg)) {
|
|
1692
|
-
throw new UnsafeOutboundSourceError(
|
|
1325
|
+
throw new UnsafeOutboundSourceError(path5, "active SVG content is disabled");
|
|
1693
1326
|
}
|
|
1694
1327
|
if (/<!\s*(?:DOCTYPE|ENTITY)\b/i.test(svg)) {
|
|
1695
1328
|
throw new UnsafeOutboundSourceError(
|
|
1696
|
-
|
|
1329
|
+
path5,
|
|
1697
1330
|
"SVG document types and entities are disabled"
|
|
1698
1331
|
);
|
|
1699
1332
|
}
|
|
@@ -1702,20 +1335,20 @@ function assertSafeSvg(svg, path8, allowedHosts) {
|
|
|
1702
1335
|
while (match = refPattern.exec(svg)) {
|
|
1703
1336
|
const ref = match[2].trim();
|
|
1704
1337
|
if (!ref || ref.startsWith("#") || ref.startsWith("data:")) continue;
|
|
1705
|
-
assertAllowedUrl(ref,
|
|
1338
|
+
assertAllowedUrl(ref, path5, allowedHosts);
|
|
1706
1339
|
}
|
|
1707
1340
|
for (const ref of extractCssReferences(svg)) {
|
|
1708
1341
|
if (ref.startsWith("#") || ref.startsWith("data:")) continue;
|
|
1709
|
-
assertAllowedUrl(ref,
|
|
1342
|
+
assertAllowedUrl(ref, path5, allowedHosts);
|
|
1710
1343
|
}
|
|
1711
1344
|
}
|
|
1712
|
-
function assertSafeEncodedImage(source,
|
|
1345
|
+
function assertSafeEncodedImage(source, path5, allowedHosts) {
|
|
1713
1346
|
let candidate = source.trim();
|
|
1714
1347
|
let declaredSvg = false;
|
|
1715
1348
|
if (candidate.startsWith("data:")) {
|
|
1716
1349
|
const comma = candidate.indexOf(",");
|
|
1717
1350
|
if (comma < 0) {
|
|
1718
|
-
throw new UnsafeOutboundSourceError(
|
|
1351
|
+
throw new UnsafeOutboundSourceError(path5, "malformed image data URL");
|
|
1719
1352
|
}
|
|
1720
1353
|
const metadata = candidate.slice(5, comma);
|
|
1721
1354
|
declaredSvg = metadata.split(";")[0].toLowerCase() === "image/svg+xml";
|
|
@@ -1724,7 +1357,7 @@ function assertSafeEncodedImage(source, path8, allowedHosts) {
|
|
|
1724
1357
|
try {
|
|
1725
1358
|
candidate = /(?:^|;)base64(?:;|$)/i.test(metadata) ? Buffer.from(payload, "base64").toString("utf8") : decodeURIComponent(payload);
|
|
1726
1359
|
} catch {
|
|
1727
|
-
throw new UnsafeOutboundSourceError(
|
|
1360
|
+
throw new UnsafeOutboundSourceError(path5, "malformed SVG data URL");
|
|
1728
1361
|
}
|
|
1729
1362
|
} else if (!/<\s*svg\b/i.test(candidate)) {
|
|
1730
1363
|
candidate = Buffer.from(candidate, "base64").toString("utf8");
|
|
@@ -1732,37 +1365,37 @@ function assertSafeEncodedImage(source, path8, allowedHosts) {
|
|
|
1732
1365
|
const svgStart = candidate.search(/<\s*svg\b/i);
|
|
1733
1366
|
if (svgStart < 0) {
|
|
1734
1367
|
if (declaredSvg) {
|
|
1735
|
-
throw new UnsafeOutboundSourceError(
|
|
1368
|
+
throw new UnsafeOutboundSourceError(path5, "malformed SVG image data");
|
|
1736
1369
|
}
|
|
1737
1370
|
return;
|
|
1738
1371
|
}
|
|
1739
|
-
assertSafeSvg(candidate,
|
|
1372
|
+
assertSafeSvg(candidate, path5, allowedHosts);
|
|
1740
1373
|
}
|
|
1741
|
-
function assertSafeResources(resources,
|
|
1374
|
+
function assertSafeResources(resources, path5, allowedHosts) {
|
|
1742
1375
|
if (typeof resources.js === "string" && resources.js.trim()) {
|
|
1743
1376
|
throw new UnsafeOutboundSourceError(
|
|
1744
|
-
`${
|
|
1377
|
+
`${path5}.js`,
|
|
1745
1378
|
"remote renderer JavaScript resources are disabled"
|
|
1746
1379
|
);
|
|
1747
1380
|
}
|
|
1748
1381
|
if (typeof resources.css === "string") {
|
|
1749
1382
|
for (const ref of extractCssReferences(resources.css)) {
|
|
1750
1383
|
if (ref.startsWith("data:")) continue;
|
|
1751
|
-
assertAllowedUrl(ref, `${
|
|
1384
|
+
assertAllowedUrl(ref, `${path5}.css`, allowedHosts);
|
|
1752
1385
|
}
|
|
1753
1386
|
}
|
|
1754
1387
|
if (Array.isArray(resources.files) && resources.files.length > 0) {
|
|
1755
1388
|
throw new UnsafeOutboundSourceError(
|
|
1756
|
-
`${
|
|
1389
|
+
`${path5}.files`,
|
|
1757
1390
|
"remote renderer JavaScript resources are disabled"
|
|
1758
1391
|
);
|
|
1759
1392
|
}
|
|
1760
1393
|
}
|
|
1761
|
-
function assertNoRemoteReferences(value,
|
|
1394
|
+
function assertNoRemoteReferences(value, path5, allowedHosts, seen) {
|
|
1762
1395
|
if (typeof value === "string") {
|
|
1763
1396
|
if (/\b(?:fetch|XMLHttpRequest|WebSocket|EventSource)\s*\(/i.test(value) || /\b(?:javascript|file):/i.test(value)) {
|
|
1764
1397
|
throw new UnsafeOutboundSourceError(
|
|
1765
|
-
|
|
1398
|
+
path5,
|
|
1766
1399
|
"network-capable JavaScript and file URLs are disabled"
|
|
1767
1400
|
);
|
|
1768
1401
|
}
|
|
@@ -1771,11 +1404,11 @@ function assertNoRemoteReferences(value, path8, allowedHosts, seen) {
|
|
|
1771
1404
|
...extractCssReferences(value)
|
|
1772
1405
|
]) {
|
|
1773
1406
|
if (url.startsWith("data:")) continue;
|
|
1774
|
-
assertAllowedUrl(url,
|
|
1407
|
+
assertAllowedUrl(url, path5, allowedHosts);
|
|
1775
1408
|
}
|
|
1776
1409
|
if (/^\/\//.test(value.trim())) {
|
|
1777
1410
|
throw new UnsafeOutboundSourceError(
|
|
1778
|
-
|
|
1411
|
+
path5,
|
|
1779
1412
|
"protocol-relative URLs are disabled"
|
|
1780
1413
|
);
|
|
1781
1414
|
}
|
|
@@ -1785,20 +1418,20 @@ function assertNoRemoteReferences(value, path8, allowedHosts, seen) {
|
|
|
1785
1418
|
seen.add(value);
|
|
1786
1419
|
if (Array.isArray(value)) {
|
|
1787
1420
|
value.forEach(
|
|
1788
|
-
(entry, index) => assertNoRemoteReferences(entry, `${
|
|
1421
|
+
(entry, index) => assertNoRemoteReferences(entry, `${path5}[${index}]`, allowedHosts, seen)
|
|
1789
1422
|
);
|
|
1790
1423
|
return;
|
|
1791
1424
|
}
|
|
1792
1425
|
for (const [key, child] of Object.entries(value)) {
|
|
1793
|
-
assertNoRemoteReferences(child, `${
|
|
1426
|
+
assertNoRemoteReferences(child, `${path5}.${key}`, allowedHosts, seen);
|
|
1794
1427
|
}
|
|
1795
1428
|
}
|
|
1796
|
-
function visit(value,
|
|
1429
|
+
function visit(value, path5, containerKey, allowedHosts, seen) {
|
|
1797
1430
|
if (!value || typeof value !== "object" || seen.has(value)) return;
|
|
1798
1431
|
seen.add(value);
|
|
1799
1432
|
if (Array.isArray(value)) {
|
|
1800
1433
|
value.forEach(
|
|
1801
|
-
(entry, index) => visit(entry, `${
|
|
1434
|
+
(entry, index) => visit(entry, `${path5}[${index}]`, containerKey, allowedHosts, seen)
|
|
1802
1435
|
);
|
|
1803
1436
|
return;
|
|
1804
1437
|
}
|
|
@@ -1806,7 +1439,7 @@ function visit(value, path8, containerKey, allowedHosts, seen) {
|
|
|
1806
1439
|
for (const key of Object.keys(record)) {
|
|
1807
1440
|
if (DANGEROUS_KEYS.has(key)) {
|
|
1808
1441
|
throw new UnsafeOutboundSourceError(
|
|
1809
|
-
`${
|
|
1442
|
+
`${path5}.${key}`,
|
|
1810
1443
|
"prototype mutation keys are disabled"
|
|
1811
1444
|
);
|
|
1812
1445
|
}
|
|
@@ -1814,31 +1447,31 @@ function visit(value, path8, containerKey, allowedHosts, seen) {
|
|
|
1814
1447
|
const kind = typeof record.kind === "string" ? record.kind : void 0;
|
|
1815
1448
|
if (kind === "file" && typeof record.path === "string") {
|
|
1816
1449
|
throw new UnsafeOutboundSourceError(
|
|
1817
|
-
`${
|
|
1450
|
+
`${path5}.path`,
|
|
1818
1451
|
"local file sources are disabled for HTTP requests"
|
|
1819
1452
|
);
|
|
1820
1453
|
}
|
|
1821
1454
|
if ((kind === "url" || kind === "variable") && typeof record.url === "string") {
|
|
1822
|
-
assertAllowedUrl(record.url, `${
|
|
1455
|
+
assertAllowedUrl(record.url, `${path5}.url`, allowedHosts);
|
|
1823
1456
|
}
|
|
1824
1457
|
const componentName = typeof record.name === "string" ? record.name.toLowerCase() : void 0;
|
|
1825
1458
|
const props = isRecord(record.props) ? record.props : void 0;
|
|
1826
1459
|
if (componentName && props) {
|
|
1827
1460
|
if (COMPONENT_SOURCE_NAMES.has(componentName) && typeof props.path === "string") {
|
|
1828
|
-
assertAllowedUrl(props.path, `${
|
|
1829
|
-
assertSafeEncodedImage(props.path, `${
|
|
1461
|
+
assertAllowedUrl(props.path, `${path5}.props.path`, allowedHosts);
|
|
1462
|
+
assertSafeEncodedImage(props.path, `${path5}.props.path`, allowedHosts);
|
|
1830
1463
|
}
|
|
1831
1464
|
if (COMPONENT_SOURCE_NAMES.has(componentName) && typeof props.base64 === "string") {
|
|
1832
1465
|
assertSafeEncodedImage(
|
|
1833
1466
|
props.base64,
|
|
1834
|
-
`${
|
|
1467
|
+
`${path5}.props.base64`,
|
|
1835
1468
|
allowedHosts
|
|
1836
1469
|
);
|
|
1837
1470
|
}
|
|
1838
1471
|
if ((componentName === "highcharts" || componentName === "visual") && typeof props.serverUrl === "string") {
|
|
1839
1472
|
assertAllowedUrl(
|
|
1840
1473
|
props.serverUrl,
|
|
1841
|
-
`${
|
|
1474
|
+
`${path5}.props.serverUrl`,
|
|
1842
1475
|
allowedHosts
|
|
1843
1476
|
);
|
|
1844
1477
|
}
|
|
@@ -1846,30 +1479,30 @@ function visit(value, path8, containerKey, allowedHosts, seen) {
|
|
|
1846
1479
|
if (isRecord(props.resources)) {
|
|
1847
1480
|
assertSafeResources(
|
|
1848
1481
|
props.resources,
|
|
1849
|
-
`${
|
|
1482
|
+
`${path5}.props.resources`,
|
|
1850
1483
|
allowedHosts
|
|
1851
1484
|
);
|
|
1852
1485
|
}
|
|
1853
1486
|
assertNoRemoteReferences(
|
|
1854
1487
|
props.options,
|
|
1855
|
-
`${
|
|
1488
|
+
`${path5}.props.options`,
|
|
1856
1489
|
allowedHosts,
|
|
1857
1490
|
/* @__PURE__ */ new WeakSet()
|
|
1858
1491
|
);
|
|
1859
1492
|
}
|
|
1860
1493
|
}
|
|
1861
1494
|
if (containerKey === "image" && typeof record.path === "string") {
|
|
1862
|
-
assertAllowedUrl(record.path, `${
|
|
1863
|
-
assertSafeEncodedImage(record.path, `${
|
|
1495
|
+
assertAllowedUrl(record.path, `${path5}.path`, allowedHosts);
|
|
1496
|
+
assertSafeEncodedImage(record.path, `${path5}.path`, allowedHosts);
|
|
1864
1497
|
}
|
|
1865
1498
|
if (typeof record.svg === "string") {
|
|
1866
|
-
assertSafeSvg(record.svg, `${
|
|
1499
|
+
assertSafeSvg(record.svg, `${path5}.svg`, allowedHosts);
|
|
1867
1500
|
}
|
|
1868
1501
|
if (containerKey === "resources") {
|
|
1869
|
-
assertSafeResources(record,
|
|
1502
|
+
assertSafeResources(record, path5, allowedHosts);
|
|
1870
1503
|
}
|
|
1871
1504
|
for (const [key, child] of Object.entries(record)) {
|
|
1872
|
-
visit(child, `${
|
|
1505
|
+
visit(child, `${path5}.${key}`, key, allowedHosts, seen);
|
|
1873
1506
|
}
|
|
1874
1507
|
}
|
|
1875
1508
|
function assertSafeOutboundSources(value, policy, rootPath = "request") {
|
|
@@ -1890,9 +1523,9 @@ var init_outbound_source_policy = __esm({
|
|
|
1890
1523
|
"use strict";
|
|
1891
1524
|
init_esm_shims();
|
|
1892
1525
|
UnsafeOutboundSourceError = class extends Error {
|
|
1893
|
-
constructor(
|
|
1894
|
-
super(`Unsafe outbound source at ${
|
|
1895
|
-
this.path =
|
|
1526
|
+
constructor(path5, reason) {
|
|
1527
|
+
super(`Unsafe outbound source at ${path5}: ${reason}`);
|
|
1528
|
+
this.path = path5;
|
|
1896
1529
|
this.name = "UnsafeOutboundSourceError";
|
|
1897
1530
|
}
|
|
1898
1531
|
};
|
|
@@ -1902,7 +1535,7 @@ var init_outbound_source_policy = __esm({
|
|
|
1902
1535
|
});
|
|
1903
1536
|
|
|
1904
1537
|
// src/server/rasterize-route.ts
|
|
1905
|
-
import
|
|
1538
|
+
import path3 from "path";
|
|
1906
1539
|
import { Type as Type2 } from "@sinclair/typebox";
|
|
1907
1540
|
import { bodyLimit } from "hono/body-limit";
|
|
1908
1541
|
import { HTTPException as HTTPException3 } from "hono/http-exception";
|
|
@@ -1911,7 +1544,9 @@ import {
|
|
|
1911
1544
|
DEFAULT_VISUAL_DPI,
|
|
1912
1545
|
MIN_VISUAL_DPI,
|
|
1913
1546
|
MAX_VISUAL_DPI,
|
|
1914
|
-
MAX_RASTERIZE_BATCH_SLIDES
|
|
1547
|
+
MAX_RASTERIZE_BATCH_SLIDES,
|
|
1548
|
+
MAX_RASTERIZE_FONTS,
|
|
1549
|
+
MAX_RASTERIZE_FONT_BYTES
|
|
1915
1550
|
} from "@json-to-office/shared";
|
|
1916
1551
|
import {
|
|
1917
1552
|
createLibreOfficePptxRasterizer,
|
|
@@ -1953,11 +1588,23 @@ function assertPixelBudget(slides) {
|
|
|
1953
1588
|
});
|
|
1954
1589
|
}
|
|
1955
1590
|
}
|
|
1591
|
+
function assertFontBudget(fonts) {
|
|
1592
|
+
if (!fonts?.length) return;
|
|
1593
|
+
const bytes = fonts.reduce(
|
|
1594
|
+
(sum, f) => sum + Math.ceil(f.data.length * 0.75),
|
|
1595
|
+
0
|
|
1596
|
+
);
|
|
1597
|
+
if (bytes > MAX_RASTERIZE_FONT_BYTES) {
|
|
1598
|
+
throw new HTTPException3(400, {
|
|
1599
|
+
message: "Requested font payload is too large"
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1956
1603
|
function resolveSafeBaseDir(baseDir) {
|
|
1957
1604
|
if (baseDir === void 0) return void 0;
|
|
1958
|
-
const resolved =
|
|
1605
|
+
const resolved = path3.resolve(baseDir);
|
|
1959
1606
|
const cwd = process.cwd();
|
|
1960
|
-
if (resolved !== cwd && !resolved.startsWith(cwd +
|
|
1607
|
+
if (resolved !== cwd && !resolved.startsWith(cwd + path3.sep)) {
|
|
1961
1608
|
throw new HTTPException3(400, {
|
|
1962
1609
|
message: "baseDir must be inside the server working directory"
|
|
1963
1610
|
});
|
|
@@ -1980,13 +1627,22 @@ function toHttpException(error) {
|
|
|
1980
1627
|
message: "Internal server error during rasterization"
|
|
1981
1628
|
});
|
|
1982
1629
|
}
|
|
1630
|
+
function resolveMaxBodyBytes(explicit) {
|
|
1631
|
+
const fromEnv = Number.parseInt(
|
|
1632
|
+
process.env.MAX_RASTERIZE_BODY_SIZE ?? "",
|
|
1633
|
+
10
|
|
1634
|
+
);
|
|
1635
|
+
const configured = explicit ?? (Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : config.requestLimits.maxBodySize);
|
|
1636
|
+
return Math.min(configured, MAX_RASTERIZE_BODY_CEILING);
|
|
1637
|
+
}
|
|
1983
1638
|
function registerRasterizeRoute(router, options = {}) {
|
|
1984
1639
|
const getRasterizer = options.getRasterizer ?? getSharedRasterizer;
|
|
1985
1640
|
const getBatchRasterizer = options.getBatchRasterizer ?? getSharedBatchRasterizer;
|
|
1641
|
+
const maxBodyBytes = resolveMaxBodyBytes(options.maxBodyBytes);
|
|
1986
1642
|
const shared = [
|
|
1987
1643
|
...options.preMiddleware ?? [],
|
|
1988
1644
|
bodyLimit({
|
|
1989
|
-
maxSize:
|
|
1645
|
+
maxSize: maxBodyBytes,
|
|
1990
1646
|
onError: () => {
|
|
1991
1647
|
throw new HTTPException3(413, { message: "Request body too large" });
|
|
1992
1648
|
}
|
|
@@ -2006,10 +1662,11 @@ function registerRasterizeRoute(router, options = {}) {
|
|
|
2006
1662
|
...shared,
|
|
2007
1663
|
tbValidator(RasterizeRequestSchema),
|
|
2008
1664
|
async (c) => {
|
|
2009
|
-
const { presentation, dpi, baseDir } = getValidated(c, "json");
|
|
1665
|
+
const { presentation, dpi, baseDir, fonts } = getValidated(c, "json");
|
|
2010
1666
|
const safeBaseDir = resolveSafeBaseDir(baseDir);
|
|
2011
1667
|
const effectiveDpi = clampVisualDpi(dpi ?? DEFAULT_VISUAL_DPI);
|
|
2012
1668
|
assertPixelBudget([{ presentation, dpi: effectiveDpi }]);
|
|
1669
|
+
assertFontBudget(fonts);
|
|
2013
1670
|
const result = await guard(async () => {
|
|
2014
1671
|
if (options.sourcePolicy) {
|
|
2015
1672
|
assertSafeOutboundSources(
|
|
@@ -2021,7 +1678,12 @@ function registerRasterizeRoute(router, options = {}) {
|
|
|
2021
1678
|
return getRasterizer()({
|
|
2022
1679
|
presentation,
|
|
2023
1680
|
dpi: effectiveDpi,
|
|
2024
|
-
baseDir: safeBaseDir
|
|
1681
|
+
baseDir: safeBaseDir,
|
|
1682
|
+
// Conditional so a fontless request reaches the engine with no
|
|
1683
|
+
// `fonts` key at all — an `undefined` would still be an absent
|
|
1684
|
+
// digest, but keeping the object identical to the pre-change shape
|
|
1685
|
+
// makes the cache-key equivalence obvious.
|
|
1686
|
+
...fonts?.length && { fonts }
|
|
2025
1687
|
});
|
|
2026
1688
|
});
|
|
2027
1689
|
return c.json(result);
|
|
@@ -2032,13 +1694,14 @@ function registerRasterizeRoute(router, options = {}) {
|
|
|
2032
1694
|
...shared,
|
|
2033
1695
|
tbValidator(RasterizeBatchRequestSchema),
|
|
2034
1696
|
async (c) => {
|
|
2035
|
-
const { slides, baseDir } = getValidated(c, "json");
|
|
1697
|
+
const { slides, baseDir, fonts } = getValidated(c, "json");
|
|
2036
1698
|
const safeBaseDir = resolveSafeBaseDir(baseDir);
|
|
2037
1699
|
const effectiveSlides = slides.map((slide) => ({
|
|
2038
1700
|
presentation: slide.presentation,
|
|
2039
1701
|
dpi: clampVisualDpi(slide.dpi ?? DEFAULT_VISUAL_DPI)
|
|
2040
1702
|
}));
|
|
2041
1703
|
assertPixelBudget(effectiveSlides);
|
|
1704
|
+
assertFontBudget(fonts);
|
|
2042
1705
|
const result = await guard(async () => {
|
|
2043
1706
|
if (options.sourcePolicy) {
|
|
2044
1707
|
slides.forEach(
|
|
@@ -2051,7 +1714,8 @@ function registerRasterizeRoute(router, options = {}) {
|
|
|
2051
1714
|
}
|
|
2052
1715
|
return getBatchRasterizer()({
|
|
2053
1716
|
slides: effectiveSlides,
|
|
2054
|
-
baseDir: safeBaseDir
|
|
1717
|
+
baseDir: safeBaseDir,
|
|
1718
|
+
...fonts?.length && { fonts }
|
|
2055
1719
|
});
|
|
2056
1720
|
});
|
|
2057
1721
|
return c.json({
|
|
@@ -2068,20 +1732,47 @@ function registerRasterizeRoute(router, options = {}) {
|
|
|
2068
1732
|
}
|
|
2069
1733
|
);
|
|
2070
1734
|
}
|
|
2071
|
-
var RasterizeRequestSchema, RasterizeBatchRequestSchema, sharedRasterizer, sharedBatchRasterizer, jsonOnly, DEFAULT_SLIDE_WIDTH_IN, DEFAULT_SLIDE_HEIGHT_IN, MAX_SLIDE_PIXELS, MAX_BATCH_PIXELS;
|
|
1735
|
+
var BASE64_PATTERN, RasterizeFontFaceSchema, FontsSchema, RasterizeRequestSchema, RasterizeBatchRequestSchema, sharedRasterizer, sharedBatchRasterizer, jsonOnly, DEFAULT_SLIDE_WIDTH_IN, DEFAULT_SLIDE_HEIGHT_IN, MAX_SLIDE_PIXELS, MAX_BATCH_PIXELS, MAX_RASTERIZE_BODY_CEILING;
|
|
2072
1736
|
var init_rasterize_route = __esm({
|
|
2073
1737
|
"src/server/rasterize-route.ts"() {
|
|
2074
1738
|
"use strict";
|
|
2075
1739
|
init_esm_shims();
|
|
1740
|
+
init_config();
|
|
2076
1741
|
init_typebox_validator();
|
|
2077
1742
|
init_outbound_source_policy();
|
|
1743
|
+
BASE64_PATTERN = "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$";
|
|
1744
|
+
RasterizeFontFaceSchema = Type2.Object(
|
|
1745
|
+
{
|
|
1746
|
+
family: Type2.String({ minLength: 1, maxLength: 128 }),
|
|
1747
|
+
weight: Type2.Integer({ minimum: 1, maximum: 1e3 }),
|
|
1748
|
+
italic: Type2.Boolean(),
|
|
1749
|
+
data: Type2.String({
|
|
1750
|
+
minLength: 1,
|
|
1751
|
+
maxLength: 4 * 1024 * 1024,
|
|
1752
|
+
pattern: BASE64_PATTERN
|
|
1753
|
+
}),
|
|
1754
|
+
format: Type2.Optional(
|
|
1755
|
+
Type2.Union([
|
|
1756
|
+
Type2.Literal("ttf"),
|
|
1757
|
+
Type2.Literal("otf"),
|
|
1758
|
+
Type2.Literal("woff"),
|
|
1759
|
+
Type2.Literal("woff2")
|
|
1760
|
+
])
|
|
1761
|
+
)
|
|
1762
|
+
},
|
|
1763
|
+
{ additionalProperties: false }
|
|
1764
|
+
);
|
|
1765
|
+
FontsSchema = Type2.Optional(
|
|
1766
|
+
Type2.Array(RasterizeFontFaceSchema, { maxItems: MAX_RASTERIZE_FONTS })
|
|
1767
|
+
);
|
|
2078
1768
|
RasterizeRequestSchema = Type2.Object(
|
|
2079
1769
|
{
|
|
2080
1770
|
presentation: Type2.Object({}, { additionalProperties: true }),
|
|
2081
1771
|
dpi: Type2.Optional(
|
|
2082
1772
|
Type2.Number({ minimum: MIN_VISUAL_DPI, maximum: MAX_VISUAL_DPI })
|
|
2083
1773
|
),
|
|
2084
|
-
baseDir: Type2.Optional(Type2.String())
|
|
1774
|
+
baseDir: Type2.Optional(Type2.String()),
|
|
1775
|
+
fonts: FontsSchema
|
|
2085
1776
|
},
|
|
2086
1777
|
{ additionalProperties: false }
|
|
2087
1778
|
);
|
|
@@ -2099,7 +1790,13 @@ var init_rasterize_route = __esm({
|
|
|
2099
1790
|
),
|
|
2100
1791
|
{ minItems: 1, maxItems: MAX_RASTERIZE_BATCH_SLIDES }
|
|
2101
1792
|
),
|
|
2102
|
-
baseDir: Type2.Optional(Type2.String())
|
|
1793
|
+
baseDir: Type2.Optional(Type2.String()),
|
|
1794
|
+
// Request-level, shared by every slide, exactly like `baseDir`. NOT
|
|
1795
|
+
// inside the per-slide object above, whose `additionalProperties: false`
|
|
1796
|
+
// must keep rejecting per-slide fonts: a uniform slide shape is what
|
|
1797
|
+
// lets batch dedupe and the disk-cache key stay identical to the
|
|
1798
|
+
// single-slide path.
|
|
1799
|
+
fonts: FontsSchema
|
|
2103
1800
|
},
|
|
2104
1801
|
{ additionalProperties: false }
|
|
2105
1802
|
);
|
|
@@ -2116,19 +1813,20 @@ var init_rasterize_route = __esm({
|
|
|
2116
1813
|
DEFAULT_SLIDE_HEIGHT_IN = 7.5;
|
|
2117
1814
|
MAX_SLIDE_PIXELS = 64e6;
|
|
2118
1815
|
MAX_BATCH_PIXELS = 256e6;
|
|
1816
|
+
MAX_RASTERIZE_BODY_CEILING = 64 * 1024 * 1024;
|
|
2119
1817
|
}
|
|
2120
1818
|
});
|
|
2121
1819
|
|
|
2122
1820
|
// src/server/services/template-media-inliner.ts
|
|
2123
|
-
import { promises as
|
|
2124
|
-
import
|
|
1821
|
+
import { promises as fs2 } from "fs";
|
|
1822
|
+
import path4 from "path";
|
|
2125
1823
|
function isRecord2(value) {
|
|
2126
1824
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2127
1825
|
}
|
|
2128
1826
|
function isInlineCandidate(source) {
|
|
2129
1827
|
if (!source || source.includes("\0")) return false;
|
|
2130
1828
|
if (source.startsWith("data:")) return false;
|
|
2131
|
-
if (
|
|
1829
|
+
if (path4.isAbsolute(source)) return false;
|
|
2132
1830
|
try {
|
|
2133
1831
|
new URL(source);
|
|
2134
1832
|
return false;
|
|
@@ -2140,17 +1838,17 @@ async function toDataUrl(source, baseDir, state) {
|
|
|
2140
1838
|
let cacheKey = source;
|
|
2141
1839
|
let result = null;
|
|
2142
1840
|
try {
|
|
2143
|
-
const resolved = await
|
|
1841
|
+
const resolved = await fs2.realpath(path4.resolve(baseDir, source));
|
|
2144
1842
|
cacheKey = resolved;
|
|
2145
|
-
const
|
|
2146
|
-
if (
|
|
2147
|
-
const contained = resolved === state.baseDirReal || resolved.startsWith(state.baseDirReal +
|
|
1843
|
+
const cached = state.cache.get(cacheKey);
|
|
1844
|
+
if (cached !== void 0) return cached;
|
|
1845
|
+
const contained = resolved === state.baseDirReal || resolved.startsWith(state.baseDirReal + path4.sep);
|
|
2148
1846
|
if (contained) {
|
|
2149
|
-
const mime = MIME_BY_EXTENSION[
|
|
1847
|
+
const mime = MIME_BY_EXTENSION[path4.extname(resolved).toLowerCase()];
|
|
2150
1848
|
if (mime) {
|
|
2151
|
-
const stat = await
|
|
1849
|
+
const stat = await fs2.stat(resolved);
|
|
2152
1850
|
if (stat.isFile() && stat.size <= state.maxFileBytes && stat.size <= state.totalBudget) {
|
|
2153
|
-
const buffer = await
|
|
1851
|
+
const buffer = await fs2.readFile(resolved);
|
|
2154
1852
|
state.totalBudget -= buffer.length;
|
|
2155
1853
|
result = `data:${mime};base64,${buffer.toString("base64")}`;
|
|
2156
1854
|
}
|
|
@@ -2198,7 +1896,7 @@ async function inlineTemplateMedia(jsonDefinition, baseDir, options = {}) {
|
|
|
2198
1896
|
}
|
|
2199
1897
|
let baseDirReal;
|
|
2200
1898
|
try {
|
|
2201
|
-
baseDirReal = await
|
|
1899
|
+
baseDirReal = await fs2.realpath(baseDir);
|
|
2202
1900
|
} catch {
|
|
2203
1901
|
return jsonDefinition;
|
|
2204
1902
|
}
|
|
@@ -2242,9 +1940,9 @@ import {
|
|
|
2242
1940
|
import { dirname } from "path";
|
|
2243
1941
|
function createFormatRouter(adapter) {
|
|
2244
1942
|
const router = new Hono2();
|
|
2245
|
-
const assertRequestSources = (value,
|
|
1943
|
+
const assertRequestSources = (value, path5) => {
|
|
2246
1944
|
try {
|
|
2247
|
-
assertSafeOutboundSources(value, config.outboundSources,
|
|
1945
|
+
assertSafeOutboundSources(value, config.outboundSources, path5);
|
|
2248
1946
|
} catch (error) {
|
|
2249
1947
|
if (error instanceof UnsafeOutboundSourceError) {
|
|
2250
1948
|
throw new HTTPException4(400, { message: error.message });
|
|
@@ -4155,9 +3853,9 @@ var init_unified_server = __esm({
|
|
|
4155
3853
|
return await apiApp.fetch(c.req.raw, c.env);
|
|
4156
3854
|
});
|
|
4157
3855
|
this.app.use("*", async (c, next) => {
|
|
4158
|
-
const
|
|
4159
|
-
if (
|
|
4160
|
-
if (/\.(ts|tsx|js|jsx|css|map)$/.test(
|
|
3856
|
+
const path5 = c.req.path;
|
|
3857
|
+
if (path5.startsWith("/api")) {
|
|
3858
|
+
if (/\.(ts|tsx|js|jsx|css|map)$/.test(path5)) {
|
|
4161
3859
|
return next();
|
|
4162
3860
|
}
|
|
4163
3861
|
const response = await apiApp.fetch(c.req.raw, c.env);
|
|
@@ -4260,8 +3958,8 @@ var init_unified_server = __esm({
|
|
|
4260
3958
|
}
|
|
4261
3959
|
if (this.viteServer) {
|
|
4262
3960
|
this.app.use("*", async (c, next) => {
|
|
4263
|
-
const
|
|
4264
|
-
if ((
|
|
3961
|
+
const path5 = c.req.path;
|
|
3962
|
+
if ((path5.startsWith("/api") || path5 === "/health") && !/\.(ts|tsx|js|jsx|css|map)$/.test(path5)) {
|
|
4265
3963
|
return next();
|
|
4266
3964
|
}
|
|
4267
3965
|
const env = c.env;
|
|
@@ -4280,7 +3978,7 @@ var init_unified_server = __esm({
|
|
|
4280
3978
|
}
|
|
4281
3979
|
async setupBuiltClient(clientPath) {
|
|
4282
3980
|
const { serveStatic } = await import("@hono/node-server/serve-static");
|
|
4283
|
-
const
|
|
3981
|
+
const fs3 = await import("fs");
|
|
4284
3982
|
const { extname } = await import("path");
|
|
4285
3983
|
const mime = await import("mime-types");
|
|
4286
3984
|
const format = this.adapter.name.replace(/[^a-zA-Z0-9]/g, "");
|
|
@@ -4288,7 +3986,7 @@ var init_unified_server = __esm({
|
|
|
4288
3986
|
"/assets/*",
|
|
4289
3987
|
serveStatic({
|
|
4290
3988
|
root: clientPath,
|
|
4291
|
-
rewriteRequestPath: (
|
|
3989
|
+
rewriteRequestPath: (path5) => path5.replace(/^\/assets/, "/assets")
|
|
4292
3990
|
})
|
|
4293
3991
|
);
|
|
4294
3992
|
this.app.use("/*", async (c, next) => {
|
|
@@ -4297,9 +3995,9 @@ var init_unified_server = __esm({
|
|
|
4297
3995
|
const ext = extname(reqPath);
|
|
4298
3996
|
if (ext) {
|
|
4299
3997
|
const filePath = resolve(clientPath, reqPath.slice(1));
|
|
4300
|
-
if (
|
|
3998
|
+
if (fs3.existsSync(filePath)) {
|
|
4301
3999
|
const mimeType = mime.lookup(filePath) || "application/octet-stream";
|
|
4302
|
-
const content =
|
|
4000
|
+
const content = fs3.readFileSync(filePath);
|
|
4303
4001
|
c.header("Content-Type", mimeType);
|
|
4304
4002
|
return c.body(content);
|
|
4305
4003
|
}
|
|
@@ -4311,8 +4009,8 @@ var init_unified_server = __esm({
|
|
|
4311
4009
|
if (reqPath.startsWith("/api") || reqPath === "/health")
|
|
4312
4010
|
return c.notFound();
|
|
4313
4011
|
const indexPath = resolve(clientPath, "index.html");
|
|
4314
|
-
if (
|
|
4315
|
-
let html =
|
|
4012
|
+
if (fs3.existsSync(indexPath)) {
|
|
4013
|
+
let html = fs3.readFileSync(indexPath, "utf-8");
|
|
4316
4014
|
html = html.replace(
|
|
4317
4015
|
"</head>",
|
|
4318
4016
|
`<script>window.__JTO_FORMAT__ = '${format}';</script>
|
|
@@ -4473,7 +4171,7 @@ function createDevCommand(adapter) {
|
|
|
4473
4171
|
}
|
|
4474
4172
|
|
|
4475
4173
|
// src/cli.ts
|
|
4476
|
-
var PACKAGE_VERSION = true ? "0.
|
|
4174
|
+
var PACKAGE_VERSION = true ? "0.35.0" : "dev-mode";
|
|
4477
4175
|
var program = new Command2();
|
|
4478
4176
|
program.name("jto").description("JSON to Office CLI - Generate .docx and .pptx from JSON").version(PACKAGE_VERSION);
|
|
4479
4177
|
registerCoreCommands(program, {
|