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) => {