@bendyline/squisq-cli 1.0.0 → 1.1.0
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 +121 -0
- package/dist/__tests__/convert.test.d.ts +7 -0
- package/dist/__tests__/convert.test.d.ts.map +1 -0
- package/dist/__tests__/convert.test.js +127 -0
- package/dist/__tests__/convert.test.js.map +1 -0
- package/dist/__tests__/readInput.test.d.ts +5 -0
- package/dist/__tests__/readInput.test.d.ts.map +1 -0
- package/dist/__tests__/readInput.test.js +120 -0
- package/dist/__tests__/readInput.test.js.map +1 -0
- package/dist/api.d.ts +106 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +324 -0
- package/dist/api.js.map +1 -0
- package/dist/commands/convert.d.ts +15 -0
- package/dist/commands/convert.d.ts.map +1 -0
- package/dist/commands/convert.js +186 -0
- package/dist/commands/convert.js.map +1 -0
- package/dist/commands/video.d.ts +12 -0
- package/dist/commands/video.d.ts.map +1 -0
- package/dist/commands/video.js +122 -0
- package/dist/commands/video.js.map +1 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -425
- package/dist/index.js.map +1 -1
- package/dist/nativeEncoder-EXDP2O5B.js +0 -0
- package/dist/nativeEncoder-MWHOONST.js +88 -0
- package/dist/nativeEncoder-MWHOONST.js.map +1 -0
- package/dist/util/detectFfmpeg.d.ts +15 -0
- package/dist/util/detectFfmpeg.d.ts.map +1 -0
- package/dist/util/detectFfmpeg.js +29 -0
- package/dist/util/detectFfmpeg.js.map +1 -0
- package/dist/util/nativeEncoder.d.ts +27 -0
- package/dist/util/nativeEncoder.d.ts.map +1 -0
- package/dist/util/nativeEncoder.js +106 -0
- package/dist/util/nativeEncoder.js.map +1 -0
- package/dist/util/readInput.d.ts +31 -0
- package/dist/util/readInput.d.ts.map +1 -0
- package/dist/util/readInput.js +142 -0
- package/dist/util/readInput.js.map +1 -0
- package/package.json +17 -5
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* video command
|
|
3
|
+
*
|
|
4
|
+
* Renders a squisq document to MP4 video by delegating to the
|
|
5
|
+
* programmatic renderDocToMp4 API.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* squisq video <input> [-o output.mp4] [--fps 30] [--quality normal] [--orientation landscape]
|
|
9
|
+
*/
|
|
10
|
+
import { mkdir } from 'node:fs/promises';
|
|
11
|
+
import { dirname, basename, extname, resolve } from 'node:path';
|
|
12
|
+
import { readInput } from '../util/readInput.js';
|
|
13
|
+
import { renderDocToMp4 } from '../api.js';
|
|
14
|
+
const VALID_QUALITIES = ['draft', 'normal', 'high'];
|
|
15
|
+
const VALID_ORIENTATIONS = ['landscape', 'portrait'];
|
|
16
|
+
const VALID_CAPTIONS = ['off', 'standard', 'social'];
|
|
17
|
+
export function registerVideoCommand(program) {
|
|
18
|
+
program
|
|
19
|
+
.command('video')
|
|
20
|
+
.description('Render a squisq document to MP4 video')
|
|
21
|
+
.argument('<input>', 'Path to .md file, .zip/.dbk container, or folder')
|
|
22
|
+
.argument('[output]', 'Output MP4 path (default: <input>.mp4)')
|
|
23
|
+
.option('-o, --output <path>', 'Output MP4 path (default: <input>.mp4)')
|
|
24
|
+
.option('--fps <number>', 'Frames per second (default: 30)', '30')
|
|
25
|
+
.option('--quality <level>', `Encoding quality: ${VALID_QUALITIES.join(', ')} (default: normal)`, 'normal')
|
|
26
|
+
.option('--orientation <orient>', `Video orientation: ${VALID_ORIENTATIONS.join(', ')} (default: landscape)`, 'landscape')
|
|
27
|
+
.option('--captions <style>', `Caption style: ${VALID_CAPTIONS.join(', ')} (default: off)`, 'off')
|
|
28
|
+
.option('--width <pixels>', 'Override video width')
|
|
29
|
+
.option('--height <pixels>', 'Override video height')
|
|
30
|
+
.action(async (inputPath, outputArg, opts) => {
|
|
31
|
+
try {
|
|
32
|
+
// Positional output arg takes precedence, then -o flag
|
|
33
|
+
if (outputArg && !opts.output) {
|
|
34
|
+
opts.output = outputArg;
|
|
35
|
+
}
|
|
36
|
+
await runVideo(inputPath, opts);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
40
|
+
console.error(`Error: ${message}`);
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
async function runVideo(inputPath, opts) {
|
|
46
|
+
const resolvedInput = resolve(inputPath);
|
|
47
|
+
// Validate options
|
|
48
|
+
const fps = parseInt(opts.fps ?? '30', 10);
|
|
49
|
+
if (isNaN(fps) || fps < 1 || fps > 120) {
|
|
50
|
+
throw new Error('FPS must be a number between 1 and 120');
|
|
51
|
+
}
|
|
52
|
+
const quality = opts.quality ?? 'normal';
|
|
53
|
+
if (!VALID_QUALITIES.includes(quality)) {
|
|
54
|
+
throw new Error(`Invalid quality "${quality}". Valid: ${VALID_QUALITIES.join(', ')}`);
|
|
55
|
+
}
|
|
56
|
+
const orientation = opts.orientation ?? 'landscape';
|
|
57
|
+
if (!VALID_ORIENTATIONS.includes(orientation)) {
|
|
58
|
+
throw new Error(`Invalid orientation "${orientation}". Valid: ${VALID_ORIENTATIONS.join(', ')}`);
|
|
59
|
+
}
|
|
60
|
+
const captions = opts.captions ?? 'off';
|
|
61
|
+
if (!VALID_CAPTIONS.includes(captions)) {
|
|
62
|
+
throw new Error(`Invalid captions "${captions}". Valid: ${VALID_CAPTIONS.join(', ')}`);
|
|
63
|
+
}
|
|
64
|
+
const captionStyle = captions === 'off' ? undefined : captions;
|
|
65
|
+
// Determine output path
|
|
66
|
+
const inputBasename = basename(resolvedInput);
|
|
67
|
+
const inputExt = extname(inputBasename);
|
|
68
|
+
const baseName = inputExt ? inputBasename.slice(0, -inputExt.length) : inputBasename;
|
|
69
|
+
const outputPath = opts.output
|
|
70
|
+
? resolve(opts.output)
|
|
71
|
+
: resolve(dirname(resolvedInput), `${baseName}.mp4`);
|
|
72
|
+
// Ensure output directory exists
|
|
73
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
74
|
+
// ── Step 1: Read input ──────────────────────────────────────────
|
|
75
|
+
console.error(`Reading: ${resolvedInput}`);
|
|
76
|
+
const result = await readInput(resolvedInput);
|
|
77
|
+
const { container } = result;
|
|
78
|
+
// ── Step 2: Get or parse Doc ────────────────────────────────────
|
|
79
|
+
let doc;
|
|
80
|
+
if (result.doc) {
|
|
81
|
+
console.error('Using pre-built Doc JSON');
|
|
82
|
+
doc = result.doc;
|
|
83
|
+
}
|
|
84
|
+
else if (result.markdownDoc) {
|
|
85
|
+
const { markdownToDoc } = await import('@bendyline/squisq/doc');
|
|
86
|
+
doc = markdownToDoc(result.markdownDoc);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
throw new Error('No document found in input');
|
|
90
|
+
}
|
|
91
|
+
console.error(`Rendering: ${fps} fps, quality: ${quality}, orientation: ${orientation}, captions: ${captions}`);
|
|
92
|
+
// ── Step 3: Render via programmatic API ─────────────────────────
|
|
93
|
+
const result2 = await renderDocToMp4(doc, container, {
|
|
94
|
+
outputPath,
|
|
95
|
+
fps,
|
|
96
|
+
quality: quality,
|
|
97
|
+
orientation: orientation,
|
|
98
|
+
width: opts.width ? parseInt(opts.width, 10) : undefined,
|
|
99
|
+
height: opts.height ? parseInt(opts.height, 10) : undefined,
|
|
100
|
+
captionStyle,
|
|
101
|
+
coverPreRoll: 2,
|
|
102
|
+
onProgress: (phase, percent) => {
|
|
103
|
+
writeProgress(phase, percent, 100);
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
clearProgress();
|
|
107
|
+
console.error(` ✓ ${outputPath} (${result2.duration.toFixed(1)}s, ${result2.frameCount} frames)`);
|
|
108
|
+
console.error('Done.');
|
|
109
|
+
}
|
|
110
|
+
// ── Progress helpers ──────────────────────────────────────────────
|
|
111
|
+
const BAR_WIDTH = 30;
|
|
112
|
+
function writeProgress(label, current, total, detail) {
|
|
113
|
+
const pct = Math.min(100, Math.round((current / total) * 100));
|
|
114
|
+
const filled = Math.round((pct / 100) * BAR_WIDTH);
|
|
115
|
+
const bar = '█'.repeat(filled) + '░'.repeat(BAR_WIDTH - filled);
|
|
116
|
+
const suffix = detail ? ` (${detail})` : '';
|
|
117
|
+
process.stderr.write(`\r ${label}: ${bar} ${pct}%${suffix} `);
|
|
118
|
+
}
|
|
119
|
+
function clearProgress() {
|
|
120
|
+
process.stderr.write('\r' + ' '.repeat(80) + '\r');
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=video.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"video.js","sourceRoot":"","sources":["../../src/commands/video.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* squisq CLI
|
|
3
|
+
*
|
|
4
|
+
* Command-line tool for converting and processing Squisq documents.
|
|
5
|
+
* Designed for easy addition of future subcommands (e.g., import).
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* squisq convert <input> [options]
|
|
9
|
+
* squisq --help
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
|
package/dist/index.js
CHANGED
|
@@ -1,428 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
".jpeg": "image/jpeg",
|
|
22
|
-
".png": "image/png",
|
|
23
|
-
".gif": "image/gif",
|
|
24
|
-
".webp": "image/webp",
|
|
25
|
-
".svg": "image/svg+xml",
|
|
26
|
-
".mp3": "audio/mpeg",
|
|
27
|
-
".wav": "audio/wav",
|
|
28
|
-
".ogg": "audio/ogg",
|
|
29
|
-
".mp4": "video/mp4",
|
|
30
|
-
".webm": "video/webm"
|
|
31
|
-
};
|
|
32
|
-
function mimeFromExt(filePath) {
|
|
33
|
-
return MIME_TYPES[extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
34
|
-
}
|
|
35
|
-
async function walkDir(root, prefix = "") {
|
|
36
|
-
const entries = await readdir(root, { withFileTypes: true });
|
|
37
|
-
const paths = [];
|
|
38
|
-
for (const entry of entries) {
|
|
39
|
-
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
40
|
-
if (entry.isDirectory()) {
|
|
41
|
-
paths.push(...await walkDir(join(root, entry.name), relPath));
|
|
42
|
-
} else if (entry.isFile()) {
|
|
43
|
-
paths.push(relPath);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return paths;
|
|
47
|
-
}
|
|
48
|
-
async function readInput(inputPath) {
|
|
49
|
-
const info = await stat(inputPath);
|
|
50
|
-
if (info.isDirectory()) {
|
|
51
|
-
return readFolder(inputPath);
|
|
52
|
-
}
|
|
53
|
-
const ext = extname(inputPath).toLowerCase();
|
|
54
|
-
if (ext === ".zip" || ext === ".dbk") {
|
|
55
|
-
return readContainer(inputPath);
|
|
56
|
-
}
|
|
57
|
-
return readMarkdownFile(inputPath);
|
|
58
|
-
}
|
|
59
|
-
async function readMarkdownFile(filePath) {
|
|
60
|
-
const content = await readFile(filePath, "utf-8");
|
|
61
|
-
const container = new MemoryContentContainer();
|
|
62
|
-
await container.writeDocument(content);
|
|
63
|
-
const markdownDoc = parseMarkdown(content);
|
|
64
|
-
return { container, markdownDoc };
|
|
65
|
-
}
|
|
66
|
-
async function readContainer(filePath) {
|
|
67
|
-
const data = await readFile(filePath);
|
|
68
|
-
const container = await zipToContainer(
|
|
69
|
-
data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength)
|
|
70
|
-
);
|
|
71
|
-
const markdown = await container.readDocument();
|
|
72
|
-
if (!markdown) {
|
|
73
|
-
throw new Error(`No markdown document found in container: ${filePath}`);
|
|
74
|
-
}
|
|
75
|
-
const markdownDoc = parseMarkdown(markdown);
|
|
76
|
-
return { container, markdownDoc };
|
|
77
|
-
}
|
|
78
|
-
async function readFolder(dirPath) {
|
|
79
|
-
const container = new MemoryContentContainer();
|
|
80
|
-
const files = await walkDir(dirPath);
|
|
81
|
-
for (const relPath of files) {
|
|
82
|
-
const absPath = join(dirPath, relPath);
|
|
83
|
-
const data = await readFile(absPath);
|
|
84
|
-
await container.writeFile(
|
|
85
|
-
relPath,
|
|
86
|
-
new Uint8Array(data.buffer, data.byteOffset, data.byteLength),
|
|
87
|
-
mimeFromExt(relPath)
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
const markdown = await container.readDocument();
|
|
91
|
-
if (!markdown) {
|
|
92
|
-
throw new Error(`No markdown document found in folder: ${dirPath}`);
|
|
93
|
-
}
|
|
94
|
-
const markdownDoc = parseMarkdown(markdown);
|
|
95
|
-
return { container, markdownDoc };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// src/commands/convert.ts
|
|
99
|
-
var ALL_FORMATS = ["docx", "pdf", "html", "dbk"];
|
|
100
|
-
function parseFormats(value) {
|
|
101
|
-
const requested = value.split(",").map((s) => s.trim().toLowerCase());
|
|
102
|
-
const valid = [];
|
|
103
|
-
for (const r of requested) {
|
|
104
|
-
if (ALL_FORMATS.includes(r)) {
|
|
105
|
-
valid.push(r);
|
|
106
|
-
} else {
|
|
107
|
-
console.warn(`Unknown format "${r}" \u2014 skipping. Valid: ${ALL_FORMATS.join(", ")}`);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
if (valid.length === 0) {
|
|
111
|
-
throw new Error(`No valid formats specified. Valid: ${ALL_FORMATS.join(", ")}`);
|
|
112
|
-
}
|
|
113
|
-
return valid;
|
|
114
|
-
}
|
|
115
|
-
function registerConvertCommand(program2) {
|
|
116
|
-
program2.command("convert").description("Convert a markdown document to DOCX, PDF, HTML, and DBK container formats").argument("<input>", "Path to .md file, .zip/.dbk container, or folder").option("-o, --output-dir <dir>", "Output directory (default: same as input)").option(
|
|
117
|
-
"-f, --formats <list>",
|
|
118
|
-
`Comma-separated formats to produce (default: all). Valid: ${ALL_FORMATS.join(", ")}`
|
|
119
|
-
).action(async (inputPath, opts) => {
|
|
120
|
-
try {
|
|
121
|
-
await runConvert(inputPath, opts);
|
|
122
|
-
} catch (err) {
|
|
123
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
124
|
-
console.error(`Error: ${message}`);
|
|
125
|
-
process.exitCode = 1;
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
async function runConvert(inputPath, opts) {
|
|
130
|
-
const resolvedInput = resolve(inputPath);
|
|
131
|
-
const formats = opts.formats ? parseFormats(opts.formats) : [...ALL_FORMATS];
|
|
132
|
-
const outputDir = opts.outputDir ? resolve(opts.outputDir) : dirname(resolvedInput);
|
|
133
|
-
const inputBasename = basename(resolvedInput);
|
|
134
|
-
const inputExt = extname2(inputBasename);
|
|
135
|
-
const baseName = inputExt ? inputBasename.slice(0, -inputExt.length) : inputBasename;
|
|
136
|
-
await mkdir(outputDir, { recursive: true });
|
|
137
|
-
console.error(`Reading: ${resolvedInput}`);
|
|
138
|
-
const { container, markdownDoc } = await readInput(resolvedInput);
|
|
139
|
-
for (const format of formats) {
|
|
140
|
-
const outPath = join2(outputDir, `${baseName}.${format}`);
|
|
141
|
-
switch (format) {
|
|
142
|
-
case "docx": {
|
|
143
|
-
const { markdownDocToDocx } = await import("@bendyline/squisq-formats/docx");
|
|
144
|
-
const buf = await markdownDocToDocx(markdownDoc);
|
|
145
|
-
await writeFile(outPath, Buffer.from(buf));
|
|
146
|
-
break;
|
|
147
|
-
}
|
|
148
|
-
case "pdf": {
|
|
149
|
-
const { markdownDocToPdf } = await import("@bendyline/squisq-formats/pdf");
|
|
150
|
-
const buf = await markdownDocToPdf(markdownDoc);
|
|
151
|
-
await writeFile(outPath, Buffer.from(buf));
|
|
152
|
-
break;
|
|
153
|
-
}
|
|
154
|
-
case "html": {
|
|
155
|
-
const { markdownToDoc } = await import("@bendyline/squisq/doc");
|
|
156
|
-
const { docToHtml, collectImagePaths } = await import("@bendyline/squisq-formats/html");
|
|
157
|
-
const { PLAYER_BUNDLE } = await import("@bendyline/squisq-react/standalone-source");
|
|
158
|
-
const doc = markdownToDoc(markdownDoc);
|
|
159
|
-
const imagePaths = collectImagePaths(doc);
|
|
160
|
-
const images = /* @__PURE__ */ new Map();
|
|
161
|
-
for (const imgPath of imagePaths) {
|
|
162
|
-
const data = await container.readFile(imgPath);
|
|
163
|
-
if (data) {
|
|
164
|
-
images.set(imgPath, data);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
const html = docToHtml(doc, {
|
|
168
|
-
playerScript: PLAYER_BUNDLE,
|
|
169
|
-
images,
|
|
170
|
-
title: baseName,
|
|
171
|
-
mode: "static"
|
|
172
|
-
});
|
|
173
|
-
await writeFile(outPath, html, "utf-8");
|
|
174
|
-
break;
|
|
175
|
-
}
|
|
176
|
-
case "dbk": {
|
|
177
|
-
const { containerToZip } = await import("@bendyline/squisq-formats/container");
|
|
178
|
-
const blob = await containerToZip(container);
|
|
179
|
-
const buf = await blob.arrayBuffer();
|
|
180
|
-
await writeFile(outPath, Buffer.from(buf));
|
|
181
|
-
break;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
console.error(` \u2713 ${outPath}`);
|
|
185
|
-
}
|
|
186
|
-
console.error("Done.");
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// src/commands/video.ts
|
|
190
|
-
import { mkdir as mkdir2 } from "fs/promises";
|
|
191
|
-
import { dirname as dirname2, basename as basename2, extname as extname3, resolve as resolve2 } from "path";
|
|
192
|
-
|
|
193
|
-
// src/util/detectFfmpeg.ts
|
|
194
|
-
import { execFile } from "child_process";
|
|
195
|
-
async function detectFfmpeg() {
|
|
196
|
-
const command = process.platform === "win32" ? "where" : "which";
|
|
197
|
-
return new Promise((resolve3) => {
|
|
198
|
-
execFile(command, ["ffmpeg"], { timeout: 5e3 }, (err, stdout) => {
|
|
199
|
-
if (err || !stdout.trim()) {
|
|
200
|
-
resolve3(null);
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
const path = stdout.trim().split("\n")[0].trim();
|
|
204
|
-
resolve3(path);
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// src/commands/video.ts
|
|
210
|
-
var VALID_QUALITIES = ["draft", "normal", "high"];
|
|
211
|
-
var VALID_ORIENTATIONS = ["landscape", "portrait"];
|
|
212
|
-
var VALID_CAPTIONS = ["off", "standard", "social"];
|
|
213
|
-
function registerVideoCommand(program2) {
|
|
214
|
-
program2.command("video").description("Render a squisq document to MP4 video").argument("<input>", "Path to .md file, .zip/.dbk container, or folder").argument("[output]", "Output MP4 path (default: <input>.mp4)").option("-o, --output <path>", "Output MP4 path (default: <input>.mp4)").option("--fps <number>", "Frames per second (default: 30)", "30").option(
|
|
215
|
-
"--quality <level>",
|
|
216
|
-
`Encoding quality: ${VALID_QUALITIES.join(", ")} (default: normal)`,
|
|
217
|
-
"normal"
|
|
218
|
-
).option(
|
|
219
|
-
"--orientation <orient>",
|
|
220
|
-
`Video orientation: ${VALID_ORIENTATIONS.join(", ")} (default: landscape)`,
|
|
221
|
-
"landscape"
|
|
222
|
-
).option(
|
|
223
|
-
"--captions <style>",
|
|
224
|
-
`Caption style: ${VALID_CAPTIONS.join(", ")} (default: off)`,
|
|
225
|
-
"off"
|
|
226
|
-
).option("--width <pixels>", "Override video width").option("--height <pixels>", "Override video height").action(async (inputPath, outputArg, opts) => {
|
|
227
|
-
try {
|
|
228
|
-
if (outputArg && !opts.output) {
|
|
229
|
-
opts.output = outputArg;
|
|
230
|
-
}
|
|
231
|
-
await runVideo(inputPath, opts);
|
|
232
|
-
} catch (err) {
|
|
233
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
234
|
-
console.error(`Error: ${message}`);
|
|
235
|
-
process.exitCode = 1;
|
|
236
|
-
}
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
async function runVideo(inputPath, opts) {
|
|
240
|
-
const resolvedInput = resolve2(inputPath);
|
|
241
|
-
const fps = parseInt(opts.fps ?? "30", 10);
|
|
242
|
-
if (isNaN(fps) || fps < 1 || fps > 120) {
|
|
243
|
-
throw new Error("FPS must be a number between 1 and 120");
|
|
244
|
-
}
|
|
245
|
-
const quality = opts.quality ?? "normal";
|
|
246
|
-
if (!VALID_QUALITIES.includes(quality)) {
|
|
247
|
-
throw new Error(`Invalid quality "${quality}". Valid: ${VALID_QUALITIES.join(", ")}`);
|
|
248
|
-
}
|
|
249
|
-
const orientation = opts.orientation ?? "landscape";
|
|
250
|
-
if (!VALID_ORIENTATIONS.includes(orientation)) {
|
|
251
|
-
throw new Error(
|
|
252
|
-
`Invalid orientation "${orientation}". Valid: ${VALID_ORIENTATIONS.join(", ")}`
|
|
253
|
-
);
|
|
254
|
-
}
|
|
255
|
-
const captions = opts.captions ?? "off";
|
|
256
|
-
if (!VALID_CAPTIONS.includes(captions)) {
|
|
257
|
-
throw new Error(`Invalid captions "${captions}". Valid: ${VALID_CAPTIONS.join(", ")}`);
|
|
258
|
-
}
|
|
259
|
-
const captionStyle = captions === "off" ? void 0 : captions;
|
|
260
|
-
const inputBasename = basename2(resolvedInput);
|
|
261
|
-
const inputExt = extname3(inputBasename);
|
|
262
|
-
const baseName = inputExt ? inputBasename.slice(0, -inputExt.length) : inputBasename;
|
|
263
|
-
const outputPath = opts.output ? resolve2(opts.output) : resolve2(dirname2(resolvedInput), `${baseName}.mp4`);
|
|
264
|
-
await mkdir2(dirname2(outputPath), { recursive: true });
|
|
265
|
-
console.error(`Reading: ${resolvedInput}`);
|
|
266
|
-
const { container, markdownDoc } = await readInput(resolvedInput);
|
|
267
|
-
const { markdownToDoc } = await import("@bendyline/squisq/doc");
|
|
268
|
-
const doc = markdownToDoc(markdownDoc);
|
|
269
|
-
const { collectImagePaths } = await import("@bendyline/squisq-formats/html");
|
|
270
|
-
const imagePaths = collectImagePaths(doc);
|
|
271
|
-
const images = /* @__PURE__ */ new Map();
|
|
272
|
-
for (const imgPath of imagePaths) {
|
|
273
|
-
const data = await container.readFile(imgPath);
|
|
274
|
-
if (data) {
|
|
275
|
-
images.set(imgPath, data);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
const audio = /* @__PURE__ */ new Map();
|
|
279
|
-
let concatenatedAudio = null;
|
|
280
|
-
if (doc.audio?.segments?.length) {
|
|
281
|
-
for (const seg of doc.audio.segments) {
|
|
282
|
-
const data = await container.readFile(seg.src);
|
|
283
|
-
if (data) {
|
|
284
|
-
audio.set(seg.src, data);
|
|
285
|
-
audio.set(seg.name, data);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
if (audio.size > 0) {
|
|
289
|
-
const firstSeg = doc.audio.segments[0];
|
|
290
|
-
const firstData = await container.readFile(firstSeg.src);
|
|
291
|
-
if (firstData) {
|
|
292
|
-
concatenatedAudio = new Uint8Array(firstData);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
const { generateRenderHtml } = await import("@bendyline/squisq-video");
|
|
297
|
-
const { PLAYER_BUNDLE } = await import("@bendyline/squisq-react/standalone-source");
|
|
298
|
-
const { resolveDimensions } = await import("@bendyline/squisq-video");
|
|
299
|
-
const dimensions = resolveDimensions({
|
|
300
|
-
orientation,
|
|
301
|
-
width: opts.width ? parseInt(opts.width, 10) : void 0,
|
|
302
|
-
height: opts.height ? parseInt(opts.height, 10) : void 0
|
|
303
|
-
});
|
|
304
|
-
const renderHtml = generateRenderHtml(doc, {
|
|
305
|
-
playerScript: PLAYER_BUNDLE,
|
|
306
|
-
images,
|
|
307
|
-
audio: audio.size > 0 ? audio : void 0,
|
|
308
|
-
width: dimensions.width,
|
|
309
|
-
height: dimensions.height,
|
|
310
|
-
captionStyle
|
|
311
|
-
});
|
|
312
|
-
console.error(
|
|
313
|
-
`Viewport: ${dimensions.width}x${dimensions.height}, ${fps} fps, quality: ${quality}, captions: ${captions}`
|
|
314
|
-
);
|
|
315
|
-
const { chromium } = await import("playwright-core");
|
|
316
|
-
const browser = await chromium.launch({ headless: true });
|
|
317
|
-
const page = await browser.newPage({
|
|
318
|
-
viewport: { width: dimensions.width, height: dimensions.height }
|
|
319
|
-
});
|
|
320
|
-
const pageErrors = [];
|
|
321
|
-
page.on("pageerror", (err) => pageErrors.push(err.message));
|
|
322
|
-
page.on("console", (msg) => {
|
|
323
|
-
if (msg.type() === "error") pageErrors.push(msg.text());
|
|
324
|
-
});
|
|
325
|
-
await page.setContent(renderHtml, { waitUntil: "load" });
|
|
326
|
-
await page.waitForTimeout(500);
|
|
327
|
-
try {
|
|
328
|
-
await page.waitForFunction(
|
|
329
|
-
() => typeof window.getDuration === "function",
|
|
330
|
-
{ timeout: 15e3 }
|
|
331
|
-
);
|
|
332
|
-
} catch {
|
|
333
|
-
await browser.close();
|
|
334
|
-
const errorDetail = pageErrors.length ? `
|
|
335
|
-
Page errors:
|
|
336
|
-
${pageErrors.join("\n ")}` : "\nNo page errors captured \u2014 the player may have failed to mount.";
|
|
337
|
-
throw new Error(`Render API did not initialize within 15 seconds.${errorDetail}`);
|
|
338
|
-
}
|
|
339
|
-
const duration = await page.evaluate(() => {
|
|
340
|
-
return window.getDuration();
|
|
341
|
-
});
|
|
342
|
-
if (duration <= 0) {
|
|
343
|
-
await browser.close();
|
|
344
|
-
throw new Error("Document has zero duration \u2014 nothing to render");
|
|
345
|
-
}
|
|
346
|
-
const totalFrames = Math.ceil(duration * fps);
|
|
347
|
-
console.error(`Duration: ${duration.toFixed(1)}s, ${totalFrames} frames to capture`);
|
|
348
|
-
const frames = [];
|
|
349
|
-
const frameInterval = 1 / fps;
|
|
350
|
-
const hasCover = await page.evaluate(() => {
|
|
351
|
-
const w = window;
|
|
352
|
-
return typeof w.hasCoverBlock === "function" ? w.hasCoverBlock() : false;
|
|
353
|
-
});
|
|
354
|
-
if (hasCover) {
|
|
355
|
-
const coverFrameCount = 2 * fps;
|
|
356
|
-
await page.evaluate(() => {
|
|
357
|
-
window.showCover();
|
|
358
|
-
});
|
|
359
|
-
await page.waitForTimeout(100);
|
|
360
|
-
const coverScreenshot = await page.screenshot({ type: "png" });
|
|
361
|
-
const coverFrame = new Uint8Array(coverScreenshot);
|
|
362
|
-
for (let i = 0; i < coverFrameCount; i++) {
|
|
363
|
-
frames.push(coverFrame);
|
|
364
|
-
}
|
|
365
|
-
await page.evaluate(() => {
|
|
366
|
-
window.hideCover();
|
|
367
|
-
});
|
|
368
|
-
writeProgress("Capturing", coverFrameCount, totalFrames + coverFrameCount);
|
|
369
|
-
}
|
|
370
|
-
const totalWithCover = totalFrames + frames.length;
|
|
371
|
-
for (let i = 0; i < totalFrames; i++) {
|
|
372
|
-
const time = i * frameInterval;
|
|
373
|
-
await page.evaluate((t) => {
|
|
374
|
-
return window.seekTo(t);
|
|
375
|
-
}, time);
|
|
376
|
-
const screenshot = await page.screenshot({ type: "png" });
|
|
377
|
-
frames.push(new Uint8Array(screenshot));
|
|
378
|
-
if (i % Math.max(1, Math.floor(fps / 2)) === 0 || i === totalFrames - 1) {
|
|
379
|
-
writeProgress("Capturing", frames.length, totalWithCover);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
clearProgress();
|
|
383
|
-
await browser.close();
|
|
384
|
-
const ffmpegPath = await detectFfmpeg();
|
|
385
|
-
const exportOptions = {
|
|
386
|
-
fps,
|
|
387
|
-
quality,
|
|
388
|
-
orientation,
|
|
389
|
-
width: dimensions.width,
|
|
390
|
-
height: dimensions.height,
|
|
391
|
-
onProgress: (percent, phase) => {
|
|
392
|
-
writeProgress("Encoding", percent, 100, phase);
|
|
393
|
-
}
|
|
394
|
-
};
|
|
395
|
-
if (ffmpegPath) {
|
|
396
|
-
console.error(`Using native ffmpeg: ${ffmpegPath}`);
|
|
397
|
-
const { framesToMp4Native } = await import("./nativeEncoder-EXDP2O5B.js");
|
|
398
|
-
await framesToMp4Native(ffmpegPath, frames, concatenatedAudio, outputPath, exportOptions);
|
|
399
|
-
} else {
|
|
400
|
-
throw new Error(
|
|
401
|
-
"ffmpeg is required but not found in PATH.\nInstall it with:\n macOS: brew install ffmpeg\n Ubuntu: sudo apt install ffmpeg\n Windows: winget install ffmpeg"
|
|
402
|
-
);
|
|
403
|
-
}
|
|
404
|
-
clearProgress();
|
|
405
|
-
console.error(` \u2713 ${outputPath}`);
|
|
406
|
-
console.error("Done.");
|
|
407
|
-
}
|
|
408
|
-
var BAR_WIDTH = 30;
|
|
409
|
-
function writeProgress(label, current, total, detail) {
|
|
410
|
-
const pct = Math.min(100, Math.round(current / total * 100));
|
|
411
|
-
const filled = Math.round(pct / 100 * BAR_WIDTH);
|
|
412
|
-
const bar = "\u2588".repeat(filled) + "\u2591".repeat(BAR_WIDTH - filled);
|
|
413
|
-
const suffix = detail ? ` (${detail})` : "";
|
|
414
|
-
process.stderr.write(`\r ${label}: ${bar} ${pct}%${suffix} `);
|
|
415
|
-
}
|
|
416
|
-
function clearProgress() {
|
|
417
|
-
process.stderr.write("\r" + " ".repeat(80) + "\r");
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// src/index.ts
|
|
421
|
-
console.error(
|
|
422
|
-
"\x1B[36m{[\x1B[0m \x1B[1msquiggly square\x1B[0m \x1B[2m\u2014\x1B[0m \x1B[1msquisq\x1B[0m \x1B[2m\u2014\x1B[0m \x1B[33mv1.0.0\x1B[0m \x1B[36m]}\x1B[0m"
|
|
423
|
-
);
|
|
424
|
-
var program = new Command();
|
|
425
|
-
program.name("squisq").description("Squisq CLI \u2014 convert and process markdown-based documents").version("1.0.0");
|
|
1
|
+
/**
|
|
2
|
+
* squisq CLI
|
|
3
|
+
*
|
|
4
|
+
* Command-line tool for converting and processing Squisq documents.
|
|
5
|
+
* Designed for easy addition of future subcommands (e.g., import).
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* squisq convert <input> [options]
|
|
9
|
+
* squisq --help
|
|
10
|
+
*/
|
|
11
|
+
import { Command } from 'commander';
|
|
12
|
+
import { registerConvertCommand } from './commands/convert.js';
|
|
13
|
+
import { registerVideoCommand } from './commands/video.js';
|
|
14
|
+
// Colored banner: cyan brackets, bold white text, dim version
|
|
15
|
+
console.error('\x1b[36m{[\x1b[0m \x1b[1msquiggly square\x1b[0m \x1b[2m—\x1b[0m \x1b[1msquisq\x1b[0m \x1b[2m—\x1b[0m \x1b[33mv1.0.0\x1b[0m \x1b[36m]}\x1b[0m');
|
|
16
|
+
const program = new Command();
|
|
17
|
+
program
|
|
18
|
+
.name('squisq')
|
|
19
|
+
.description('Squisq CLI — convert and process markdown-based documents')
|
|
20
|
+
.version('1.0.0');
|
|
426
21
|
registerConvertCommand(program);
|
|
427
22
|
registerVideoCommand(program);
|
|
428
23
|
program.parse();
|