fix(editor): handle paired inline code input and complete markdown rendering
This commit is contained in:
@@ -7,6 +7,27 @@ import githubDark from '@shikijs/themes/github-dark'
|
||||
import githubLight from '@shikijs/themes/github-light'
|
||||
import { renderMermaid } from '@/services/mermaidService'
|
||||
import { appendDiagramControls } from './diagramControls'
|
||||
import katex from 'katex'
|
||||
import 'katex/dist/katex.min.css'
|
||||
|
||||
function mathHtml(source: string, displayMode: boolean) {
|
||||
const result = katex.renderToString(source, {displayMode, throwOnError:false, trust:false, maxExpand:1000, output:'html'})
|
||||
const label = source.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<')
|
||||
return `<${displayMode ? 'div' : 'span'} class="markdown-math" role="math" aria-label="${label}">${result}</${displayMode ? 'div' : 'span'}>`
|
||||
}
|
||||
|
||||
marked.use({extensions:[
|
||||
{name:'blockMath',level:'block',tokenizer(source) {
|
||||
const match = /^ {0,3}\$\$\s*\n?([\s\S]+?)\n?\$\$[ \t]*(?:\n|$)/.exec(source)
|
||||
if (match) return {type:'blockMath',raw:match[0],text:match[1]!.trim()}
|
||||
return undefined
|
||||
}, renderer(token) { return mathHtml(token.text, true) }},
|
||||
{name:'inlineMath',level:'inline',start(source) { return source.indexOf('$') },tokenizer(source) {
|
||||
const match = /^\$([^$\n]+?)\$(?!\$)/.exec(source)
|
||||
if (match && !/^\s|\s$/.test(match[1]!)) return {type:'inlineMath',raw:match[0],text:match[1]!}
|
||||
return undefined
|
||||
},renderer(token) { return mathHtml(token.text, false) }},
|
||||
]})
|
||||
|
||||
marked.setOptions({ gfm: true, breaks: true })
|
||||
|
||||
@@ -72,6 +93,10 @@ export async function renderMarkdown(source: string, options?: { theme?: 'light'
|
||||
mermaidBlocks.push({ pre: code.parentElement!, source: code.textContent ?? '' })
|
||||
continue
|
||||
}
|
||||
if (requestedLanguage.toLowerCase() === 'latex') {
|
||||
code.parentElement?.replaceWith(document.createRange().createContextualFragment(mathHtml(code.textContent ?? '', true)))
|
||||
continue
|
||||
}
|
||||
const highlighted = await highlightCode(code.textContent ?? '', requestedLanguage)
|
||||
const fragment = document.createRange().createContextualFragment(highlighted)
|
||||
code.parentElement?.replaceWith(fragment)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// @vitest-environment jsdom
|
||||
import { expect, it } from 'vitest'
|
||||
import { renderMarkdown } from './markdown'
|
||||
|
||||
it('renders CommonMark and GFM formats, escaped literals and safe HTML', async () => {
|
||||
const source = ['# H1','## H2','### H3','#### H4','##### H5','###### H6',
|
||||
'**bold** *italic* ~~deleted~~ `inline` ``a`b``', '> quote', '- item\n - nested',
|
||||
'1. first\n2. second', '- [x] done\n- [ ] todo', '[link][ref]\n\n[ref]: https://example.com',
|
||||
'', '| A | B |\n| --- | --- |\n| x | y |', '---',
|
||||
'line \nbreak', '\\`literal\\`', '<script>alert(1)</script><img src="x" onerror="alert(1)">',
|
||||
'```unknown-language\n<script>literal</script>\n```'].join('\n\n')
|
||||
const root = document.createElement('div')
|
||||
root.innerHTML = await renderMarkdown(source)
|
||||
for (const selector of ['h1','h2','h3','h4','h5','h6','strong','em','del','blockquote','ul','ol','table','hr','br','a','img','input[type=checkbox]','pre code']) expect(root.querySelector(selector), selector).not.toBeNull()
|
||||
expect(root.querySelector('code')?.textContent).toBe('inline')
|
||||
expect(root.textContent).toContain('`literal`')
|
||||
expect(root.querySelector('pre code')?.textContent).toContain('<script>literal</script>')
|
||||
expect(root.querySelector('script,[onerror]')).toBeNull()
|
||||
})
|
||||
|
||||
it('renders inline, display and editor LaTeX fences while leaving code literals alone', async () => {
|
||||
const root = document.createElement('div')
|
||||
root.innerHTML = await renderMarkdown('$x^2$\n\n$$\nx^2\n$$\n\n```LaTeX\nx^2\n```\n\n`$literal$`\n\n```text\n$literal$\n```')
|
||||
expect(root.querySelectorAll('.katex')).toHaveLength(3)
|
||||
expect(root.querySelector('code')?.textContent).toBe('$literal$')
|
||||
expect(root.querySelector('pre code')?.textContent).toContain('$literal$')
|
||||
})
|
||||
Reference in New Issue
Block a user