diff --git a/src/hooks/useMarkdown.hook.ts b/src/hooks/useMarkdown.hook.ts index 223e9ac..5fa6562 100644 --- a/src/hooks/useMarkdown.hook.ts +++ b/src/hooks/useMarkdown.hook.ts @@ -25,6 +25,7 @@ import { } from "@/utils/decodeBase64ToUTF8" import { html5Media } from "@/utils/markdown/markdown-html5-media" import { markdownItTablerIcons } from "@/utils/markdown/markdown-it-tabler-icons" +import { renderFallback } from "@/utils/markdown/renderFallback" const TIKZ_BUNDLE_URL = "https://cdn.jsdelivr.net/gh/artisticat1/obsidian-tikzjax@0.5.2/tikzjax.js" @@ -399,7 +400,14 @@ const renderMarkdown = (content: string, env?: Record) => { // unhighlighted because nothing else invalidates the computed. if (content.includes("```")) void shikijiReady.value slugger.reset() - return env ? md.render(content, env) : md.render(content) + try { + return env ? md.render(content, env) : md.render(content) + } catch (error) { + // Never let a plugin failure blank the note or dump a raw error — + // degrade to the note's raw text. + console.error("markdown render failed", error) + return renderFallback(content) + } } export const renderCodeFile = async ({ diff --git a/src/utils/markdown/renderFallback.spec.ts b/src/utils/markdown/renderFallback.spec.ts new file mode 100644 index 0000000..170c866 --- /dev/null +++ b/src/utils/markdown/renderFallback.spec.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest" + +import { renderFallback } from "./renderFallback" + +describe("renderFallback", () => { + it("shows the raw content inside a pre with a gentle notice", () => { + const html = renderFallback("# hello") + + expect(html).toContain("note-render-fallback") + expect(html).toContain("couldn't be fully rendered") + expect(html).toContain("
# hello
") + }) + + it("escapes HTML so raw content can't inject markup into v-html", () => { + const html = renderFallback(' & "q"') + + expect(html).toContain( + "<img src=x onerror="alert(1)"> & "q"" + ) + expect(html).not.toContain(" { + expect(renderFallback("<")).toContain("
&lt;
") + }) +}) diff --git a/src/utils/markdown/renderFallback.ts b/src/utils/markdown/renderFallback.ts new file mode 100644 index 0000000..6cb8b40 --- /dev/null +++ b/src/utils/markdown/renderFallback.ts @@ -0,0 +1,14 @@ +const escapeHtml = (value: string): string => + value + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + +/** + * Calm degradation for a note that failed to render: never a raw error dump + * or a silent blank — show the note's raw text (escaped) with a gentle notice, + * so the content is still readable. + */ +export const renderFallback = (content: string): string => + `

This note couldn't be fully rendered — showing its raw text.

${escapeHtml(content)}
`