From 082b8632c66de8b6c430edf62640597b23674d74 Mon Sep 17 00:00:00 2001 From: Frederic Cabassut Date: Mon, 7 Sep 2026 19:09:05 +0200 Subject: [PATCH] fix: render empty task list items as checkboxes in note previews The task-list markdown rule only matched a marker followed by a space ("[ ] text"). markdown-it trims trailing whitespace from inline content, so an item with no text arrived as exactly "[ ]" and rendered as literal brackets in the card preview, while the editor showed a checkbox. Match the marker followed by a space or end-of-content instead, and strip only what was matched so "[ ]x" glued to text stays literal. Co-Authored-By: Claude Fable 5.1 --- src/lib/utils/markdown.test.ts | 19 +++++++++++++++++++ src/lib/utils/markdown.ts | 10 +++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/lib/utils/markdown.test.ts b/src/lib/utils/markdown.test.ts index 71f2ecf..b7c562a 100644 --- a/src/lib/utils/markdown.test.ts +++ b/src/lib/utils/markdown.test.ts @@ -95,6 +95,25 @@ describe('renderMarkdown', () => { expect(html).toContain('disabled'); expect(html).toContain('class="task-checkbox"'); }); + + it('should render an empty task item as a checkbox, not literal brackets', () => { + const html = renderMarkdown('- [ ] Train tickets\n- [ ]'); + expect(html).not.toContain('[ ]'); + expect(html.match(/type="checkbox"/g)).toHaveLength(2); + }); + + it('should render an empty checked task item as a checked checkbox', () => { + const html = renderMarkdown('- [x]'); + expect(html).not.toContain('[x]'); + expect(html).toContain(''); + expect(html).toContain('class="task-list"'); + }); + + it('should not treat brackets glued to text as a task marker', () => { + const html = renderMarkdown('- [ ]x'); + expect(html).not.toContain('type="checkbox"'); + expect(html).toContain('[ ]x'); + }); }); describe('stripMarkdown', () => { diff --git a/src/lib/utils/markdown.ts b/src/lib/utils/markdown.ts index 215ad04..ff933c1 100644 --- a/src/lib/utils/markdown.ts +++ b/src/lib/utils/markdown.ts @@ -33,12 +33,16 @@ md.core.ruler.after('inline', 'task-lists', (state) => { for (let i = 0; i < tokens.length; i++) { if (tokens[i].type !== 'inline') continue; const content = tokens[i].content; - if (content.startsWith('[ ] ') || content.startsWith('[x] ')) { - const checked = content.startsWith('[x] '); + // markdown-it trims trailing whitespace from inline content, so an item + // with no text after the marker arrives as exactly "[ ]" — match the + // marker followed by a space or end-of-content. + const marker = /^\[( |x)\](?: |$)/.exec(content); + if (marker) { + const checked = marker[1] === 'x'; const checkbox = checked ? ' ' : ' '; - tokens[i].content = content.slice(4); + tokens[i].content = content.slice(marker[0].length); tokens[i].children = md.parseInline(tokens[i].content, state.env)[0].children; // Prepend checkbox const token = new state.Token('html_inline', '', 0);