@bendyline/squisq-cli 2.4.2 → 2.4.4
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/THIRD_PARTY_LICENSES.txt +1 -1
- package/dist/{api-KYHSJM5T.js → api-36UERSSU.js} +1 -1
- package/dist/api.js +230 -71
- package/dist/{chunk-VK6WPOSG.js → chunk-6OBPEBL4.js} +233 -73
- package/dist/index.js +18 -14
- package/dist/squisq-player-icons.global.js +1 -0
- package/dist/squisq-player.full.global.js +402 -401
- package/dist/squisq-player.global.js +56 -55
- package/package.json +5 -5
|
@@ -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-36UERSSU.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-36UERSSU.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;
|
|
574
668
|
}
|
|
575
|
-
function
|
|
576
|
-
|
|
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);
|
|
577
690
|
}
|
|
578
|
-
function
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
691
|
+
function isMissingFileError(error) {
|
|
692
|
+
return typeof error === "object" && error !== null && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
693
|
+
}
|
|
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);
|
|
700
|
+
}
|
|
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);
|
|
@@ -686,18 +815,26 @@ function throwIfAborted(signal) {
|
|
|
686
815
|
import { ConversionError } from "@bendyline/squisq-formats";
|
|
687
816
|
var playerBundlePromise;
|
|
688
817
|
var fullPlayerBundlePromise;
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
818
|
+
var playerIconBootstrapPromise;
|
|
819
|
+
function loadPlayerIconBootstrap() {
|
|
820
|
+
playerIconBootstrapPromise ??= readFile3(
|
|
821
|
+
new URL("../dist/squisq-player-icons.global.js", import.meta.url),
|
|
692
822
|
"utf8"
|
|
693
823
|
);
|
|
824
|
+
return playerIconBootstrapPromise;
|
|
825
|
+
}
|
|
826
|
+
function loadPlayerBundle() {
|
|
827
|
+
playerBundlePromise ??= Promise.all([
|
|
828
|
+
loadPlayerIconBootstrap(),
|
|
829
|
+
readFile3(new URL("../dist/squisq-player.global.js", import.meta.url), "utf8")
|
|
830
|
+
]).then(([icons, player]) => `${icons}${player}`);
|
|
694
831
|
return playerBundlePromise;
|
|
695
832
|
}
|
|
696
833
|
function loadFullPlayerBundle() {
|
|
697
|
-
fullPlayerBundlePromise ??=
|
|
698
|
-
|
|
699
|
-
"utf8"
|
|
700
|
-
);
|
|
834
|
+
fullPlayerBundlePromise ??= Promise.all([
|
|
835
|
+
loadPlayerIconBootstrap(),
|
|
836
|
+
readFile3(new URL("../dist/squisq-player.full.global.js", import.meta.url), "utf8")
|
|
837
|
+
]).then(([icons, player]) => `${icons}${player}`);
|
|
701
838
|
return fullPlayerBundlePromise;
|
|
702
839
|
}
|
|
703
840
|
async function convert(source, to, options = {}) {
|
|
@@ -751,7 +888,7 @@ async function captureDocFrames(doc, container, options) {
|
|
|
751
888
|
audio.set(seg.name, data);
|
|
752
889
|
}
|
|
753
890
|
}
|
|
754
|
-
const mediaSrcs = new Set(
|
|
891
|
+
const mediaSrcs = new Set(resolveMediaSchedule2(doc).map((clip) => clip.src));
|
|
755
892
|
for (const block of flattenBlocks2(doc.blocks)) {
|
|
756
893
|
for (const layer of block.layers ?? []) {
|
|
757
894
|
if (layer.type === "video") mediaSrcs.add(layer.content.src);
|
|
@@ -1002,7 +1139,7 @@ async function renderDocToGif(doc, container, options) {
|
|
|
1002
1139
|
options.signal?.throwIfAborted();
|
|
1003
1140
|
options.onProgress?.("done", 100);
|
|
1004
1141
|
options.signal?.throwIfAborted();
|
|
1005
|
-
const hasAudio = (doc.audio?.segments?.length ?? 0) > 0 ||
|
|
1142
|
+
const hasAudio = (doc.audio?.segments?.length ?? 0) > 0 || resolveMediaSchedule2(doc).some((clip) => clip.kind === "audio");
|
|
1006
1143
|
return {
|
|
1007
1144
|
duration: capture.totalDuration,
|
|
1008
1145
|
frameCount,
|
|
@@ -1017,8 +1154,30 @@ async function extractThumbnails(options) {
|
|
|
1017
1154
|
const { videoPath, outputDir, slug, sizes, force, signal } = options;
|
|
1018
1155
|
const { existsSync } = await import("fs");
|
|
1019
1156
|
const { rm: rm2 } = await import("fs/promises");
|
|
1020
|
-
const {
|
|
1157
|
+
const { isAbsolute: isAbsolute2, relative: relative2, resolve: resolve2, sep: sep2 } = await import("path");
|
|
1021
1158
|
signal?.throwIfAborted();
|
|
1159
|
+
if (typeof slug !== "string" || !/^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,126}[A-Za-z0-9])?$/.test(slug)) {
|
|
1160
|
+
throw new TypeError(
|
|
1161
|
+
"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."
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
for (const [index, thumb] of sizes.entries()) {
|
|
1165
|
+
if (!Number.isSafeInteger(thumb.width) || thumb.width <= 0) {
|
|
1166
|
+
throw new TypeError(`Thumbnail sizes[${index}].width must be a positive integer.`);
|
|
1167
|
+
}
|
|
1168
|
+
if (!Number.isSafeInteger(thumb.height) || thumb.height <= 0) {
|
|
1169
|
+
throw new TypeError(`Thumbnail sizes[${index}].height must be a positive integer.`);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
const resolvedOutputDir = resolve2(outputDir);
|
|
1173
|
+
const outputPaths = sizes.map((thumb) => {
|
|
1174
|
+
const outputPath = resolve2(resolvedOutputDir, `${slug}-${thumb.width}x${thumb.height}.jpg`);
|
|
1175
|
+
const rel = relative2(resolvedOutputDir, outputPath);
|
|
1176
|
+
if (rel === ".." || rel.startsWith(`..${sep2}`) || isAbsolute2(rel)) {
|
|
1177
|
+
throw new TypeError("Thumbnail output path must remain inside outputDir.");
|
|
1178
|
+
}
|
|
1179
|
+
return outputPath;
|
|
1180
|
+
});
|
|
1022
1181
|
const ffmpegPath = (await detectFfmpegDetailed(signal))?.path ?? null;
|
|
1023
1182
|
if (!ffmpegPath) {
|
|
1024
1183
|
throw new Error(
|
|
@@ -1027,9 +1186,9 @@ async function extractThumbnails(options) {
|
|
|
1027
1186
|
}
|
|
1028
1187
|
const generatedPaths = [];
|
|
1029
1188
|
try {
|
|
1030
|
-
for (const thumb of sizes) {
|
|
1189
|
+
for (const [index, thumb] of sizes.entries()) {
|
|
1031
1190
|
signal?.throwIfAborted();
|
|
1032
|
-
const outputPath =
|
|
1191
|
+
const outputPath = outputPaths[index];
|
|
1033
1192
|
if (!force && existsSync(outputPath)) continue;
|
|
1034
1193
|
try {
|
|
1035
1194
|
await runFfmpeg(
|
|
@@ -1056,6 +1215,7 @@ async function extractThumbnails(options) {
|
|
|
1056
1215
|
}
|
|
1057
1216
|
|
|
1058
1217
|
export {
|
|
1218
|
+
DocInputValidationError,
|
|
1059
1219
|
readInput,
|
|
1060
1220
|
getFfmpegVersion,
|
|
1061
1221
|
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-6OBPEBL4.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 {
|