@bendyline/squisq-cli 2.3.0 → 2.3.2

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.
@@ -11,7 +11,7 @@ import {
11
11
  readInput,
12
12
  renderDocToGif,
13
13
  renderDocToMp4
14
- } from "./chunk-GYHQHKO5.js";
14
+ } from "./chunk-ZEN42AFN.js";
15
15
  import {
16
16
  GIF_EXPORT_DEFAULTS,
17
17
  framesToGifNative,
package/dist/api.d.ts CHANGED
@@ -23,7 +23,7 @@ interface Mp4FormatOptions {
23
23
  orientation?: VideoOrientation;
24
24
  width?: number;
25
25
  height?: number;
26
- captionStyle?: 'standard' | 'social';
26
+ captionStyle?: 'off' | 'standard' | 'social';
27
27
  coverPreRoll?: number;
28
28
  animationsEnabled?: boolean;
29
29
  }
@@ -34,7 +34,7 @@ interface GifFormatOptions {
34
34
  orientation?: VideoOrientation;
35
35
  width?: number;
36
36
  height?: number;
37
- captionStyle?: 'standard' | 'social';
37
+ captionStyle?: 'off' | 'standard' | 'social';
38
38
  coverPreRoll?: number;
39
39
  animationsEnabled?: boolean;
40
40
  loop?: number;
@@ -258,8 +258,8 @@ interface RenderDocToMp4Options {
258
258
  width?: number;
259
259
  /** Override video height in pixels. */
260
260
  height?: number;
261
- /** Caption style to bake into the video (default: none). */
262
- captionStyle?: 'standard' | 'social';
261
+ /** Caption mode to bake into the video (default: off). */
262
+ captionStyle?: 'off' | 'standard' | 'social';
263
263
  /** Render layer animations and block transitions (default: true). */
264
264
  animationsEnabled?: boolean;
265
265
  /**
@@ -289,8 +289,8 @@ interface RenderDocToGifOptions {
289
289
  width?: number;
290
290
  /** Override output height (default: 540 landscape / 960 portrait). */
291
291
  height?: number;
292
- /** Caption style to bake into the GIF (default: none). */
293
- captionStyle?: 'standard' | 'social';
292
+ /** Caption mode to bake into the GIF (default: standard). */
293
+ captionStyle?: 'off' | 'standard' | 'social';
294
294
  /** Seconds of cover-slide pre-roll (default: 0). */
295
295
  coverPreRoll?: number;
296
296
  /** Render layer animations and block transitions (default: false). */
package/dist/api.js CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  } from "./chunk-Z3BHD4AF.js";
9
9
 
10
10
  // src/api.ts
11
+ import { readFile as readFile3 } from "fs/promises";
11
12
  import { resolveMediaSchedule } from "@bendyline/squisq/schemas";
12
13
  import { flattenBlocks } from "@bendyline/squisq/doc";
13
14
  import { ffmpegGifOutputArgs, generateRenderHtml } from "@bendyline/squisq-video";
@@ -96,7 +97,7 @@ async function buildMixedAudioTrack(doc, container, ffmpegPath, coverPreRoll, si
96
97
  return mixTimelineClips(ffmpegPath, usable, signal);
97
98
  }
98
99
  async function mixTimelineClips(ffmpegPath, usable, signal) {
99
- const { writeFile, readFile: readFile3, mkdir, rm: rm2 } = await import("fs/promises");
100
+ const { writeFile, readFile: readFile4, mkdir, rm: rm2 } = await import("fs/promises");
100
101
  const { join: join3 } = await import("path");
101
102
  const { tmpdir: tmpdir2 } = await import("os");
102
103
  const { randomBytes: randomBytes2 } = await import("crypto");
@@ -144,7 +145,7 @@ async function mixTimelineClips(ffmpegPath, usable, signal) {
144
145
  signal
145
146
  });
146
147
  signal?.throwIfAborted();
147
- const data = await readFile3(outputPath);
148
+ const data = await readFile4(outputPath);
148
149
  signal?.throwIfAborted();
149
150
  return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
150
151
  } finally {
@@ -663,17 +664,25 @@ function throwIfAborted(signal) {
663
664
 
664
665
  // src/api.ts
665
666
  import { ConversionError } from "@bendyline/squisq-formats";
667
+ var playerBundlePromise;
668
+ function loadPlayerBundle() {
669
+ playerBundlePromise ??= readFile3(
670
+ new URL("../dist/squisq-player.global.js", import.meta.url),
671
+ "utf8"
672
+ );
673
+ return playerBundlePromise;
674
+ }
666
675
  async function convert(source, to, options = {}) {
667
676
  return formatsConvert(source, to, {
668
677
  registry: createCliRegistry(),
669
- resolvePlayerScript: () => import("@bendyline/squisq-react/standalone-source").then((m) => m.PLAYER_BUNDLE),
678
+ resolvePlayerScript: loadPlayerBundle,
670
679
  ...options
671
680
  });
672
681
  }
673
682
  async function prepareConversion(source, options = {}) {
674
683
  return formatsPrepareConversion(source, {
675
684
  registry: createCliRegistry(),
676
- resolvePlayerScript: () => import("@bendyline/squisq-react/standalone-source").then((m) => m.PLAYER_BUNDLE),
685
+ resolvePlayerScript: loadPlayerBundle,
677
686
  ...options
678
687
  });
679
688
  }
@@ -732,10 +741,10 @@ async function captureDocFrames(doc, container, options) {
732
741
  }
733
742
  onProgress?.("generating render HTML", 10);
734
743
  signal?.throwIfAborted();
735
- const { PLAYER_BUNDLE } = await import("@bendyline/squisq-react/standalone-source");
744
+ const playerBundle = await loadPlayerBundle();
736
745
  signal?.throwIfAborted();
737
746
  const renderHtml = generateRenderHtml(doc, {
738
- playerScript: PLAYER_BUNDLE,
747
+ playerScript: playerBundle,
739
748
  images,
740
749
  audio: audio.size > 0 ? audio : void 0,
741
750
  width,
@@ -876,7 +885,7 @@ async function renderDocToMp4(doc, container, options) {
876
885
  fps,
877
886
  width: dimensions.width,
878
887
  height: dimensions.height,
879
- captionStyle: options.captionStyle,
888
+ captionStyle: options.captionStyle === "off" ? void 0 : options.captionStyle,
880
889
  coverPreRoll: options.coverPreRoll ?? 0,
881
890
  animationsEnabled: options.animationsEnabled ?? true,
882
891
  onProgress: options.onProgress,
@@ -939,7 +948,7 @@ async function renderDocToGif(doc, container, options) {
939
948
  fps,
940
949
  width,
941
950
  height,
942
- captionStyle: options.captionStyle,
951
+ captionStyle: options.captionStyle === "off" ? void 0 : options.captionStyle ?? "standard",
943
952
  coverPreRoll: options.coverPreRoll ?? 0,
944
953
  animationsEnabled: options.animationsEnabled ?? false,
945
954
  onProgress: options.onProgress,
@@ -5,6 +5,7 @@ import {
5
5
  } from "./chunk-GGVASRPG.js";
6
6
 
7
7
  // src/api.ts
8
+ import { readFile as readFile3 } from "fs/promises";
8
9
  import { resolveMediaSchedule } from "@bendyline/squisq/schemas";
9
10
  import { flattenBlocks } from "@bendyline/squisq/doc";
10
11
  import { ffmpegGifOutputArgs, generateRenderHtml } from "@bendyline/squisq-video";
@@ -93,7 +94,7 @@ async function buildMixedAudioTrack(doc, container, ffmpegPath, coverPreRoll, si
93
94
  return mixTimelineClips(ffmpegPath, usable, signal);
94
95
  }
95
96
  async function mixTimelineClips(ffmpegPath, usable, signal) {
96
- const { writeFile, readFile: readFile3, mkdir, rm: rm2 } = await import("fs/promises");
97
+ const { writeFile, readFile: readFile4, mkdir, rm: rm2 } = await import("fs/promises");
97
98
  const { join: join3 } = await import("path");
98
99
  const { tmpdir: tmpdir2 } = await import("os");
99
100
  const { randomBytes: randomBytes2 } = await import("crypto");
@@ -141,7 +142,7 @@ async function mixTimelineClips(ffmpegPath, usable, signal) {
141
142
  signal
142
143
  });
143
144
  signal?.throwIfAborted();
144
- const data = await readFile3(outputPath);
145
+ const data = await readFile4(outputPath);
145
146
  signal?.throwIfAborted();
146
147
  return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
147
148
  } finally {
@@ -307,7 +308,7 @@ function mp4Format() {
307
308
  const animationsEnabled = typeof mp4Opts.animationsEnabled === "boolean" ? mp4Opts.animationsEnabled : MP4_DEFAULTS.animationsEnabled;
308
309
  const outputPath = join(tmpdir(), `squisq-mp4-${randomBytes(8).toString("hex")}.mp4`);
309
310
  try {
310
- const { renderDocToMp4: renderDocToMp42 } = await import("./api-5SX73ZKT.js");
311
+ const { renderDocToMp4: renderDocToMp42 } = await import("./api-A7FKMZY6.js");
311
312
  await renderDocToMp42(input.doc, input.container, {
312
313
  outputPath,
313
314
  fps,
@@ -347,7 +348,7 @@ function gifFormat() {
347
348
  const defaultHeight = portrait ? GIF_DEFAULTS.width : GIF_DEFAULTS.height;
348
349
  const outputPath = join(tmpdir(), `squisq-gif-${randomBytes(8).toString("hex")}.gif`);
349
350
  try {
350
- const { renderDocToGif: renderDocToGif2 } = await import("./api-5SX73ZKT.js");
351
+ const { renderDocToGif: renderDocToGif2 } = await import("./api-A7FKMZY6.js");
351
352
  const result = await renderDocToGif2(input.doc, input.container, {
352
353
  outputPath,
353
354
  fps: typeof gifOpts.fps === "number" ? gifOpts.fps : GIF_DEFAULTS.fps,
@@ -660,17 +661,25 @@ function throwIfAborted(signal) {
660
661
 
661
662
  // src/api.ts
662
663
  import { ConversionError } from "@bendyline/squisq-formats";
664
+ var playerBundlePromise;
665
+ function loadPlayerBundle() {
666
+ playerBundlePromise ??= readFile3(
667
+ new URL("../dist/squisq-player.global.js", import.meta.url),
668
+ "utf8"
669
+ );
670
+ return playerBundlePromise;
671
+ }
663
672
  async function convert(source, to, options = {}) {
664
673
  return formatsConvert(source, to, {
665
674
  registry: createCliRegistry(),
666
- resolvePlayerScript: () => import("@bendyline/squisq-react/standalone-source").then((m) => m.PLAYER_BUNDLE),
675
+ resolvePlayerScript: loadPlayerBundle,
667
676
  ...options
668
677
  });
669
678
  }
670
679
  async function prepareConversion(source, options = {}) {
671
680
  return formatsPrepareConversion(source, {
672
681
  registry: createCliRegistry(),
673
- resolvePlayerScript: () => import("@bendyline/squisq-react/standalone-source").then((m) => m.PLAYER_BUNDLE),
682
+ resolvePlayerScript: loadPlayerBundle,
674
683
  ...options
675
684
  });
676
685
  }
@@ -729,10 +738,10 @@ async function captureDocFrames(doc, container, options) {
729
738
  }
730
739
  onProgress?.("generating render HTML", 10);
731
740
  signal?.throwIfAborted();
732
- const { PLAYER_BUNDLE } = await import("@bendyline/squisq-react/standalone-source");
741
+ const playerBundle = await loadPlayerBundle();
733
742
  signal?.throwIfAborted();
734
743
  const renderHtml = generateRenderHtml(doc, {
735
- playerScript: PLAYER_BUNDLE,
744
+ playerScript: playerBundle,
736
745
  images,
737
746
  audio: audio.size > 0 ? audio : void 0,
738
747
  width,
@@ -873,7 +882,7 @@ async function renderDocToMp4(doc, container, options) {
873
882
  fps,
874
883
  width: dimensions.width,
875
884
  height: dimensions.height,
876
- captionStyle: options.captionStyle,
885
+ captionStyle: options.captionStyle === "off" ? void 0 : options.captionStyle,
877
886
  coverPreRoll: options.coverPreRoll ?? 0,
878
887
  animationsEnabled: options.animationsEnabled ?? true,
879
888
  onProgress: options.onProgress,
@@ -936,7 +945,7 @@ async function renderDocToGif(doc, container, options) {
936
945
  fps,
937
946
  width,
938
947
  height,
939
- captionStyle: options.captionStyle,
948
+ captionStyle: options.captionStyle === "off" ? void 0 : options.captionStyle ?? "standard",
940
949
  coverPreRoll: options.coverPreRoll ?? 0,
941
950
  animationsEnabled: options.animationsEnabled ?? false,
942
951
  onProgress: options.onProgress,
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  readInput,
8
8
  renderDocToGif,
9
9
  renderDocToMp4
10
- } from "./chunk-GYHQHKO5.js";
10
+ } from "./chunk-ZEN42AFN.js";
11
11
  import "./chunk-GGVASRPG.js";
12
12
 
13
13
  // src/index.ts
@@ -17,6 +17,7 @@ import { Command } from "commander";
17
17
  // src/commands/convert.ts
18
18
  import { mkdir } from "fs/promises";
19
19
  import { dirname, basename, extname, join, resolve } from "path";
20
+ import { Option } from "commander";
20
21
  import { BUILTIN_FORMAT_IDS, ConversionError } from "@bendyline/squisq-formats";
21
22
 
22
23
  // src/util/applyTransformPipeline.ts
@@ -163,6 +164,12 @@ function parseFormats(value) {
163
164
  `Unknown format${unknown.length > 1 ? "s" : ""}: ${hints}. Valid: ${VALID_FORMATS.join(", ")}`
164
165
  );
165
166
  }
167
+ const duplicates = [...new Set(requested.filter((id, index) => requested.indexOf(id) !== index))];
168
+ if (duplicates.length > 0) {
169
+ throw new Error(
170
+ `Duplicate format${duplicates.length > 1 ? "s" : ""}: ${duplicates.map((id) => `"${id}"`).join(", ")}. Each format may only be requested once.`
171
+ );
172
+ }
166
173
  return requested;
167
174
  }
168
175
  function formatFromOutputPath(outputPath) {
@@ -180,12 +187,21 @@ function formatFromOutputPath(outputPath) {
180
187
  function registerConvertCommand(program2) {
181
188
  program2.command("convert").description(
182
189
  "Convert a document to DOCX, PPTX, PDF, HTML, EPUB, MP4, animated GIF, and container formats"
183
- ).argument("<input>", "Path to .md/.docx/.pptx/.pdf/.xlsx/.csv/.html file, .zip/.dbk, or folder").option("-o, --output <file>", "Single output file (format inferred from its extension)").option(
184
- "-d, --output-dir <dir>",
185
- "Output directory for multi-format export (default: same as input)"
186
- ).option(
187
- "-f, --formats <list>",
188
- `Comma-separated formats to produce (default: a standard set). Valid: ${VALID_FORMATS.join(", ")}`
190
+ ).argument("<input>", "Path to .md/.docx/.pptx/.pdf/.xlsx/.csv/.html file, .zip/.dbk, or folder").addOption(
191
+ new Option(
192
+ "-o, --output <file>",
193
+ "Single output file (format inferred from its extension)"
194
+ ).conflicts(["outputDir", "formats"])
195
+ ).addOption(
196
+ new Option(
197
+ "-d, --output-dir <dir>",
198
+ "Output directory for multi-format export (default: same as input)"
199
+ ).conflicts("output")
200
+ ).addOption(
201
+ new Option(
202
+ "-f, --formats <list>",
203
+ `Comma-separated formats to produce (default: a standard set). Valid: ${VALID_FORMATS.join(", ")}`
204
+ ).conflicts("output")
189
205
  ).option("--overwrite", "Replace existing output files (default: refuse and exit non-zero)").option("-t, --theme <id>", "Squisq theme ID to apply (e.g., documentary, cinematic, bold)").option(
190
206
  "--transform <style>",
191
207
  "Transform style to apply before export (e.g., documentary, magazine, minimal)"
@@ -305,8 +321,7 @@ function registerVideoCommand(program2) {
305
321
  "landscape"
306
322
  ).option(
307
323
  "--captions <style>",
308
- `Caption style: ${VALID_CAPTIONS.join(", ")} (default: off)`,
309
- "off"
324
+ `Caption style: ${VALID_CAPTIONS.join(", ")} (default: off for MP4, standard for GIF)`
310
325
  ).option(
311
326
  "--no-auto-templates",
312
327
  "Disable content-aware template auto-picking for unannotated headings"
@@ -322,9 +337,10 @@ function registerVideoCommand(program2) {
322
337
  `GIF dithering: ${VALID_GIF_DITHERS.join(", ")} (default: sierra2_4a)`
323
338
  ).option("--bayer-scale <number>", "GIF Bayer dither strength from 0 to 5 (default: 3)").action(async (inputPath, outputArg, opts) => {
324
339
  try {
325
- if (outputArg && !opts.output) {
326
- opts.output = outputArg;
340
+ if (outputArg && opts.output) {
341
+ throw new Error("The positional output and --output cannot be used together.");
327
342
  }
343
+ if (outputArg) opts.output = outputArg;
328
344
  await runVideo(inputPath, opts);
329
345
  } catch (err) {
330
346
  const message = err instanceof Error ? err.message : String(err);
@@ -351,9 +367,9 @@ async function runVideo(inputPath, opts) {
351
367
  }
352
368
  const outputFormat = requestedFormat ?? extensionFormat ?? "mp4";
353
369
  const maxFps = outputFormat === "gif" ? 100 : 120;
354
- const fps = parseInt(opts.fps ?? (outputFormat === "gif" ? "10" : "30"), 10);
355
- if (isNaN(fps) || fps < 1 || fps > maxFps) {
356
- throw new Error(`FPS must be a number between 1 and ${maxFps}`);
370
+ const fps = Number(opts.fps ?? (outputFormat === "gif" ? "10" : "30"));
371
+ if (!Number.isSafeInteger(fps) || fps < 1 || fps > maxFps) {
372
+ throw new Error(`FPS must be an integer between 1 and ${maxFps}`);
357
373
  }
358
374
  const quality = opts.quality ?? "normal";
359
375
  if (outputFormat === "gif" && opts.quality !== void 0) {
@@ -368,16 +384,15 @@ async function runVideo(inputPath, opts) {
368
384
  `Invalid orientation "${orientation}". Valid: ${VALID_ORIENTATIONS.join(", ")}`
369
385
  );
370
386
  }
371
- const width = opts.width === void 0 ? void 0 : parseInt(opts.width, 10);
372
- const height = opts.height === void 0 ? void 0 : parseInt(opts.height, 10);
387
+ const width = opts.width === void 0 ? void 0 : Number(opts.width);
388
+ const height = opts.height === void 0 ? void 0 : Number(opts.height);
373
389
  validateVideoExportOptions({ width, height, orientation });
374
- const captions = opts.captions ?? "off";
390
+ const captions = opts.captions ?? (outputFormat === "gif" ? "standard" : "off");
375
391
  if (!VALID_CAPTIONS.includes(captions)) {
376
392
  throw new Error(`Invalid captions "${captions}". Valid: ${VALID_CAPTIONS.join(", ")}`);
377
393
  }
378
- const captionStyle = captions === "off" ? void 0 : captions;
379
- const coverPreRoll = parseFloat(opts.coverPreroll ?? "2");
380
- if (isNaN(coverPreRoll) || coverPreRoll < 0) {
394
+ const coverPreRoll = Number(opts.coverPreroll ?? "2");
395
+ if (!Number.isFinite(coverPreRoll) || coverPreRoll < 0) {
381
396
  throw new Error("Cover pre-roll must be a number of seconds >= 0");
382
397
  }
383
398
  const animationsEnabled = opts.animations ?? outputFormat === "mp4";
@@ -445,7 +460,7 @@ async function runVideo(inputPath, opts) {
445
460
  orientation,
446
461
  width,
447
462
  height,
448
- captionStyle,
463
+ captionStyle: captions,
449
464
  coverPreRoll,
450
465
  animationsEnabled,
451
466
  onProgress: (phase, percent) => writeProgress(phase, percent, 100)