From 9e1847329bfb33c47922b363003ab464a0838dd1 Mon Sep 17 00:00:00 2001 From: bboysoul Date: Tue, 15 Sep 2026 13:56:44 +0800 Subject: [PATCH] fix: render task notes with standard markdown --- frontend/package.json | 2 +- frontend/pnpm-lock.yaml | 73 ++++++++++++++++ frontend/src/lib/task-utils.test.ts | 16 ++-- frontend/src/lib/task-utils.ts | 103 ++++------------------- frontend/src/markdown-it-task-lists.d.ts | 12 +++ frontend/src/style.css | 2 +- frontend/src/style.test.ts | 2 + 7 files changed, 117 insertions(+), 93 deletions(-) create mode 100644 frontend/src/markdown-it-task-lists.d.ts diff --git a/frontend/package.json b/frontend/package.json index 81ab0f2..e5d9650 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1 +1 @@ -{"name":"dodo-frontend","private":true,"version":"0.1.0","type":"module","packageManager":"pnpm@9.15.9","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"]}} \ No newline at end of file +{"name":"dodo-frontend","private":true,"version":"0.1.0","type":"module","packageManager":"pnpm@9.15.9","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"]}} \ No newline at end of file diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 78861a2..f5a122d 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -20,6 +20,12 @@ importers: lucide-vue-next: specifier: ^0.468.0 version: 0.468.0(vue@3.5.42(typescript@5.9.3)) + 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(vue@3.5.42(typescript@5.9.3)) @@ -36,6 +42,9 @@ importers: '@tailwindcss/vite': specifier: latest version: 4.3.3(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) + '@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/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.2.0': + resolution: {integrity: sha512-NoQ2yGlLWj4wpxMs+TYmRKk3thDrQ97agr7sFqfLsAlvoS8SNQuTrlObhFqG9iugdTtgOE9jpJ6FNM4ZGsa5xQ==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/node@26.4.1': resolution: {integrity: sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==} @@ -489,6 +507,9 @@ packages: alien-signals@3.2.1: resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==} + argparse@3.0.2: + resolution: {integrity: sha512-mFdDM6WqWKraGLsVb+C9CahPnzTXOefAOLq3jYcca2YZ8bEWpr++Tzj+zSaKW9+X9L5uSxcm1AZ3Y6aZJ09OhQ==} + aria-hidden@1.2.6: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} @@ -772,6 +793,9 @@ packages: resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} engines: {node: '>= 12.0.0'} + linkify-it@6.1.0: + resolution: {integrity: sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==} + local-pkg@1.2.1: resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==} engines: {node: '>=14'} @@ -795,9 +819,19 @@ packages: magic-string@1.2.3: resolution: {integrity: sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g==} + markdown-it-task-lists@2.1.1: + resolution: {integrity: sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==} + + markdown-it@15.0.2: + resolution: {integrity: sha512-q4IGxMv56jCqT4OCRCADBoDP3LO4MhmTXjFbphHPXs4g3j9Xg5RDnxqN8IF/3vIWEU+VCnUq+7JUg/cfy2E6Qw==} + hasBin: true + mdn-data@2.27.1: resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + mdurl@2.1.0: + resolution: {integrity: sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==} + mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} @@ -848,6 +882,10 @@ packages: resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} engines: {node: ^10 || ^12 || >=14} + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -941,6 +979,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + uc.micro@3.0.0: + resolution: {integrity: sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==} + ufo@1.6.4: resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} @@ -1397,6 +1438,15 @@ snapshots: '@types/estree@1.0.9': {} + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.2.0': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdurl@2.0.0': {} + '@types/node@26.4.1': dependencies: undici-types: 8.3.0 @@ -1536,6 +1586,8 @@ snapshots: alien-signals@3.2.1: {} + argparse@3.0.2: {} + aria-hidden@1.2.6: dependencies: tslib: 2.8.1 @@ -1763,6 +1815,10 @@ snapshots: lightningcss-win32-arm64-msvc: 1.33.0 lightningcss-win32-x64-msvc: 1.33.0 + linkify-it@6.1.0: + dependencies: + uc.micro: 3.0.0 + local-pkg@1.2.1: dependencies: mlly: 1.8.2 @@ -1787,8 +1843,21 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 + markdown-it-task-lists@2.1.1: {} + + markdown-it@15.0.2: + 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 + mdn-data@2.27.1: {} + mdurl@2.1.0: {} + mlly@1.8.2: dependencies: acorn: 8.18.0 @@ -1838,6 +1907,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + punycode.js@2.3.1: {} + punycode@2.3.1: {} quansync@0.2.11: {} @@ -1932,6 +2003,8 @@ snapshots: typescript@5.9.3: {} + uc.micro@3.0.0: {} + ufo@1.6.4: {} undici-types@8.3.0: {} diff --git a/frontend/src/lib/task-utils.test.ts b/frontend/src/lib/task-utils.test.ts index 595cc57..bb97fe0 100644 --- a/frontend/src/lib/task-utils.test.ts +++ b/frontend/src/lib/task-utils.test.ts @@ -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') + 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') expect(html).toContain('

Plan

') - expect(html).toContain('
Note
') - expect(html).toContain('
  1. first
') - expect(html).toContain('type="checkbox" disabled checked') - expect(html).toContain('
const x = 1
') + expect(html).toContain('

First line
\nsecond line

') + expect(html).toMatch(/
  • \s*

    parent<\/p>\s*