Files
tuanwei-uniapp/utils/markdown.js
2026-07-20 11:56:02 +08:00

126 lines
3.7 KiB
JavaScript

/**
* Lightweight markdown to HTML converter for uni-app rich-text component.
* Supports: headings, bold, italic, inline code, code blocks,
* unordered lists, ordered lists, blockquotes, horizontal rules, line breaks.
*/
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}
/**
* Convert markdown string to HTML string.
* @param {string} text
* @returns {string}
*/
export function renderMarkdown(text) {
if (!text) return ''
// 1. Protect code blocks first (```)
const codeBlocks = []
let html = text.replace(/```([\w]*)\n?([\s\S]*?)```/g, (_, lang, code) => {
const idx = codeBlocks.length
const escaped = escapeHtml(code.replace(/^\n/, '').replace(/\n$/, ''))
const langAttr = lang ? ` class="language-${lang}"` : ''
codeBlocks.push(`<pre><code${langAttr}>${escaped}</code></pre>`)
return `\x00CODE${idx}\x00`
})
// 2. Escape remaining HTML to prevent injection
html = html.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
// 3. Protect inline code (`)
const inlineCodes = []
html = html.replace(/`([^`\n]+)`/g, (_, code) => {
const idx = inlineCodes.length
inlineCodes.push(`<code>${escapeHtml(code)}</code>`)
return `\x00INLINE${idx}\x00`
})
// 4. Headings (must be at line start)
html = html.replace(/^#{4,6} (.+)$/gm, '<h6>$1</h6>')
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>')
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>')
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>')
// 5. Horizontal rules
html = html.replace(/^[ \t]*(?:---+|===+|\*\*\*+)[ \t]*$/gm, '<hr/>')
// 6. Blockquotes
html = html.replace(/^&gt; ?(.*)$/gm, '<blockquote>$1</blockquote>')
// 7. Bold + Italic
html = html.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>')
html = html.replace(/___(.+?)___/g, '<strong><em>$1</em></strong>')
// 8. Bold
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
html = html.replace(/__(.+?)__/g, '<strong>$1</strong>')
// 9. Italic
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>')
html = html.replace(/_(.+?)_/g, '<em>$1</em>')
// 10. Lists — collect consecutive list lines into <ul> or <ol>
const lines = html.split('\n')
const result = []
let inUl = false
let inOl = false
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
const ulMatch = line.match(/^[ \t]*[-*+] (.+)$/)
const olMatch = line.match(/^[ \t]*\d+\. (.+)$/)
if (ulMatch) {
if (!inUl) {
if (inOl) { result.push('</ol>'); inOl = false }
result.push('<ul>')
inUl = true
}
result.push(`<li>${ulMatch[1]}</li>`)
} else if (olMatch) {
if (!inOl) {
if (inUl) { result.push('</ul>'); inUl = false }
result.push('<ol>')
inOl = true
}
result.push(`<li>${olMatch[1]}</li>`)
} else {
if (inUl) { result.push('</ul>'); inUl = false }
if (inOl) { result.push('</ol>'); inOl = false }
result.push(line)
}
}
if (inUl) result.push('</ul>')
if (inOl) result.push('</ol>')
html = result.join('\n')
// 11. Paragraphs — split by blank lines, wrap plain text blocks in <p>
const blocks = html.split(/\n{2,}/)
html = blocks.map(block => {
block = block.trim()
if (!block) return ''
// Already wrapped in a block-level tag — leave as-is
if (/^<(h[1-6]|ul|ol|li|pre|blockquote|hr)[\s/>]/.test(block)) return block
// Has multiple lines → join with <br/>
block = block.replace(/\n/g, '<br/>')
return `<p>${block}</p>`
}).join('\n')
// 12. Restore inline codes and code blocks
inlineCodes.forEach((code, idx) => {
html = html.replace(`\x00INLINE${idx}\x00`, code)
})
codeBlocks.forEach((code, idx) => {
html = html.replace(`\x00CODE${idx}\x00`, code)
})
return html
}