@bendyline/squisq-cli 2.4.1 → 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 +6 -7
- package/THIRD_PARTY_LICENSES.txt +1 -1
- package/dist/{api-HBPJQ7MA.js → api-PBBQQMRP.js} +1 -1
- package/dist/api.js +247 -67
- package/dist/{chunk-WR2BZJ77.js → chunk-X3GFPKRJ.js} +250 -69
- package/dist/index.js +18 -14
- package/dist/squisq-player.full.global.js +4169 -0
- package/dist/squisq-player.global.js +52 -52
- package/package.json +4 -4
|
@@ -6,8 +6,8 @@ import {
|
|
|
6
6
|
|
|
7
7
|
// src/api.ts
|
|
8
8
|
import { readFile as readFile3 } from "fs/promises";
|
|
9
|
-
import { resolveMediaSchedule } from "@bendyline/squisq/schemas";
|
|
10
|
-
import { flattenBlocks } from "@bendyline/squisq/doc";
|
|
9
|
+
import { resolveMediaSchedule as resolveMediaSchedule2 } from "@bendyline/squisq/schemas";
|
|
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";
|
|
13
13
|
import {
|
|
@@ -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) => {
|
|
@@ -268,6 +268,27 @@ function createMediaBudget() {
|
|
|
268
268
|
};
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
// src/util/playerBundle.ts
|
|
272
|
+
import { flattenBlocks } from "@bendyline/squisq/doc";
|
|
273
|
+
function selectStandalonePlayerVariant(doc) {
|
|
274
|
+
for (const block of flattenBlocks(doc.blocks)) {
|
|
275
|
+
if (block.layers?.some((layer) => layer.type === "mermaid")) return "full";
|
|
276
|
+
if (containsMermaidFence(block.contents)) return "full";
|
|
277
|
+
}
|
|
278
|
+
return "light";
|
|
279
|
+
}
|
|
280
|
+
function containsMermaidFence(value) {
|
|
281
|
+
if (Array.isArray(value)) return value.some(containsMermaidFence);
|
|
282
|
+
if (!isRecord(value)) return false;
|
|
283
|
+
if (value.type === "code" && typeof value.lang === "string" && value.lang.trim().toLowerCase() === "mermaid") {
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
return containsMermaidFence(value.children);
|
|
287
|
+
}
|
|
288
|
+
function isRecord(value) {
|
|
289
|
+
return typeof value === "object" && value !== null;
|
|
290
|
+
}
|
|
291
|
+
|
|
271
292
|
// src/registry.ts
|
|
272
293
|
import { randomBytes } from "crypto";
|
|
273
294
|
import { readFile, rm } from "fs/promises";
|
|
@@ -309,7 +330,7 @@ function mp4Format() {
|
|
|
309
330
|
const animationsEnabled = typeof mp4Opts.animationsEnabled === "boolean" ? mp4Opts.animationsEnabled : MP4_DEFAULTS.animationsEnabled;
|
|
310
331
|
const outputPath = join(tmpdir(), `squisq-mp4-${randomBytes(8).toString("hex")}.mp4`);
|
|
311
332
|
try {
|
|
312
|
-
const { renderDocToMp4: renderDocToMp42 } = await import("./api-
|
|
333
|
+
const { renderDocToMp4: renderDocToMp42 } = await import("./api-PBBQQMRP.js");
|
|
313
334
|
await renderDocToMp42(input.doc, input.container, {
|
|
314
335
|
outputPath,
|
|
315
336
|
fps,
|
|
@@ -350,7 +371,7 @@ function gifFormat() {
|
|
|
350
371
|
const defaultHeight = portrait ? GIF_DEFAULTS.width : GIF_DEFAULTS.height;
|
|
351
372
|
const outputPath = join(tmpdir(), `squisq-gif-${randomBytes(8).toString("hex")}.gif`);
|
|
352
373
|
try {
|
|
353
|
-
const { renderDocToGif: renderDocToGif2 } = await import("./api-
|
|
374
|
+
const { renderDocToGif: renderDocToGif2 } = await import("./api-PBBQQMRP.js");
|
|
354
375
|
const result = await renderDocToGif2(input.doc, input.container, {
|
|
355
376
|
outputPath,
|
|
356
377
|
fps: typeof gifOpts.fps === "number" ? gifOpts.fps : GIF_DEFAULTS.fps,
|
|
@@ -394,13 +415,45 @@ function createCliRegistry() {
|
|
|
394
415
|
import { MemoryContentContainer as MemoryContentContainer2 } from "@bendyline/squisq/storage";
|
|
395
416
|
|
|
396
417
|
// src/util/readInput.ts
|
|
397
|
-
import { readFile as readFile2, readdir, stat } from "fs/promises";
|
|
398
|
-
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";
|
|
399
431
|
import { parseMarkdown, stringifyMarkdown } from "@bendyline/squisq/markdown";
|
|
400
432
|
import { markdownToDoc, resolveAudioMapping } from "@bendyline/squisq/doc";
|
|
433
|
+
import { resolveMediaSchedule, validateDocSchema } from "@bendyline/squisq/schemas";
|
|
401
434
|
import { MemoryContentContainer } from "@bendyline/squisq/storage";
|
|
402
435
|
import { zipToContainer } from "@bendyline/squisq-formats/container";
|
|
403
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
|
+
}
|
|
404
457
|
var MIME_TYPES = {
|
|
405
458
|
".md": "text/markdown",
|
|
406
459
|
".txt": "text/plain",
|
|
@@ -415,7 +468,11 @@ var MIME_TYPES = {
|
|
|
415
468
|
".wav": "audio/wav",
|
|
416
469
|
".ogg": "audio/ogg",
|
|
417
470
|
".mp4": "video/mp4",
|
|
418
|
-
".webm": "video/webm"
|
|
471
|
+
".webm": "video/webm",
|
|
472
|
+
".woff": "font/woff",
|
|
473
|
+
".woff2": "font/woff2",
|
|
474
|
+
".ttf": "font/ttf",
|
|
475
|
+
".otf": "font/otf"
|
|
419
476
|
};
|
|
420
477
|
var IMPORTER_EXTS = [".docx", ".pptx", ".pdf", ".xlsx", ".csv", ".html", ".htm"];
|
|
421
478
|
function mimeFromExt(filePath) {
|
|
@@ -441,8 +498,10 @@ async function readInput(inputPath, options) {
|
|
|
441
498
|
throwIfAborted(options?.signal);
|
|
442
499
|
const result = await readInputRaw(inputPath, options);
|
|
443
500
|
throwIfAborted(options?.signal);
|
|
501
|
+
assertValidDoc(result.doc, inputPath);
|
|
444
502
|
const doc = await resolveAudioMapping(result.doc, result.container);
|
|
445
503
|
throwIfAborted(options?.signal);
|
|
504
|
+
assertValidDoc(doc, inputPath);
|
|
446
505
|
return doc === result.doc ? result : { ...result, doc };
|
|
447
506
|
}
|
|
448
507
|
async function readInputRaw(inputPath, options) {
|
|
@@ -495,69 +554,160 @@ async function readUtf8File(filePath, signal) {
|
|
|
495
554
|
}
|
|
496
555
|
async function readMarkdownFile(filePath, signal) {
|
|
497
556
|
const content = await readUtf8File(filePath, signal);
|
|
498
|
-
const container = new MemoryContentContainer();
|
|
499
|
-
await container.writeDocument(content);
|
|
500
|
-
throwIfAborted(signal);
|
|
501
557
|
const markdownDoc = parseMarkdown(content);
|
|
502
|
-
|
|
558
|
+
const doc = markdownToDoc(markdownDoc);
|
|
559
|
+
const container = await buildBareMarkdownContainer(filePath, content, doc, signal);
|
|
560
|
+
return { doc, container, markdownDoc, sourceFormat: "md" };
|
|
503
561
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
fail(`expected a JSON object, got ${Array.isArray(parsed) ? "an array" : typeof parsed}`);
|
|
517
|
-
}
|
|
518
|
-
const doc = parsed;
|
|
519
|
-
if (!Array.isArray(doc.blocks)) {
|
|
520
|
-
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
|
+
}
|
|
521
574
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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`);
|
|
525
579
|
}
|
|
526
580
|
}
|
|
527
|
-
|
|
528
|
-
|
|
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;
|
|
529
630
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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;
|
|
533
645
|
}
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
fail(`"audio.segments[${index}]" must be an object`);
|
|
540
|
-
}
|
|
541
|
-
if (!isFiniteNumber(segment.duration)) {
|
|
542
|
-
fail(
|
|
543
|
-
`"audio.segments[${index}].duration" must be a finite number, got ${describe(segment.duration)}`
|
|
544
|
-
);
|
|
545
|
-
}
|
|
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);
|
|
546
651
|
}
|
|
547
652
|
}
|
|
548
|
-
}
|
|
549
|
-
return {
|
|
550
|
-
...doc,
|
|
551
|
-
audio: doc.audio ?? { segments: [] }
|
|
552
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");
|
|
693
|
+
}
|
|
694
|
+
function formatMiB(bytes) {
|
|
695
|
+
return `${Math.round(bytes / (1024 * 1024))} MB`;
|
|
553
696
|
}
|
|
554
|
-
function
|
|
555
|
-
|
|
697
|
+
function assertValidDoc(value, source) {
|
|
698
|
+
const issues = validateDocSchema(value);
|
|
699
|
+
if (issues.length > 0) throw new DocInputValidationError(source, issues);
|
|
556
700
|
}
|
|
557
|
-
function
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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;
|
|
561
711
|
}
|
|
562
712
|
async function readDocJsonFile(filePath, signal) {
|
|
563
713
|
const content = await readUtf8File(filePath, signal);
|
|
@@ -664,6 +814,7 @@ function throwIfAborted(signal) {
|
|
|
664
814
|
// src/api.ts
|
|
665
815
|
import { ConversionError } from "@bendyline/squisq-formats";
|
|
666
816
|
var playerBundlePromise;
|
|
817
|
+
var fullPlayerBundlePromise;
|
|
667
818
|
function loadPlayerBundle() {
|
|
668
819
|
playerBundlePromise ??= readFile3(
|
|
669
820
|
new URL("../dist/squisq-player.global.js", import.meta.url),
|
|
@@ -671,6 +822,13 @@ function loadPlayerBundle() {
|
|
|
671
822
|
);
|
|
672
823
|
return playerBundlePromise;
|
|
673
824
|
}
|
|
825
|
+
function loadFullPlayerBundle() {
|
|
826
|
+
fullPlayerBundlePromise ??= readFile3(
|
|
827
|
+
new URL("../dist/squisq-player.full.global.js", import.meta.url),
|
|
828
|
+
"utf8"
|
|
829
|
+
);
|
|
830
|
+
return fullPlayerBundlePromise;
|
|
831
|
+
}
|
|
674
832
|
async function convert(source, to, options = {}) {
|
|
675
833
|
return formatsConvert(source, to, {
|
|
676
834
|
registry: createCliRegistry(),
|
|
@@ -722,8 +880,8 @@ async function captureDocFrames(doc, container, options) {
|
|
|
722
880
|
audio.set(seg.name, data);
|
|
723
881
|
}
|
|
724
882
|
}
|
|
725
|
-
const mediaSrcs = new Set(
|
|
726
|
-
for (const block of
|
|
883
|
+
const mediaSrcs = new Set(resolveMediaSchedule2(doc).map((clip) => clip.src));
|
|
884
|
+
for (const block of flattenBlocks2(doc.blocks)) {
|
|
727
885
|
for (const layer of block.layers ?? []) {
|
|
728
886
|
if (layer.type === "video") mediaSrcs.add(layer.content.src);
|
|
729
887
|
}
|
|
@@ -740,7 +898,7 @@ async function captureDocFrames(doc, container, options) {
|
|
|
740
898
|
}
|
|
741
899
|
onProgress?.("generating render HTML", 10);
|
|
742
900
|
signal?.throwIfAborted();
|
|
743
|
-
const playerBundle = await loadPlayerBundle();
|
|
901
|
+
const playerBundle = await (selectStandalonePlayerVariant(doc) === "full" ? loadFullPlayerBundle() : loadPlayerBundle());
|
|
744
902
|
signal?.throwIfAborted();
|
|
745
903
|
const renderHtml = generateRenderHtml(doc, {
|
|
746
904
|
playerScript: playerBundle,
|
|
@@ -973,7 +1131,7 @@ async function renderDocToGif(doc, container, options) {
|
|
|
973
1131
|
options.signal?.throwIfAborted();
|
|
974
1132
|
options.onProgress?.("done", 100);
|
|
975
1133
|
options.signal?.throwIfAborted();
|
|
976
|
-
const hasAudio = (doc.audio?.segments?.length ?? 0) > 0 ||
|
|
1134
|
+
const hasAudio = (doc.audio?.segments?.length ?? 0) > 0 || resolveMediaSchedule2(doc).some((clip) => clip.kind === "audio");
|
|
977
1135
|
return {
|
|
978
1136
|
duration: capture.totalDuration,
|
|
979
1137
|
frameCount,
|
|
@@ -988,8 +1146,30 @@ async function extractThumbnails(options) {
|
|
|
988
1146
|
const { videoPath, outputDir, slug, sizes, force, signal } = options;
|
|
989
1147
|
const { existsSync } = await import("fs");
|
|
990
1148
|
const { rm: rm2 } = await import("fs/promises");
|
|
991
|
-
const {
|
|
1149
|
+
const { isAbsolute: isAbsolute2, relative: relative2, resolve: resolve2, sep: sep2 } = await import("path");
|
|
992
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
|
+
});
|
|
993
1173
|
const ffmpegPath = (await detectFfmpegDetailed(signal))?.path ?? null;
|
|
994
1174
|
if (!ffmpegPath) {
|
|
995
1175
|
throw new Error(
|
|
@@ -998,9 +1178,9 @@ async function extractThumbnails(options) {
|
|
|
998
1178
|
}
|
|
999
1179
|
const generatedPaths = [];
|
|
1000
1180
|
try {
|
|
1001
|
-
for (const thumb of sizes) {
|
|
1181
|
+
for (const [index, thumb] of sizes.entries()) {
|
|
1002
1182
|
signal?.throwIfAborted();
|
|
1003
|
-
const outputPath =
|
|
1183
|
+
const outputPath = outputPaths[index];
|
|
1004
1184
|
if (!force && existsSync(outputPath)) continue;
|
|
1005
1185
|
try {
|
|
1006
1186
|
await runFfmpeg(
|
|
@@ -1027,6 +1207,7 @@ async function extractThumbnails(options) {
|
|
|
1027
1207
|
}
|
|
1028
1208
|
|
|
1029
1209
|
export {
|
|
1210
|
+
DocInputValidationError,
|
|
1030
1211
|
readInput,
|
|
1031
1212
|
getFfmpegVersion,
|
|
1032
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 {
|