/** * Markdown autoformat extension for TipTap. * * Adds custom input rules that are NOT provided by built-in TipTap extensions. * - [text](url) → clickable link * - `- [ ] ` → checkbox task-list item (GFM task list). See #722. * - `- [x] ` → checked task-list item. See #722. * * Other rules (headings, bullet/ordered lists, code blocks, bold, italic, * strikethrough, inline code, blockquote, horizontal rule) are already * provided by StarterKit. */ import { Extension, InputRule } from '@tiptap/core'; import { linkInputRegex as _unusedLinkRegex } from '../autoformat-rules.js'; void _unusedLinkRegex; export const MarkdownAutoformat = Extension.create({ name: 'markdownAutoformat', addInputRules() { const rules: InputRule[] = []; // [text](url) → Link // When user types [link text](https://example.com), replace with a // linked text node. Requires the Link extension to be registered. const linkType = this.editor.schema.marks.link; if (linkType) { rules.push( new InputRule({ find: /(?:^|\s)\[([^\]]+)\]\(([^)]+)\)$/, handler: ({ state, range, match }) => { const linkText = match[1]; const href = match[2]; // Determine if we matched a leading space const fullMatch = match[0]; const hasLeadingSpace = fullMatch !== `[${linkText}](${href})`; const start = hasLeadingSpace ? range.from + 1 : range.from; const mark = linkType.create({ href }); const textNode = state.schema.text(linkText, [mark]); const tr = state.tr; tr.replaceWith(start, range.to, textNode); }, }) ); } // GFM task-list autoformat: `- [ ] ` or `- [x] ` at the very start of // an empty paragraph transforms the paragraph into a single-item // TaskList. #722 — TaskList's built-in input rule doesn't accept the // GFM markdown form (it only reacts to literal `- [ ] ` but only as a // child of an existing bullet-list context in some versions). const taskListType = this.editor.schema.nodes.taskList; const taskItemType = this.editor.schema.nodes.taskItem; const paragraphType = this.editor.schema.nodes.paragraph; if (taskListType && taskItemType && paragraphType) { rules.push( new InputRule({ // Match at the very start of a paragraph: - [ ] or * [x] etc. find: /^\s*(?:[-*+])\s+\[([xX ])\]\s$/, handler: ({ state, range, match, chain: _c, commands }) => { void _c; void commands; const checked = /[xX]/.test(match[1] ?? ' '); const { tr } = state; const $from = state.doc.resolve(range.from); // Replace the whole paragraph start (from block start) with a // taskList > taskItem > paragraph (empty). Delete the "- [ ] " // literal prefix so the cursor sits inside the task item ready // for the user to type. const blockStart = $from.before($from.depth); const blockEnd = $from.after($from.depth); const emptyPara = paragraphType.create(); const taskItem = taskItemType.create({ checked }, emptyPara); const taskList = taskListType.create(null, taskItem); tr.replaceRangeWith(blockStart, blockEnd, taskList); }, }) ); } return rules; }, });