126 lines
3.7 KiB
JavaScript
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, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
}
|
|
|
|
/**
|
|
* 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, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
|
|
// 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(/^> ?(.*)$/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
|
|
}
|