Full document, spreadsheet, slideshow, and diagram tooling
1/**
2 * Markdown autoformat extension for TipTap.
3 *
4 * Adds custom input rules that are NOT provided by built-in TipTap extensions.
5 * - [text](url) → clickable link
6 * - `- [ ] ` → checkbox task-list item (GFM task list). See #722.
7 * - `- [x] ` → checked task-list item. See #722.
8 *
9 * Other rules (headings, bullet/ordered lists, code blocks, bold, italic,
10 * strikethrough, inline code, blockquote, horizontal rule) are already
11 * provided by StarterKit.
12 */
13
14import { Extension, InputRule } from '@tiptap/core';
15import { linkInputRegex as _unusedLinkRegex } from '../autoformat-rules.js';
16void _unusedLinkRegex;
17
18export const MarkdownAutoformat = Extension.create({
19 name: 'markdownAutoformat',
20
21 addInputRules() {
22 const rules: InputRule[] = [];
23
24 // [text](url) → Link
25 // When user types [link text](https://example.com), replace with a
26 // linked text node. Requires the Link extension to be registered.
27 const linkType = this.editor.schema.marks.link;
28 if (linkType) {
29 rules.push(
30 new InputRule({
31 find: /(?:^|\s)\[([^\]]+)\]\(([^)]+)\)$/,
32 handler: ({ state, range, match }) => {
33 const linkText = match[1];
34 const href = match[2];
35
36 // Determine if we matched a leading space
37 const fullMatch = match[0];
38 const hasLeadingSpace = fullMatch !== `[${linkText}](${href})`;
39 const start = hasLeadingSpace ? range.from + 1 : range.from;
40
41 const mark = linkType.create({ href });
42 const textNode = state.schema.text(linkText, [mark]);
43
44 const tr = state.tr;
45 tr.replaceWith(start, range.to, textNode);
46 },
47 })
48 );
49 }
50
51 // GFM task-list autoformat: `- [ ] ` or `- [x] ` at the very start of
52 // an empty paragraph transforms the paragraph into a single-item
53 // TaskList. #722 — TaskList's built-in input rule doesn't accept the
54 // GFM markdown form (it only reacts to literal `- [ ] ` but only as a
55 // child of an existing bullet-list context in some versions).
56 const taskListType = this.editor.schema.nodes.taskList;
57 const taskItemType = this.editor.schema.nodes.taskItem;
58 const paragraphType = this.editor.schema.nodes.paragraph;
59 if (taskListType && taskItemType && paragraphType) {
60 rules.push(
61 new InputRule({
62 // Match at the very start of a paragraph: - [ ] or * [x] etc.
63 find: /^\s*(?:[-*+])\s+\[([xX ])\]\s$/,
64 handler: ({ state, range, match, chain: _c, commands }) => {
65 void _c; void commands;
66 const checked = /[xX]/.test(match[1] ?? ' ');
67 const { tr } = state;
68 const $from = state.doc.resolve(range.from);
69 // Replace the whole paragraph start (from block start) with a
70 // taskList > taskItem > paragraph (empty). Delete the "- [ ] "
71 // literal prefix so the cursor sits inside the task item ready
72 // for the user to type.
73 const blockStart = $from.before($from.depth);
74 const blockEnd = $from.after($from.depth);
75 const emptyPara = paragraphType.create();
76 const taskItem = taskItemType.create({ checked }, emptyPara);
77 const taskList = taskListType.create(null, taskItem);
78 tr.replaceRangeWith(blockStart, blockEnd, taskList);
79 },
80 })
81 );
82 }
83
84 return rules;
85 },
86});