Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: emit TipTap-compatible HTML for task list markdown paste

The markdown parser generated <li class="task-list-item"><input type="checkbox">
but TipTap only recognizes <li data-type="taskItem" data-checked="..."> and
<ul data-type="taskList">. Pasted markdown checkboxes were silently dropped
or rendered as broken plain list items.

Now emits the correct TipTap node attributes so task lists round-trip
through paste and import correctly.

+47 -12
+36 -6
src/docs/markdown-parser.ts
··· 30 30 * Custom plugin: task list checkboxes (GFM style) 31 31 * Converts `- [ ] text` and `- [x] text` into checkbox list items. 32 32 */ 33 + /** 34 + * Scan a bullet_list's children and return true if ANY list_item 35 + * starts with a GFM checkbox pattern `[ ]` / `[x]`. 36 + */ 37 + function listContainsTaskItems(tokens: Token[], listOpenIdx: number): boolean { 38 + let depth = 0; 39 + for (let i = listOpenIdx; i < tokens.length; i++) { 40 + if (tokens[i].type === 'bullet_list_open') depth++; 41 + if (tokens[i].type === 'bullet_list_close') { depth--; if (depth === 0) break; } 42 + if (depth === 1 && tokens[i].type === 'list_item_open') { 43 + const content = tokens[i + 2]; // list_item_open -> paragraph_open -> inline 44 + if (content?.type === 'inline' && /^\[([ xX])\]\s*/.test(content.content)) { 45 + return true; 46 + } 47 + } 48 + } 49 + return false; 50 + } 51 + 33 52 const taskListPlugin: MarkdownItPlugin = function taskListPlugin(md: MarkdownIt): void { 34 - const defaultRender = md.renderer.rules.list_item_open || 53 + // --- Override bullet_list_open: emit <ul data-type="taskList"> when items have checkboxes --- 54 + const defaultListOpen = md.renderer.rules.bullet_list_open || 55 + function (tokens: Token[], idx: number, options: MarkdownItOptions, _env: unknown, self: Renderer): string { 56 + return self.renderToken(tokens, idx, options); 57 + }; 58 + 59 + md.renderer.rules.bullet_list_open = function (tokens: Token[], idx: number, options: MarkdownItOptions, env: unknown, self: Renderer): string { 60 + if (listContainsTaskItems(tokens, idx)) { 61 + return '<ul data-type="taskList">'; 62 + } 63 + return defaultListOpen(tokens, idx, options, env, self); 64 + }; 65 + 66 + // --- Override list_item_open: emit TipTap-compatible task item attributes --- 67 + const defaultItemOpen = md.renderer.rules.list_item_open || 35 68 function (tokens: Token[], idx: number, options: MarkdownItOptions, _env: unknown, self: Renderer): string { 36 69 return self.renderToken(tokens, idx, options); 37 70 }; 38 71 39 72 md.renderer.rules.list_item_open = function (tokens: Token[], idx: number, options: MarkdownItOptions, env: unknown, self: Renderer): string { 40 - // Look at the inline content of this list item 41 73 const contentToken = tokens[idx + 2]; // list_item_open -> paragraph_open -> inline 42 74 if (contentToken && contentToken.type === 'inline' && contentToken.content) { 43 75 const match = contentToken.content.match(/^\[([ xX])\]\s*/); ··· 45 77 const checked = match[1].toLowerCase() === 'x'; 46 78 // Remove the checkbox syntax from the content 47 79 contentToken.content = contentToken.content.replace(/^\[([ xX])\]\s*/, ''); 48 - // Also update children if present 49 80 if (contentToken.children && contentToken.children.length > 0) { 50 81 const firstChild = contentToken.children[0]; 51 82 if (firstChild.type === 'text') { 52 83 firstChild.content = firstChild.content.replace(/^\[([ xX])\]\s*/, ''); 53 84 } 54 85 } 55 - const checkedAttr = checked ? ' checked=""' : ''; 56 - return '<li class="task-list-item"><input type="checkbox"' + checkedAttr + ' disabled="">' ; 86 + return `<li data-type="taskItem" data-checked="${checked}">`; 57 87 } 58 88 } 59 - return defaultRender(tokens, idx, options, env, self); 89 + return defaultItemOpen(tokens, idx, options, env, self); 60 90 }; 61 91 }; 62 92
+11 -6
tests/markdown-parser.test.ts
··· 166 166 it('converts unchecked task items - [ ]', () => { 167 167 const md = '- [ ] Todo item'; 168 168 const html = markdownToHtml(md); 169 - expect(html).toContain('type="checkbox"'); 170 - expect(html).not.toMatch(/checked/i); 169 + expect(html).toContain('data-type="taskList"'); 170 + expect(html).toContain('data-type="taskItem"'); 171 + expect(html).toContain('data-checked="false"'); 172 + expect(html).toContain('Todo item'); 171 173 }); 172 174 173 175 it('converts checked task items - [x]', () => { 174 176 const md = '- [x] Done item'; 175 177 const html = markdownToHtml(md); 176 - expect(html).toContain('type="checkbox"'); 177 - expect(html).toContain('checked'); 178 + expect(html).toContain('data-type="taskList"'); 179 + expect(html).toContain('data-type="taskItem"'); 180 + expect(html).toContain('data-checked="true"'); 181 + expect(html).toContain('Done item'); 178 182 }); 179 183 180 184 it('handles mixed task items', () => { 181 185 const md = '- [x] Done\n- [ ] Pending\n- [x] Also done'; 182 186 const html = markdownToHtml(md); 183 - const checkboxCount = (html.match(/type="checkbox"/g) || []).length; 184 - expect(checkboxCount).toBe(3); 187 + const taskItemCount = (html.match(/data-type="taskItem"/g) || []).length; 188 + expect(taskItemCount).toBe(3); 189 + expect(html).toContain('data-type="taskList"'); 185 190 }); 186 191 }); 187 192