Full document, spreadsheet, slideshow, and diagram tooling
1/**
2 * Tests for autoformat rule definitions and matching (src/docs/autoformat-rules.ts).
3 * Covers AUTOFORMAT_RULES data, resolveAutoformat matching, parseLinkMatch, and linkInputRegex.
4 */
5import { describe, it, expect } from 'vitest';
6import {
7 AUTOFORMAT_RULES,
8 linkInputRegex,
9 resolveAutoformat,
10 parseLinkMatch,
11} from '../src/docs/autoformat-rules.js';
12
13// =====================================================================
14// AUTOFORMAT_RULES — data integrity
15// =====================================================================
16
17describe('AUTOFORMAT_RULES', () => {
18 it('has at least 10 rules', () => {
19 expect(AUTOFORMAT_RULES.length).toBeGreaterThanOrEqual(10);
20 });
21
22 it('each rule has required fields', () => {
23 for (const rule of AUTOFORMAT_RULES) {
24 expect(rule.id).toBeTruthy();
25 expect(rule.description).toBeTruthy();
26 expect(rule.trigger).toBeTruthy();
27 expect(rule.regex).toBeInstanceOf(RegExp);
28 expect(rule.source).toBeTruthy();
29 expect(typeof rule.custom).toBe('boolean');
30 }
31 });
32
33 it('has unique IDs', () => {
34 const ids = AUTOFORMAT_RULES.map(r => r.id);
35 expect(new Set(ids).size).toBe(ids.length);
36 });
37
38 it('has exactly one custom rule (link)', () => {
39 const custom = AUTOFORMAT_RULES.filter(r => r.custom);
40 expect(custom.length).toBe(1);
41 expect(custom[0].id).toBe('link');
42 });
43
44 it('contains expected rule IDs', () => {
45 const ids = AUTOFORMAT_RULES.map(r => r.id);
46 expect(ids).toContain('heading1');
47 expect(ids).toContain('heading2');
48 expect(ids).toContain('heading3');
49 expect(ids).toContain('bulletList');
50 expect(ids).toContain('orderedList');
51 expect(ids).toContain('blockquote');
52 expect(ids).toContain('codeBlock');
53 expect(ids).toContain('bold');
54 expect(ids).toContain('italic');
55 expect(ids).toContain('strikethrough');
56 expect(ids).toContain('link');
57 });
58});
59
60// =====================================================================
61// linkInputRegex
62// =====================================================================
63
64describe('linkInputRegex', () => {
65 it('matches [text](url) at end of string', () => {
66 const match = '[Example](https://example.com)'.match(linkInputRegex);
67 expect(match).not.toBeNull();
68 expect(match![1]).toBe('Example');
69 expect(match![2]).toBe('https://example.com');
70 });
71
72 it('matches with leading space', () => {
73 const match = 'some text [link](http://a.com)'.match(linkInputRegex);
74 expect(match).not.toBeNull();
75 expect(match![1]).toBe('link');
76 });
77
78 it('does not match without closing paren', () => {
79 expect('[text](url'.match(linkInputRegex)).toBeNull();
80 });
81
82 it('does not match without brackets', () => {
83 expect('text(url)'.match(linkInputRegex)).toBeNull();
84 });
85
86 it('does not match empty text', () => {
87 expect('[](url)'.match(linkInputRegex)).toBeNull();
88 });
89});
90
91// =====================================================================
92// resolveAutoformat
93// =====================================================================
94
95describe('resolveAutoformat', () => {
96 describe('block-level rules', () => {
97 it('matches # + space as heading1', () => {
98 const result = resolveAutoformat('# ');
99 expect(result).not.toBeNull();
100 expect(result!.id).toBe('heading1');
101 });
102
103 it('matches ## + space as heading2', () => {
104 const result = resolveAutoformat('## ');
105 expect(result).not.toBeNull();
106 expect(result!.id).toBe('heading2');
107 });
108
109 it('matches ### + space as heading3', () => {
110 const result = resolveAutoformat('### ');
111 expect(result).not.toBeNull();
112 expect(result!.id).toBe('heading3');
113 });
114
115 it('matches > + space as blockquote', () => {
116 const result = resolveAutoformat('> ');
117 expect(result).not.toBeNull();
118 expect(result!.id).toBe('blockquote');
119 });
120
121 it('matches - + space as bulletList', () => {
122 const result = resolveAutoformat('- ');
123 expect(result).not.toBeNull();
124 expect(result!.id).toBe('bulletList');
125 });
126
127 it('matches * + space as bulletList', () => {
128 const result = resolveAutoformat('* ');
129 expect(result).not.toBeNull();
130 expect(result!.id).toBe('bulletList');
131 });
132
133 it('matches 1. + space as orderedList', () => {
134 const result = resolveAutoformat('1. ');
135 expect(result).not.toBeNull();
136 expect(result!.id).toBe('orderedList');
137 });
138
139 it('matches ``` + space as codeBlock', () => {
140 const result = resolveAutoformat('``` ');
141 expect(result).not.toBeNull();
142 expect(result!.id).toBe('codeBlock');
143 });
144
145 it('matches ```js + newline as codeBlock with language', () => {
146 const result = resolveAutoformat('```js\n');
147 expect(result).not.toBeNull();
148 expect(result!.id).toBe('codeBlock');
149 });
150 });
151
152 describe('inline mark rules', () => {
153 it('matches **text** as bold', () => {
154 const result = resolveAutoformat(' **hello**');
155 expect(result).not.toBeNull();
156 expect(result!.id).toBe('bold');
157 });
158
159 it('matches *text* as italic', () => {
160 const result = resolveAutoformat(' *hello*');
161 expect(result).not.toBeNull();
162 expect(result!.id).toBe('italic');
163 });
164
165 it('matches ~~text~~ as strikethrough', () => {
166 const result = resolveAutoformat(' ~~hello~~');
167 expect(result).not.toBeNull();
168 expect(result!.id).toBe('strikethrough');
169 });
170
171 it('matches `code` as inlineCode', () => {
172 const result = resolveAutoformat('`code`');
173 expect(result).not.toBeNull();
174 expect(result!.id).toBe('inlineCode');
175 });
176 });
177
178 describe('custom rules', () => {
179 it('matches [text](url) as link', () => {
180 const result = resolveAutoformat(' [Example](https://example.com)');
181 expect(result).not.toBeNull();
182 expect(result!.id).toBe('link');
183 });
184 });
185
186 describe('non-matches', () => {
187 it('returns null for plain text', () => {
188 expect(resolveAutoformat('hello world')).toBeNull();
189 });
190
191 it('returns null for empty string', () => {
192 expect(resolveAutoformat('')).toBeNull();
193 });
194
195 it('returns null for partial markdown', () => {
196 expect(resolveAutoformat('#no-space')).toBeNull();
197 });
198 });
199});
200
201// =====================================================================
202// parseLinkMatch
203// =====================================================================
204
205describe('parseLinkMatch', () => {
206 it('extracts text and href from regex match', () => {
207 const match = ' [My Link](https://example.com)'.match(linkInputRegex)!;
208 const parsed = parseLinkMatch(match);
209 expect(parsed.text).toBe('My Link');
210 expect(parsed.href).toBe('https://example.com');
211 });
212
213 it('handles URL with path and query', () => {
214 const match = ' [Docs](https://example.com/docs?v=1)'.match(linkInputRegex)!;
215 const parsed = parseLinkMatch(match);
216 expect(parsed.text).toBe('Docs');
217 expect(parsed.href).toBe('https://example.com/docs?v=1');
218 });
219});