feat: improve task note markdown editor
ci / gitleaks (push) Successful in 7s
ci / docker (push) Successful in 5m44s

This commit is contained in:
2026-09-11 17:37:53 +08:00
parent 9ec9d60633
commit 7caa78f318
5 changed files with 156 additions and 17 deletions
+79 -8
View File
@@ -17,6 +17,38 @@ const escapeHtml = (value: string) =>
.replace(/"/g, '"')
.replace(/'/g, ''')
export type MarkdownFormat = 'heading' | 'bold' | 'italic' | 'bullet' | 'ordered' | 'task' | 'link' | 'code' | 'codeblock' | 'quote'
export function applyMarkdownFormat(value: string, start: number, end: number, format: MarkdownFormat) {
const selected = value.slice(start, end)
const replace = (replacement: string, selectionStart: number, selectionEnd: number) => ({
value: value.slice(0, start) + replacement + value.slice(end),
start: start + selectionStart,
end: start + selectionEnd,
})
const wrap = (before: string, after = before, fallback = '文字') => {
const content = selected || fallback
return replace(`${before}${content}${after}`, before.length, before.length + content.length)
}
const prefixLines = (prefix: string, fallback = '列表项') => {
const content = selected || fallback
const replacement = content.split('\n').map((line) => `${prefix}${line}`).join('\n')
return replace(replacement, prefix.length, replacement.length)
}
if (format === 'heading') return prefixLines('## ', '标题')
if (format === 'bold') return wrap('**')
if (format === 'italic') return wrap('*')
if (format === 'bullet') return prefixLines('- ')
if (format === 'ordered') return prefixLines('1. ')
if (format === 'task') return prefixLines('- [ ] ')
if (format === 'link') return selected
? replace(`[${selected}](https://)`, 1, 1 + selected.length)
: replace('[链接文字](https://)', 1, 5)
if (format === 'code') return wrap('`', '`', '代码')
if (format === 'codeblock') return wrap('```\n', '\n```', '代码')
return prefixLines('> ', '引用')
}
const renderInline = (value: string) => {
let out = escapeHtml(value)
out = out.replace(/`([^`]+)`/g, '<code>$1</code>')
@@ -32,17 +64,45 @@ const renderInline = (value: string) => {
export function renderMarkdown(markdown = '') {
const lines = markdown.replace(/\r\n/g, '\n').split('\n')
const html: string[] = []
let inList = false
let listType: 'ul' | 'ol' | null = null
let inCode = false
let codeLanguage = ''
const codeLines: string[] = []
const closeList = () => {
if (inList) {
html.push('</ul>')
inList = false
if (listType) {
html.push(`</${listType}>`)
listType = null
}
}
const openList = (type: 'ul' | 'ol') => {
if (listType === type) return
closeList()
html.push(`<${type}>`)
listType = type
}
for (const raw of lines) {
const line = raw.trimEnd()
const fence = line.match(/^```([\w-]*)\s*$/)
if (fence) {
closeList()
if (inCode) {
const languageClass = codeLanguage ? ` class="language-${escapeHtml(codeLanguage)}"` : ''
html.push(`<pre><code${languageClass}>${escapeHtml(codeLines.join('\n'))}</code></pre>`)
codeLines.length = 0
codeLanguage = ''
inCode = false
} else {
inCode = true
codeLanguage = fence[1]
}
continue
}
if (inCode) {
codeLines.push(raw)
continue
}
if (!line.trim()) {
closeList()
continue
@@ -53,18 +113,29 @@ export function renderMarkdown(markdown = '') {
} else if (line.startsWith('## ')) {
closeList()
html.push(`<h2>${renderInline(line.slice(3))}</h2>`)
} else if (line.startsWith('> ')) {
closeList()
html.push(`<blockquote>${renderInline(line.slice(2))}</blockquote>`)
} else if (/^\d+\. /.test(line)) {
openList('ol')
html.push(`<li>${renderInline(line.replace(/^\d+\. /, ''))}</li>`)
} else if (/^[-*] \[[ xX]\] /.test(line)) {
openList('ul')
const checked = /^[-*] \[[xX]\] /.test(line)
html.push(`<li class="task-list-item"><input type="checkbox" disabled${checked ? ' checked' : ''}>${renderInline(line.replace(/^[-*] \[[ xX]\] /, ''))}</li>`)
} else if (/^[-*] /.test(line)) {
if (!inList) {
html.push('<ul>')
inList = true
}
openList('ul')
html.push(`<li>${renderInline(line.slice(2))}</li>`)
} else if (/^(---|\*\*\*)$/.test(line.trim())) {
closeList()
html.push('<hr>')
} else {
closeList()
html.push(`<p>${renderInline(line)}</p>`)
}
}
closeList()
if (inCode) html.push(`<pre><code>${escapeHtml(['```' + codeLanguage, ...codeLines].join('\n'))}</code></pre>`)
return html.join('')
}