|
|
@@ -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 = /(?<!\$)\$(?!\$)((?:[^$\\]|\\.)+?)\$(?!\$)/g
|
|
|
+ result = result.replace(inlineMathRegex, (_, math) => {
|
|
|
+ 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<string> {
|
|
|
+ const codeBlockRegex = /<pre><code(?:\s+class="language-([^"]*)")?>([\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<string>
|
|
|
+
|
|
|
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<string> {
|
|
|
+ const html = await nativeParser(markdown)
|
|
|
+ const withMath = renderMathExpressions(html)
|
|
|
+ return highlightCodeBlocks(withMath)
|
|
|
+ },
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return jsParser
|
|
|
},
|
|
|
})
|