From a203dc6e46f952911e22e98982546887f64654b8 Mon Sep 17 00:00:00 2001 From: Adam Teichert Date: Wed, 10 Dec 2025 17:44:02 -0700 Subject: [PATCH] mermaid diagrams are rendered as image tags with link to mermaid server rendering; uses pako for compression as expected --- package.json | 1 + pnpm-lock.yaml | 8 +++++++ .../htmlMarkdownUtils.mermaid.test.ts | 22 +++++++++++++++++++ src/services/htmlMarkdownUtils.ts | 17 +++++++++++--- 4 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 src/services/htmlMarkdownUtils.mermaid.test.ts diff --git a/package.json b/package.json index 77d4429..c65b636 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "marked-katex-extension": "^5.1.5", "mcp-handler": "^1.0.0", "next": "^15.3.5", + "pako": "^2.1.0", "react": "^19.1.0", "react-dom": "^19.1.0", "socket.io": "^4.8.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ff1c95..d7a48f4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,6 +65,9 @@ importers: next: specifier: ^15.3.5 version: 15.3.5(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + pako: + specifier: ^2.1.0 + version: 2.1.0 react: specifier: ^19.1.0 version: 19.1.0 @@ -2628,6 +2631,9 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -5853,6 +5859,8 @@ snapshots: dependencies: p-limit: 3.1.0 + pako@2.1.0: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 diff --git a/src/services/htmlMarkdownUtils.mermaid.test.ts b/src/services/htmlMarkdownUtils.mermaid.test.ts new file mode 100644 index 0000000..0d12f06 --- /dev/null +++ b/src/services/htmlMarkdownUtils.mermaid.test.ts @@ -0,0 +1,22 @@ +import { describe, it, expect } from 'vitest'; +import { markdownToHtmlNoImages } from './htmlMarkdownUtils'; + +describe('markdownToHtmlNoImages', () => { + it('renders mermaid diagrams correctly using pako compression', () => { + const markdown = ` +\`\`\`mermaid +graph TD; + A-->B; + A-->C; + B-->D; + C-->D; +\`\`\` + `; + const html = markdownToHtmlNoImages(markdown); + + // The expected URL part for the graph above when compressed with pako + const expectedUrlPart = "pako:eNqrVkrOT0lVslJKL0osyFAIcbGOyVMAAkddXTsnJLYzlO0EZMPUOIPZSjpKualFuYmZKUpW1UolGam5IONSUtMSS3NKlGprAQJ0Gx4"; + + expect(html).toContain(`https://mermaid.ink/img/${expectedUrlPart}`); + }); +}); diff --git a/src/services/htmlMarkdownUtils.ts b/src/services/htmlMarkdownUtils.ts index 59b48d2..abb905b 100644 --- a/src/services/htmlMarkdownUtils.ts +++ b/src/services/htmlMarkdownUtils.ts @@ -2,6 +2,7 @@ import { marked } from "marked"; import DOMPurify from "isomorphic-dompurify"; import markedKatex from "marked-katex-extension"; +import pako from "pako"; import { LocalCourseSettings } from "@/features/local/course/localCourseSettings"; const mermaidExtension = { @@ -22,9 +23,17 @@ const mermaidExtension = { } }, renderer(token: { text: string }) { - const base64 = btoa(token.text); - const url = `https://mermaid.ink/img/${base64}?type=svg`; - console.log(token.text, url); + const data = JSON.stringify({ + code: token.text, + mermaid: { theme: "default" }, + }); + const compressed = pako.deflate(data, { level: 9 }); + const binaryString = Array.from(compressed, (byte) => + String.fromCharCode(byte) + ).join(""); + const base64 = btoa(binaryString).replace(/\+/g, "-").replace(/\//g, "_"); + const url = `https://mermaid.ink/img/pako:${base64}?type=svg`; + // console.log(token.text, url); return `Mermaid diagram`; }, }; @@ -63,6 +72,8 @@ export function convertImagesToCanvasImages( }, {} as { [key: string]: string }); for (const imageSrc of imageSources) { + if (imageSrc.startsWith("http://") || imageSrc.startsWith("https://")) + continue; const destinationUrl = imageLookup[imageSrc]; if (typeof destinationUrl === "undefined") { console.log(