From c737776958d45fbd30434d9aa49289a93acf72c8 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Thu, 22 Jan 2026 16:18:39 +0530 Subject: refactor(desktop): move markdown rendering to rust (#10000) --- packages/ui/src/context/marked.tsx | 103 ++++++++++++++++++++++++++++++++++++- 1 file changed, 101 insertions(+), 2 deletions(-) (limited to 'packages/ui/src') diff --git a/packages/ui/src/context/marked.tsx b/packages/ui/src/context/marked.tsx index 6cf1dd54e..71881353a 100644 --- a/packages/ui/src/context/marked.tsx +++ b/packages/ui/src/context/marked.tsx @@ -1,6 +1,7 @@ import { marked } from "marked" import markedKatex from "marked-katex-extension" import markedShiki from "marked-shiki" +import katex from "katex" import { bundledLanguages, type BundledLanguage } from "shiki" import { createSimpleContext } from "./helper" import { getSharedHighlighter, registerCustomTheme, ThemeRegistrationResolved } from "@pierre/diffs" @@ -375,10 +376,95 @@ registerCustomTheme("OpenCode", () => { } as unknown as ThemeRegistrationResolved) }) +function renderMathInText(text: string): string { + let result = text + + // Display math: $$...$$ + const displayMathRegex = /\$\$([\s\S]*?)\$\$/g + result = result.replace(displayMathRegex, (_, math) => { + try { + return katex.renderToString(math, { + displayMode: true, + throwOnError: false, + }) + } catch { + return `$$${math}$$` + } + }) + + // Inline math: $...$ + const inlineMathRegex = /(? { + try { + return katex.renderToString(math, { + displayMode: false, + throwOnError: false, + }) + } catch { + return `$${math}$` + } + }) + + return result +} + +function renderMathExpressions(html: string): string { + // Split on code/pre/kbd tags to avoid processing their contents + const codeBlockPattern = /(<(?:pre|code|kbd)[^>]*>[\s\S]*?<\/(?:pre|code|kbd)>)/gi + const parts = html.split(codeBlockPattern) + + return parts + .map((part, i) => { + // Odd indices are the captured code blocks - leave them alone + if (i % 2 === 1) return part + // Process math only in non-code parts + return renderMathInText(part) + }) + .join("") +} + +async function highlightCodeBlocks(html: string): Promise { + const codeBlockRegex = /
([\s\S]*?)<\/code><\/pre>/g
+  const matches = [...html.matchAll(codeBlockRegex)]
+  if (matches.length === 0) return html
+
+  const highlighter = await getSharedHighlighter({ themes: ["OpenCode"], langs: [] })
+
+  let result = html
+  for (const match of matches) {
+    const [fullMatch, lang, escapedCode] = match
+    const code = escapedCode
+      .replace(/</g, "<")
+      .replace(/>/g, ">")
+      .replace(/&/g, "&")
+      .replace(/"/g, '"')
+      .replace(/'/g, "'")
+
+    let language = lang || "text"
+    if (!(language in bundledLanguages)) {
+      language = "text"
+    }
+    if (!highlighter.getLoadedLanguages().includes(language)) {
+      await highlighter.loadLanguage(language as BundledLanguage)
+    }
+
+    const highlighted = highlighter.codeToHtml(code, {
+      lang: language,
+      theme: "OpenCode",
+      tabindex: false,
+    })
+    result = result.replace(fullMatch, () => highlighted)
+  }
+
+  return result
+}
+
+export type NativeMarkdownParser = (markdown: string) => Promise
+
 export const { use: useMarked, provider: MarkedProvider } = createSimpleContext({
   name: "Marked",
-  init: () => {
-    return marked.use(
+  init: (props: { nativeParser?: NativeMarkdownParser }) => {
+    const jsParser = marked.use(
       {
         renderer: {
           link({ href, title, text }) {
@@ -407,5 +493,18 @@ export const { use: useMarked, provider: MarkedProvider } = createSimpleContext(
         },
       }),
     )
+
+    if (props.nativeParser) {
+      const nativeParser = props.nativeParser
+      return {
+        async parse(markdown: string): Promise {
+          const html = await nativeParser(markdown)
+          const withMath = renderMathExpressions(html)
+          return highlightCodeBlocks(withMath)
+        },
+      }
+    }
+
+    return jsParser
   },
 })
-- 
cgit v1.2.3