Generate web slides from Markdoc

feat(cli): add build command

graham.systems 66285b6c e31d6b12

verified
Changed files
+167 -1
cli
core
templates
+1
.gitignore
··· 1 + dist/
+14
cli/build.ts
··· 1 + import { Command } from "@cliffy/command"; 2 + import { buildStatic } from "../core/renderer.ts"; 3 + 4 + export const build = new Command() 5 + .arguments("<source:string>") 6 + .option("-o, --output <output:string>", "Output directory", { 7 + default: "./dist", 8 + }) 9 + .description("Build static presentation") 10 + .action(async (options, source: string) => { 11 + const { output } = options; 12 + await buildStatic(source, output); 13 + console.log(`Static build complete! Output: ${output}/index.html`); 14 + });
+27
core/renderer.ts
··· 1 1 import Markdoc, { Node } from "@markdoc/markdoc"; 2 2 import { createMarkdocConfig } from "./markdoc-config.ts"; 3 + import { Eta } from "@eta-dev/eta"; 4 + 5 + const eta = new Eta({ views: Deno.cwd() + "/templates" }); 3 6 4 7 export async function renderBody(path: string) { 5 8 const file = await Deno.readTextFile(path); ··· 33 36 body: Markdoc.renderers.html(await Promise.resolve(tree)), 34 37 }; 35 38 } 39 + 40 + export async function buildStatic(sourcePath: string, outputDir: string) { 41 + // Ensure output directory exists 42 + await Deno.mkdir(outputDir, { recursive: true }); 43 + 44 + // Get the presentation title from the first heading 45 + const sourceContent = await Deno.readTextFile(sourcePath); 46 + const titleMatch = sourceContent.match(/^# (.+)$/m); 47 + const title = titleMatch ? titleMatch[1] : "Presentation"; 48 + 49 + // Render the presentation 50 + const { body, scripts } = await renderBody(sourcePath); 51 + 52 + // Generate static HTML 53 + const html = eta.render("static", { 54 + body, 55 + scripts: Array.from(scripts), 56 + title 57 + }); 58 + 59 + // Write to output file 60 + const outputPath = `${outputDir}/index.html`; 61 + await Deno.writeTextFile(outputPath, html); 62 + }
+2 -1
deno.json
··· 1 1 { 2 2 "tasks": { 3 - "dev": "deno run --unstable-broadcast-channel --allow-env --allow-net --allow-read --allow-write --watch main.ts dev" 3 + "dev": "deno run --unstable-broadcast-channel --allow-env --allow-net --allow-read --allow-write --watch main.ts dev", 4 + "build": "deno run --allow-env --allow-read --allow-write main.ts build" 4 5 }, 5 6 "imports": { 6 7 "@cliffy/command": "jsr:@cliffy/command@1.0.0-rc.8",
+2
main.ts
··· 1 1 import { Command } from "@cliffy/command"; 2 2 import { dev } from "./cli/dev.ts"; 3 + import { build } from "./cli/build.ts"; 3 4 4 5 await new Command() 5 6 .name("morkdeck") 6 7 .version("0.0.0") 7 8 .description("Turn Markdoc files into web-powered slide decks") 8 9 .command("dev", dev) 10 + .command("build", build) 9 11 .parse();
+121
templates/static.eta
··· 1 + <!doctype html> 2 + <html> 3 + <head> 4 + <title><%= it.title || "Presentation" %></title> 5 + <link rel="preconnect" href="https://fonts.googleapis.com"> 6 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 7 + <link href="https://fonts.googleapis.com/css2?family=Recursive:slnt,wght,CASL,CRSV,MONO@-15..0,300..1000,0..1,0..1,0..1&display=swap" rel="stylesheet"> 8 + 9 + <style> 10 + body { 11 + margin: 0; 12 + box-sizing: border-box; 13 + font-family: "Recursive", sans-serif; 14 + background-color: #191724; 15 + color: #908caa; 16 + } 17 + 18 + h1, h2, h3, h4, h5, h6 { 19 + display: block; 20 + color: #e0def4; 21 + margin-bottom: 2cqi; 22 + } 23 + 24 + p, ul, ol { 25 + font-size: 2.5cqi; 26 + margin: 0; 27 + } 28 + 29 + pre { 30 + margin: 0; 31 + font-variation-settings: "MONO" 1; 32 + font-family: "Recursive", monospace; 33 + } 34 + 35 + code { 36 + font-variation-settings: "MONO" 1; 37 + font-family: "Recursive", monospace; 38 + } 39 + 40 + article { 41 + width: 100vw; 42 + height: 100vh; 43 + overflow-y: scroll; 44 + scroll-behavior: smooth; 45 + scroll-snap-type: y mandatory; 46 + } 47 + 48 + section { 49 + width: 100%; 50 + height: 100%; 51 + scroll-snap-align: center; 52 + position: relative; 53 + } 54 + 55 + section > div { 56 + container: slide / inline-size; 57 + box-sizing: border-box; 58 + position: absolute; 59 + inset: 0; 60 + margin: auto; 61 + aspect-ratio: 16 / 9; 62 + max-width: 100%; 63 + max-height: 100%; 64 + 65 + display: flex; 66 + flex-direction: column; 67 + justify-content: center; 68 + align-items: center; 69 + 70 + padding: 6cqmin; 71 + background: #1f1d2e; 72 + } 73 + 74 + h1 { 75 + font-size: 8cqi; 76 + margin: 0; 77 + font-weight: 900; 78 + } 79 + 80 + h2 { 81 + font-size: 6cqi; 82 + margin-top: 0; 83 + margin-bottom: 3cqi; 84 + font-weight: 900; 85 + } 86 + 87 + h1:first-of-type { 88 + font-size: 10cqi; 89 + margin-bottom: 3cqi; 90 + font-weight: 1000; 91 + line-height: 0.8em; 92 + } 93 + 94 + h1:first-of-type ~ :is(h2, h3, h4, h5, h6, p) { 95 + font-size: 2.8cqi; 96 + font-weight: 700; 97 + margin: 0; 98 + } 99 + 100 + div:has(h1:first-of-type) { 101 + display: flex; 102 + flex-direction: column; 103 + justify-content: center; 104 + } 105 + 106 + .shiki { 107 + padding: 2cqi; 108 + font-size: 2cqi; 109 + border-radius: 1em; 110 + } 111 + </style> 112 + </head> 113 + <body> 114 + <article> 115 + <%~ it.body %> 116 + </article> 117 + <% it.scripts.forEach(src => { %> 118 + <script src="<%= src %>"></script> 119 + <% }) %> 120 + </body> 121 + </html>