diff --git a/components/FileViewer.tsx b/components/FileViewer.tsx index 40ee2ec74..a99a1813f 100644 --- a/components/FileViewer.tsx +++ b/components/FileViewer.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState, useRef, useCallback, type CSSProperties, type MouseEvent } from "react"; +import { useEffect, useState, useRef, useCallback, useMemo, type CSSProperties, type MouseEvent } from "react"; import { Prism as SyntaxHighlighter, createElement as renderSyntaxNode, @@ -19,7 +19,7 @@ import { } from "@/lib/file-types"; import { encodeFilePathForApi, getFileDirectory, getFileName, getRelativeFilePath } from "@/lib/file-paths"; import { resolveLocalFileHref } from "@/lib/file-links"; -import { markdownPreviewRehypePlugins, markdownPreviewRemarkPlugins } from "@/lib/markdown"; +import { markdownPreviewRehypePlugins, markdownPreviewRemarkPlugins, normalizeDisplayMath } from "@/lib/markdown"; import { CodeBlock, MermaidBlock } from "./MermaidBlock"; import { parseUnifiedPatch } from "@/lib/patch"; import type { GitFileDiffResponse } from "@/lib/git-types"; @@ -895,6 +895,11 @@ function TextFileViewer({ filePath, cwd, sourceSessionId, onOpenFile, onMentionL if (!hasGitDiff && displayMode === "diff") setDisplayMode("source"); }, [displayMode, hasGitDiff]); + const markdownPreview = useMemo( + () => (data?.language === "markdown" ? normalizeDisplayMath(data.content) : ""), + [data], + ); + useEffect(() => { const updateSelectedLineRange = () => { const root = contentRef.current; @@ -1146,7 +1151,7 @@ function TextFileViewer({ filePath, cwd, sourceSessionId, onOpenFile, onMentionL }, }} > - {data.content} + {markdownPreview} ) : ( diff --git a/components/MarkdownBody.tsx b/components/MarkdownBody.tsx index 01bb4f251..0a4a8a47a 100644 --- a/components/MarkdownBody.tsx +++ b/components/MarkdownBody.tsx @@ -4,7 +4,7 @@ import { useMemo, type MouseEvent } from "react"; import ReactMarkdown from "react-markdown"; import { resolveLocalFileHref } from "@/lib/file-links"; import { encodeFilePathForApi } from "@/lib/file-paths"; -import { markdownRehypePlugins, markdownRemarkPlugins } from "@/lib/markdown"; +import { markdownRehypePlugins, markdownRemarkPlugins, normalizeDisplayMath } from "@/lib/markdown"; import { MermaidBlock, CodeBlock } from "./MermaidBlock"; interface MarkdownBodyProps { @@ -98,32 +98,3 @@ export function MarkdownBody({ children, className, isStreaming, cwd, onOpenFile ); } - -function normalizeDisplayMath(markdown: string): string { - const lineBreak = markdown.includes("\r\n") ? "\r\n" : "\n"; - const lines = markdown.split(/\r?\n/); - let fence: { marker: string; size: number } | null = null; - - return lines - .map((line) => { - const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/); - if (fenceMatch) { - const marker = fenceMatch[1][0]; - const size = fenceMatch[1].length; - if (!fence) fence = { marker, size }; - else if (marker === fence.marker && size >= fence.size) fence = null; - return line; - } - - if (fence) return line; - - const displayMathMatch = line.match(/^([ \t]{0,3})\$\$(.+)\$\$[ \t]*$/); - if (!displayMathMatch) return line; - - const math = displayMathMatch[2].trim(); - if (!math) return line; - - return `${displayMathMatch[1]}$$${lineBreak}${math}${lineBreak}${displayMathMatch[1]}$$`; - }) - .join(lineBreak); -} diff --git a/lib/markdown.ts b/lib/markdown.ts index 9ccd5940b..f85af1e26 100644 --- a/lib/markdown.ts +++ b/lib/markdown.ts @@ -14,8 +14,37 @@ const markdownSanitizeSchema = { strip: [...(defaultSchema.strip || []), "iframe", "object", "style", "form"], }; +export function normalizeDisplayMath(markdown: string): string { + const lineBreak = markdown.includes("\r\n") ? "\r\n" : "\n"; + const lines = markdown.split(/\r?\n/); + let fence: { marker: string; size: number } | null = null; + + return lines + .map((line) => { + const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/); + if (fenceMatch) { + const marker = fenceMatch[1][0]; + const size = fenceMatch[1].length; + if (!fence) fence = { marker, size }; + else if (marker === fence.marker && size >= fence.size) fence = null; + return line; + } + + if (fence) return line; + + const displayMathMatch = line.match(/^([ \t]{0,3})\$\$(.+)\$\$[ \t]*$/); + if (!displayMathMatch) return line; + + const math = displayMathMatch[2].trim(); + if (!math) return line; + + return `${displayMathMatch[1]}$$${lineBreak}${math}${lineBreak}${displayMathMatch[1]}$$`; + }) + .join(lineBreak); +} + export const markdownRemarkPlugins: ReactMarkdownOptions["remarkPlugins"] = [remarkGfm, remarkMath]; -export const markdownPreviewRemarkPlugins: ReactMarkdownOptions["remarkPlugins"] = [remarkGfm]; +export const markdownPreviewRemarkPlugins: ReactMarkdownOptions["remarkPlugins"] = [remarkGfm, remarkMath]; export const markdownRehypePlugins: ReactMarkdownOptions["rehypePlugins"] = [ rehypeRaw, @@ -26,4 +55,5 @@ export const markdownRehypePlugins: ReactMarkdownOptions["rehypePlugins"] = [ export const markdownPreviewRehypePlugins: ReactMarkdownOptions["rehypePlugins"] = [ rehypeRaw, [rehypeSanitize, markdownSanitizeSchema], + [rehypeKatex, { throwOnError: false, strict: false }], ];