deckrun 1.3.0 → 1.6.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 +217 -35
- package/dist/editor-content.js +86 -1
- package/dist/editor.js +528 -216
- package/dist/fragments.js +71 -0
- package/dist/generate.js +1040 -45
- package/dist/index.js +353 -53
- package/dist/lint.js +218 -0
- package/dist/parser.js +85 -0
- package/dist/pdf.js +5 -1
- package/dist/presentation-options.js +287 -0
- package/dist/preview.js +125 -7
- package/dist/rich-content.js +170 -0
- package/dist/themes.js +8 -5
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readFileSync } from "fs";
|
|
|
3
3
|
import { readFile } from "fs/promises";
|
|
4
4
|
import { createServer } from "http";
|
|
5
5
|
import { createRequire } from "module";
|
|
6
|
-
import { resolve, dirname, basename, extname } from "path";
|
|
6
|
+
import { resolve, dirname, basename, extname, join, isAbsolute, relative } from "path";
|
|
7
7
|
import { Command } from "commander";
|
|
8
8
|
import open from "open";
|
|
9
9
|
import { parseSlides } from "./parser.js";
|
|
@@ -12,6 +12,9 @@ import { DEFAULT_SIZE, DEFAULT_THEME, findFont, findSize, findTheme, fontListing
|
|
|
12
12
|
import { generateEditorHtml } from "./editor.js";
|
|
13
13
|
import { generatePreviewHtml } from "./preview.js";
|
|
14
14
|
import { findBrowser, renderPdfSerial, PdfError } from "./pdf.js";
|
|
15
|
+
import { DEFAULT_TEMPLATE, DEFAULT_TRANSITION, findTemplate, findTransition, resolveTemplateName, resolveTransitionName, templateListing, transitionListing, } from "./presentation-options.js";
|
|
16
|
+
import { lintMarkdown } from "./lint.js";
|
|
17
|
+
const moduleRequire = createRequire(import.meta.url);
|
|
15
18
|
const c = {
|
|
16
19
|
reset: "\x1b[0m",
|
|
17
20
|
bold: "\x1b[1m",
|
|
@@ -23,8 +26,7 @@ const c = {
|
|
|
23
26
|
};
|
|
24
27
|
function packageVersion() {
|
|
25
28
|
try {
|
|
26
|
-
|
|
27
|
-
return require("../package.json").version;
|
|
29
|
+
return moduleRequire("../package.json").version;
|
|
28
30
|
}
|
|
29
31
|
catch {
|
|
30
32
|
return "0.0.0";
|
|
@@ -135,6 +137,21 @@ function sendJson(res, payload) {
|
|
|
135
137
|
});
|
|
136
138
|
res.end(JSON.stringify(payload));
|
|
137
139
|
}
|
|
140
|
+
/** Resolve bundled math/diagram assets installed with the npm package. */
|
|
141
|
+
function vendorAsset(pathname) {
|
|
142
|
+
const name = pathname.replace(/^\/__vendor\/?/, "");
|
|
143
|
+
if (name === "katex.min.css" || name === "katex.min.js" || name.startsWith("fonts/")) {
|
|
144
|
+
const katexDist = dirname(moduleRequire.resolve("katex"));
|
|
145
|
+
const target = resolve(katexDist, name);
|
|
146
|
+
const fromRoot = relative(katexDist, target);
|
|
147
|
+
return !isAbsolute(fromRoot) && !fromRoot.startsWith("..") ? target : null;
|
|
148
|
+
}
|
|
149
|
+
if (name === "mermaid.min.js") {
|
|
150
|
+
const mermaidDist = dirname(moduleRequire.resolve("mermaid"));
|
|
151
|
+
return join(mermaidDist, "mermaid.min.js");
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
138
155
|
/** Decks built from editor content, addressable so a new tab can load them. */
|
|
139
156
|
const decks = new Map();
|
|
140
157
|
let deckSeq = 0;
|
|
@@ -168,7 +185,7 @@ function serveStashedDeck(id, res) {
|
|
|
168
185
|
}
|
|
169
186
|
async function handleEditorRoute(mode, pathname, req, res) {
|
|
170
187
|
if (pathname === "/__preview" && req.method === "GET") {
|
|
171
|
-
sendHtml(res, generatePreviewHtml(mode.theme, mode.size, mode.fonts));
|
|
188
|
+
sendHtml(res, generatePreviewHtml(mode.theme, mode.size, mode.fonts, mode.template, mode.transition));
|
|
172
189
|
return true;
|
|
173
190
|
}
|
|
174
191
|
if (pathname === "/__parse" && req.method === "POST") {
|
|
@@ -191,13 +208,15 @@ async function handleEditorRoute(mode, pathname, req, res) {
|
|
|
191
208
|
}
|
|
192
209
|
const theme = resolveThemeName(body.theme);
|
|
193
210
|
const size = resolveSizeName(body.size);
|
|
211
|
+
const template = resolveTemplateName(body.template);
|
|
212
|
+
const transition = resolveTransitionName(body.transition);
|
|
194
213
|
const title = deckTitle(slides, body.title?.trim() || "deckrun");
|
|
195
214
|
// A deck built for printing must not open behind a fullscreen prompt.
|
|
196
215
|
const forPrint = body.print === true;
|
|
197
216
|
const path = stashDeck(generateHtml(slides, title, forPrint ? false : mode.fullscreen, theme, size, {
|
|
198
217
|
head: body.head,
|
|
199
218
|
body: body.body,
|
|
200
|
-
}));
|
|
219
|
+
}, { template, transition, standalone: body.standalone === true }));
|
|
201
220
|
sendJson(res, { path: forPrint ? `${path}&print=1` : path });
|
|
202
221
|
return true;
|
|
203
222
|
}
|
|
@@ -221,8 +240,10 @@ async function handleEditorRoute(mode, pathname, req, res) {
|
|
|
221
240
|
}
|
|
222
241
|
const theme = resolveThemeName(body.theme);
|
|
223
242
|
const size = resolveSizeName(body.size);
|
|
243
|
+
const template = resolveTemplateName(body.template);
|
|
244
|
+
const transition = resolveTransitionName(body.transition);
|
|
224
245
|
const title = deckTitle(slides, body.title?.trim() || "deckrun");
|
|
225
|
-
const path = stashDeck(generateHtml(slides, title, false, theme, size, { head: body.head, body: body.body }));
|
|
246
|
+
const path = stashDeck(generateHtml(slides, title, false, theme, size, { head: body.head, body: body.body }, { template, transition }));
|
|
226
247
|
try {
|
|
227
248
|
const pdf = await renderPdfSerial(`${mode.origin}${path}`, browser);
|
|
228
249
|
const filename = safeFilename(body.title?.trim() || title) + ".pdf";
|
|
@@ -281,8 +302,8 @@ async function handleEditorRoute(mode, pathname, req, res) {
|
|
|
281
302
|
res.end(JSON.stringify({ error: "fetch failed", detail: `upstream responded ${upstream.status}` }));
|
|
282
303
|
return true;
|
|
283
304
|
}
|
|
284
|
-
const
|
|
285
|
-
if (
|
|
305
|
+
const rawContent = await upstream.text();
|
|
306
|
+
if (rawContent.length > MAX_BODY) {
|
|
286
307
|
res.writeHead(413, { "Content-Type": "application/json" });
|
|
287
308
|
res.end(JSON.stringify({
|
|
288
309
|
error: "too large",
|
|
@@ -290,12 +311,48 @@ async function handleEditorRoute(mode, pathname, req, res) {
|
|
|
290
311
|
}));
|
|
291
312
|
return true;
|
|
292
313
|
}
|
|
293
|
-
if (!
|
|
314
|
+
if (!rawContent.trim()) {
|
|
294
315
|
res.writeHead(422, { "Content-Type": "application/json" });
|
|
295
316
|
res.end(JSON.stringify({ error: "empty document" }));
|
|
296
317
|
return true;
|
|
297
318
|
}
|
|
298
|
-
|
|
319
|
+
const contentType = (upstream.headers.get("content-type") ?? "").toLowerCase();
|
|
320
|
+
const pathname = target.pathname.toLowerCase();
|
|
321
|
+
let isHtml = false;
|
|
322
|
+
if (pathname.endsWith(".html") || pathname.endsWith(".htm")) {
|
|
323
|
+
isHtml = true;
|
|
324
|
+
}
|
|
325
|
+
else if (pathname.endsWith(".md") || pathname.endsWith(".markdown")) {
|
|
326
|
+
isHtml = false;
|
|
327
|
+
}
|
|
328
|
+
else if (contentType.includes("text/html") ||
|
|
329
|
+
contentType.includes("application/xhtml+xml")) {
|
|
330
|
+
isHtml = true;
|
|
331
|
+
}
|
|
332
|
+
else if (contentType.includes("text/markdown") ||
|
|
333
|
+
contentType.includes("text/x-markdown") ||
|
|
334
|
+
contentType.includes("text/plain")) {
|
|
335
|
+
isHtml = false;
|
|
336
|
+
}
|
|
337
|
+
else if (/<!doctype\s+html/i.test(rawContent) || /<html[\s>]/i.test(rawContent)) {
|
|
338
|
+
isHtml = true;
|
|
339
|
+
}
|
|
340
|
+
const defaultName = target.pathname.split("/").filter(Boolean).pop() || target.hostname;
|
|
341
|
+
let title;
|
|
342
|
+
if (isHtml) {
|
|
343
|
+
title = docTitle(rawContent, defaultName);
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
const slides = parseSlides(rawContent);
|
|
347
|
+
title = deckTitle(slides, defaultName);
|
|
348
|
+
}
|
|
349
|
+
sendJson(res, {
|
|
350
|
+
kind: isHtml ? "html" : "markdown",
|
|
351
|
+
content: rawContent,
|
|
352
|
+
html: isHtml ? rawContent : undefined,
|
|
353
|
+
markdown: !isHtml ? rawContent : undefined,
|
|
354
|
+
title,
|
|
355
|
+
});
|
|
299
356
|
return true;
|
|
300
357
|
}
|
|
301
358
|
if (pathname === "/__present-doc" && req.method === "POST") {
|
|
@@ -370,7 +427,54 @@ async function serve(mode, baseDir, port) {
|
|
|
370
427
|
else
|
|
371
428
|
sendHtml(res, mode.kind === "deck"
|
|
372
429
|
? mode.html
|
|
373
|
-
: generateEditorHtml(mode.theme, mode.size, mode.fonts));
|
|
430
|
+
: generateEditorHtml(mode.theme, mode.size, mode.fonts, mode.template, mode.transition));
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (mode.kind === "deck" && mode.remoteDoc && pathname === "/__remote-doc") {
|
|
434
|
+
sendHtml(res, mode.remoteDoc);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (pathname.startsWith("/__vendor/")) {
|
|
438
|
+
const asset = vendorAsset(pathname);
|
|
439
|
+
if (!asset) {
|
|
440
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
441
|
+
res.end("Not found");
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const data = await readFile(asset);
|
|
445
|
+
res.writeHead(200, {
|
|
446
|
+
"Content-Type": getMime(asset),
|
|
447
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
448
|
+
});
|
|
449
|
+
res.end(data);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (mode.kind === "deck" && pathname === "/__pdf") {
|
|
453
|
+
const browser = await findBrowser();
|
|
454
|
+
if (!browser) {
|
|
455
|
+
res.writeHead(501, { "Content-Type": "application/json" });
|
|
456
|
+
res.end(JSON.stringify({
|
|
457
|
+
error: "no browser",
|
|
458
|
+
detail: "No Chrome, Chromium, Edge, or Brave found. Set DECKRUN_BROWSER to one to export PDFs directly.",
|
|
459
|
+
}));
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
463
|
+
const pdf = await renderPdfSerial(`${mode.origin}/`, browser);
|
|
464
|
+
const filename = safeFilename(mode.title || "deck") + ".pdf";
|
|
465
|
+
res.writeHead(200, {
|
|
466
|
+
"Content-Type": "application/pdf",
|
|
467
|
+
"Content-Length": pdf.length,
|
|
468
|
+
"Content-Disposition": `attachment; filename="${filename}"`,
|
|
469
|
+
"Cache-Control": "no-store",
|
|
470
|
+
});
|
|
471
|
+
res.end(pdf);
|
|
472
|
+
}
|
|
473
|
+
catch (err) {
|
|
474
|
+
const detail = err instanceof PdfError ? err.message : "rendering failed";
|
|
475
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
476
|
+
res.end(JSON.stringify({ error: "render failed", detail }));
|
|
477
|
+
}
|
|
374
478
|
return;
|
|
375
479
|
}
|
|
376
480
|
if (mode.kind === "editor" && (await handleEditorRoute(mode, pathname, req, res))) {
|
|
@@ -378,7 +482,8 @@ async function serve(mode, baseDir, port) {
|
|
|
378
482
|
}
|
|
379
483
|
// Everything else comes off disk, relative to the working directory.
|
|
380
484
|
const filePath = resolve(baseDir, pathname.replace(/^\/+/, ""));
|
|
381
|
-
|
|
485
|
+
const fromBase = relative(baseDir, filePath);
|
|
486
|
+
if (isAbsolute(fromBase) || fromBase.startsWith("..")) {
|
|
382
487
|
res.writeHead(403);
|
|
383
488
|
res.end("Forbidden");
|
|
384
489
|
return;
|
|
@@ -407,9 +512,9 @@ async function serve(mode, baseDir, port) {
|
|
|
407
512
|
const program = new Command();
|
|
408
513
|
program
|
|
409
514
|
.name("deckrun")
|
|
410
|
-
.description("Present a Markdown file in the browser. Run without a file to write
|
|
515
|
+
.description("Present a Markdown file, HTML file, or public URL in the browser. Run without a file or URL to write in the built-in editor.")
|
|
411
516
|
.version(packageVersion(), "-v, --version", "Print the version number")
|
|
412
|
-
.argument("[file]", "Markdown file to present. Omit it to open the editor.")
|
|
517
|
+
.argument("[file]", "Markdown file, HTML file, or public URL to present. Omit it to open the editor.")
|
|
413
518
|
.option("-p, --port <number>", "Port to serve on", "7890")
|
|
414
519
|
.option("--no-open", "Do not automatically open the browser")
|
|
415
520
|
.option("--fullscreen", "Auto-enter fullscreen on first interaction")
|
|
@@ -417,9 +522,13 @@ program
|
|
|
417
522
|
.option("--size <name>", "Type size: s, m, l, or xl", DEFAULT_SIZE)
|
|
418
523
|
.option("--head-font <name>", "Override the theme's heading face (see --list-fonts)")
|
|
419
524
|
.option("--body-font <name>", "Override the theme's body face (see --list-fonts)")
|
|
525
|
+
.option("--template <name>", "Composition template (see --list-templates)", DEFAULT_TEMPLATE)
|
|
526
|
+
.option("--transition <name>", "Slide transition (see --list-transitions)", DEFAULT_TRANSITION)
|
|
420
527
|
.option("--list-themes", "Print every theme and exit")
|
|
421
528
|
.option("--list-sizes", "Print every type size and exit")
|
|
422
529
|
.option("--list-fonts", "Print every font face and exit")
|
|
530
|
+
.option("--list-templates", "Print every composition template and exit")
|
|
531
|
+
.option("--list-transitions", "Print every slide transition and exit")
|
|
423
532
|
.action(async (file, opts) => {
|
|
424
533
|
if (opts.listThemes) {
|
|
425
534
|
for (const line of themeListing())
|
|
@@ -436,6 +545,16 @@ program
|
|
|
436
545
|
console.log(line);
|
|
437
546
|
process.exit(0);
|
|
438
547
|
}
|
|
548
|
+
if (opts.listTemplates) {
|
|
549
|
+
for (const line of templateListing())
|
|
550
|
+
console.log(line);
|
|
551
|
+
process.exit(0);
|
|
552
|
+
}
|
|
553
|
+
if (opts.listTransitions) {
|
|
554
|
+
for (const line of transitionListing())
|
|
555
|
+
console.log(line);
|
|
556
|
+
process.exit(0);
|
|
557
|
+
}
|
|
439
558
|
const named = findTheme(opts.theme);
|
|
440
559
|
if (!named) {
|
|
441
560
|
console.error(`deckrun: unknown theme '${opts.theme}'. Run --list-themes to see them all.`);
|
|
@@ -446,6 +565,16 @@ program
|
|
|
446
565
|
console.error(`deckrun: unknown size '${opts.size}'. Run --list-sizes to see them all.`);
|
|
447
566
|
process.exit(1);
|
|
448
567
|
}
|
|
568
|
+
const templated = findTemplate(opts.template);
|
|
569
|
+
if (!templated) {
|
|
570
|
+
console.error(`deckrun: unknown template '${opts.template}'. Run --list-templates to see them all.`);
|
|
571
|
+
process.exit(1);
|
|
572
|
+
}
|
|
573
|
+
const transitioned = findTransition(opts.transition);
|
|
574
|
+
if (!transitioned) {
|
|
575
|
+
console.error(`deckrun: unknown transition '${opts.transition}'. Run --list-transitions to see them all.`);
|
|
576
|
+
process.exit(1);
|
|
577
|
+
}
|
|
449
578
|
// Both face flags are optional; unset means the theme keeps its own.
|
|
450
579
|
const fonts = { head: null, body: null };
|
|
451
580
|
for (const [flag, slot] of [
|
|
@@ -464,75 +593,246 @@ program
|
|
|
464
593
|
}
|
|
465
594
|
const theme = named;
|
|
466
595
|
const size = sized;
|
|
596
|
+
const template = templated;
|
|
597
|
+
const transition = transitioned;
|
|
467
598
|
const fullscreen = !!opts.fullscreen;
|
|
468
599
|
let mode;
|
|
469
600
|
let baseDir;
|
|
470
601
|
if (file) {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const ext = extname(absPath).toLowerCase();
|
|
474
|
-
if (ext === ".html" || ext === ".htm") {
|
|
475
|
-
let rawHtml;
|
|
602
|
+
if (/^https?:\/\//i.test(file)) {
|
|
603
|
+
let target;
|
|
476
604
|
try {
|
|
477
|
-
|
|
605
|
+
target = new URL(file);
|
|
478
606
|
}
|
|
479
607
|
catch {
|
|
480
|
-
console.error(`deckrun:
|
|
608
|
+
console.error(`deckrun: invalid URL '${file}'`);
|
|
481
609
|
process.exit(1);
|
|
482
610
|
}
|
|
483
|
-
|
|
484
|
-
console.error(`${c.dim}deckrun: --size, --head-font, and --body-font only apply to Markdown decks; ignored for an HTML doc.${c.reset}`);
|
|
485
|
-
}
|
|
486
|
-
const title = docTitle(rawHtml, basename(absPath, extname(absPath)));
|
|
487
|
-
mode = {
|
|
488
|
-
kind: "deck",
|
|
489
|
-
html: generateDocHtml(`/${basename(absPath)}`, title, fullscreen, theme),
|
|
490
|
-
};
|
|
491
|
-
console.log(`${c.dim}presenting ${basename(absPath)} · ${THEMES[theme].label}${c.reset}`);
|
|
492
|
-
}
|
|
493
|
-
else {
|
|
494
|
-
let markdown;
|
|
611
|
+
let upstream;
|
|
495
612
|
try {
|
|
496
|
-
|
|
613
|
+
upstream = await fetch(target, {
|
|
614
|
+
redirect: "follow",
|
|
615
|
+
signal: AbortSignal.timeout(15_000),
|
|
616
|
+
headers: { "User-Agent": "deckrun" },
|
|
617
|
+
});
|
|
497
618
|
}
|
|
498
|
-
catch {
|
|
499
|
-
console.error(`deckrun: cannot
|
|
619
|
+
catch (err) {
|
|
620
|
+
console.error(`deckrun: cannot fetch '${file}': ${err instanceof Error ? err.message : "network error"}`);
|
|
621
|
+
process.exit(1);
|
|
622
|
+
}
|
|
623
|
+
if (!upstream.ok) {
|
|
624
|
+
console.error(`deckrun: fetch failed for '${file}' (HTTP ${upstream.status})`);
|
|
500
625
|
process.exit(1);
|
|
501
626
|
}
|
|
502
|
-
const
|
|
503
|
-
if (
|
|
504
|
-
console.error(
|
|
627
|
+
const rawContent = await upstream.text();
|
|
628
|
+
if (!rawContent.trim()) {
|
|
629
|
+
console.error(`deckrun: empty document fetched from '${file}'`);
|
|
505
630
|
process.exit(1);
|
|
506
631
|
}
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
632
|
+
const contentType = (upstream.headers.get("content-type") ?? "").toLowerCase();
|
|
633
|
+
const pathname = target.pathname.toLowerCase();
|
|
634
|
+
let isHtml = false;
|
|
635
|
+
if (pathname.endsWith(".html") || pathname.endsWith(".htm")) {
|
|
636
|
+
isHtml = true;
|
|
637
|
+
}
|
|
638
|
+
else if (pathname.endsWith(".md") || pathname.endsWith(".markdown")) {
|
|
639
|
+
isHtml = false;
|
|
640
|
+
}
|
|
641
|
+
else if (contentType.includes("text/html") ||
|
|
642
|
+
contentType.includes("application/xhtml+xml")) {
|
|
643
|
+
isHtml = true;
|
|
644
|
+
}
|
|
645
|
+
else if (contentType.includes("text/markdown") ||
|
|
646
|
+
contentType.includes("text/x-markdown") ||
|
|
647
|
+
contentType.includes("text/plain")) {
|
|
648
|
+
isHtml = false;
|
|
649
|
+
}
|
|
650
|
+
else if (/<!doctype\s+html/i.test(rawContent) || /<html[\s>]/i.test(rawContent)) {
|
|
651
|
+
isHtml = true;
|
|
652
|
+
}
|
|
653
|
+
baseDir = process.cwd();
|
|
654
|
+
const defaultName = target.pathname.split("/").filter(Boolean).pop() || target.hostname;
|
|
655
|
+
if (isHtml) {
|
|
656
|
+
if (opts.size !== DEFAULT_SIZE || opts.headFont || opts.bodyFont ||
|
|
657
|
+
opts.template !== DEFAULT_TEMPLATE || opts.transition !== DEFAULT_TRANSITION) {
|
|
658
|
+
console.error(`${c.dim}deckrun: --size, font, template, and transition options only apply to Markdown decks; ignored for an HTML doc.${c.reset}`);
|
|
659
|
+
}
|
|
660
|
+
const title = docTitle(rawContent, defaultName);
|
|
661
|
+
let docHtml = rawContent;
|
|
662
|
+
if (!/<base\s/i.test(docHtml)) {
|
|
663
|
+
if (/<head[^>]*>/i.test(docHtml)) {
|
|
664
|
+
docHtml = docHtml.replace(/<head[^>]*>/i, (m) => `${m}\n <base href="${target.href}">`);
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
docHtml = `<base href="${target.href}">\n` + docHtml;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
mode = {
|
|
671
|
+
kind: "deck",
|
|
672
|
+
html: generateDocHtml("/__remote-doc", title, fullscreen, theme),
|
|
673
|
+
remoteDoc: docHtml,
|
|
674
|
+
};
|
|
675
|
+
console.log(`${c.dim}presenting ${file} · ${THEMES[theme].label}${c.reset}`);
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
const slides = parseSlides(rawContent);
|
|
679
|
+
if (slides.length === 0) {
|
|
680
|
+
console.error("deckrun: no slides found in the fetched content.");
|
|
681
|
+
process.exit(1);
|
|
682
|
+
}
|
|
683
|
+
const title = deckTitle(slides, defaultName);
|
|
684
|
+
mode = {
|
|
685
|
+
kind: "deck",
|
|
686
|
+
html: generateHtml(slides, title, fullscreen, theme, size, fonts, { template, transition }),
|
|
687
|
+
};
|
|
688
|
+
const faces = [
|
|
689
|
+
fonts.head ? `head ${fontName(fonts.head)}` : "",
|
|
690
|
+
fonts.body ? `body ${fontName(fonts.body)}` : "",
|
|
691
|
+
].filter(Boolean).join(" · ");
|
|
692
|
+
console.log(`${c.dim}${slides.length} slide${slides.length !== 1 ? "s" : ""} from ${file} · ${THEMES[theme].label} · ${template} · ${transition} · type ${size}${faces ? " · " + faces : ""}${c.reset}`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
const absPath = resolve(process.cwd(), file);
|
|
697
|
+
baseDir = dirname(absPath);
|
|
698
|
+
const ext = extname(absPath).toLowerCase();
|
|
699
|
+
if (ext === ".html" || ext === ".htm") {
|
|
700
|
+
let rawHtml;
|
|
701
|
+
try {
|
|
702
|
+
rawHtml = readFileSync(absPath, "utf-8");
|
|
703
|
+
}
|
|
704
|
+
catch {
|
|
705
|
+
console.error(`deckrun: cannot read file '${file}'`);
|
|
706
|
+
process.exit(1);
|
|
707
|
+
}
|
|
708
|
+
if (opts.size !== DEFAULT_SIZE || opts.headFont || opts.bodyFont ||
|
|
709
|
+
opts.template !== DEFAULT_TEMPLATE || opts.transition !== DEFAULT_TRANSITION) {
|
|
710
|
+
console.error(`${c.dim}deckrun: --size, font, template, and transition options only apply to Markdown decks; ignored for an HTML doc.${c.reset}`);
|
|
711
|
+
}
|
|
712
|
+
const title = docTitle(rawHtml, basename(absPath, extname(absPath)));
|
|
713
|
+
mode = {
|
|
714
|
+
kind: "deck",
|
|
715
|
+
html: generateDocHtml(`/${basename(absPath)}`, title, fullscreen, theme),
|
|
716
|
+
};
|
|
717
|
+
console.log(`${c.dim}presenting ${basename(absPath)} · ${THEMES[theme].label}${c.reset}`);
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
let markdown;
|
|
721
|
+
try {
|
|
722
|
+
markdown = readFileSync(absPath, "utf-8");
|
|
723
|
+
}
|
|
724
|
+
catch {
|
|
725
|
+
console.error(`deckrun: cannot read file '${file}'`);
|
|
726
|
+
process.exit(1);
|
|
727
|
+
}
|
|
728
|
+
const slides = parseSlides(markdown);
|
|
729
|
+
if (slides.length === 0) {
|
|
730
|
+
console.error("deckrun: no slides found in the file.");
|
|
731
|
+
process.exit(1);
|
|
732
|
+
}
|
|
733
|
+
const title = deckTitle(slides, basename(absPath, extname(absPath)));
|
|
734
|
+
mode = {
|
|
735
|
+
kind: "deck",
|
|
736
|
+
title,
|
|
737
|
+
html: generateHtml(slides, title, fullscreen, theme, size, fonts, { template, transition }),
|
|
738
|
+
};
|
|
739
|
+
const faces = [
|
|
740
|
+
fonts.head ? `head ${fontName(fonts.head)}` : "",
|
|
741
|
+
fonts.body ? `body ${fontName(fonts.body)}` : "",
|
|
742
|
+
].filter(Boolean).join(" · ");
|
|
743
|
+
console.log(`${c.dim}${slides.length} slide${slides.length !== 1 ? "s" : ""} from ${basename(absPath)} · ${THEMES[theme].label} · ${template} · ${transition} · type ${size}${faces ? " · " + faces : ""}${c.reset}`);
|
|
744
|
+
}
|
|
517
745
|
}
|
|
518
746
|
}
|
|
519
747
|
else {
|
|
520
748
|
baseDir = process.cwd();
|
|
521
|
-
mode = { kind: "editor", theme, size, fonts, fullscreen };
|
|
749
|
+
mode = { kind: "editor", theme, size, fonts, template, transition, fullscreen };
|
|
522
750
|
}
|
|
523
751
|
const port = await findFreePort(parseInt(opts.port, 10));
|
|
524
|
-
|
|
525
|
-
mode.origin = `http://127.0.0.1:${port}`;
|
|
752
|
+
mode.origin = `http://127.0.0.1:${port}`;
|
|
526
753
|
const url = await serve(mode, baseDir, port);
|
|
527
754
|
const label = mode.kind === "editor" ? "editor" : "present";
|
|
528
755
|
console.log(`${c.bold}${c.magenta}${label}${c.reset} ${c.dim}→${c.reset} ${c.cyan}${c.bold}${url}${c.reset} ${c.dim}(Ctrl+C to stop)${c.reset}`);
|
|
529
756
|
if (mode.kind === "editor") {
|
|
530
757
|
console.log(`${c.dim}write on the left, live deck on the right. autosaves to your browser.${c.reset}`);
|
|
531
|
-
console.log(`${c.dim}Cmd/Ctrl+K inserts anything ·
|
|
758
|
+
console.log(`${c.dim}Cmd/Ctrl+K inserts anything · template/theme controls recompose live · Cmd/Ctrl+Enter presents${c.reset}`);
|
|
532
759
|
}
|
|
533
760
|
if (opts.open !== false)
|
|
534
761
|
await open(url);
|
|
535
762
|
// Keep the process alive until interrupted.
|
|
536
763
|
await new Promise(() => { });
|
|
537
764
|
});
|
|
765
|
+
program
|
|
766
|
+
.command("lint")
|
|
767
|
+
.description("Check Markdown decks for common authoring and rendering problems")
|
|
768
|
+
.argument("<files...>", "Markdown files to check; use - to read standard input")
|
|
769
|
+
.option("--format <format>", "Output format: stylish or json", "stylish")
|
|
770
|
+
.option("--max-warnings <number>", "Warnings allowed before the command fails", "0")
|
|
771
|
+
.action((files, opts) => {
|
|
772
|
+
if (opts.format !== "stylish" && opts.format !== "json") {
|
|
773
|
+
console.error("deckrun lint: --format must be 'stylish' or 'json'.");
|
|
774
|
+
process.exitCode = 2;
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
const maxWarnings = Number.parseInt(opts.maxWarnings, 10);
|
|
778
|
+
if (!Number.isInteger(maxWarnings) || maxWarnings < -1) {
|
|
779
|
+
console.error("deckrun lint: --max-warnings must be -1 or a non-negative integer.");
|
|
780
|
+
process.exitCode = 2;
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
const reports = [];
|
|
784
|
+
for (const file of files) {
|
|
785
|
+
let markdown;
|
|
786
|
+
try {
|
|
787
|
+
markdown = file === "-" ? readFileSync(0, "utf-8") : readFileSync(resolve(process.cwd(), file), "utf-8");
|
|
788
|
+
}
|
|
789
|
+
catch {
|
|
790
|
+
reports.push({
|
|
791
|
+
file,
|
|
792
|
+
slides: 0,
|
|
793
|
+
errors: 1,
|
|
794
|
+
warnings: 0,
|
|
795
|
+
issues: [{
|
|
796
|
+
rule: "file-read",
|
|
797
|
+
severity: "error",
|
|
798
|
+
message: "The file could not be read.",
|
|
799
|
+
line: 1,
|
|
800
|
+
column: 1,
|
|
801
|
+
}],
|
|
802
|
+
});
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
const result = lintMarkdown(markdown);
|
|
806
|
+
reports.push({ file, ...result });
|
|
807
|
+
}
|
|
808
|
+
const errors = reports.reduce((sum, report) => sum + report.errors, 0);
|
|
809
|
+
const warnings = reports.reduce((sum, report) => sum + report.warnings, 0);
|
|
810
|
+
const issueCount = errors + warnings;
|
|
811
|
+
if (opts.format === "json") {
|
|
812
|
+
console.log(JSON.stringify({ files: reports, errors, warnings }, null, 2));
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
for (const report of reports) {
|
|
816
|
+
if (!report.issues.length)
|
|
817
|
+
continue;
|
|
818
|
+
console.log(`\n${report.file}`);
|
|
819
|
+
for (const item of report.issues) {
|
|
820
|
+
const position = `${item.line}:${item.column}`.padEnd(9);
|
|
821
|
+
const severity = item.severity.padEnd(7);
|
|
822
|
+
const slide = item.slide ? `slide ${item.slide} · ` : "";
|
|
823
|
+
console.log(` ${position} ${severity} ${slide}${item.message} ${item.rule}`);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
if (issueCount === 0) {
|
|
827
|
+
const slides = reports.reduce((sum, report) => sum + report.slides, 0);
|
|
828
|
+
console.log(`✓ ${files.length} file${files.length === 1 ? "" : "s"}, ${slides} slide${slides === 1 ? "" : "s"}, no problems`);
|
|
829
|
+
}
|
|
830
|
+
else {
|
|
831
|
+
console.log(`\n✖ ${issueCount} problem${issueCount === 1 ? "" : "s"} (${errors} error${errors === 1 ? "" : "s"}, ${warnings} warning${warnings === 1 ? "" : "s"})`);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
if (errors > 0 || (maxWarnings !== -1 && warnings > maxWarnings)) {
|
|
835
|
+
process.exitCode = 1;
|
|
836
|
+
}
|
|
837
|
+
});
|
|
538
838
|
program.parse(process.argv);
|