fix: render task notes with standard markdown
This commit is contained in:
@@ -1 +1 @@
|
||||
{"name":"dodo-frontend","private":true,"version":"0.1.0","type":"module","packageManager":"[email protected]","scripts":{"dev":"vite --host 0.0.0.0","build":"vue-tsc -b && vite build","test":"vitest run"},"dependencies":{"@vitejs/plugin-vue":"latest","class-variance-authority":"latest","clsx":"latest","lucide-vue-next":"^0.468.0","reka-ui":"latest","tailwind-merge":"latest","vue":"latest","vue-router":"latest"},"devDependencies":{"@tailwindcss/vite":"latest","@types/node":"latest","jsdom":"^30.0.1","tailwindcss":"latest","typescript":"^5.7.2","vite":"latest","vitest":"latest","vue-tsc":"latest"},"pnpm":{"onlyBuiltDependencies":["vue-demi"]}}
|
||||
{"name":"dodo-frontend","private":true,"version":"0.1.0","type":"module","packageManager":"[email protected]","scripts":{"dev":"vite --host 0.0.0.0","build":"vue-tsc -b && vite build","test":"vitest run"},"dependencies":{"@vitejs/plugin-vue":"latest","class-variance-authority":"latest","clsx":"latest","lucide-vue-next":"^0.468.0","markdown-it":"^15.0.2","markdown-it-task-lists":"^2.1.1","reka-ui":"latest","tailwind-merge":"latest","vue":"latest","vue-router":"latest"},"devDependencies":{"@tailwindcss/vite":"latest","@types/markdown-it":"^14.2.0","@types/node":"latest","jsdom":"^30.0.1","tailwindcss":"latest","typescript":"^5.7.2","vite":"latest","vitest":"latest","vue-tsc":"latest"},"pnpm":{"onlyBuiltDependencies":["vue-demi"]}}
|
||||
Generated
+73
@@ -20,6 +20,12 @@ importers:
|
||||
lucide-vue-next:
|
||||
specifier: ^0.468.0
|
||||
version: 0.468.0([email protected]([email protected]))
|
||||
markdown-it:
|
||||
specifier: ^15.0.2
|
||||
version: 15.0.2
|
||||
markdown-it-task-lists:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
reka-ui:
|
||||
specifier: latest
|
||||
version: 2.10.4([email protected]([email protected]))
|
||||
@@ -36,6 +42,9 @@ importers:
|
||||
'@tailwindcss/vite':
|
||||
specifier: latest
|
||||
version: 4.3.3([email protected](@types/[email protected])([email protected]))
|
||||
'@types/markdown-it':
|
||||
specifier: ^14.2.0
|
||||
version: 14.2.0
|
||||
'@types/node':
|
||||
specifier: latest
|
||||
version: 26.4.1
|
||||
@@ -384,6 +393,15 @@ packages:
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-NoQ2yGlLWj4wpxMs+TYmRKk3thDrQ97agr7sFqfLsAlvoS8SNQuTrlObhFqG9iugdTtgOE9jpJ6FNM4ZGsa5xQ==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==}
|
||||
|
||||
@@ -489,6 +507,9 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-mFdDM6WqWKraGLsVb+C9CahPnzTXOefAOLq3jYcca2YZ8bEWpr++Tzj+zSaKW9+X9L5uSxcm1AZ3Y6aZJ09OhQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -772,6 +793,9 @@ packages:
|
||||
resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -795,9 +819,19 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-q4IGxMv56jCqT4OCRCADBoDP3LO4MhmTXjFbphHPXs4g3j9Xg5RDnxqN8IF/3vIWEU+VCnUq+7JUg/cfy2E6Qw==}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
|
||||
|
||||
@@ -848,6 +882,10 @@ packages:
|
||||
resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -941,6 +979,9 @@ packages:
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==}
|
||||
|
||||
@@ -1397,6 +1438,15 @@ snapshots:
|
||||
|
||||
'@types/[email protected]': {}
|
||||
|
||||
'@types/[email protected]': {}
|
||||
|
||||
'@types/[email protected]':
|
||||
dependencies:
|
||||
'@types/linkify-it': 5.0.0
|
||||
'@types/mdurl': 2.0.0
|
||||
|
||||
'@types/[email protected]': {}
|
||||
|
||||
'@types/[email protected]':
|
||||
dependencies:
|
||||
undici-types: 8.3.0
|
||||
@@ -1536,6 +1586,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
@@ -1763,6 +1815,10 @@ snapshots:
|
||||
lightningcss-win32-arm64-msvc: 1.33.0
|
||||
lightningcss-win32-x64-msvc: 1.33.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
uc.micro: 3.0.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
mlly: 1.8.2
|
||||
@@ -1787,8 +1843,21 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.6.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
argparse: 3.0.2
|
||||
entities: 8.0.0
|
||||
linkify-it: 6.1.0
|
||||
mdurl: 2.1.0
|
||||
punycode.js: 2.3.1
|
||||
uc.micro: 3.0.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
acorn: 8.18.0
|
||||
@@ -1838,6 +1907,8 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -1932,6 +2003,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
@@ -86,16 +86,20 @@ describe('task utilities', () => {
|
||||
expect(buildTaskRecurrencePayload('none', { afterCompletionDays: '1' })).toEqual({})
|
||||
})
|
||||
|
||||
it('renders safe practical markdown and strips unsafe html', () => {
|
||||
const html = renderMarkdown('# Plan\n> Note\n1. first\n- [x] done\n\n```js\nconst x = 1\n```\n**bold** [link](https://example.com)\n<script>alert(1)</script>')
|
||||
it('renders safe standard markdown with paragraphs, nesting, tasks, and fenced code', () => {
|
||||
const html = renderMarkdown('# Plan\n\nFirst line\nsecond line\n\n- parent\n - child\n\n- [x] done\n\n```js\nconst x = 1\n```\n\n**bold** [link](https://example.com)\n<script>alert(1)</script>')
|
||||
expect(html).toContain('<h1>Plan</h1>')
|
||||
expect(html).toContain('<blockquote>Note</blockquote>')
|
||||
expect(html).toContain('<ol><li>first</li></ol>')
|
||||
expect(html).toContain('type="checkbox" disabled checked')
|
||||
expect(html).toContain('<pre><code class="language-js">const x = 1</code></pre>')
|
||||
expect(html).toContain('<p>First line<br>\nsecond line</p>')
|
||||
expect(html).toMatch(/<li>\s*<p>parent<\/p>\s*<ul>\s*<li>child<\/li>\s*<\/ul>\s*<\/li>/)
|
||||
expect(html).toContain('class="task-list-item')
|
||||
expect(html).toContain('type="checkbox"')
|
||||
expect(html).toContain('checked=""')
|
||||
expect(html).toContain('<pre><code class="language-js">const x = 1\n</code></pre>')
|
||||
expect(html).toContain('<strong>bold</strong>')
|
||||
expect(html).toContain('target="_blank"')
|
||||
expect(html).toContain('rel="noopener noreferrer"')
|
||||
expect(html).not.toContain('<script>')
|
||||
expect(html).toContain('<script>alert(1)</script>')
|
||||
})
|
||||
|
||||
it('inserts markdown around selections and prefixes selected lines', () => {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import taskLists from 'markdown-it-task-lists'
|
||||
|
||||
export type MinimalTask = {
|
||||
id: string
|
||||
title: string
|
||||
@@ -49,94 +52,24 @@ export function applyMarkdownFormat(value: string, start: number, end: number, f
|
||||
return prefixLines('> ', '引用')
|
||||
}
|
||||
|
||||
const renderInline = (value: string) => {
|
||||
let out = escapeHtml(value)
|
||||
out = out.replace(/`([^`]+)`/g, '<code>$1</code>')
|
||||
out = out.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
||||
out = out.replace(/\*([^*]+)\*/g, '<em>$1</em>')
|
||||
out = out.replace(
|
||||
/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g,
|
||||
'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>',
|
||||
)
|
||||
return out
|
||||
const markdownRenderer = new MarkdownIt({
|
||||
breaks: true,
|
||||
html: false,
|
||||
linkify: true,
|
||||
typographer: false,
|
||||
})
|
||||
markdownRenderer.use(taskLists, { enabled: false, label: false })
|
||||
|
||||
const defaultLinkOpen = markdownRenderer.renderer.rules.link_open
|
||||
markdownRenderer.renderer.rules.link_open = (tokens, index, options, env, self) => {
|
||||
const token = tokens[index]
|
||||
token.attrSet('target', '_blank')
|
||||
token.attrSet('rel', 'noopener noreferrer')
|
||||
return defaultLinkOpen ? defaultLinkOpen(tokens, index, options, env, self) : self.renderToken(tokens, index, options)
|
||||
}
|
||||
|
||||
export function renderMarkdown(markdown = '') {
|
||||
const lines = markdown.replace(/\r\n/g, '\n').split('\n')
|
||||
const html: string[] = []
|
||||
let listType: 'ul' | 'ol' | null = null
|
||||
let inCode = false
|
||||
let codeLanguage = ''
|
||||
const codeLines: string[] = []
|
||||
|
||||
const closeList = () => {
|
||||
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
|
||||
}
|
||||
if (line.startsWith('# ')) {
|
||||
closeList()
|
||||
html.push(`<h1>${renderInline(line.slice(2))}</h1>`)
|
||||
} 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)) {
|
||||
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('')
|
||||
return markdownRenderer.render(markdown)
|
||||
}
|
||||
|
||||
export function filterTasks<T extends MinimalTask>(tasks: T[], query: string) {
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
declare module 'markdown-it-task-lists' {
|
||||
import type MarkdownIt from 'markdown-it'
|
||||
|
||||
type TaskListOptions = {
|
||||
enabled?: boolean
|
||||
label?: boolean
|
||||
labelAfter?: boolean
|
||||
}
|
||||
|
||||
const taskLists: MarkdownIt.PluginWithOptions<TaskListOptions>
|
||||
export default taskLists
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -288,6 +288,8 @@ describe('completion feedback motion', () => {
|
||||
expect(css).toContain('.markdown-toolbar{display:flex;')
|
||||
expect(css).toContain('overflow-x:auto')
|
||||
expect(css).toContain('.markdown-toolbar button{min-width:44px;min-height:44px;')
|
||||
expect(css).toContain('.markdown-preview>.contains-task-list{list-style:none;padding-left:3px}')
|
||||
expect(css).toContain('.markdown-preview .contains-task-list .contains-task-list{padding-left:24px}')
|
||||
})
|
||||
|
||||
it('keeps all task detail changes on the unified save button', () => {
|
||||
|
||||
Reference in New Issue
Block a user