Generate web slides from Markdoc

feat(core): generate favicon from content

graham.systems 571e1ac6 c9551333

verified
Changed files
+99
packages
+33
deno.lock
··· 11 "jsr:@eta-dev/eta@^3.5.0": "3.5.0", 12 "jsr:@std/async@^1.0.14": "1.0.14", 13 "jsr:@std/cli@^1.0.21": "1.0.21", 14 "jsr:@std/encoding@^1.0.10": "1.0.10", 15 "jsr:@std/fmt@^1.0.8": "1.0.8", 16 "jsr:@std/fmt@~1.0.2": "1.0.8", ··· 26 "jsr:@std/text@~1.0.7": "1.0.15", 27 "npm:@lit/context@^1.1.6": "1.1.6", 28 "npm:@markdoc/markdoc@*": "0.5.2", 29 "npm:@types/hast@^3.0.4": "3.0.4", 30 "npm:@types/node@*": "22.15.15", 31 "npm:esbuild@~0.25.5": "0.25.8", ··· 33 "npm:esbuild@~0.25.9": "0.25.9", 34 "npm:hast-util-is-element@3": "3.0.0", 35 "npm:lit@^3.3.1": "3.3.1", 36 "npm:motion@^12.23.12": "12.23.12", 37 "npm:shiki@^3.8.1": "3.8.1", 38 "npm:xstate@^5.20.2": "5.20.2" 39 }, ··· 86 "@std/cli@1.0.21": { 87 "integrity": "cd25b050bdf6282e321854e3822bee624f07aca7636a3a76d95f77a3a919ca2a" 88 }, 89 "@std/encoding@1.0.10": { 90 "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1" 91 }, ··· 135 } 136 }, 137 "npm": { 138 "@esbuild/aix-ppc64@0.25.8": { 139 "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", 140 "os": ["aix"], ··· 463 "@shikijs/vscode-textmate@10.0.2": { 464 "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==" 465 }, 466 "@types/hast@3.0.4": { 467 "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", 468 "dependencies": [ ··· 688 }, 689 "micromark-util-types@2.0.2": { 690 "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==" 691 }, 692 "motion-dom@12.23.12": { 693 "integrity": "sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==", ··· 716 "regex-recursion" 717 ] 718 }, 719 "property-information@7.1.0": { 720 "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==" 721 }, ··· 825 "https://esm.sh/@types/markdown-it@~14.1.2/lib/token.d.mts": "https://esm.sh/@types/markdown-it@14.1.2/lib/token.d.mts" 826 }, 827 "remote": { 828 "https://esm.sh/@markdoc/markdoc@0.5.2": "76be956a8424e9b05c24b594a7714b39730fbd2b8e577db754606a211dfb4b89", 829 "https://esm.sh/@markdoc/markdoc@0.5.2/denonext/markdoc.mjs": "3d8780797f37e2a81843a33cea9081ad7c500f49549badc6aee2b1bec5d97149" 830 }, ··· 841 "packages/core": { 842 "dependencies": [ 843 "jsr:@eta-dev/eta@^3.5.0", 844 "jsr:@std/path@^1.1.1", 845 "npm:@types/hast@^3.0.4", 846 "npm:hast-util-is-element@3", 847 "npm:shiki@^3.8.1" 848 ] 849 },
··· 11 "jsr:@eta-dev/eta@^3.5.0": "3.5.0", 12 "jsr:@std/async@^1.0.14": "1.0.14", 13 "jsr:@std/cli@^1.0.21": "1.0.21", 14 + "jsr:@std/crypto@^1.0.5": "1.0.5", 15 "jsr:@std/encoding@^1.0.10": "1.0.10", 16 "jsr:@std/fmt@^1.0.8": "1.0.8", 17 "jsr:@std/fmt@~1.0.2": "1.0.8", ··· 27 "jsr:@std/text@~1.0.7": "1.0.15", 28 "npm:@lit/context@^1.1.6": "1.1.6", 29 "npm:@markdoc/markdoc@*": "0.5.2", 30 + "npm:@types/blobshape@^1.0.3": "1.0.3", 31 "npm:@types/hast@^3.0.4": "3.0.4", 32 "npm:@types/node@*": "22.15.15", 33 "npm:esbuild@~0.25.5": "0.25.8", ··· 35 "npm:esbuild@~0.25.9": "0.25.9", 36 "npm:hast-util-is-element@3": "3.0.0", 37 "npm:lit@^3.3.1": "3.3.1", 38 + "npm:mini-svg-data-uri@^1.4.4": "1.4.4", 39 "npm:motion@^12.23.12": "12.23.12", 40 + "npm:polished@^4.3.1": "4.3.1", 41 "npm:shiki@^3.8.1": "3.8.1", 42 "npm:xstate@^5.20.2": "5.20.2" 43 }, ··· 90 "@std/cli@1.0.21": { 91 "integrity": "cd25b050bdf6282e321854e3822bee624f07aca7636a3a76d95f77a3a919ca2a" 92 }, 93 + "@std/crypto@1.0.5": { 94 + "integrity": "0dcfbb319fe0bba1bd3af904ceb4f948cde1b92979ec1614528380ed308a3b40" 95 + }, 96 "@std/encoding@1.0.10": { 97 "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1" 98 }, ··· 142 } 143 }, 144 "npm": { 145 + "@babel/runtime@7.28.3": { 146 + "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==" 147 + }, 148 "@esbuild/aix-ppc64@0.25.8": { 149 "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", 150 "os": ["aix"], ··· 473 "@shikijs/vscode-textmate@10.0.2": { 474 "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==" 475 }, 476 + "@types/blobshape@1.0.3": { 477 + "integrity": "sha512-jbJZ9AIebK77LoeN8PjPyAs88GJCDKO+ZpWCJlLlV2FXkeYScQsvkZmlGHGvSRk/WFmUOdbGJ75umLATGX1lsw==" 478 + }, 479 "@types/hast@3.0.4": { 480 "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", 481 "dependencies": [ ··· 701 }, 702 "micromark-util-types@2.0.2": { 703 "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==" 704 + }, 705 + "mini-svg-data-uri@1.4.4": { 706 + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", 707 + "bin": true 708 }, 709 "motion-dom@12.23.12": { 710 "integrity": "sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==", ··· 733 "regex-recursion" 734 ] 735 }, 736 + "polished@4.3.1": { 737 + "integrity": "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==", 738 + "dependencies": [ 739 + "@babel/runtime" 740 + ] 741 + }, 742 "property-information@7.1.0": { 743 "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==" 744 }, ··· 848 "https://esm.sh/@types/markdown-it@~14.1.2/lib/token.d.mts": "https://esm.sh/@types/markdown-it@14.1.2/lib/token.d.mts" 849 }, 850 "remote": { 851 + "https://deno.land/std@0.93.0/hash/sha256.ts": "2a06afd9c27942b87ffc8a93b3270065b5fe4ea144fe0939e5d050bfb86d40db", 852 + "https://deno.land/x/color_hash@v2.0.1/lib/bkdr-hash.ts": "8005d57742d5d8779d8bafd86e9f142ebdb6f350514f89b451036abf8bff428b", 853 + "https://deno.land/x/color_hash@v2.0.1/lib/colors.ts": "924fa3b20409325afe408f30ed6de539c08ea66916900642e9c6fa57af1487a5", 854 + "https://deno.land/x/color_hash@v2.0.1/lib/sha256.ts": "7a1aa66825cb75e2554ff942ff110048fb372cf261512093c2aa79105d44e716", 855 + "https://deno.land/x/color_hash@v2.0.1/mod.ts": "67d9e365f72652d14b5b723a1eb165297f990757ba1c0ac5d5612e755d76c2ca", 856 "https://esm.sh/@markdoc/markdoc@0.5.2": "76be956a8424e9b05c24b594a7714b39730fbd2b8e577db754606a211dfb4b89", 857 "https://esm.sh/@markdoc/markdoc@0.5.2/denonext/markdoc.mjs": "3d8780797f37e2a81843a33cea9081ad7c500f49549badc6aee2b1bec5d97149" 858 }, ··· 869 "packages/core": { 870 "dependencies": [ 871 "jsr:@eta-dev/eta@^3.5.0", 872 + "jsr:@std/crypto@^1.0.5", 873 + "jsr:@std/encoding@^1.0.10", 874 "jsr:@std/path@^1.1.1", 875 + "npm:@types/blobshape@^1.0.3", 876 "npm:@types/hast@^3.0.4", 877 "npm:hast-util-is-element@3", 878 + "npm:mini-svg-data-uri@^1.4.4", 879 + "npm:polished@^4.3.1", 880 "npm:shiki@^3.8.1" 881 ] 882 },
+6
packages/core/deno.json
··· 5 "imports": { 6 "@eta-dev/eta": "jsr:@eta-dev/eta@^3.5.0", 7 "@markdoc/markdoc": "https://esm.sh/@markdoc/markdoc@0.5.2", 8 "@std/path": "jsr:@std/path@^1.1.1", 9 "hast": "npm:@types/hast@^3.0.4", 10 "hast-util-is-element": "npm:hast-util-is-element@^3.0.0", 11 "shiki": "npm:shiki@^3.8.1" 12 } 13 }
··· 5 "imports": { 6 "@eta-dev/eta": "jsr:@eta-dev/eta@^3.5.0", 7 "@markdoc/markdoc": "https://esm.sh/@markdoc/markdoc@0.5.2", 8 + "@std/crypto": "jsr:@std/crypto@^1.0.5", 9 + "@std/encoding": "jsr:@std/encoding@^1.0.10", 10 "@std/path": "jsr:@std/path@^1.1.1", 11 + "@types/blobshape": "npm:@types/blobshape@^1.0.3", 12 + "color-hash": "https://deno.land/x/color_hash@v2.0.1/mod.ts", 13 "hast": "npm:@types/hast@^3.0.4", 14 "hast-util-is-element": "npm:hast-util-is-element@^3.0.0", 15 + "mini-svg-data-uri": "npm:mini-svg-data-uri@^1.4.4", 16 + "polished": "npm:polished@^4.3.1", 17 "shiki": "npm:shiki@^3.8.1" 18 } 19 }
+46
packages/core/favicon.ts
···
··· 1 + import { crypto } from "@std/crypto"; 2 + import { encodeHex } from "@std/encoding/hex"; 3 + import ColorHash from "color-hash"; 4 + import svgToUri from "mini-svg-data-uri"; 5 + import { adjustHue, hsl } from "polished"; 6 + 7 + const colorHash = new ColorHash(); 8 + 9 + /** 10 + * Generates a data URI for a favicon SVG based on content hash 11 + * @param content - The content to hash for favicon generation 12 + * @param size - The size of the favicon (default: 16) 13 + * @returns A data URI string for the generated favicon 14 + */ 15 + export async function generateFavicon( 16 + content: string, 17 + size: number = 16, 18 + ): Promise<string> { 19 + const encoder = new TextEncoder(); 20 + const hash = await crypto.subtle.digest("SHA-256", encoder.encode(content)); 21 + const hashStr = encodeHex(hash); 22 + 23 + // Generate colors 24 + const baseColor = hsl(...colorHash.hsl(hashStr)); 25 + const complementaryColor = adjustHue(180, baseColor); 26 + 27 + // Generate shapes and construct SVG directly 28 + const rectRadius = size * 0.25; 29 + const center = size / 2; 30 + const circleRadius = size * 0.25; 31 + 32 + const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewbox="0 0 ${size} ${size}">` + 33 + `<path d="M ${rectRadius.toFixed(2)} 0 ` + 34 + `L ${(size - rectRadius).toFixed(2)} 0 ` + 35 + `Q ${size} 0 ${size} ${rectRadius.toFixed(2)} ` + 36 + `L ${size} ${(size - rectRadius).toFixed(2)} ` + 37 + `Q ${size} ${size} ${(size - rectRadius).toFixed(2)} ${size} ` + 38 + `L ${rectRadius.toFixed(2)} ${size} ` + 39 + `Q 0 ${size} 0 ${(size - rectRadius).toFixed(2)} ` + 40 + `L 0 ${rectRadius.toFixed(2)} ` + 41 + `Q 0 0 ${rectRadius.toFixed(2)} 0 Z" fill="${baseColor}" />` + 42 + `<circle cx="${center}" cy="${center}" r="${circleRadius.toFixed(2)}" fill="${complementaryColor}" />` + 43 + `</svg>`; 44 + 45 + return svgToUri(svg); 46 + }
+7
packages/core/renderer.ts
··· 1 import Markdoc, { Node } from "@markdoc/markdoc"; 2 import { createMarkdocConfig } from "./markdoc-config.ts"; 3 import { Eta } from "@eta-dev/eta"; 4 import { resolve } from "@std/path"; 5 import type { RenderOptions } from "./types.ts"; ··· 12 path: string, 13 options?: RenderOptions, 14 ) { 15 const file = await Deno.readTextFile(path); 16 const ast = Markdoc.parse(file); 17 ··· 40 includes.add("live-reload"); 41 } 42 43 const titleMatch = file.match(/^# (.+)$/m); 44 const title = titleMatch ? titleMatch[1] : "Presentation"; 45 46 return eta.render("presentation", { 47 body: Markdoc.renderers.html(await Promise.resolve(tree)), 48 title, 49 includes, 50 }); 51 }
··· 1 import Markdoc, { Node } from "@markdoc/markdoc"; 2 import { createMarkdocConfig } from "./markdoc-config.ts"; 3 + import { generateFavicon } from "./favicon.ts"; 4 import { Eta } from "@eta-dev/eta"; 5 import { resolve } from "@std/path"; 6 import type { RenderOptions } from "./types.ts"; ··· 13 path: string, 14 options?: RenderOptions, 15 ) { 16 + const encoder = new TextEncoder(); 17 const file = await Deno.readTextFile(path); 18 const ast = Markdoc.parse(file); 19 ··· 42 includes.add("live-reload"); 43 } 44 45 + // Extract and/or set title 46 const titleMatch = file.match(/^# (.+)$/m); 47 const title = titleMatch ? titleMatch[1] : "Presentation"; 48 49 + // Generate favicon 50 + const favicon = await generateFavicon(file, 16); 51 + 52 return eta.render("presentation", { 53 body: Markdoc.renderers.html(await Promise.resolve(tree)), 54 title, 55 includes, 56 + favicon, 57 }); 58 }
+5
packages/core/templates/partials/favicon.eta
···
··· 1 + <link 2 + rel="icon" 3 + sizes="all" 4 + href="<%= it.favicon %>" 5 + />
+2
packages/core/templates/presentation.eta
··· 3 <head> 4 <title><%= it.title || (it.devMode ? "Dev" : "Presentation") %></title> 5 6 <%~ include("./partials/slide-styles") %> 7 8 <script src="static/bundle.js"></script>
··· 3 <head> 4 <title><%= it.title || (it.devMode ? "Dev" : "Presentation") %></title> 5 6 + <%~ include("./partials/favicon", { favicon: it.favicon }) %> 7 + 8 <%~ include("./partials/slide-styles") %> 9 10 <script src="static/bundle.js"></script>