diff options
| author | Adam Malczewski <[email protected]> | 2026-05-20 13:28:08 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-05-20 13:28:08 +0900 |
| commit | 19144599bcb6d6a180d4a2cfe1bdc7f58780ac0c (patch) | |
| tree | c70e98d9ec5f86bc91634c67cede795ea40ec604 | |
| parent | a41b4c2482b8134ca8a7daca168af4ecc6cc8492 (diff) | |
| download | dispatch-19144599bcb6d6a180d4a2cfe1bdc7f58780ac0c.tar.gz dispatch-19144599bcb6d6a180d4a2cfe1bdc7f58780ac0c.zip | |
fix: highlight.js crash from unknown languages, add lazy loading and hot preload set
| -rw-r--r-- | packages/frontend/src/lib/components/MarkdownRenderer.svelte | 135 |
1 files changed, 125 insertions, 10 deletions
diff --git a/packages/frontend/src/lib/components/MarkdownRenderer.svelte b/packages/frontend/src/lib/components/MarkdownRenderer.svelte index 808159e..88dd42a 100644 --- a/packages/frontend/src/lib/components/MarkdownRenderer.svelte +++ b/packages/frontend/src/lib/components/MarkdownRenderer.svelte @@ -2,30 +2,134 @@ import { Marked } from "marked"; import { markedHighlight } from "marked-highlight"; import hljs from "highlight.js/lib/core"; + // Hot set — matches roughly what ChatGPT preloads. Registered eagerly so + // common code blocks highlight on first paint without a network roundtrip. import bash from "highlight.js/lib/languages/bash"; + import c from "highlight.js/lib/languages/c"; + import cpp from "highlight.js/lib/languages/cpp"; + import csharp from "highlight.js/lib/languages/csharp"; + import css from "highlight.js/lib/languages/css"; + import go from "highlight.js/lib/languages/go"; + import java from "highlight.js/lib/languages/java"; import javascript from "highlight.js/lib/languages/javascript"; import json from "highlight.js/lib/languages/json"; + import markdown from "highlight.js/lib/languages/markdown"; + import php from "highlight.js/lib/languages/php"; + import plaintext from "highlight.js/lib/languages/plaintext"; import python from "highlight.js/lib/languages/python"; + import ruby from "highlight.js/lib/languages/ruby"; + import rust from "highlight.js/lib/languages/rust"; + import shell from "highlight.js/lib/languages/shell"; + import sql from "highlight.js/lib/languages/sql"; import typescript from "highlight.js/lib/languages/typescript"; + import xml from "highlight.js/lib/languages/xml"; + import yaml from "highlight.js/lib/languages/yaml"; hljs.registerLanguage("bash", bash); - hljs.registerLanguage("sh", bash); - hljs.registerLanguage("shell", bash); + hljs.registerLanguage("c", c); + hljs.registerLanguage("cpp", cpp); + hljs.registerLanguage("csharp", csharp); + hljs.registerLanguage("css", css); + hljs.registerLanguage("go", go); + hljs.registerLanguage("java", java); hljs.registerLanguage("javascript", javascript); - hljs.registerLanguage("js", javascript); hljs.registerLanguage("json", json); + hljs.registerLanguage("markdown", markdown); + hljs.registerLanguage("php", php); + hljs.registerLanguage("plaintext", plaintext); hljs.registerLanguage("python", python); - hljs.registerLanguage("py", python); + hljs.registerLanguage("ruby", ruby); + hljs.registerLanguage("rust", rust); + hljs.registerLanguage("shell", shell); + hljs.registerLanguage("sql", sql); hljs.registerLanguage("typescript", typescript); - hljs.registerLanguage("ts", typescript); + hljs.registerLanguage("xml", xml); + hljs.registerLanguage("yaml", yaml); + + // Normalize common aliases to their canonical highlight.js language names. + // The canonical name is what we'll attempt to dynamically import. + const ALIASES: Record<string, string> = { + js: "javascript", + jsx: "javascript", + mjs: "javascript", + cjs: "javascript", + ts: "typescript", + tsx: "typescript", + py: "python", + py3: "python", + rb: "ruby", + sh: "bash", + shell: "bash", + zsh: "bash", + yml: "yaml", + "c++": "cpp", + cxx: "cpp", + "c#": "csharp", + cs: "csharp", + htm: "xml", + html: "xml", + svg: "xml", + md: "markdown", + mdx: "markdown", + dockerfile: "dockerfile", + golang: "go", + rs: "rust", + kt: "kotlin", + ps1: "powershell", + }; + + function normalizeLang(lang: string): string { + const lower = lang.toLowerCase().trim(); + return ALIASES[lower] ?? lower; + } + + const loadCache = new Map<string, Promise<boolean>>(); + + async function ensureLanguage(lang: string): Promise<boolean> { + const name = normalizeLang(lang); + if (hljs.getLanguage(name)) return true; + if (loadCache.has(name)) return loadCache.get(name)!; + + const promise = (async () => { + try { + // Vite resolves this template literal at build time into a glob + // over all matching language modules, so each is a separate chunk. + const mod = await import(`highlight.js/lib/languages/${name}`); + hljs.registerLanguage(name, mod.default); + return true; + } catch { + return false; + } + })(); + + loadCache.set(name, promise); + return promise; + } + + function escapeHtml(s: string): string { + return s + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } const md = new Marked( markedHighlight({ emptyLangClass: "hljs", langPrefix: "hljs language-", - highlight(code: string, lang: string) { - const language = hljs.getLanguage(lang) ? lang : "plaintext"; - return hljs.highlight(code, { language, ignoreIllegals: true }).value; + async: true, + async highlight(code: string, lang: string): Promise<string> { + if (!lang) return escapeHtml(code); + const name = normalizeLang(lang); + const loaded = await ensureLanguage(name); + if (!loaded) return escapeHtml(code); + try { + return hljs.highlight(code, { language: name, ignoreIllegals: true }).value; + } catch { + return escapeHtml(code); + } }, }), { @@ -47,9 +151,20 @@ return out; } - const html = $derived.by(() => { + let html = $state(""); + let renderToken = 0; + + $effect(() => { const src = streaming ? closeOpenDelimiters(text) : text; - return md.parse(src) as string; + const myToken = ++renderToken; + (async () => { + try { + const result = (await md.parse(src)) as string; + if (myToken === renderToken) html = result; + } catch { + // swallow — keeps last successful render visible + } + })(); }); </script> |
