@bendyline/squisq-cli 2.5.5 → 2.6.1
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/README.md +59 -1
- package/dist/{api-E5PDCULT.js → api-O3IMGDPH.js} +13 -3
- package/dist/api.d.ts +65 -3
- package/dist/api.js +192 -71
- package/dist/{chunk-SFBZGL54.js → chunk-2ZJW7F62.js} +193 -72
- package/dist/index.js +172 -15
- package/dist/squisq-player.full.global.js +437 -434
- package/dist/squisq-player.global.js +68 -65
- package/package.json +5 -5
|
@@ -5,11 +5,12 @@ import {
|
|
|
5
5
|
} from "./chunk-GGVASRPG.js";
|
|
6
6
|
|
|
7
7
|
// src/api.ts
|
|
8
|
-
import { readFile as readFile3 } from "fs/promises";
|
|
8
|
+
import { mkdir, readFile as readFile3, writeFile } from "fs/promises";
|
|
9
|
+
import { dirname as dirname2, resolve as resolvePath } from "path";
|
|
9
10
|
import { resolveMediaSchedule as resolveMediaSchedule2 } from "@bendyline/squisq/schemas";
|
|
10
11
|
import { flattenBlocks as flattenBlocks2 } from "@bendyline/squisq/doc";
|
|
11
12
|
import { ffmpegGifOutputArgs, generateRenderHtml } from "@bendyline/squisq-video";
|
|
12
|
-
import { resolveDimensions } from "@bendyline/squisq-video";
|
|
13
|
+
import { resolveDashboardDimensions, resolveDimensions } from "@bendyline/squisq-video";
|
|
13
14
|
import {
|
|
14
15
|
convert as formatsConvert,
|
|
15
16
|
prepareConversion as formatsPrepareConversion
|
|
@@ -94,13 +95,13 @@ async function buildMixedAudioTrack(doc, container, ffmpegPath, coverPreRoll, si
|
|
|
94
95
|
return mixTimelineClips(ffmpegPath, usable, signal);
|
|
95
96
|
}
|
|
96
97
|
async function mixTimelineClips(ffmpegPath, usable, signal) {
|
|
97
|
-
const { writeFile, readFile: readFile4, mkdir, rm: rm2 } = await import("fs/promises");
|
|
98
|
+
const { writeFile: writeFile2, readFile: readFile4, mkdir: mkdir2, rm: rm2 } = await import("fs/promises");
|
|
98
99
|
const { join: join3 } = await import("path");
|
|
99
100
|
const { tmpdir: tmpdir2 } = await import("os");
|
|
100
101
|
const { randomBytes: randomBytes2 } = await import("crypto");
|
|
101
102
|
signal?.throwIfAborted();
|
|
102
103
|
const workDir = join3(tmpdir2(), `squisq-audio-mix-${randomBytes2(8).toString("hex")}`);
|
|
103
|
-
await
|
|
104
|
+
await mkdir2(workDir, { recursive: true });
|
|
104
105
|
const ms = (s) => Math.max(0, Math.round(s * 1e3));
|
|
105
106
|
try {
|
|
106
107
|
signal?.throwIfAborted();
|
|
@@ -110,7 +111,7 @@ async function mixTimelineClips(ffmpegPath, usable, signal) {
|
|
|
110
111
|
for (const { clip, buffer } of usable) {
|
|
111
112
|
signal?.throwIfAborted();
|
|
112
113
|
const p = join3(workDir, `clip-${inputs.length}.mp3`);
|
|
113
|
-
await
|
|
114
|
+
await writeFile2(p, new Uint8Array(buffer));
|
|
114
115
|
signal?.throwIfAborted();
|
|
115
116
|
const i = inputs.push(p) - 1;
|
|
116
117
|
const delayMs = ms(clip.startSec);
|
|
@@ -330,7 +331,7 @@ function mp4Format() {
|
|
|
330
331
|
const animationsEnabled = typeof mp4Opts.animationsEnabled === "boolean" ? mp4Opts.animationsEnabled : MP4_DEFAULTS.animationsEnabled;
|
|
331
332
|
const outputPath = join(tmpdir(), `squisq-mp4-${randomBytes(8).toString("hex")}.mp4`);
|
|
332
333
|
try {
|
|
333
|
-
const { renderDocToMp4: renderDocToMp42 } = await import("./api-
|
|
334
|
+
const { renderDocToMp4: renderDocToMp42 } = await import("./api-O3IMGDPH.js");
|
|
334
335
|
await renderDocToMp42(input.doc, input.container, {
|
|
335
336
|
outputPath,
|
|
336
337
|
fps,
|
|
@@ -371,7 +372,7 @@ function gifFormat() {
|
|
|
371
372
|
const defaultHeight = portrait ? GIF_DEFAULTS.width : GIF_DEFAULTS.height;
|
|
372
373
|
const outputPath = join(tmpdir(), `squisq-gif-${randomBytes(8).toString("hex")}.gif`);
|
|
373
374
|
try {
|
|
374
|
-
const { renderDocToGif: renderDocToGif2 } = await import("./api-
|
|
375
|
+
const { renderDocToGif: renderDocToGif2 } = await import("./api-O3IMGDPH.js");
|
|
375
376
|
const result = await renderDocToGif2(input.doc, input.container, {
|
|
376
377
|
outputPath,
|
|
377
378
|
fps: typeof gifOpts.fps === "number" ? gifOpts.fps : GIF_DEFAULTS.fps,
|
|
@@ -404,10 +405,38 @@ function gifFormat() {
|
|
|
404
405
|
}
|
|
405
406
|
};
|
|
406
407
|
}
|
|
408
|
+
function pngFormat() {
|
|
409
|
+
return {
|
|
410
|
+
id: "png",
|
|
411
|
+
templateAnnotationHandling: "rendered",
|
|
412
|
+
label: "Dashboard Image",
|
|
413
|
+
mimeType: "image/png",
|
|
414
|
+
extensions: [".png"],
|
|
415
|
+
async exportDoc(input, options) {
|
|
416
|
+
options.signal?.throwIfAborted();
|
|
417
|
+
const pngOpts = options.formatOptions?.png ?? {};
|
|
418
|
+
const { renderDocToDashboardPng: renderDocToDashboardPng2 } = await import("./api-O3IMGDPH.js");
|
|
419
|
+
const result = await renderDocToDashboardPng2(input.doc, input.container, {
|
|
420
|
+
resolution: pngOpts.resolution,
|
|
421
|
+
width: pngOpts.width,
|
|
422
|
+
height: pngOpts.height,
|
|
423
|
+
layout: pngOpts.layout,
|
|
424
|
+
style: pngOpts.style,
|
|
425
|
+
title: pngOpts.title,
|
|
426
|
+
documentTitle: options.title ?? input.baseName,
|
|
427
|
+
signal: options.signal,
|
|
428
|
+
onProgress: pngOpts.onProgress
|
|
429
|
+
});
|
|
430
|
+
options.signal?.throwIfAborted();
|
|
431
|
+
return { bytes: result.bytes, mimeType: "image/png", suggestedFilename: "", warnings: [] };
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
}
|
|
407
435
|
function createCliRegistry() {
|
|
408
436
|
const registry = defaultRegistry();
|
|
409
437
|
registry.register(mp4Format());
|
|
410
438
|
registry.register(gifFormat());
|
|
439
|
+
registry.register(pngFormat());
|
|
411
440
|
return registry;
|
|
412
441
|
}
|
|
413
442
|
|
|
@@ -824,6 +853,12 @@ function throwIfAborted(signal) {
|
|
|
824
853
|
}
|
|
825
854
|
|
|
826
855
|
// src/api.ts
|
|
856
|
+
import {
|
|
857
|
+
DASHBOARD_RESOLUTIONS,
|
|
858
|
+
DEFAULT_DASHBOARD_RESOLUTION,
|
|
859
|
+
resolveDashboardDimensions as resolveDashboardDimensions2,
|
|
860
|
+
validateDashboardImageDimensions
|
|
861
|
+
} from "@bendyline/squisq-video";
|
|
827
862
|
import { ConversionError } from "@bendyline/squisq-formats";
|
|
828
863
|
var playerBundlePromise;
|
|
829
864
|
var fullPlayerBundlePromise;
|
|
@@ -863,17 +898,18 @@ async function prepareConversion(source, options = {}) {
|
|
|
863
898
|
...options
|
|
864
899
|
});
|
|
865
900
|
}
|
|
866
|
-
async function
|
|
867
|
-
const {
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
901
|
+
async function withRenderPage(doc, container, options, fn) {
|
|
902
|
+
const {
|
|
903
|
+
signal,
|
|
904
|
+
width,
|
|
905
|
+
height,
|
|
906
|
+
includeAudio,
|
|
907
|
+
captionStyle,
|
|
908
|
+
animationsEnabled,
|
|
909
|
+
displayMode,
|
|
910
|
+
dashboard,
|
|
911
|
+
onProgress
|
|
912
|
+
} = options;
|
|
877
913
|
onProgress?.("collecting media", 0);
|
|
878
914
|
signal?.throwIfAborted();
|
|
879
915
|
const { collectImagePaths } = await import("@bendyline/squisq-formats/html");
|
|
@@ -890,14 +926,16 @@ async function captureDocFrames(doc, container, options) {
|
|
|
890
926
|
}
|
|
891
927
|
}
|
|
892
928
|
const audio = /* @__PURE__ */ new Map();
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
929
|
+
if (includeAudio) {
|
|
930
|
+
for (const seg of doc.audio?.segments ?? []) {
|
|
931
|
+
signal?.throwIfAborted();
|
|
932
|
+
const data = await container.readFile(seg.src);
|
|
933
|
+
signal?.throwIfAborted();
|
|
934
|
+
if (data) {
|
|
935
|
+
budget.admit(seg.src, data);
|
|
936
|
+
audio.set(seg.src, data);
|
|
937
|
+
audio.set(seg.name, data);
|
|
938
|
+
}
|
|
901
939
|
}
|
|
902
940
|
}
|
|
903
941
|
const mediaSrcs = new Set(resolveMediaSchedule2(doc).map((clip) => clip.src));
|
|
@@ -927,7 +965,9 @@ async function captureDocFrames(doc, container, options) {
|
|
|
927
965
|
width,
|
|
928
966
|
height,
|
|
929
967
|
captionStyle,
|
|
930
|
-
animationsEnabled
|
|
968
|
+
animationsEnabled,
|
|
969
|
+
displayMode,
|
|
970
|
+
dashboard
|
|
931
971
|
});
|
|
932
972
|
onProgress?.("launching browser", 15);
|
|
933
973
|
signal?.throwIfAborted();
|
|
@@ -953,7 +993,6 @@ async function captureDocFrames(doc, container, options) {
|
|
|
953
993
|
}
|
|
954
994
|
signal?.addEventListener("abort", handleAbort, { once: true });
|
|
955
995
|
let renderAPI = null;
|
|
956
|
-
const capturedFrames = new CapturedFrameCollector();
|
|
957
996
|
try {
|
|
958
997
|
const page = await browser.newPage({ viewport: { width, height } });
|
|
959
998
|
const pageErrors = [];
|
|
@@ -994,58 +1033,135 @@ Page errors:
|
|
|
994
1033
|
return api;
|
|
995
1034
|
});
|
|
996
1035
|
signal?.throwIfAborted();
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1036
|
+
return await fn({ page, renderAPI });
|
|
1037
|
+
} finally {
|
|
1038
|
+
signal?.removeEventListener("abort", handleAbort);
|
|
1039
|
+
await renderAPI?.dispose().catch(() => void 0);
|
|
1040
|
+
await browser.close().catch(() => void 0);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
async function captureDocFrames(doc, container, options) {
|
|
1044
|
+
const { fps, width, height, captionStyle, coverPreRoll, animationsEnabled, onProgress, signal } = options;
|
|
1045
|
+
signal?.throwIfAborted();
|
|
1046
|
+
resolveAppliedCoverPreRoll(coverPreRoll, true);
|
|
1047
|
+
const ffmpegPath = (await detectFfmpegDetailed(signal))?.path ?? null;
|
|
1048
|
+
signal?.throwIfAborted();
|
|
1049
|
+
if (!ffmpegPath) {
|
|
1050
|
+
throw new Error(
|
|
1051
|
+
"ffmpeg is required but not found in PATH.\nInstall it with:\n macOS: brew install ffmpeg\n Ubuntu: sudo apt install ffmpeg\n Windows: winget install ffmpeg\nOr: npm install ffmpeg-static, or set SQUISQ_FFMPEG to an ffmpeg binary."
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
const capturedFrames = new CapturedFrameCollector();
|
|
1055
|
+
try {
|
|
1056
|
+
return await withRenderPage(
|
|
1057
|
+
doc,
|
|
1058
|
+
container,
|
|
1059
|
+
{ signal, width, height, includeAudio: true, captionStyle, animationsEnabled, onProgress },
|
|
1060
|
+
async ({ page, renderAPI }) => {
|
|
1061
|
+
const docDuration = await renderAPI.evaluate((api) => api.getDuration());
|
|
1062
|
+
signal?.throwIfAborted();
|
|
1063
|
+
if (docDuration <= 0) throw new Error("Document has zero duration \u2014 nothing to render");
|
|
1064
|
+
const hasCover = coverPreRoll > 0 ? await renderAPI.evaluate((api) => api.hasCoverBlock()) : false;
|
|
1065
|
+
const appliedCoverPreRoll = resolveAppliedCoverPreRoll(coverPreRoll, hasCover);
|
|
1066
|
+
const storyFrameCount = Math.ceil(docDuration * fps);
|
|
1067
|
+
const preRollFrameCount = Math.ceil(appliedCoverPreRoll * fps);
|
|
1068
|
+
const totalFrames = preRollFrameCount + storyFrameCount;
|
|
1069
|
+
onProgress?.("capturing frames", 20);
|
|
1030
1070
|
capturedFrames.throwIfAborted(signal);
|
|
1071
|
+
if (preRollFrameCount > 0) {
|
|
1072
|
+
capturedFrames.throwIfAborted(signal);
|
|
1073
|
+
await renderAPI.evaluate((api) => api.showCover());
|
|
1074
|
+
await page.waitForTimeout(100);
|
|
1075
|
+
capturedFrames.throwIfAborted(signal);
|
|
1076
|
+
const coverFrame = await page.screenshot({ type: "png" });
|
|
1077
|
+
capturedFrames.throwIfAborted(signal);
|
|
1078
|
+
capturedFrames.append(coverFrame, preRollFrameCount);
|
|
1079
|
+
await renderAPI.evaluate((api) => api.hideCover());
|
|
1080
|
+
}
|
|
1081
|
+
const frameInterval = 1 / fps;
|
|
1082
|
+
for (let i = 0; i < storyFrameCount; i++) {
|
|
1083
|
+
capturedFrames.throwIfAborted(signal);
|
|
1084
|
+
const time = i * frameInterval;
|
|
1085
|
+
await renderAPI.evaluate((api, t) => api.seekTo(t), time);
|
|
1086
|
+
const frame = await page.screenshot({ type: "png" });
|
|
1087
|
+
capturedFrames.throwIfAborted(signal);
|
|
1088
|
+
capturedFrames.append(frame);
|
|
1089
|
+
if (i % Math.max(1, Math.floor(fps / 2)) === 0 || i === storyFrameCount - 1) {
|
|
1090
|
+
onProgress?.(
|
|
1091
|
+
"capturing frames",
|
|
1092
|
+
20 + Math.round(capturedFrames.frameCount / totalFrames * 60)
|
|
1093
|
+
);
|
|
1094
|
+
capturedFrames.throwIfAborted(signal);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
return {
|
|
1098
|
+
frames: capturedFrames.release(),
|
|
1099
|
+
totalDuration: docDuration + appliedCoverPreRoll,
|
|
1100
|
+
appliedCoverPreRoll,
|
|
1101
|
+
ffmpegPath
|
|
1102
|
+
};
|
|
1031
1103
|
}
|
|
1032
|
-
|
|
1033
|
-
return {
|
|
1034
|
-
frames: capturedFrames.release(),
|
|
1035
|
-
totalDuration: docDuration + appliedCoverPreRoll,
|
|
1036
|
-
appliedCoverPreRoll,
|
|
1037
|
-
ffmpegPath
|
|
1038
|
-
};
|
|
1104
|
+
);
|
|
1039
1105
|
} catch (err) {
|
|
1040
1106
|
capturedFrames.clear();
|
|
1041
1107
|
signal?.throwIfAborted();
|
|
1042
1108
|
throw err;
|
|
1043
|
-
} finally {
|
|
1044
|
-
signal?.removeEventListener("abort", handleAbort);
|
|
1045
|
-
await renderAPI?.dispose().catch(() => void 0);
|
|
1046
|
-
await browser.close().catch(() => void 0);
|
|
1047
1109
|
}
|
|
1048
1110
|
}
|
|
1111
|
+
async function renderDocToDashboardPng(doc, container, options = {}) {
|
|
1112
|
+
const {
|
|
1113
|
+
signal,
|
|
1114
|
+
outputPath,
|
|
1115
|
+
resolution,
|
|
1116
|
+
width,
|
|
1117
|
+
height,
|
|
1118
|
+
layout,
|
|
1119
|
+
title,
|
|
1120
|
+
style,
|
|
1121
|
+
documentTitle,
|
|
1122
|
+
onProgress
|
|
1123
|
+
} = options;
|
|
1124
|
+
signal?.throwIfAborted();
|
|
1125
|
+
const dimensions = resolveDashboardDimensions({ resolution, width, height });
|
|
1126
|
+
const bytes = await withRenderPage(
|
|
1127
|
+
doc,
|
|
1128
|
+
container,
|
|
1129
|
+
{
|
|
1130
|
+
signal,
|
|
1131
|
+
width: dimensions.width,
|
|
1132
|
+
height: dimensions.height,
|
|
1133
|
+
includeAudio: false,
|
|
1134
|
+
animationsEnabled: false,
|
|
1135
|
+
displayMode: "dashboard",
|
|
1136
|
+
dashboard: { layout, title, style, documentTitle },
|
|
1137
|
+
onProgress
|
|
1138
|
+
},
|
|
1139
|
+
async ({ page, renderAPI }) => {
|
|
1140
|
+
onProgress?.("rendering dashboard", 50);
|
|
1141
|
+
signal?.throwIfAborted();
|
|
1142
|
+
await renderAPI.evaluate((api) => api.seekTo(0));
|
|
1143
|
+
signal?.throwIfAborted();
|
|
1144
|
+
await page.waitForTimeout(100);
|
|
1145
|
+
signal?.throwIfAborted();
|
|
1146
|
+
onProgress?.("capturing image", 85);
|
|
1147
|
+
return await page.screenshot({ type: "png" });
|
|
1148
|
+
}
|
|
1149
|
+
);
|
|
1150
|
+
let writtenPath;
|
|
1151
|
+
if (outputPath) {
|
|
1152
|
+
const absolute = resolvePath(outputPath);
|
|
1153
|
+
await mkdir(dirname2(absolute), { recursive: true });
|
|
1154
|
+
await writeFile(absolute, bytes);
|
|
1155
|
+
writtenPath = absolute;
|
|
1156
|
+
}
|
|
1157
|
+
onProgress?.("done", 100);
|
|
1158
|
+
return {
|
|
1159
|
+
bytes,
|
|
1160
|
+
width: dimensions.width,
|
|
1161
|
+
height: dimensions.height,
|
|
1162
|
+
...writtenPath ? { outputPath: writtenPath } : {}
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1049
1165
|
async function renderDocToMp4(doc, container, options) {
|
|
1050
1166
|
options.signal?.throwIfAborted();
|
|
1051
1167
|
const fps = options.fps ?? 30;
|
|
@@ -1235,10 +1351,15 @@ export {
|
|
|
1235
1351
|
CapturedFrameBudgetError,
|
|
1236
1352
|
convert,
|
|
1237
1353
|
prepareConversion,
|
|
1354
|
+
renderDocToDashboardPng,
|
|
1238
1355
|
renderDocToMp4,
|
|
1239
1356
|
renderDocToGif,
|
|
1240
1357
|
extractThumbnails,
|
|
1241
1358
|
MemoryContentContainer2 as MemoryContentContainer,
|
|
1359
|
+
DASHBOARD_RESOLUTIONS,
|
|
1360
|
+
DEFAULT_DASHBOARD_RESOLUTION,
|
|
1361
|
+
resolveDashboardDimensions2 as resolveDashboardDimensions,
|
|
1362
|
+
validateDashboardImageDimensions,
|
|
1242
1363
|
ConversionError,
|
|
1243
1364
|
createCliRegistry
|
|
1244
1365
|
};
|
package/dist/index.js
CHANGED
|
@@ -6,9 +6,10 @@ import {
|
|
|
6
6
|
detectFfmpegDetailed,
|
|
7
7
|
getFfmpegVersion,
|
|
8
8
|
readInput,
|
|
9
|
+
renderDocToDashboardPng,
|
|
9
10
|
renderDocToGif,
|
|
10
11
|
renderDocToMp4
|
|
11
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-2ZJW7F62.js";
|
|
12
13
|
import "./chunk-GGVASRPG.js";
|
|
13
14
|
|
|
14
15
|
// src/index.ts
|
|
@@ -137,7 +138,7 @@ function suggestId(value, candidates) {
|
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
// src/commands/convert.ts
|
|
140
|
-
var VALID_FORMATS = [...BUILTIN_FORMAT_IDS, "mp4", "gif"];
|
|
141
|
+
var VALID_FORMATS = [...BUILTIN_FORMAT_IDS, "mp4", "gif", "png"];
|
|
141
142
|
var DEFAULT_FORMATS = [
|
|
142
143
|
"docx",
|
|
143
144
|
"pptx",
|
|
@@ -187,7 +188,7 @@ function formatFromOutputPath(outputPath) {
|
|
|
187
188
|
}
|
|
188
189
|
function registerConvertCommand(program2) {
|
|
189
190
|
program2.command("convert").description(
|
|
190
|
-
"Convert a document to DOCX, PPTX, PDF, HTML, EPUB, MP4, animated GIF, and container formats"
|
|
191
|
+
"Convert a document to DOCX, PPTX, PDF, HTML, EPUB, MP4, animated GIF, dashboard PNG, and container formats"
|
|
191
192
|
).argument("<input>", "Path to .md/.docx/.pptx/.pdf/.xlsx/.csv/.html file, .zip/.dbk, or folder").addOption(
|
|
192
193
|
new Option(
|
|
193
194
|
"-o, --output <file>",
|
|
@@ -498,8 +499,157 @@ function clearProgress() {
|
|
|
498
499
|
process.stderr.write("\r" + " ".repeat(80) + "\r");
|
|
499
500
|
}
|
|
500
501
|
|
|
502
|
+
// src/commands/image.ts
|
|
503
|
+
import { mkdir as mkdir3 } from "fs/promises";
|
|
504
|
+
import { dirname as dirname3, basename as basename3, extname as extname3, resolve as resolve3 } from "path";
|
|
505
|
+
import {
|
|
506
|
+
DASHBOARD_AUTO_LAYOUT_ID,
|
|
507
|
+
DASHBOARD_STYLE_IDS,
|
|
508
|
+
listDashboardLayouts,
|
|
509
|
+
resolveDashboardStyleId
|
|
510
|
+
} from "@bendyline/squisq/doc";
|
|
511
|
+
import {
|
|
512
|
+
DASHBOARD_RESOLUTIONS,
|
|
513
|
+
DEFAULT_DASHBOARD_RESOLUTION,
|
|
514
|
+
resolveDashboardDimensions
|
|
515
|
+
} from "@bendyline/squisq-video";
|
|
516
|
+
var IMAGE_COMMAND_NAME = "image";
|
|
517
|
+
var VALID_IMAGE_FORMATS = ["png"];
|
|
518
|
+
function registerImageCommand(program2) {
|
|
519
|
+
const resolutionIds = DASHBOARD_RESOLUTIONS.map((preset) => preset.id).join(", ");
|
|
520
|
+
program2.command(IMAGE_COMMAND_NAME).description("Render a squisq document's Dashboard to a PNG image").argument("<input>", "Path to .md/.json file, .zip/.dbk container, or folder").argument("[output]", "Output .png path (default: <input>.png)").option("-o, --output <path>", "Output .png path").option("--format <format>", `Output format: ${VALID_IMAGE_FORMATS.join(", ")} (default: png)`).option(
|
|
521
|
+
"--resolution <preset>",
|
|
522
|
+
`Named resolution: ${resolutionIds} (default: ${DEFAULT_DASHBOARD_RESOLUTION})`
|
|
523
|
+
).option("--width <pixels>", "Custom image width (requires --height; excludes --resolution)").option("--height <pixels>", "Custom image height (requires --width; excludes --resolution)").option(
|
|
524
|
+
"--layout <id>",
|
|
525
|
+
`Dashboard layout id, or "${DASHBOARD_AUTO_LAYOUT_ID}" to pick by block count (default)`
|
|
526
|
+
).option(
|
|
527
|
+
"--style <variant>",
|
|
528
|
+
`Cell style variant: ${DASHBOARD_STYLE_IDS.join(", ")} (default: the document's own setting)`
|
|
529
|
+
).option("--title", "Include the document-title band (default: the document's own setting)").option("--no-title", "Hide the document-title band").option(
|
|
530
|
+
"--no-auto-templates",
|
|
531
|
+
"Disable content-aware template auto-picking for unannotated headings"
|
|
532
|
+
).option("-t, --theme <id>", "Squisq theme ID to apply (e.g., documentary, cinematic, bold)").option(
|
|
533
|
+
"--transform <style>",
|
|
534
|
+
"Transform style to apply before rendering (e.g., documentary, magazine, minimal)"
|
|
535
|
+
).option("--overwrite", "Replace an existing output file (default: refuse and exit non-zero)").action(async (inputPath, outputArg, opts) => {
|
|
536
|
+
try {
|
|
537
|
+
if (outputArg && opts.output) {
|
|
538
|
+
throw new Error("The positional output and --output cannot be used together.");
|
|
539
|
+
}
|
|
540
|
+
if (outputArg) opts.output = outputArg;
|
|
541
|
+
await runImage(inputPath, opts);
|
|
542
|
+
} catch (err) {
|
|
543
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
544
|
+
console.error(`Error: ${message}`);
|
|
545
|
+
process.exitCode = 1;
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
async function runImage(inputPath, opts) {
|
|
550
|
+
const resolvedInput = resolve3(inputPath);
|
|
551
|
+
const requestedFormat = opts.format?.toLowerCase();
|
|
552
|
+
if (requestedFormat !== void 0 && !VALID_IMAGE_FORMATS.includes(requestedFormat)) {
|
|
553
|
+
throw new Error(
|
|
554
|
+
`Invalid format "${requestedFormat}". Valid: ${VALID_IMAGE_FORMATS.join(", ")}`
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
if (opts.output && extname3(opts.output).toLowerCase() !== ".png") {
|
|
558
|
+
throw new Error("Output path must end in .png");
|
|
559
|
+
}
|
|
560
|
+
const width = opts.width === void 0 ? void 0 : Number(opts.width);
|
|
561
|
+
const height = opts.height === void 0 ? void 0 : Number(opts.height);
|
|
562
|
+
const dimensions = resolveDashboardDimensions({
|
|
563
|
+
resolution: opts.resolution,
|
|
564
|
+
width,
|
|
565
|
+
height
|
|
566
|
+
});
|
|
567
|
+
if (opts.layout !== void 0 && opts.layout.trim().length === 0) {
|
|
568
|
+
throw new Error("The --layout id must be a non-empty string.");
|
|
569
|
+
}
|
|
570
|
+
const requestedStyle = opts.style === void 0 ? void 0 : resolveDashboardStyleId(opts.style);
|
|
571
|
+
if (opts.style !== void 0 && requestedStyle === void 0) {
|
|
572
|
+
throw new Error(
|
|
573
|
+
`Unknown dashboard style "${opts.style}". Valid: ${DASHBOARD_STYLE_IDS.join(", ")}`
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
if (opts.transform) {
|
|
577
|
+
await assertValidTransformStyle(opts.transform);
|
|
578
|
+
}
|
|
579
|
+
const inputBasename = basename3(resolvedInput);
|
|
580
|
+
const inputExt = extname3(inputBasename);
|
|
581
|
+
const baseName = inputExt ? inputBasename.slice(0, -inputExt.length) : inputBasename;
|
|
582
|
+
const outputPath = opts.output ? resolve3(opts.output) : resolve3(dirname3(resolvedInput), `${baseName}.png`);
|
|
583
|
+
await assertOutputsWritable([outputPath], opts.overwrite);
|
|
584
|
+
await mkdir3(dirname3(outputPath), { recursive: true });
|
|
585
|
+
console.error(`Reading: ${resolvedInput}`);
|
|
586
|
+
const result = await readInput(resolvedInput);
|
|
587
|
+
const { container } = result;
|
|
588
|
+
if (opts.theme) {
|
|
589
|
+
await assertValidThemeId(opts.theme, result);
|
|
590
|
+
}
|
|
591
|
+
let doc = result.doc;
|
|
592
|
+
if (result.markdownDoc) {
|
|
593
|
+
if (opts.autoTemplates === false) {
|
|
594
|
+
const { markdownToDoc, resolveAudioMapping } = await import("@bendyline/squisq/doc");
|
|
595
|
+
doc = await resolveAudioMapping(
|
|
596
|
+
markdownToDoc(result.markdownDoc, { autoTemplates: false }),
|
|
597
|
+
container
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
} else {
|
|
601
|
+
console.error("Using pre-built Doc JSON");
|
|
602
|
+
}
|
|
603
|
+
const requestedLayout = opts.layout?.trim().toLowerCase();
|
|
604
|
+
if (requestedLayout && requestedLayout !== DASHBOARD_AUTO_LAYOUT_ID) {
|
|
605
|
+
const available = listDashboardLayouts(doc);
|
|
606
|
+
if (!available.some((layout) => layout.id === requestedLayout)) {
|
|
607
|
+
const known = available.map((layout) => layout.id).join(", ");
|
|
608
|
+
throw new Error(
|
|
609
|
+
`Unknown dashboard layout "${requestedLayout}". Valid: ${DASHBOARD_AUTO_LAYOUT_ID}, ${known}`
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (opts.transform) {
|
|
614
|
+
doc = await applyTransformToDoc(doc, opts.transform, opts.theme);
|
|
615
|
+
console.error(` Applied transform: ${opts.transform}`);
|
|
616
|
+
}
|
|
617
|
+
if (opts.theme) {
|
|
618
|
+
doc = { ...doc, themeId: opts.theme };
|
|
619
|
+
}
|
|
620
|
+
console.error(
|
|
621
|
+
`Rendering dashboard PNG: ${dimensions.width}\xD7${dimensions.height}, layout: ${requestedLayout ?? DASHBOARD_AUTO_LAYOUT_ID}, style: ${requestedStyle ?? "document"}, title: ${titleReadout(opts.title)}`
|
|
622
|
+
);
|
|
623
|
+
const rendered = await renderDocToDashboardPng(doc, container, {
|
|
624
|
+
outputPath,
|
|
625
|
+
...opts.resolution !== void 0 ? { resolution: opts.resolution } : width !== void 0 && height !== void 0 ? { width, height } : {},
|
|
626
|
+
layout: requestedLayout,
|
|
627
|
+
style: requestedStyle,
|
|
628
|
+
title: opts.title,
|
|
629
|
+
documentTitle: baseName,
|
|
630
|
+
onProgress: (phase, percent) => writeProgress2(phase, percent, 100)
|
|
631
|
+
});
|
|
632
|
+
clearProgress2();
|
|
633
|
+
console.error(` \u2713 ${rendered.outputPath} (${rendered.width}\xD7${rendered.height})`);
|
|
634
|
+
console.error("Done.");
|
|
635
|
+
}
|
|
636
|
+
function titleReadout(title) {
|
|
637
|
+
if (title === void 0) return "document";
|
|
638
|
+
return title ? "on" : "off";
|
|
639
|
+
}
|
|
640
|
+
var BAR_WIDTH2 = 30;
|
|
641
|
+
function writeProgress2(label, current, total) {
|
|
642
|
+
const pct = Math.min(100, Math.round(current / total * 100));
|
|
643
|
+
const filled = Math.round(pct / 100 * BAR_WIDTH2);
|
|
644
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(BAR_WIDTH2 - filled);
|
|
645
|
+
process.stderr.write(`\r ${label}: ${bar} ${pct}% `);
|
|
646
|
+
}
|
|
647
|
+
function clearProgress2() {
|
|
648
|
+
process.stderr.write("\r" + " ".repeat(80) + "\r");
|
|
649
|
+
}
|
|
650
|
+
|
|
501
651
|
// src/commands/validate.ts
|
|
502
|
-
import { resolve as
|
|
652
|
+
import { resolve as resolve4 } from "path";
|
|
503
653
|
import { validateMarkdownDoc } from "@bendyline/squisq/doc";
|
|
504
654
|
function registerValidateCommand(program2) {
|
|
505
655
|
program2.command("validate").description("Validate a squisq markdown document and report structural problems").argument("<input>", "Path to .md file, .zip/.dbk container, or folder").option("--json", "Output diagnostics as JSON (for tooling and agents)").option("--strict", "Exit non-zero on warnings as well as errors").action(async (inputPath, opts) => {
|
|
@@ -518,7 +668,7 @@ function registerValidateCommand(program2) {
|
|
|
518
668
|
});
|
|
519
669
|
}
|
|
520
670
|
async function runValidate(inputPath, opts) {
|
|
521
|
-
const resolvedInput =
|
|
671
|
+
const resolvedInput = resolve4(inputPath);
|
|
522
672
|
const { container, markdownDoc, doc } = await readInput(resolvedInput);
|
|
523
673
|
if (!markdownDoc) {
|
|
524
674
|
const diagnostics = doc.diagnostics ?? [];
|
|
@@ -585,13 +735,16 @@ var defaultDoctorRuntime = {
|
|
|
585
735
|
log: (line) => console.error(line)
|
|
586
736
|
};
|
|
587
737
|
function registerDoctorCommand(program2) {
|
|
588
|
-
program2.command("doctor").description(
|
|
738
|
+
program2.command("doctor").description(
|
|
739
|
+
"Check that ffmpeg and Playwright Chromium are available for video/image rendering"
|
|
740
|
+
).action(async () => {
|
|
589
741
|
const allPresent = await runDoctor();
|
|
590
742
|
process.exitCode = allPresent ? 0 : 1;
|
|
591
743
|
});
|
|
592
744
|
}
|
|
593
745
|
async function runDoctor(runtime = defaultDoctorRuntime) {
|
|
594
|
-
let
|
|
746
|
+
let ffmpegOk = true;
|
|
747
|
+
let chromiumOk = true;
|
|
595
748
|
runtime.log(`Node: ${runtime.nodeVersion}`);
|
|
596
749
|
try {
|
|
597
750
|
const detection = await runtime.detectFfmpeg();
|
|
@@ -601,7 +754,7 @@ async function runDoctor(runtime = defaultDoctorRuntime) {
|
|
|
601
754
|
`ffmpeg: ${detection.path} (source: ${detection.source}) \u2014 ${version2 ?? "version unknown"}`
|
|
602
755
|
);
|
|
603
756
|
} else {
|
|
604
|
-
|
|
757
|
+
ffmpegOk = false;
|
|
605
758
|
runtime.log("ffmpeg: not found.");
|
|
606
759
|
runtime.log(" Install it with:");
|
|
607
760
|
runtime.log(" macOS: brew install ffmpeg");
|
|
@@ -610,7 +763,7 @@ async function runDoctor(runtime = defaultDoctorRuntime) {
|
|
|
610
763
|
runtime.log(" Or: npm install ffmpeg-static, or set SQUISQ_FFMPEG.");
|
|
611
764
|
}
|
|
612
765
|
} catch (err) {
|
|
613
|
-
|
|
766
|
+
ffmpegOk = false;
|
|
614
767
|
runtime.log(`ffmpeg: ${err instanceof Error ? err.message : String(err)}`);
|
|
615
768
|
}
|
|
616
769
|
try {
|
|
@@ -618,18 +771,21 @@ async function runDoctor(runtime = defaultDoctorRuntime) {
|
|
|
618
771
|
await browser.close();
|
|
619
772
|
runtime.log(`Chromium: available (${browser.executablePath})`);
|
|
620
773
|
} catch (err) {
|
|
621
|
-
|
|
774
|
+
chromiumOk = false;
|
|
622
775
|
const detail = err instanceof Error ? err.message.split("\n")[0] : String(err);
|
|
623
776
|
runtime.log(`Chromium: not available \u2014 ${detail}`);
|
|
624
777
|
runtime.log(" Run: npx playwright install chromium");
|
|
625
778
|
}
|
|
779
|
+
runtime.log(`video/gif (ffmpeg + Chromium): ${ffmpegOk && chromiumOk ? "ready" : "not ready"}`);
|
|
780
|
+
runtime.log(`image PNG (Chromium only): ${chromiumOk ? "ready" : "not ready"}`);
|
|
781
|
+
const allPresent = ffmpegOk && chromiumOk;
|
|
626
782
|
runtime.log(allPresent ? "\n\u2713 All checks passed" : "\n\u2717 Some checks failed");
|
|
627
783
|
return allPresent;
|
|
628
784
|
}
|
|
629
785
|
|
|
630
786
|
// src/commands/transform.ts
|
|
631
|
-
import { mkdir as
|
|
632
|
-
import { dirname as
|
|
787
|
+
import { mkdir as mkdir4, readFile } from "fs/promises";
|
|
788
|
+
import { dirname as dirname4, resolve as resolve5 } from "path";
|
|
633
789
|
import { Option as Option2 } from "commander";
|
|
634
790
|
import {
|
|
635
791
|
DEFAULT_WRAP_WIDTH,
|
|
@@ -662,7 +818,7 @@ function registerTransformCommand(program2) {
|
|
|
662
818
|
});
|
|
663
819
|
}
|
|
664
820
|
async function runTransform(inputPath, opts) {
|
|
665
|
-
const resolvedInput =
|
|
821
|
+
const resolvedInput = resolve5(inputPath);
|
|
666
822
|
let source;
|
|
667
823
|
try {
|
|
668
824
|
source = await readFile(resolvedInput, { encoding: "utf-8" });
|
|
@@ -704,9 +860,9 @@ async function runTransform(inputPath, opts) {
|
|
|
704
860
|
await writeFileGuarded(resolvedInput, bytes, true);
|
|
705
861
|
console.error(`\u2713 ${summary} \u2192 ${resolvedInput}`);
|
|
706
862
|
} else if (opts.output) {
|
|
707
|
-
const resolvedOutput =
|
|
863
|
+
const resolvedOutput = resolve5(opts.output);
|
|
708
864
|
await assertOutputsWritable([resolvedOutput], opts.overwrite);
|
|
709
|
-
await
|
|
865
|
+
await mkdir4(dirname4(resolvedOutput), { recursive: true });
|
|
710
866
|
await writeFileGuarded(resolvedOutput, bytes, opts.overwrite);
|
|
711
867
|
console.error(`\u2713 ${summary} \u2192 ${resolvedOutput}`);
|
|
712
868
|
} else {
|
|
@@ -728,6 +884,7 @@ var program = new Command2();
|
|
|
728
884
|
program.name("squisq").description("Squisq CLI \u2014 convert and process markdown-based documents").version(version);
|
|
729
885
|
registerConvertCommand(program);
|
|
730
886
|
registerVideoCommand(program);
|
|
887
|
+
registerImageCommand(program);
|
|
731
888
|
registerValidateCommand(program);
|
|
732
889
|
registerDoctorCommand(program);
|
|
733
890
|
registerTransformCommand(program);
|