diff --git a/frontend/src/views/StudyView.vue b/frontend/src/views/StudyView.vue
index 29179f6..3d05e91 100644
--- a/frontend/src/views/StudyView.vue
+++ b/frontend/src/views/StudyView.vue
@@ -353,37 +353,93 @@ marked.use({
renderer,
})
+function decodeHTMLEntities(text: string): string {
+ return text
+ .replace(/&/g, '&')
+ .replace(/</g, '<')
+ .replace(/>/g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, "'")
+}
+
function renderMarkdown(text: string): string {
if (!text) return ''
// 0. Extract and protect Mermaid blocks
const mermaidBlocks: string[] = []
- let processed = text.replace(/```mermaid\r?\\n([\s\S]*?)\r?\\n```/g, (match, code) => {
+ let workingText = text
+ workingText = workingText.replace(/```mermaid\r?\\n([\s\S]*?)\r?\\n```/g, (match, code) => {
const placeholder = `MERMAIDBLOCKPLACEHOLDER${mermaidBlocks.length}`
mermaidBlocks.push(code.trim())
return placeholder
})
- // 1. Render display math $$...$$
- processed = processed.replace(/\$\$([\s\S]+?)\$\$/g, (match, math) => {
- try {
- return `
${katex.renderToString(math.trim(), { displayMode: true, throwOnError: false })}
`
- } catch (e) {
- return match
- }
+ const mathBlocks: { placeholder: string; isBlock: boolean; rawExpr: string; renderedHtml: string }[] = []
+ let placeholderCount = 0
+
+ // Normalize slash/backslash ctdot representations to standard cdots
+ workingText = workingText
+ .replace(/\\ctdot/g, '\\cdots')
+ .replace(/\/ctdot/g, '\\cdots')
+
+ // 1. Extract block math: $$...$$ and \[...\]
+ workingText = workingText.replace(/\$\$(.*?)\$\$/gs, (match, expr) => {
+ const placeholder = `MATHBLOCKPLACEHOLDER${placeholderCount++}`
+ mathBlocks.push({ placeholder, isBlock: true, rawExpr: expr, renderedHtml: '' })
+ return placeholder
})
- // 2. Render inline math $...$
- processed = processed.replace(/\$([^\n]+?)\$/g, (match, math) => {
- try {
- return `${katex.renderToString(math.trim(), { displayMode: false, throwOnError: false })}`
- } catch (e) {
- return match
- }
+ workingText = workingText.replace(/\\\[(.*?)\\\]/gs, (match, expr) => {
+ const placeholder = `MATHBLOCKPLACEHOLDER${placeholderCount++}`
+ mathBlocks.push({ placeholder, isBlock: true, rawExpr: expr, renderedHtml: '' })
+ return placeholder
})
- // 3. Render markdown
- let html = marked.parse(processed) as string
+ // 2. Extract inline math: $...$ and \(...\)
+ workingText = workingText.replace(/(? {
+ const placeholder = `INLINEMATHPLACEHOLDER${placeholderCount++}`
+ mathBlocks.push({ placeholder, isBlock: false, rawExpr: expr, renderedHtml: '' })
+ return placeholder
+ })
+
+ workingText = workingText.replace(/\\\(.*?\\\)/g, (match) => {
+ const expr = match.slice(2, -2)
+ const placeholder = `INLINEMATHPLACEHOLDER${placeholderCount++}`
+ mathBlocks.push({ placeholder, isBlock: false, rawExpr: expr, renderedHtml: '' })
+ return placeholder
+ })
+
+ // 3. Render Markdown with marked.parse
+ let html = ''
+ try {
+ html = marked.parse(workingText) as string
+ } catch (e) {
+ console.error("Markdown parsing failed:", e)
+ html = workingText
+ }
+
+ // 4. Render KaTeX and swap placeholders
+ for (const block of mathBlocks) {
+ try {
+ let cleanExpr = decodeHTMLEntities(block.rawExpr.trim())
+ cleanExpr = cleanExpr.replace(/\\ctdot/g, '\\cdots').replace(/\/ctdot/g, '\\cdots')
+ const rendered = katex.renderToString(cleanExpr, {
+ displayMode: block.isBlock,
+ throwOnError: false
+ })
+ block.renderedHtml = block.isBlock
+ ? `${rendered}
`
+ : `${rendered}`
+ } catch (err) {
+ console.error("KaTeX parse error for expr:", block.rawExpr, err)
+ const delim = block.isBlock ? '$$' : '$'
+ block.renderedHtml = block.isBlock
+ ? `${delim}${block.rawExpr}${delim}
`
+ : `${delim}${block.rawExpr}${delim}`
+ }
+
+ html = html.replace(block.placeholder, block.renderedHtml)
+ }
// 5. Restore protected Mermaid blocks
mermaidBlocks.forEach((code, idx) => {