diff --git a/deno.lock b/deno.lock index 7407574..901bfb7 100644 --- a/deno.lock +++ b/deno.lock @@ -11,6 +11,7 @@ "jsr:@eta-dev/eta@^3.5.0": "3.5.0", "jsr:@std/async@^1.0.14": "1.0.14", "jsr:@std/cli@^1.0.21": "1.0.21", + "jsr:@std/crypto@^1.0.5": "1.0.5", "jsr:@std/encoding@^1.0.10": "1.0.10", "jsr:@std/fmt@^1.0.8": "1.0.8", "jsr:@std/fmt@~1.0.2": "1.0.8", @@ -26,6 +27,7 @@ "jsr:@std/text@~1.0.7": "1.0.15", "npm:@lit/context@^1.1.6": "1.1.6", "npm:@markdoc/markdoc@*": "0.5.2", + "npm:@types/blobshape@^1.0.3": "1.0.3", "npm:@types/hast@^3.0.4": "3.0.4", "npm:@types/node@*": "22.15.15", "npm:esbuild@~0.25.5": "0.25.8", @@ -33,7 +35,9 @@ "npm:esbuild@~0.25.9": "0.25.9", "npm:hast-util-is-element@3": "3.0.0", "npm:lit@^3.3.1": "3.3.1", + "npm:mini-svg-data-uri@^1.4.4": "1.4.4", "npm:motion@^12.23.12": "12.23.12", + "npm:polished@^4.3.1": "4.3.1", "npm:shiki@^3.8.1": "3.8.1", "npm:xstate@^5.20.2": "5.20.2" }, @@ -86,6 +90,9 @@ "@std/cli@1.0.21": { "integrity": "cd25b050bdf6282e321854e3822bee624f07aca7636a3a76d95f77a3a919ca2a" }, + "@std/crypto@1.0.5": { + "integrity": "0dcfbb319fe0bba1bd3af904ceb4f948cde1b92979ec1614528380ed308a3b40" + }, "@std/encoding@1.0.10": { "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1" }, @@ -135,6 +142,9 @@ } }, "npm": { + "@babel/runtime@7.28.3": { + "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==" + }, "@esbuild/aix-ppc64@0.25.8": { "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", "os": ["aix"], @@ -463,6 +473,9 @@ "@shikijs/vscode-textmate@10.0.2": { "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==" }, + "@types/blobshape@1.0.3": { + "integrity": "sha512-jbJZ9AIebK77LoeN8PjPyAs88GJCDKO+ZpWCJlLlV2FXkeYScQsvkZmlGHGvSRk/WFmUOdbGJ75umLATGX1lsw==" + }, "@types/hast@3.0.4": { "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", "dependencies": [ @@ -689,6 +702,10 @@ "micromark-util-types@2.0.2": { "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==" }, + "mini-svg-data-uri@1.4.4": { + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "bin": true + }, "motion-dom@12.23.12": { "integrity": "sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==", "dependencies": [ @@ -716,6 +733,12 @@ "regex-recursion" ] }, + "polished@4.3.1": { + "integrity": "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==", + "dependencies": [ + "@babel/runtime" + ] + }, "property-information@7.1.0": { "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==" }, @@ -825,6 +848,11 @@ "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" }, "remote": { + "https://deno.land/std@0.93.0/hash/sha256.ts": "2a06afd9c27942b87ffc8a93b3270065b5fe4ea144fe0939e5d050bfb86d40db", + "https://deno.land/x/color_hash@v2.0.1/lib/bkdr-hash.ts": "8005d57742d5d8779d8bafd86e9f142ebdb6f350514f89b451036abf8bff428b", + "https://deno.land/x/color_hash@v2.0.1/lib/colors.ts": "924fa3b20409325afe408f30ed6de539c08ea66916900642e9c6fa57af1487a5", + "https://deno.land/x/color_hash@v2.0.1/lib/sha256.ts": "7a1aa66825cb75e2554ff942ff110048fb372cf261512093c2aa79105d44e716", + "https://deno.land/x/color_hash@v2.0.1/mod.ts": "67d9e365f72652d14b5b723a1eb165297f990757ba1c0ac5d5612e755d76c2ca", "https://esm.sh/@markdoc/markdoc@0.5.2": "76be956a8424e9b05c24b594a7714b39730fbd2b8e577db754606a211dfb4b89", "https://esm.sh/@markdoc/markdoc@0.5.2/denonext/markdoc.mjs": "3d8780797f37e2a81843a33cea9081ad7c500f49549badc6aee2b1bec5d97149" }, @@ -841,9 +869,14 @@ "packages/core": { "dependencies": [ "jsr:@eta-dev/eta@^3.5.0", + "jsr:@std/crypto@^1.0.5", + "jsr:@std/encoding@^1.0.10", "jsr:@std/path@^1.1.1", + "npm:@types/blobshape@^1.0.3", "npm:@types/hast@^3.0.4", "npm:hast-util-is-element@3", + "npm:mini-svg-data-uri@^1.4.4", + "npm:polished@^4.3.1", "npm:shiki@^3.8.1" ] }, diff --git a/packages/core/deno.json b/packages/core/deno.json index 41de566..28b0c84 100644 --- a/packages/core/deno.json +++ b/packages/core/deno.json @@ -5,9 +5,15 @@ "imports": { "@eta-dev/eta": "jsr:@eta-dev/eta@^3.5.0", "@markdoc/markdoc": "https://esm.sh/@markdoc/markdoc@0.5.2", + "@std/crypto": "jsr:@std/crypto@^1.0.5", + "@std/encoding": "jsr:@std/encoding@^1.0.10", "@std/path": "jsr:@std/path@^1.1.1", + "@types/blobshape": "npm:@types/blobshape@^1.0.3", + "color-hash": "https://deno.land/x/color_hash@v2.0.1/mod.ts", "hast": "npm:@types/hast@^3.0.4", "hast-util-is-element": "npm:hast-util-is-element@^3.0.0", + "mini-svg-data-uri": "npm:mini-svg-data-uri@^1.4.4", + "polished": "npm:polished@^4.3.1", "shiki": "npm:shiki@^3.8.1" } } diff --git a/packages/core/favicon.ts b/packages/core/favicon.ts new file mode 100644 index 0000000..3c9d172 --- /dev/null +++ b/packages/core/favicon.ts @@ -0,0 +1,46 @@ +import { crypto } from "@std/crypto"; +import { encodeHex } from "@std/encoding/hex"; +import ColorHash from "color-hash"; +import svgToUri from "mini-svg-data-uri"; +import { adjustHue, hsl } from "polished"; + +const colorHash = new ColorHash(); + +/** + * Generates a data URI for a favicon SVG based on content hash + * @param content - The content to hash for favicon generation + * @param size - The size of the favicon (default: 16) + * @returns A data URI string for the generated favicon + */ +export async function generateFavicon( + content: string, + size: number = 16, +): Promise { + const encoder = new TextEncoder(); + const hash = await crypto.subtle.digest("SHA-256", encoder.encode(content)); + const hashStr = encodeHex(hash); + + // Generate colors + const baseColor = hsl(...colorHash.hsl(hashStr)); + const complementaryColor = adjustHue(180, baseColor); + + // Generate shapes and construct SVG directly + const rectRadius = size * 0.25; + const center = size / 2; + const circleRadius = size * 0.25; + + const svg = `` + + `` + + `` + + ``; + + return svgToUri(svg); +} \ No newline at end of file diff --git a/packages/core/renderer.ts b/packages/core/renderer.ts index 710c937..5bffd2f 100644 --- a/packages/core/renderer.ts +++ b/packages/core/renderer.ts @@ -1,5 +1,6 @@ import Markdoc, { Node } from "@markdoc/markdoc"; import { createMarkdocConfig } from "./markdoc-config.ts"; +import { generateFavicon } from "./favicon.ts"; import { Eta } from "@eta-dev/eta"; import { resolve } from "@std/path"; import type { RenderOptions } from "./types.ts"; @@ -12,6 +13,7 @@ export async function renderPresentationHtml( path: string, options?: RenderOptions, ) { + const encoder = new TextEncoder(); const file = await Deno.readTextFile(path); const ast = Markdoc.parse(file); @@ -40,12 +42,17 @@ export async function renderPresentationHtml( includes.add("live-reload"); } + // Extract and/or set title const titleMatch = file.match(/^# (.+)$/m); const title = titleMatch ? titleMatch[1] : "Presentation"; + // Generate favicon + const favicon = await generateFavicon(file, 16); + return eta.render("presentation", { body: Markdoc.renderers.html(await Promise.resolve(tree)), title, includes, + favicon, }); } diff --git a/packages/core/templates/partials/favicon.eta b/packages/core/templates/partials/favicon.eta new file mode 100644 index 0000000..e133482 --- /dev/null +++ b/packages/core/templates/partials/favicon.eta @@ -0,0 +1,5 @@ + diff --git a/packages/core/templates/presentation.eta b/packages/core/templates/presentation.eta index b020392..3fdf92c 100644 --- a/packages/core/templates/presentation.eta +++ b/packages/core/templates/presentation.eta @@ -3,6 +3,8 @@ <%= it.title || (it.devMode ? "Dev" : "Presentation") %> + <%~ include("./partials/favicon", { favicon: it.favicon }) %> + <%~ include("./partials/slide-styles") %>