@bendyline/squisq-cli 2.4.2 → 2.4.3
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 +5 -6
- package/dist/{api-KYHSJM5T.js → api-PBBQQMRP.js} +1 -1
- package/dist/api.js +215 -64
- package/dist/{chunk-VK6WPOSG.js → chunk-X3GFPKRJ.js} +218 -66
- package/dist/index.js +18 -14
- package/dist/squisq-player.full.global.js +400 -400
- package/dist/squisq-player.global.js +52 -52
- package/package.json +4 -4
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
|
|
7
7
|
// src/api.ts
|
|
8
8
|
import { readFile as readFile3 } from "fs/promises";
|
|
9
|
-
import { resolveMediaSchedule } from "@bendyline/squisq/schemas";
|
|
9
|
+
import { resolveMediaSchedule as resolveMediaSchedule2 } from "@bendyline/squisq/schemas";
|
|
10
10
|
import { flattenBlocks as flattenBlocks2 } from "@bendyline/squisq/doc";
|
|
11
11
|
import { ffmpegGifOutputArgs, generateRenderHtml } from "@bendyline/squisq-video";
|
|
12
12
|
import { resolveDimensions } from "@bendyline/squisq-video";
|
|
@@ -19,17 +19,17 @@ import {
|
|
|
19
19
|
import { execFile } from "child_process";
|
|
20
20
|
function run(command, args, signal) {
|
|
21
21
|
signal?.throwIfAborted();
|
|
22
|
-
return new Promise((
|
|
22
|
+
return new Promise((resolve2, reject) => {
|
|
23
23
|
execFile(command, args, { timeout: 5e3, signal }, (err, stdout) => {
|
|
24
24
|
if (signal?.aborted) {
|
|
25
25
|
reject(signal.reason);
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
28
|
if (err || !stdout.trim()) {
|
|
29
|
-
|
|
29
|
+
resolve2(null);
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
|
-
|
|
32
|
+
resolve2(stdout.trim());
|
|
33
33
|
});
|
|
34
34
|
});
|
|
35
35
|
}
|
|
@@ -73,7 +73,7 @@ async function detectFfmpegDetailed(signal) {
|
|
|
73
73
|
import { computeAudioTimeline } from "@bendyline/squisq-video";
|
|
74
74
|
async function buildMixedAudioTrack(doc, container, ffmpegPath, coverPreRoll, signal) {
|
|
75
75
|
signal?.throwIfAborted();
|
|
76
|
-
const timeline = computeAudioTimeline(doc, coverPreRoll);
|
|
76
|
+
const timeline = computeAudioTimeline(doc, coverPreRoll, { includeVideoAudio: false });
|
|
77
77
|
if (timeline.length === 0) return null;
|
|
78
78
|
const bytesBySrc = /* @__PURE__ */ new Map();
|
|
79
79
|
const readSrc = async (src) => {
|
|
@@ -330,7 +330,7 @@ function mp4Format() {
|
|
|
330
330
|
const animationsEnabled = typeof mp4Opts.animationsEnabled === "boolean" ? mp4Opts.animationsEnabled : MP4_DEFAULTS.animationsEnabled;
|
|
331
331
|
const outputPath = join(tmpdir(), `squisq-mp4-${randomBytes(8).toString("hex")}.mp4`);
|
|
332
332
|
try {
|
|
333
|
-
const { renderDocToMp4: renderDocToMp42 } = await import("./api-
|
|
333
|
+
const { renderDocToMp4: renderDocToMp42 } = await import("./api-PBBQQMRP.js");
|
|
334
334
|
await renderDocToMp42(input.doc, input.container, {
|
|
335
335
|
outputPath,
|
|
336
336
|
fps,
|
|
@@ -371,7 +371,7 @@ function gifFormat() {
|
|
|
371
371
|
const defaultHeight = portrait ? GIF_DEFAULTS.width : GIF_DEFAULTS.height;
|
|
372
372
|
const outputPath = join(tmpdir(), `squisq-gif-${randomBytes(8).toString("hex")}.gif`);
|
|
373
373
|
try {
|
|
374
|
-
const { renderDocToGif: renderDocToGif2 } = await import("./api-
|
|
374
|
+
const { renderDocToGif: renderDocToGif2 } = await import("./api-PBBQQMRP.js");
|
|
375
375
|
const result = await renderDocToGif2(input.doc, input.container, {
|
|
376
376
|
outputPath,
|
|
377
377
|
fps: typeof gifOpts.fps === "number" ? gifOpts.fps : GIF_DEFAULTS.fps,
|
|
@@ -415,13 +415,45 @@ function createCliRegistry() {
|
|
|
415
415
|
import { MemoryContentContainer as MemoryContentContainer2 } from "@bendyline/squisq/storage";
|
|
416
416
|
|
|
417
417
|
// src/util/readInput.ts
|
|
418
|
-
import { readFile as readFile2, readdir, stat } from "fs/promises";
|
|
419
|
-
import {
|
|
418
|
+
import { readFile as readFile2, readdir, realpath, stat } from "fs/promises";
|
|
419
|
+
import {
|
|
420
|
+
basename,
|
|
421
|
+
dirname,
|
|
422
|
+
extname,
|
|
423
|
+
isAbsolute,
|
|
424
|
+
join as join2,
|
|
425
|
+
posix,
|
|
426
|
+
relative,
|
|
427
|
+
resolve,
|
|
428
|
+
sep,
|
|
429
|
+
win32
|
|
430
|
+
} from "path";
|
|
420
431
|
import { parseMarkdown, stringifyMarkdown } from "@bendyline/squisq/markdown";
|
|
421
432
|
import { markdownToDoc, resolveAudioMapping } from "@bendyline/squisq/doc";
|
|
433
|
+
import { resolveMediaSchedule, validateDocSchema } from "@bendyline/squisq/schemas";
|
|
422
434
|
import { MemoryContentContainer } from "@bendyline/squisq/storage";
|
|
423
435
|
import { zipToContainer } from "@bendyline/squisq-formats/container";
|
|
424
436
|
import { defaultRegistry as defaultRegistry2 } from "@bendyline/squisq-formats";
|
|
437
|
+
var DocInputValidationError = class extends Error {
|
|
438
|
+
constructor(source, issues) {
|
|
439
|
+
const detail = issues.map(formatSchemaIssueForError).join("; ");
|
|
440
|
+
super(`${source} is not a valid squisq Doc: ${detail}`);
|
|
441
|
+
this.name = "DocInputValidationError";
|
|
442
|
+
this.issues = issues;
|
|
443
|
+
this.diagnostics = issues.map((issue) => ({
|
|
444
|
+
severity: "error",
|
|
445
|
+
code: "invalid-doc-schema",
|
|
446
|
+
message: `${issue.path} ${issue.message}`
|
|
447
|
+
}));
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
function formatSchemaIssueForError(issue) {
|
|
451
|
+
if (issue.path === "$") {
|
|
452
|
+
const got = /\(got ([^)]+)\)/.exec(issue.message)?.[1] ?? "an invalid value";
|
|
453
|
+
return `expected a JSON object, got ${got}`;
|
|
454
|
+
}
|
|
455
|
+
return `"${issue.path}" ${issue.message}`;
|
|
456
|
+
}
|
|
425
457
|
var MIME_TYPES = {
|
|
426
458
|
".md": "text/markdown",
|
|
427
459
|
".txt": "text/plain",
|
|
@@ -436,7 +468,11 @@ var MIME_TYPES = {
|
|
|
436
468
|
".wav": "audio/wav",
|
|
437
469
|
".ogg": "audio/ogg",
|
|
438
470
|
".mp4": "video/mp4",
|
|
439
|
-
".webm": "video/webm"
|
|
471
|
+
".webm": "video/webm",
|
|
472
|
+
".woff": "font/woff",
|
|
473
|
+
".woff2": "font/woff2",
|
|
474
|
+
".ttf": "font/ttf",
|
|
475
|
+
".otf": "font/otf"
|
|
440
476
|
};
|
|
441
477
|
var IMPORTER_EXTS = [".docx", ".pptx", ".pdf", ".xlsx", ".csv", ".html", ".htm"];
|
|
442
478
|
function mimeFromExt(filePath) {
|
|
@@ -462,8 +498,10 @@ async function readInput(inputPath, options) {
|
|
|
462
498
|
throwIfAborted(options?.signal);
|
|
463
499
|
const result = await readInputRaw(inputPath, options);
|
|
464
500
|
throwIfAborted(options?.signal);
|
|
501
|
+
assertValidDoc(result.doc, inputPath);
|
|
465
502
|
const doc = await resolveAudioMapping(result.doc, result.container);
|
|
466
503
|
throwIfAborted(options?.signal);
|
|
504
|
+
assertValidDoc(doc, inputPath);
|
|
467
505
|
return doc === result.doc ? result : { ...result, doc };
|
|
468
506
|
}
|
|
469
507
|
async function readInputRaw(inputPath, options) {
|
|
@@ -516,69 +554,160 @@ async function readUtf8File(filePath, signal) {
|
|
|
516
554
|
}
|
|
517
555
|
async function readMarkdownFile(filePath, signal) {
|
|
518
556
|
const content = await readUtf8File(filePath, signal);
|
|
519
|
-
const container = new MemoryContentContainer();
|
|
520
|
-
await container.writeDocument(content);
|
|
521
|
-
throwIfAborted(signal);
|
|
522
557
|
const markdownDoc = parseMarkdown(content);
|
|
523
|
-
|
|
558
|
+
const doc = markdownToDoc(markdownDoc);
|
|
559
|
+
const container = await buildBareMarkdownContainer(filePath, content, doc, signal);
|
|
560
|
+
return { doc, container, markdownDoc, sourceFormat: "md" };
|
|
524
561
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
fail(`expected a JSON object, got ${Array.isArray(parsed) ? "an array" : typeof parsed}`);
|
|
538
|
-
}
|
|
539
|
-
const doc = parsed;
|
|
540
|
-
if (!Array.isArray(doc.blocks)) {
|
|
541
|
-
fail(`"blocks" must be an array${doc.blocks === void 0 ? " (field is missing)" : ""}`);
|
|
562
|
+
var NARRATION_EXTENSIONS = /* @__PURE__ */ new Set([".aac", ".flac", ".m4a", ".mp3", ".ogg", ".wav"]);
|
|
563
|
+
async function buildBareMarkdownContainer(filePath, content, doc, signal) {
|
|
564
|
+
const container = new MemoryContentContainer();
|
|
565
|
+
await container.writeDocument(content, basename(filePath));
|
|
566
|
+
throwIfAborted(signal);
|
|
567
|
+
const refs = collectAuthoredAssetRefs(content, doc);
|
|
568
|
+
const root = dirname(resolve(filePath));
|
|
569
|
+
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
570
|
+
throwIfAborted(signal);
|
|
571
|
+
if (entry.isFile() && NARRATION_EXTENSIONS.has(extname(entry.name).toLowerCase())) {
|
|
572
|
+
refs.add(entry.name);
|
|
573
|
+
}
|
|
542
574
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
575
|
+
refs.add("timing.json");
|
|
576
|
+
for (const ref of [...refs]) {
|
|
577
|
+
if (NARRATION_EXTENSIONS.has(extname(stripUrlSuffix(ref)).toLowerCase())) {
|
|
578
|
+
refs.add(`${stripUrlSuffix(ref)}.timing.json`);
|
|
546
579
|
}
|
|
547
580
|
}
|
|
548
|
-
|
|
549
|
-
|
|
581
|
+
const rootReal = await realpath(root);
|
|
582
|
+
let fileCount = 0;
|
|
583
|
+
let totalBytes = 0;
|
|
584
|
+
for (const authoredRef of refs) {
|
|
585
|
+
throwIfAborted(signal);
|
|
586
|
+
const safe = normalizeAssetReference(authoredRef);
|
|
587
|
+
if (!safe) continue;
|
|
588
|
+
const absolute = resolve(root, ...safe.split("/"));
|
|
589
|
+
if (!isContainedPath(root, absolute)) continue;
|
|
590
|
+
let assetReal;
|
|
591
|
+
let info;
|
|
592
|
+
try {
|
|
593
|
+
assetReal = await realpath(absolute);
|
|
594
|
+
if (!isContainedPath(rootReal, assetReal)) continue;
|
|
595
|
+
info = await stat(assetReal);
|
|
596
|
+
} catch (error) {
|
|
597
|
+
if (isMissingFileError(error)) continue;
|
|
598
|
+
throw error;
|
|
599
|
+
}
|
|
600
|
+
if (!info.isFile()) continue;
|
|
601
|
+
if (info.size > MAX_RENDER_MEDIA_FILE_BYTES) {
|
|
602
|
+
throw new Error(
|
|
603
|
+
`Sibling asset "${safe}" exceeds the ${formatMiB(MAX_RENDER_MEDIA_FILE_BYTES)} per-file input limit.`
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
if (fileCount + 1 > MAX_RENDER_MEDIA_FILES) {
|
|
607
|
+
throw new Error(
|
|
608
|
+
`Bare markdown input references more than ${MAX_RENDER_MEDIA_FILES} sibling assets.`
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
if (totalBytes + info.size > MAX_RENDER_MEDIA_TOTAL_BYTES) {
|
|
612
|
+
throw new Error(
|
|
613
|
+
`Sibling assets exceed the ${formatMiB(MAX_RENDER_MEDIA_TOTAL_BYTES)} total input limit.`
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
const data = await readBinaryFile(assetReal, signal);
|
|
617
|
+
if (data.byteLength > MAX_RENDER_MEDIA_FILE_BYTES) {
|
|
618
|
+
throw new Error(
|
|
619
|
+
`Sibling asset "${safe}" exceeds the ${formatMiB(MAX_RENDER_MEDIA_FILE_BYTES)} per-file input limit.`
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
if (totalBytes + data.byteLength > MAX_RENDER_MEDIA_TOTAL_BYTES) {
|
|
623
|
+
throw new Error(
|
|
624
|
+
`Sibling assets exceed the ${formatMiB(MAX_RENDER_MEDIA_TOTAL_BYTES)} total input limit.`
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
await container.writeFile(safe, data, mimeFromExt(safe));
|
|
628
|
+
fileCount += 1;
|
|
629
|
+
totalBytes += data.byteLength;
|
|
550
630
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
631
|
+
return container;
|
|
632
|
+
}
|
|
633
|
+
function collectAuthoredAssetRefs(content, doc) {
|
|
634
|
+
const refs = /* @__PURE__ */ new Set();
|
|
635
|
+
const add = (value) => {
|
|
636
|
+
if (typeof value === "string" && value.trim()) refs.add(value.trim());
|
|
637
|
+
};
|
|
638
|
+
const scanObject = (value, seen = /* @__PURE__ */ new Set()) => {
|
|
639
|
+
if (!value || typeof value !== "object") return;
|
|
640
|
+
if (seen.has(value)) return;
|
|
641
|
+
seen.add(value);
|
|
642
|
+
if (Array.isArray(value)) {
|
|
643
|
+
for (const item of value) scanObject(item, seen);
|
|
644
|
+
return;
|
|
554
645
|
}
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
fail(`"audio.segments[${index}]" must be an object`);
|
|
561
|
-
}
|
|
562
|
-
if (!isFiniteNumber(segment.duration)) {
|
|
563
|
-
fail(
|
|
564
|
-
`"audio.segments[${index}].duration" must be a finite number, got ${describe(segment.duration)}`
|
|
565
|
-
);
|
|
566
|
-
}
|
|
646
|
+
for (const [key, item] of Object.entries(value)) {
|
|
647
|
+
if (typeof item === "string" && ["src", "url", "heroSrc", "posterSrc", "staticSrc", "videoSrc", "imageSrc"].includes(key)) {
|
|
648
|
+
add(item);
|
|
649
|
+
} else {
|
|
650
|
+
scanObject(item, seen);
|
|
567
651
|
}
|
|
568
652
|
}
|
|
569
|
-
}
|
|
570
|
-
return {
|
|
571
|
-
...doc,
|
|
572
|
-
audio: doc.audio ?? { segments: [] }
|
|
573
653
|
};
|
|
654
|
+
scanObject(doc);
|
|
655
|
+
for (const segment of doc.audio.segments) add(segment.src);
|
|
656
|
+
for (const clip of resolveMediaSchedule(doc)) add(clip.src);
|
|
657
|
+
const patterns = [
|
|
658
|
+
/\b(?:src|href)\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s>]+))/gi,
|
|
659
|
+
/\b(?:src|audio|video|image|font)\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s}\]]+))/gi,
|
|
660
|
+
/\burl\(\s*(?:"([^"]+)"|'([^']+)'|([^\s)]+))\s*\)/gi,
|
|
661
|
+
/!?\[[^\]]*\]\(\s*<?([^\s)>]+)>?/g
|
|
662
|
+
];
|
|
663
|
+
for (const pattern of patterns) {
|
|
664
|
+
let match;
|
|
665
|
+
while (match = pattern.exec(content)) add(match.slice(1).find(Boolean));
|
|
666
|
+
}
|
|
667
|
+
return refs;
|
|
668
|
+
}
|
|
669
|
+
function normalizeAssetReference(authoredRef) {
|
|
670
|
+
let value = stripUrlSuffix(authoredRef.trim().replace(/^<|>$/g, ""));
|
|
671
|
+
try {
|
|
672
|
+
value = decodeURIComponent(value);
|
|
673
|
+
} catch {
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
if (!value || value.includes("\0") || /^[a-z][a-z\d+.-]*:/i.test(value) || posix.isAbsolute(value) || win32.isAbsolute(value) || isAbsolute(value)) {
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
const parts = value.replace(/\\/g, "/").split("/");
|
|
680
|
+
if (parts.some((part) => part === "..")) return null;
|
|
681
|
+
const normalized = parts.filter((part) => part && part !== ".").join("/");
|
|
682
|
+
return normalized || null;
|
|
683
|
+
}
|
|
684
|
+
function stripUrlSuffix(value) {
|
|
685
|
+
return value.split(/[?#]/, 1)[0] ?? value;
|
|
686
|
+
}
|
|
687
|
+
function isContainedPath(root, candidate) {
|
|
688
|
+
const rel = relative(root, candidate);
|
|
689
|
+
return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel);
|
|
690
|
+
}
|
|
691
|
+
function isMissingFileError(error) {
|
|
692
|
+
return typeof error === "object" && error !== null && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
574
693
|
}
|
|
575
|
-
function
|
|
576
|
-
return
|
|
694
|
+
function formatMiB(bytes) {
|
|
695
|
+
return `${Math.round(bytes / (1024 * 1024))} MB`;
|
|
696
|
+
}
|
|
697
|
+
function assertValidDoc(value, source) {
|
|
698
|
+
const issues = validateDocSchema(value);
|
|
699
|
+
if (issues.length > 0) throw new DocInputValidationError(source, issues);
|
|
577
700
|
}
|
|
578
|
-
function
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
701
|
+
function parseDocJson(content, source) {
|
|
702
|
+
let parsed;
|
|
703
|
+
try {
|
|
704
|
+
parsed = JSON.parse(content);
|
|
705
|
+
} catch (error) {
|
|
706
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
707
|
+
throw new Error(`${source} is not valid JSON: ${detail}`);
|
|
708
|
+
}
|
|
709
|
+
assertValidDoc(parsed, source);
|
|
710
|
+
return parsed;
|
|
582
711
|
}
|
|
583
712
|
async function readDocJsonFile(filePath, signal) {
|
|
584
713
|
const content = await readUtf8File(filePath, signal);
|
|
@@ -751,7 +880,7 @@ async function captureDocFrames(doc, container, options) {
|
|
|
751
880
|
audio.set(seg.name, data);
|
|
752
881
|
}
|
|
753
882
|
}
|
|
754
|
-
const mediaSrcs = new Set(
|
|
883
|
+
const mediaSrcs = new Set(resolveMediaSchedule2(doc).map((clip) => clip.src));
|
|
755
884
|
for (const block of flattenBlocks2(doc.blocks)) {
|
|
756
885
|
for (const layer of block.layers ?? []) {
|
|
757
886
|
if (layer.type === "video") mediaSrcs.add(layer.content.src);
|
|
@@ -1002,7 +1131,7 @@ async function renderDocToGif(doc, container, options) {
|
|
|
1002
1131
|
options.signal?.throwIfAborted();
|
|
1003
1132
|
options.onProgress?.("done", 100);
|
|
1004
1133
|
options.signal?.throwIfAborted();
|
|
1005
|
-
const hasAudio = (doc.audio?.segments?.length ?? 0) > 0 ||
|
|
1134
|
+
const hasAudio = (doc.audio?.segments?.length ?? 0) > 0 || resolveMediaSchedule2(doc).some((clip) => clip.kind === "audio");
|
|
1006
1135
|
return {
|
|
1007
1136
|
duration: capture.totalDuration,
|
|
1008
1137
|
frameCount,
|
|
@@ -1017,8 +1146,30 @@ async function extractThumbnails(options) {
|
|
|
1017
1146
|
const { videoPath, outputDir, slug, sizes, force, signal } = options;
|
|
1018
1147
|
const { existsSync } = await import("fs");
|
|
1019
1148
|
const { rm: rm2 } = await import("fs/promises");
|
|
1020
|
-
const {
|
|
1149
|
+
const { isAbsolute: isAbsolute2, relative: relative2, resolve: resolve2, sep: sep2 } = await import("path");
|
|
1021
1150
|
signal?.throwIfAborted();
|
|
1151
|
+
if (typeof slug !== "string" || !/^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,126}[A-Za-z0-9])?$/.test(slug)) {
|
|
1152
|
+
throw new TypeError(
|
|
1153
|
+
"Thumbnail slug must be 1\u2013128 filename-safe characters (letters, numbers, dot, dash, or underscore) and must start and end with a letter or number."
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
1156
|
+
for (const [index, thumb] of sizes.entries()) {
|
|
1157
|
+
if (!Number.isSafeInteger(thumb.width) || thumb.width <= 0) {
|
|
1158
|
+
throw new TypeError(`Thumbnail sizes[${index}].width must be a positive integer.`);
|
|
1159
|
+
}
|
|
1160
|
+
if (!Number.isSafeInteger(thumb.height) || thumb.height <= 0) {
|
|
1161
|
+
throw new TypeError(`Thumbnail sizes[${index}].height must be a positive integer.`);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
const resolvedOutputDir = resolve2(outputDir);
|
|
1165
|
+
const outputPaths = sizes.map((thumb) => {
|
|
1166
|
+
const outputPath = resolve2(resolvedOutputDir, `${slug}-${thumb.width}x${thumb.height}.jpg`);
|
|
1167
|
+
const rel = relative2(resolvedOutputDir, outputPath);
|
|
1168
|
+
if (rel === ".." || rel.startsWith(`..${sep2}`) || isAbsolute2(rel)) {
|
|
1169
|
+
throw new TypeError("Thumbnail output path must remain inside outputDir.");
|
|
1170
|
+
}
|
|
1171
|
+
return outputPath;
|
|
1172
|
+
});
|
|
1022
1173
|
const ffmpegPath = (await detectFfmpegDetailed(signal))?.path ?? null;
|
|
1023
1174
|
if (!ffmpegPath) {
|
|
1024
1175
|
throw new Error(
|
|
@@ -1027,9 +1178,9 @@ async function extractThumbnails(options) {
|
|
|
1027
1178
|
}
|
|
1028
1179
|
const generatedPaths = [];
|
|
1029
1180
|
try {
|
|
1030
|
-
for (const thumb of sizes) {
|
|
1181
|
+
for (const [index, thumb] of sizes.entries()) {
|
|
1031
1182
|
signal?.throwIfAborted();
|
|
1032
|
-
const outputPath =
|
|
1183
|
+
const outputPath = outputPaths[index];
|
|
1033
1184
|
if (!force && existsSync(outputPath)) continue;
|
|
1034
1185
|
try {
|
|
1035
1186
|
await runFfmpeg(
|
|
@@ -1056,6 +1207,7 @@ async function extractThumbnails(options) {
|
|
|
1056
1207
|
}
|
|
1057
1208
|
|
|
1058
1209
|
export {
|
|
1210
|
+
DocInputValidationError,
|
|
1059
1211
|
readInput,
|
|
1060
1212
|
getFfmpegVersion,
|
|
1061
1213
|
detectFfmpegDetailed,
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
DocInputValidationError,
|
|
3
4
|
convert,
|
|
4
5
|
createCliRegistry,
|
|
5
6
|
detectFfmpegDetailed,
|
|
@@ -7,7 +8,7 @@ import {
|
|
|
7
8
|
readInput,
|
|
8
9
|
renderDocToGif,
|
|
9
10
|
renderDocToMp4
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-X3GFPKRJ.js";
|
|
11
12
|
import "./chunk-GGVASRPG.js";
|
|
12
13
|
|
|
13
14
|
// src/index.ts
|
|
@@ -498,8 +499,7 @@ function clearProgress() {
|
|
|
498
499
|
}
|
|
499
500
|
|
|
500
501
|
// src/commands/validate.ts
|
|
501
|
-
import {
|
|
502
|
-
import { dirname as dirname3, extname as extname3, join as join2, resolve as resolve3 } from "path";
|
|
502
|
+
import { resolve as resolve3 } from "path";
|
|
503
503
|
import { validateMarkdownDoc } from "@bendyline/squisq/doc";
|
|
504
504
|
function registerValidateCommand(program2) {
|
|
505
505
|
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) => {
|
|
@@ -507,6 +507,10 @@ function registerValidateCommand(program2) {
|
|
|
507
507
|
const exitCode = await runValidate(inputPath, opts);
|
|
508
508
|
process.exitCode = exitCode;
|
|
509
509
|
} catch (err) {
|
|
510
|
+
if (err instanceof DocInputValidationError) {
|
|
511
|
+
process.exitCode = report(err.diagnostics, opts);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
510
514
|
const message = err instanceof Error ? err.message : String(err);
|
|
511
515
|
console.error(`Error: ${message}`);
|
|
512
516
|
process.exitCode = 2;
|
|
@@ -518,16 +522,12 @@ async function runValidate(inputPath, opts) {
|
|
|
518
522
|
const { container, markdownDoc, doc } = await readInput(resolvedInput);
|
|
519
523
|
if (!markdownDoc) {
|
|
520
524
|
const diagnostics = doc.diagnostics ?? [];
|
|
521
|
-
return report(diagnostics, opts, "doc.json input \u2014
|
|
525
|
+
return report(diagnostics, opts, "doc.json input \u2014 canonical schema valid");
|
|
522
526
|
}
|
|
523
527
|
const entries = await container.listFiles();
|
|
524
528
|
const containerPaths = new Set(entries.map((e) => e.path));
|
|
525
|
-
const isBareMarkdown = statSync(resolvedInput).isFile() && ![".zip", ".dbk"].includes(extname3(resolvedInput).toLowerCase());
|
|
526
|
-
const baseDir = dirname3(resolvedInput);
|
|
527
529
|
const hasAsset = (path) => {
|
|
528
|
-
|
|
529
|
-
if (isBareMarkdown) return existsSync(join2(baseDir, path));
|
|
530
|
-
return false;
|
|
530
|
+
return containerPaths.has(path) || containerPaths.has(path.replace(/^\.\//, ""));
|
|
531
531
|
};
|
|
532
532
|
const result = validateMarkdownDoc(markdownDoc, { assets: hasAsset });
|
|
533
533
|
return report(result.diagnostics, opts);
|
|
@@ -629,7 +629,7 @@ async function runDoctor(runtime = defaultDoctorRuntime) {
|
|
|
629
629
|
|
|
630
630
|
// src/commands/transform.ts
|
|
631
631
|
import { mkdir as mkdir3, readFile } from "fs/promises";
|
|
632
|
-
import { dirname as
|
|
632
|
+
import { dirname as dirname3, resolve as resolve4 } from "path";
|
|
633
633
|
import { Option as Option2 } from "commander";
|
|
634
634
|
import {
|
|
635
635
|
DEFAULT_WRAP_WIDTH,
|
|
@@ -682,9 +682,13 @@ async function runTransform(inputPath, opts) {
|
|
|
682
682
|
return 1;
|
|
683
683
|
}
|
|
684
684
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
685
|
+
if (!/^\d+$/.test(opts.width)) {
|
|
686
|
+
console.error(`Error: --width must be an integer between 20 and 500 (got "${opts.width}")`);
|
|
687
|
+
return 1;
|
|
688
|
+
}
|
|
689
|
+
const width = Number(opts.width);
|
|
690
|
+
if (!Number.isSafeInteger(width) || width < 20 || width > 500) {
|
|
691
|
+
console.error(`Error: --width must be an integer between 20 and 500 (got "${opts.width}")`);
|
|
688
692
|
return 1;
|
|
689
693
|
}
|
|
690
694
|
let result = source;
|
|
@@ -702,7 +706,7 @@ async function runTransform(inputPath, opts) {
|
|
|
702
706
|
} else if (opts.output) {
|
|
703
707
|
const resolvedOutput = resolve4(opts.output);
|
|
704
708
|
await assertOutputsWritable([resolvedOutput], opts.overwrite);
|
|
705
|
-
await mkdir3(
|
|
709
|
+
await mkdir3(dirname3(resolvedOutput), { recursive: true });
|
|
706
710
|
await writeFileGuarded(resolvedOutput, bytes, opts.overwrite);
|
|
707
711
|
console.error(`\u2713 ${summary} \u2192 ${resolvedOutput}`);
|
|
708
712
|
} else {
|