diff --git a/packages/astro-md/middleware.ts b/packages/astro-md/middleware.ts index 052c6ef..58c938d 100644 --- a/packages/astro-md/middleware.ts +++ b/packages/astro-md/middleware.ts @@ -6,6 +6,62 @@ import { myRemark } from "../my-remark"; import { remarkObsidian } from "../remark-obsidian"; import remarkFrontmatter from "remark-frontmatter"; +const decode = (str: string) => + str.replace(/&#x(\d+);/g, (_, hex) => String.fromCharCode(parseInt(hex, 16))); + +const theme = "nord"; + +class SyntaxHighlightRewriter implements HTMLRewriterElementContentHandlers { + private lang: string = ""; + private code: string = ""; + cache: KVNamespace; + + constructor(private runtime: Runtime["runtime"]) { + this.cache = runtime.env.KV_HIGHLIGHT; + } + + element(element: Element) { + this.lang = element.getAttribute("data-lang") ?? ""; + this.code = ""; + element.removeAndKeepContent(); + } + async text(text: Text) { + this.code += decode(text.text); + if (text.lastInTextNode) { + const hashBuffer = await crypto.subtle.digest( + "SHA-1", + new TextEncoder().encode(`${theme}-${this.lang}-${this.code}`) + ); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hash = hashArray + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); + + const cachedResult = await this.cache.get(hash); + if (cachedResult) { + text.replace(cachedResult, { html: true }); + return; + } + + const res = await fetch( + `https://highlight.val.just-be.dev?lang=${this.lang}&theme=${theme}`, + { + method: "POST", + body: this.code, + headers: { + Accept: "text/html", + }, + } + ); + const data = await res.text(); + this.runtime.ctx.waitUntil(this.cache.put(hash, data)); + text.replace(data, { html: true }); + } else { + text.remove(); + } + } +} + export const onRequest = defineMiddleware(async (context, next) => { const fileResolver = async (path: string) => { return (await context.locals.runtime.env.KV_MAPPINGS.get(path)) ?? path; @@ -18,5 +74,29 @@ export const onRequest = defineMiddleware(async (context, next) => { ], }); context.locals.render = processor.render; - return next(); + const res = await next(); + + try { + if (import.meta.env.DEV && typeof HTMLRewriter === "undefined") { + // @ts-expect-error Isn't defined on globalThis, but that's fine + globalThis.HTMLRewriter = ( + await import("@worker-tools/html-rewriter/base64") + ).HTMLRewriter; + } + + return new HTMLRewriter() + .on( + "code[data-lang]", + new SyntaxHighlightRewriter(context.locals.runtime) + ) + .on("pre:not(.shiki)", { + element: (element) => { + element.removeAndKeepContent(); + }, + }) + .transform(res); + } catch (e) { + console.error(e); + return res; + } }); diff --git a/packages/astro-md/package.json b/packages/astro-md/package.json index a27852e..9defc54 100644 --- a/packages/astro-md/package.json +++ b/packages/astro-md/package.json @@ -12,5 +12,8 @@ "dependencies": { "@astrojs/markdown-remark": "catalog:", "remark-frontmatter": "^5.0.0" + }, + "devDependencies": { + "@worker-tools/html-rewriter": "0.1.0-pre.19" } } diff --git a/packages/remark-obsidian/index.ts b/packages/remark-obsidian/index.ts index b6b163d..6595e06 100644 --- a/packages/remark-obsidian/index.ts +++ b/packages/remark-obsidian/index.ts @@ -1,9 +1,9 @@ -import type { RemarkPlugin } from "@astrojs/markdown-remark"; +import type { Node, RemarkPlugin } from "@astrojs/markdown-remark"; import { visit } from "unist-util-visit"; import internalLinkPlugin from "./internal-link"; import embedPlugin from "./embed"; import { href } from "./internal-link/utils"; -import type { EmbedNode, InternalLinkNode } from "./types"; +import type { CodeNode, EmbedNode, InternalLinkNode } from "./types"; interface RemarkObsidianOptions { fileResolver?: (path: string) => Promise; @@ -48,6 +48,16 @@ export const remarkObsidian = ( break; } }); + + visit(root, "code", (node: CodeNode) => { + if (node.lang) { + node.data = { + hProperties: { + ["data-lang"]: node.lang, + }, + }; + } + }); }; }; diff --git a/packages/remark-obsidian/types.d.ts b/packages/remark-obsidian/types.d.ts index f5bd37e..67b57d7 100644 --- a/packages/remark-obsidian/types.d.ts +++ b/packages/remark-obsidian/types.d.ts @@ -26,6 +26,13 @@ export interface EmbedNode { data?: Record; } +export interface CodeNode { + type: "code"; + lang?: string | null; + value?: string; + data?: Record; +} + declare module "micromark-util-types" { export interface TokenTypeMap extends TokenTypeMap { internalLink: "internalLink"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 356afa1..90353d3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,6 +83,10 @@ importers: remark-frontmatter: specifier: ^5.0.0 version: 5.0.0 + devDependencies: + '@worker-tools/html-rewriter': + specifier: 0.1.0-pre.19 + version: 0.1.0-pre.19 packages/my-remark: dependencies: @@ -1313,6 +1317,12 @@ packages: '@shikijs/vscode-textmate@9.2.2': resolution: {integrity: sha512-TMp15K+GGYrWlZM8+Lnj9EaHEFmOen0WJBrfa17hF7taDOYthuPPV0GWzfd/9iMij0akS/8Yw2ikquH7uVi/fg==} + '@stardazed/streams-compression@1.0.0': + resolution: {integrity: sha512-SQCiwxdIVJ5xxUBYptN+fc+tJpSDbxQuJ0+3u2SmHjIzr6JIRZ28AVFtFnGy6x6j3UBlaLx73w9rC6UAFxnd1g==} + + '@stardazed/zlib@1.0.1': + resolution: {integrity: sha512-MS6PCYiRNJ32c69+3NPyL+bu7LpiFDvMcBXnlhQnF8CKMG0R/xRVCCo422NPkQ0+Cox3xKpk5Lc1LfWFS4b3cw==} + '@tailwindcss/typography@0.5.15': resolution: {integrity: sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA==} peerDependencies: @@ -1419,6 +1429,12 @@ packages: '@vscode/l10n@0.0.18': resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} + '@worker-tools/html-rewriter@0.1.0-pre.19': + resolution: {integrity: sha512-IUbEZwvSdp8vtaB1LAZ/sBRJGNycd9a8cbkqO05rMEPm5MC59Sb+wiGCWaaRXrwQLDAyWeod4QC/q0TuSUI5EA==} + + '@worker-tools/resolvable-promise@0.2.0-pre.6': + resolution: {integrity: sha512-+5RcuruCLB/P3FYYb376RgyImXaq9BBQoOskUbgPyNK3O0AXPakrXUj1EELv+lcnUSdFws/ufdrrWODLJyoTmg==} + acorn-walk@8.3.3: resolution: {integrity: sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==} engines: {node: '>=0.4.0'} @@ -4435,6 +4451,12 @@ snapshots: '@shikijs/vscode-textmate@9.2.2': {} + '@stardazed/streams-compression@1.0.0': + dependencies: + '@stardazed/zlib': 1.0.1 + + '@stardazed/zlib@1.0.1': {} + '@tailwindcss/typography@0.5.15(tailwindcss@3.4.13)': dependencies: lodash.castarray: 4.4.0 @@ -4593,6 +4615,13 @@ snapshots: '@vscode/l10n@0.0.18': {} + '@worker-tools/html-rewriter@0.1.0-pre.19': + dependencies: + '@stardazed/streams-compression': 1.0.0 + '@worker-tools/resolvable-promise': 0.2.0-pre.6 + + '@worker-tools/resolvable-promise@0.2.0-pre.6': {} + acorn-walk@8.3.3: dependencies: acorn: 8.12.1 diff --git a/src/style.css b/src/style.css index 94f6985..6672ab7 100644 --- a/src/style.css +++ b/src/style.css @@ -48,3 +48,7 @@ blockquote ul { @apply inline-block size-[0.7rem] lg:size-[0.9rem] ml-[0.2rem] mb-[0.45rem] lg:mb-[0.6rem] align-bottom text-slate-300 stroke-slate-500; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6'%3E%3C/path%3E%3Cpolyline points='15 3 21 3 21 9'%3E%3C/polyline%3E%3Cline x1='10' y1='14' x2='21' y2='3'%3E%3C/line%3E%3C/svg%3E"); } + +.prose pre:has(code[data-lang]) { + @apply bg-inherit my-0; +} diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index 1424cef..de5dd23 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -1,8 +1,8 @@ -// Generated by Wrangler on Wed Aug 21 2024 20:59:22 GMT-0400 (Eastern Daylight Time) -// by running `wrangler types` +// Generated by Wrangler by running `wrangler types` interface Env { KV_MAPPINGS: KVNamespace; + KV_HIGHLIGHT: KVNamespace; NODE_VERSION: "v20.16.0"; PNPM_VERSION: "v9.7.0"; SITE: string; diff --git a/wrangler.toml b/wrangler.toml index 3096158..1b8fbe5 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -11,6 +11,10 @@ PNPM_VERSION = "v9.7.0" binding = "KV_MAPPINGS" id = "36e4319084c4498fa08e5bf00f36331a" +[[kv_namespaces]] +binding = "KV_HIGHLIGHT" +id = "9503a7c1e6f948cb8f1754c08e046179" + [[r2_buckets]] binding = "R2_BUCKET" bucket_name = "just-be-dev"