Full document, spreadsheet, slideshow, and diagram tooling
1/**
2 * Markdown autoformat rule definitions for the docs editor.
3 *
4 * Pure data: each rule describes a regex pattern and what action it triggers.
5 * This keeps the logic testable without requiring a TipTap instance.
6 *
7 * Most markdown autoformat rules are already provided by TipTap's built-in
8 * extensions (via StarterKit and individual extensions). This module defines
9 * only the additional rules that are NOT built-in, plus exports a complete
10 * inventory of all active rules for documentation and testing.
11 */
12import type { AutoformatRule, AutoformatMatch, ParsedLink } from './types.js';
13
14/**
15 * Regex for markdown-style links: [text](url)
16 * Captures: [1] = full match with optional leading char, [2] = link text, [3] = URL
17 *
18 * This input rule is NOT provided by @tiptap/extension-link and must be
19 * added via a custom extension.
20 */
21export const linkInputRegex = /(?:^|\s)\[([^\]]+)\]\(([^)]+)\)$/;
22
23/**
24 * All active autoformat rules in the editor, including built-in ones.
25 * Used for testing and documentation purposes.
26 */
27export const AUTOFORMAT_RULES: AutoformatRule[] = [
28 // Block-level rules (built-in via StarterKit)
29 {
30 id: 'codeBlock',
31 description: 'Triple backticks to code block',
32 trigger: '``` + Space/Enter',
33 regex: /^```([a-z]+)?[\s\n]$/,
34 source: 'StarterKit (CodeBlock)',
35 custom: false,
36 },
37 {
38 id: 'heading1',
39 description: '# to Heading 1',
40 trigger: '# + Space',
41 regex: /^(#{1,1})\s$/,
42 source: 'StarterKit (Heading)',
43 custom: false,
44 },
45 {
46 id: 'heading2',
47 description: '## to Heading 2',
48 trigger: '## + Space',
49 regex: /^(#{1,2})\s$/,
50 source: 'StarterKit (Heading)',
51 custom: false,
52 },
53 {
54 id: 'heading3',
55 description: '### to Heading 3',
56 trigger: '### + Space',
57 regex: /^(#{1,3})\s$/,
58 source: 'StarterKit (Heading)',
59 custom: false,
60 },
61 {
62 id: 'blockquote',
63 description: '> to blockquote',
64 trigger: '> + Space',
65 regex: /^\s*>\s$/,
66 source: 'StarterKit (Blockquote)',
67 custom: false,
68 },
69 {
70 id: 'bulletList',
71 description: '- or * or + to bullet list',
72 trigger: '- + Space',
73 regex: /^\s*([-+*])\s$/,
74 source: 'StarterKit (BulletList)',
75 custom: false,
76 },
77 {
78 id: 'orderedList',
79 description: '1. to ordered list',
80 trigger: '1. + Space',
81 regex: /^(\d+)\.\s$/,
82 source: 'StarterKit (OrderedList)',
83 custom: false,
84 },
85 {
86 id: 'taskItem',
87 description: '[] or [ ] or [x] to task list',
88 trigger: '[] + Space',
89 regex: /^\s*(\[([( |x])?\])\s$/,
90 source: 'TaskItem',
91 custom: false,
92 },
93 {
94 id: 'horizontalRule',
95 description: '--- or *** to horizontal rule',
96 trigger: '---',
97 regex: /^(?:---|—-|___\s|\*\*\*\s)$/,
98 source: 'StarterKit (HorizontalRule)',
99 custom: false,
100 },
101
102 // Inline mark rules (built-in via StarterKit)
103 {
104 id: 'inlineCode',
105 description: '`text` to inline code',
106 trigger: '`text`',
107 regex: /(^|[^`])`([^`]+)`(?!`)/,
108 source: 'StarterKit (Code)',
109 custom: false,
110 },
111 {
112 id: 'strikethrough',
113 description: '~~text~~ to strikethrough',
114 trigger: '~~text~~',
115 regex: /(?:^|\s)(~~(?!\s+~~)((?:[^~]+))~~(?!\s+~~))$/,
116 source: 'StarterKit (Strike)',
117 custom: false,
118 },
119 {
120 id: 'italic',
121 description: '*text* or _text_ to italic',
122 trigger: '*text*',
123 regex: /(?:^|\s)(\*(?!\s+\*)((?:[^*]+))\*(?!\s+\*))$/,
124 source: 'StarterKit (Italic)',
125 custom: false,
126 },
127 {
128 id: 'bold',
129 description: '**text** or __text__ to bold',
130 trigger: '**text**',
131 regex: /(?:^|\s)(\*\*(?!\s+\*\*)((?:[^*]+))\*\*(?!\s+\*\*))$/,
132 source: 'StarterKit (Bold)',
133 custom: false,
134 },
135
136 // Custom rules (we add these)
137 {
138 id: 'link',
139 description: '[text](url) to link',
140 trigger: '[text](url)',
141 regex: linkInputRegex,
142 source: 'MarkdownAutoformat (custom)',
143 custom: true,
144 },
145];
146
147/**
148 * Determine which autoformat rule (if any) matches the given input text.
149 */
150export function resolveAutoformat(text: string): AutoformatMatch | null {
151 for (const rule of AUTOFORMAT_RULES) {
152 const match = text.match(rule.regex);
153 if (match) {
154 return { id: rule.id, match };
155 }
156 }
157 return null;
158}
159
160/**
161 * Parse a markdown link match into its components.
162 */
163export function parseLinkMatch(match: RegExpMatchArray): ParsedLink {
164 return {
165 text: match[1],
166 href: match[2],
167 };
168}