Generate web slides from Markdoc

feat: render code snippets with shiki

graham.systems 510fb3fe 0519b379

verified
Changed files
+367 -7
parse
templates
+5 -2
deno.json
··· 1 1 { 2 2 "tasks": { 3 - "dev": "deno run --watch main.ts" 3 + "dev": "deno run --unstable-broadcast-channel --allow-env --allow-net --allow-read --allow-write --watch main.ts dev" 4 4 }, 5 5 "imports": { 6 6 "@cliffy/command": "jsr:@cliffy/command@1.0.0-rc.8", 7 7 "@eta-dev/eta": "jsr:@eta-dev/eta@^3.5.0", 8 8 "@markdoc/markdoc": "https://esm.sh/@markdoc/markdoc@0.5.2", 9 - "@std/assert": "jsr:@std/assert@1" 9 + "@std/assert": "jsr:@std/assert@1", 10 + "@types/hast": "npm:@types/hast@^3.0.4", 11 + "hast-util-is-element": "npm:hast-util-is-element@^3.0.0", 12 + "shiki": "npm:shiki@^3.8.1" 10 13 } 11 14 }
+267 -2
deno.lock
··· 11 11 "jsr:@std/internal@^1.0.6": "1.0.9", 12 12 "jsr:@std/text@~1.0.7": "1.0.15", 13 13 "npm:@markdoc/markdoc@*": "0.5.2", 14 - "npm:@types/node@*": "22.15.15" 14 + "npm:@types/hast@^3.0.4": "3.0.4", 15 + "npm:@types/node@*": "22.15.15", 16 + "npm:hast-util-is-element@3": "3.0.0", 17 + "npm:shiki@^3.8.1": "3.8.1" 15 18 }, 16 19 "jsr": { 17 20 "@cliffy/command@1.0.0-rc.8": { ··· 66 69 "@types/markdown-it" 67 70 ] 68 71 }, 72 + "@shikijs/core@3.8.1": { 73 + "integrity": "sha512-uTSXzUBQ/IgFcUa6gmGShCHr4tMdR3pxUiiWKDm8pd42UKJdYhkAYsAmHX5mTwybQ5VyGDgTjW4qKSsRvGSang==", 74 + "dependencies": [ 75 + "@shikijs/types", 76 + "@shikijs/vscode-textmate", 77 + "@types/hast", 78 + "hast-util-to-html" 79 + ] 80 + }, 81 + "@shikijs/engine-javascript@3.8.1": { 82 + "integrity": "sha512-rZRp3BM1llrHkuBPAdYAzjlF7OqlM0rm/7EWASeCcY7cRYZIrOnGIHE9qsLz5TCjGefxBFnwgIECzBs2vmOyKA==", 83 + "dependencies": [ 84 + "@shikijs/types", 85 + "@shikijs/vscode-textmate", 86 + "oniguruma-to-es" 87 + ] 88 + }, 89 + "@shikijs/engine-oniguruma@3.8.1": { 90 + "integrity": "sha512-KGQJZHlNY7c656qPFEQpIoqOuC4LrxjyNndRdzk5WKB/Ie87+NJCF1xo9KkOUxwxylk7rT6nhlZyTGTC4fCe1g==", 91 + "dependencies": [ 92 + "@shikijs/types", 93 + "@shikijs/vscode-textmate" 94 + ] 95 + }, 96 + "@shikijs/langs@3.8.1": { 97 + "integrity": "sha512-TjOFg2Wp1w07oKnXjs0AUMb4kJvujML+fJ1C5cmEj45lhjbUXtziT1x2bPQb9Db6kmPhkG5NI2tgYW1/DzhUuQ==", 98 + "dependencies": [ 99 + "@shikijs/types" 100 + ] 101 + }, 102 + "@shikijs/themes@3.8.1": { 103 + "integrity": "sha512-Vu3t3BBLifc0GB0UPg2Pox1naTemrrvyZv2lkiSw3QayVV60me1ujFQwPZGgUTmwXl1yhCPW8Lieesm0CYruLQ==", 104 + "dependencies": [ 105 + "@shikijs/types" 106 + ] 107 + }, 108 + "@shikijs/types@3.8.1": { 109 + "integrity": "sha512-5C39Q8/8r1I26suLh+5TPk1DTrbY/kn3IdWA5HdizR0FhlhD05zx5nKCqhzSfDHH3p4S0ZefxWd77DLV+8FhGg==", 110 + "dependencies": [ 111 + "@shikijs/vscode-textmate", 112 + "@types/hast" 113 + ] 114 + }, 115 + "@shikijs/vscode-textmate@10.0.2": { 116 + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==" 117 + }, 118 + "@types/hast@3.0.4": { 119 + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", 120 + "dependencies": [ 121 + "@types/unist" 122 + ] 123 + }, 69 124 "@types/linkify-it@3.0.5": { 70 125 "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==" 71 126 }, ··· 76 131 "@types/mdurl" 77 132 ] 78 133 }, 134 + "@types/mdast@4.0.4": { 135 + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", 136 + "dependencies": [ 137 + "@types/unist" 138 + ] 139 + }, 79 140 "@types/mdurl@2.0.0": { 80 141 "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==" 81 142 }, ··· 85 146 "undici-types" 86 147 ] 87 148 }, 149 + "@types/unist@3.0.3": { 150 + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" 151 + }, 152 + "@ungap/structured-clone@1.3.0": { 153 + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==" 154 + }, 155 + "ccount@2.0.1": { 156 + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==" 157 + }, 158 + "character-entities-html4@2.1.0": { 159 + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==" 160 + }, 161 + "character-entities-legacy@3.0.0": { 162 + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==" 163 + }, 164 + "comma-separated-tokens@2.0.3": { 165 + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==" 166 + }, 167 + "dequal@2.0.3": { 168 + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==" 169 + }, 170 + "devlop@1.1.0": { 171 + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", 172 + "dependencies": [ 173 + "dequal" 174 + ] 175 + }, 176 + "hast-util-is-element@3.0.0": { 177 + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", 178 + "dependencies": [ 179 + "@types/hast" 180 + ] 181 + }, 182 + "hast-util-to-html@9.0.5": { 183 + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", 184 + "dependencies": [ 185 + "@types/hast", 186 + "@types/unist", 187 + "ccount", 188 + "comma-separated-tokens", 189 + "hast-util-whitespace", 190 + "html-void-elements", 191 + "mdast-util-to-hast", 192 + "property-information", 193 + "space-separated-tokens", 194 + "stringify-entities", 195 + "zwitch" 196 + ] 197 + }, 198 + "hast-util-whitespace@3.0.0": { 199 + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", 200 + "dependencies": [ 201 + "@types/hast" 202 + ] 203 + }, 204 + "html-void-elements@3.0.0": { 205 + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==" 206 + }, 207 + "mdast-util-to-hast@13.2.0": { 208 + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", 209 + "dependencies": [ 210 + "@types/hast", 211 + "@types/mdast", 212 + "@ungap/structured-clone", 213 + "devlop", 214 + "micromark-util-sanitize-uri", 215 + "trim-lines", 216 + "unist-util-position", 217 + "unist-util-visit", 218 + "vfile" 219 + ] 220 + }, 221 + "micromark-util-character@2.1.1": { 222 + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", 223 + "dependencies": [ 224 + "micromark-util-symbol", 225 + "micromark-util-types" 226 + ] 227 + }, 228 + "micromark-util-encode@2.0.1": { 229 + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==" 230 + }, 231 + "micromark-util-sanitize-uri@2.0.1": { 232 + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", 233 + "dependencies": [ 234 + "micromark-util-character", 235 + "micromark-util-encode", 236 + "micromark-util-symbol" 237 + ] 238 + }, 239 + "micromark-util-symbol@2.0.1": { 240 + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==" 241 + }, 242 + "micromark-util-types@2.0.2": { 243 + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==" 244 + }, 245 + "oniguruma-parser@0.12.1": { 246 + "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==" 247 + }, 248 + "oniguruma-to-es@4.3.3": { 249 + "integrity": "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==", 250 + "dependencies": [ 251 + "oniguruma-parser", 252 + "regex", 253 + "regex-recursion" 254 + ] 255 + }, 256 + "property-information@7.1.0": { 257 + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==" 258 + }, 259 + "regex-recursion@6.0.2": { 260 + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", 261 + "dependencies": [ 262 + "regex-utilities" 263 + ] 264 + }, 265 + "regex-utilities@2.3.0": { 266 + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==" 267 + }, 268 + "regex@6.0.1": { 269 + "integrity": "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==", 270 + "dependencies": [ 271 + "regex-utilities" 272 + ] 273 + }, 274 + "shiki@3.8.1": { 275 + "integrity": "sha512-+MYIyjwGPCaegbpBeFN9+oOifI8CKiKG3awI/6h3JeT85c//H2wDW/xCJEGuQ5jPqtbboKNqNy+JyX9PYpGwNg==", 276 + "dependencies": [ 277 + "@shikijs/core", 278 + "@shikijs/engine-javascript", 279 + "@shikijs/engine-oniguruma", 280 + "@shikijs/langs", 281 + "@shikijs/themes", 282 + "@shikijs/types", 283 + "@shikijs/vscode-textmate", 284 + "@types/hast" 285 + ] 286 + }, 287 + "space-separated-tokens@2.0.2": { 288 + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==" 289 + }, 290 + "stringify-entities@4.0.4": { 291 + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", 292 + "dependencies": [ 293 + "character-entities-html4", 294 + "character-entities-legacy" 295 + ] 296 + }, 297 + "trim-lines@3.0.1": { 298 + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==" 299 + }, 88 300 "undici-types@6.21.0": { 89 301 "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" 302 + }, 303 + "unist-util-is@6.0.0": { 304 + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", 305 + "dependencies": [ 306 + "@types/unist" 307 + ] 308 + }, 309 + "unist-util-position@5.0.0": { 310 + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", 311 + "dependencies": [ 312 + "@types/unist" 313 + ] 314 + }, 315 + "unist-util-stringify-position@4.0.0": { 316 + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", 317 + "dependencies": [ 318 + "@types/unist" 319 + ] 320 + }, 321 + "unist-util-visit-parents@6.0.1": { 322 + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", 323 + "dependencies": [ 324 + "@types/unist", 325 + "unist-util-is" 326 + ] 327 + }, 328 + "unist-util-visit@5.0.0": { 329 + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", 330 + "dependencies": [ 331 + "@types/unist", 332 + "unist-util-is", 333 + "unist-util-visit-parents" 334 + ] 335 + }, 336 + "vfile-message@4.0.3": { 337 + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", 338 + "dependencies": [ 339 + "@types/unist", 340 + "unist-util-stringify-position" 341 + ] 342 + }, 343 + "vfile@6.0.3": { 344 + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", 345 + "dependencies": [ 346 + "@types/unist", 347 + "vfile-message" 348 + ] 349 + }, 350 + "zwitch@2.0.4": { 351 + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==" 90 352 } 91 353 }, 92 354 "redirects": { ··· 101 363 "dependencies": [ 102 364 "jsr:@cliffy/command@1.0.0-rc.8", 103 365 "jsr:@eta-dev/eta@^3.5.0", 104 - "jsr:@std/assert@1" 366 + "jsr:@std/assert@1", 367 + "npm:@types/hast@^3.0.4", 368 + "npm:hast-util-is-element@3", 369 + "npm:shiki@^3.8.1" 105 370 ] 106 371 } 107 372 }
+55 -2
parse/render.ts
··· 1 - import Markdoc, { Config, Node } from "@markdoc/markdoc"; 1 + import Markdoc, { 2 + Config, 3 + Node, 4 + RenderableTreeNode, 5 + Tag, 6 + } from "@markdoc/markdoc"; 7 + import { codeToHast } from "shiki"; 8 + import { isElement } from "hast-util-is-element"; 9 + import type { Node as HastNode } from "@types/hast"; 10 + 11 + function hastToRenderNode(node: HastNode): RenderableTreeNode { 12 + let tag: Tag; 13 + 14 + if (isElement(node)) { 15 + tag = new Tag( 16 + node.tagName, 17 + node.properties, 18 + node.children.map(hastToRenderNode), 19 + ); 20 + } else if ("value" in node) { 21 + return node.value as string; 22 + } else { 23 + tag = new Tag( 24 + "div", 25 + undefined, 26 + "children" in node 27 + ? (node.children as HastNode[]).map(hastToRenderNode) 28 + : undefined, 29 + ); 30 + } 31 + 32 + console.log(tag); 33 + 34 + return tag; 35 + } 2 36 3 37 const config: Config = { 4 38 tags: { ··· 9 43 render: "div", 10 44 }, 11 45 }, 46 + nodes: { 47 + fence: { 48 + render: "pre", 49 + attributes: { 50 + content: { type: "String", render: false }, 51 + language: { type: "String", render: false }, 52 + }, 53 + async transform(node) { 54 + const { content, language = "text" } = node.attributes; 55 + 56 + const hast = await codeToHast(content, { 57 + lang: language, 58 + theme: "rose-pine", 59 + }); 60 + 61 + return hastToRenderNode(hast); 62 + }, 63 + }, 64 + }, 12 65 }; 13 66 14 67 export async function renderBody(path: string) { ··· 37 90 38 91 const tree = Markdoc.transform(ast, config); 39 92 40 - return Markdoc.renderers.html(tree); 93 + return Markdoc.renderers.html(await Promise.resolve(tree)); 41 94 }
+23
slides.mdoc
··· 12 12 13 13 --- 14 14 15 + ## Some code 16 + 17 + ```typescript 18 + // And here's some code 19 + 20 + function hello() { 21 + return "hello" 22 + } 23 + 24 + console.log("hello") // Prints "hello" 25 + ``` 26 + 27 + --- 28 + 29 + ## And a diagram 30 + 31 + ```mermaid 32 + flowchart LR 33 + Hello --> World 34 + ``` 35 + 36 + --- 37 + 15 38 ## Thank You 16 39
+17 -1
templates/live-reload.eta
··· 26 26 margin: 0; 27 27 } 28 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 + 29 40 article { 30 41 width: 100vw; 31 42 height: 100vh; ··· 41 52 position: relative; 42 53 } 43 54 44 - section div { 55 + section > div { 45 56 container: slide / inline-size; 46 57 box-sizing: border-box; 47 58 position: absolute; ··· 92 103 justify-content: center; 93 104 } 94 105 106 + .shiki { 107 + padding: 2cqi; 108 + font-size: 2cqi; 109 + border-radius: 1em; 110 + } 95 111 </style> 96 112 </head> 97 113 <body>