quasar-ui-danx 0.4.99 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/danx.es.js +17884 -12732
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +192 -118
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +11 -2
- package/scripts/publish.sh +76 -0
- package/src/components/Utility/Code/CodeViewer.vue +31 -14
- package/src/components/Utility/Code/CodeViewerCollapsed.vue +2 -0
- package/src/components/Utility/Code/CodeViewerFooter.vue +1 -1
- package/src/components/Utility/Code/LanguageBadge.vue +278 -5
- package/src/components/Utility/Code/MarkdownContent.vue +160 -6
- package/src/components/Utility/Code/index.ts +3 -0
- package/src/components/Utility/Markdown/ContextMenu.vue +314 -0
- package/src/components/Utility/Markdown/HotkeyHelpPopover.vue +259 -0
- package/src/components/Utility/Markdown/LineTypeMenu.vue +226 -0
- package/src/components/Utility/Markdown/LinkPopover.vue +331 -0
- package/src/components/Utility/Markdown/MarkdownEditor.vue +228 -0
- package/src/components/Utility/Markdown/MarkdownEditorContent.vue +235 -0
- package/src/components/Utility/Markdown/MarkdownEditorFooter.vue +50 -0
- package/src/components/Utility/Markdown/TablePopover.vue +420 -0
- package/src/components/Utility/Markdown/index.ts +11 -0
- package/src/components/Utility/Markdown/types.ts +27 -0
- package/src/components/Utility/index.ts +1 -0
- package/src/composables/index.ts +1 -0
- package/src/composables/markdown/features/useBlockquotes.spec.ts +428 -0
- package/src/composables/markdown/features/useBlockquotes.ts +248 -0
- package/src/composables/markdown/features/useCodeBlockManager.ts +369 -0
- package/src/composables/markdown/features/useCodeBlocks.spec.ts +779 -0
- package/src/composables/markdown/features/useCodeBlocks.ts +774 -0
- package/src/composables/markdown/features/useContextMenu.ts +444 -0
- package/src/composables/markdown/features/useFocusTracking.ts +116 -0
- package/src/composables/markdown/features/useHeadings.spec.ts +834 -0
- package/src/composables/markdown/features/useHeadings.ts +290 -0
- package/src/composables/markdown/features/useInlineFormatting.spec.ts +705 -0
- package/src/composables/markdown/features/useInlineFormatting.ts +402 -0
- package/src/composables/markdown/features/useLineTypeMenu.ts +285 -0
- package/src/composables/markdown/features/useLinks.spec.ts +369 -0
- package/src/composables/markdown/features/useLinks.ts +374 -0
- package/src/composables/markdown/features/useLists.spec.ts +834 -0
- package/src/composables/markdown/features/useLists.ts +747 -0
- package/src/composables/markdown/features/usePopoverManager.ts +181 -0
- package/src/composables/markdown/features/useTables.spec.ts +1601 -0
- package/src/composables/markdown/features/useTables.ts +1107 -0
- package/src/composables/markdown/index.ts +16 -0
- package/src/composables/markdown/useMarkdownEditor.spec.ts +332 -0
- package/src/composables/markdown/useMarkdownEditor.ts +1068 -0
- package/src/composables/markdown/useMarkdownHotkeys.spec.ts +791 -0
- package/src/composables/markdown/useMarkdownHotkeys.ts +266 -0
- package/src/composables/markdown/useMarkdownSelection.ts +219 -0
- package/src/composables/markdown/useMarkdownSync.ts +549 -0
- package/src/composables/useCodeViewerEditor.spec.ts +655 -0
- package/src/composables/useCodeViewerEditor.ts +174 -20
- package/src/helpers/formats/index.ts +1 -1
- package/src/helpers/formats/markdown/escapeHtml.ts +15 -0
- package/src/helpers/formats/markdown/escapeSequences.ts +60 -0
- package/src/helpers/formats/markdown/htmlToMarkdown/convertHeadings.ts +41 -0
- package/src/helpers/formats/markdown/htmlToMarkdown/index.spec.ts +489 -0
- package/src/helpers/formats/markdown/htmlToMarkdown/index.ts +412 -0
- package/src/helpers/formats/markdown/index.ts +92 -0
- package/src/helpers/formats/markdown/linePatterns.spec.ts +495 -0
- package/src/helpers/formats/markdown/linePatterns.ts +172 -0
- package/src/helpers/formats/markdown/parseInline.ts +124 -0
- package/src/helpers/formats/markdown/render/index.ts +92 -0
- package/src/helpers/formats/markdown/render/renderFootnotes.ts +30 -0
- package/src/helpers/formats/markdown/render/renderList.ts +69 -0
- package/src/helpers/formats/markdown/render/renderTable.ts +38 -0
- package/src/helpers/formats/markdown/state.ts +58 -0
- package/src/helpers/formats/markdown/tokenize/extractDefinitions.ts +39 -0
- package/src/helpers/formats/markdown/tokenize/index.ts +139 -0
- package/src/helpers/formats/markdown/tokenize/parseBlockquote.ts +34 -0
- package/src/helpers/formats/markdown/tokenize/parseCodeBlock.ts +85 -0
- package/src/helpers/formats/markdown/tokenize/parseDefinitionList.ts +88 -0
- package/src/helpers/formats/markdown/tokenize/parseHeading.ts +65 -0
- package/src/helpers/formats/markdown/tokenize/parseHorizontalRule.ts +22 -0
- package/src/helpers/formats/markdown/tokenize/parseList.ts +119 -0
- package/src/helpers/formats/markdown/tokenize/parseParagraph.ts +59 -0
- package/src/helpers/formats/markdown/tokenize/parseTable.ts +70 -0
- package/src/helpers/formats/markdown/tokenize/parseTaskList.ts +47 -0
- package/src/helpers/formats/markdown/tokenize/utils.ts +25 -0
- package/src/helpers/formats/markdown/types.ts +63 -0
- package/src/styles/danx.scss +1 -0
- package/src/styles/themes/danx/markdown.scss +96 -0
- package/src/test/helpers/editorTestUtils.spec.ts +296 -0
- package/src/test/helpers/editorTestUtils.ts +253 -0
- package/src/test/helpers/index.ts +1 -0
- package/src/test/setup.test.ts +12 -0
- package/src/test/setup.ts +12 -0
- package/vitest.config.ts +19 -0
- package/src/helpers/formats/renderMarkdown.ts +0 -338
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
detectHeadingPattern,
|
|
4
|
+
detectListPattern,
|
|
5
|
+
detectBlockquotePattern,
|
|
6
|
+
detectCodeFenceStart,
|
|
7
|
+
isHorizontalRule,
|
|
8
|
+
detectLinePattern,
|
|
9
|
+
} from './linePatterns';
|
|
10
|
+
|
|
11
|
+
describe('linePatterns', () => {
|
|
12
|
+
describe('detectHeadingPattern', () => {
|
|
13
|
+
it('detects H1 pattern', () => {
|
|
14
|
+
expect(detectHeadingPattern('# Hello')).toEqual({ level: 1, content: 'Hello' });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('detects H2 pattern', () => {
|
|
18
|
+
expect(detectHeadingPattern('## Title')).toEqual({ level: 2, content: 'Title' });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('detects H3 pattern', () => {
|
|
22
|
+
expect(detectHeadingPattern('### Subtitle')).toEqual({ level: 3, content: 'Subtitle' });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('detects H4 pattern', () => {
|
|
26
|
+
expect(detectHeadingPattern('#### Section')).toEqual({ level: 4, content: 'Section' });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('detects H5 pattern', () => {
|
|
30
|
+
expect(detectHeadingPattern('##### Subsection')).toEqual({ level: 5, content: 'Subsection' });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('detects H6 pattern', () => {
|
|
34
|
+
expect(detectHeadingPattern('###### Small heading')).toEqual({ level: 6, content: 'Small heading' });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('returns null for hash without space', () => {
|
|
38
|
+
expect(detectHeadingPattern('#Hello')).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('returns null for just hash and space with no content', () => {
|
|
42
|
+
expect(detectHeadingPattern('# ')).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('returns null for hash with only whitespace after', () => {
|
|
46
|
+
expect(detectHeadingPattern('# ')).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('returns null for too many hashes (7+)', () => {
|
|
50
|
+
expect(detectHeadingPattern('####### Too many')).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('returns null for empty string', () => {
|
|
54
|
+
expect(detectHeadingPattern('')).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('returns null for plain text', () => {
|
|
58
|
+
expect(detectHeadingPattern('Hello World')).toBeNull();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('handles content with multiple spaces after hash', () => {
|
|
62
|
+
// The regex \s+ consumes all whitespace, so extra spaces are not in content
|
|
63
|
+
expect(detectHeadingPattern('# Multiple spaces')).toEqual({ level: 1, content: 'Multiple spaces' });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('handles content with special characters', () => {
|
|
67
|
+
expect(detectHeadingPattern('## Hello! @#$%')).toEqual({ level: 2, content: 'Hello! @#$%' });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('handles content with numbers', () => {
|
|
71
|
+
expect(detectHeadingPattern('### Chapter 1')).toEqual({ level: 3, content: 'Chapter 1' });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('returns null for hash in middle of line', () => {
|
|
75
|
+
expect(detectHeadingPattern('Text # Hello')).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('detectListPattern', () => {
|
|
80
|
+
describe('unordered lists', () => {
|
|
81
|
+
it('detects dash list item', () => {
|
|
82
|
+
expect(detectListPattern('- item')).toEqual({ type: 'unordered', content: 'item' });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('detects asterisk list item', () => {
|
|
86
|
+
expect(detectListPattern('* item')).toEqual({ type: 'unordered', content: 'item' });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('detects plus list item', () => {
|
|
90
|
+
expect(detectListPattern('+ item')).toEqual({ type: 'unordered', content: 'item' });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('returns null for dash without space', () => {
|
|
94
|
+
expect(detectListPattern('-item')).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('returns null for asterisk without space', () => {
|
|
98
|
+
expect(detectListPattern('*item')).toBeNull();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('returns null for plus without space', () => {
|
|
102
|
+
expect(detectListPattern('+item')).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('handles empty content after marker', () => {
|
|
106
|
+
expect(detectListPattern('- ')).toEqual({ type: 'unordered', content: '' });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('handles content with special characters', () => {
|
|
110
|
+
expect(detectListPattern('- Hello! @world')).toEqual({ type: 'unordered', content: 'Hello! @world' });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('handles multiple spaces after marker', () => {
|
|
114
|
+
// The regex \s+ consumes all whitespace, so extra spaces are not in content
|
|
115
|
+
expect(detectListPattern('- two spaces')).toEqual({ type: 'unordered', content: 'two spaces' });
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('ordered lists', () => {
|
|
120
|
+
it('detects single digit ordered list', () => {
|
|
121
|
+
expect(detectListPattern('1. item')).toEqual({ type: 'ordered', content: 'item' });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('detects double digit ordered list', () => {
|
|
125
|
+
expect(detectListPattern('23. item')).toEqual({ type: 'ordered', content: 'item' });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('detects triple digit ordered list', () => {
|
|
129
|
+
expect(detectListPattern('100. item')).toEqual({ type: 'ordered', content: 'item' });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('returns null for number with dot but no space', () => {
|
|
133
|
+
expect(detectListPattern('1.item')).toBeNull();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('handles empty content after number', () => {
|
|
137
|
+
expect(detectListPattern('1. ')).toEqual({ type: 'ordered', content: '' });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('handles zero as list number', () => {
|
|
141
|
+
expect(detectListPattern('0. item')).toEqual({ type: 'ordered', content: 'item' });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('handles large numbers', () => {
|
|
145
|
+
expect(detectListPattern('99999. item')).toEqual({ type: 'ordered', content: 'item' });
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('returns null for empty string', () => {
|
|
150
|
+
expect(detectListPattern('')).toBeNull();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('returns null for plain text', () => {
|
|
154
|
+
expect(detectListPattern('Hello World')).toBeNull();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('returns null for marker in middle of line', () => {
|
|
158
|
+
expect(detectListPattern('Text - item')).toBeNull();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('detectBlockquotePattern', () => {
|
|
163
|
+
it('detects blockquote with content', () => {
|
|
164
|
+
expect(detectBlockquotePattern('> quote')).toEqual({ content: 'quote' });
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('detects empty blockquote', () => {
|
|
168
|
+
expect(detectBlockquotePattern('>')).toEqual({ content: '' });
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('detects blockquote without space after marker', () => {
|
|
172
|
+
// The regex `^>\s?(.*)$` should still match ">quote" since space is optional
|
|
173
|
+
expect(detectBlockquotePattern('>quote')).toEqual({ content: 'quote' });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('detects blockquote with space but no content', () => {
|
|
177
|
+
expect(detectBlockquotePattern('> ')).toEqual({ content: '' });
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('handles content with special characters', () => {
|
|
181
|
+
expect(detectBlockquotePattern('> Hello! @world #tag')).toEqual({ content: 'Hello! @world #tag' });
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('handles multiple spaces after marker', () => {
|
|
185
|
+
expect(detectBlockquotePattern('> two spaces')).toEqual({ content: ' two spaces' });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('handles long quote content', () => {
|
|
189
|
+
const longContent = 'This is a very long quote that contains many words and characters to test the pattern detection.';
|
|
190
|
+
expect(detectBlockquotePattern(`> ${longContent}`)).toEqual({ content: longContent });
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('returns null for empty string', () => {
|
|
194
|
+
expect(detectBlockquotePattern('')).toBeNull();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('returns null for plain text', () => {
|
|
198
|
+
expect(detectBlockquotePattern('Hello World')).toBeNull();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('returns null for marker in middle of line', () => {
|
|
202
|
+
expect(detectBlockquotePattern('Text > quote')).toBeNull();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('detectCodeFenceStart', () => {
|
|
207
|
+
it('detects code fence without language', () => {
|
|
208
|
+
expect(detectCodeFenceStart('```')).toEqual({ language: '' });
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('detects code fence with javascript language', () => {
|
|
212
|
+
expect(detectCodeFenceStart('```javascript')).toEqual({ language: 'javascript' });
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('detects code fence with ts language', () => {
|
|
216
|
+
expect(detectCodeFenceStart('```ts')).toEqual({ language: 'ts' });
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('detects code fence with python language', () => {
|
|
220
|
+
expect(detectCodeFenceStart('```python')).toEqual({ language: 'python' });
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('detects code fence with vue language', () => {
|
|
224
|
+
expect(detectCodeFenceStart('```vue')).toEqual({ language: 'vue' });
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('returns null for code fence with trailing space', () => {
|
|
228
|
+
expect(detectCodeFenceStart('``` ')).toBeNull();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('returns null for code fence with space before language', () => {
|
|
232
|
+
expect(detectCodeFenceStart('``` javascript')).toBeNull();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('returns null for code fence with non-word characters in language', () => {
|
|
236
|
+
// The regex requires word characters only (\w*)
|
|
237
|
+
expect(detectCodeFenceStart('```java-script')).toBeNull();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('returns null for only two backticks', () => {
|
|
241
|
+
expect(detectCodeFenceStart('``')).toBeNull();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('returns null for single backtick', () => {
|
|
245
|
+
expect(detectCodeFenceStart('`')).toBeNull();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('returns null for four backticks', () => {
|
|
249
|
+
// The regex specifically matches exactly 3 backticks at start
|
|
250
|
+
expect(detectCodeFenceStart('````')).toBeNull();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('returns null for empty string', () => {
|
|
254
|
+
expect(detectCodeFenceStart('')).toBeNull();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('returns null for plain text', () => {
|
|
258
|
+
expect(detectCodeFenceStart('Hello World')).toBeNull();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('returns null for backticks in middle of line', () => {
|
|
262
|
+
expect(detectCodeFenceStart('Text ```javascript')).toBeNull();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('handles uppercase language identifiers', () => {
|
|
266
|
+
expect(detectCodeFenceStart('```JavaScript')).toEqual({ language: 'JavaScript' });
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('handles language with numbers', () => {
|
|
270
|
+
expect(detectCodeFenceStart('```php7')).toEqual({ language: 'php7' });
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('handles language with underscores', () => {
|
|
274
|
+
expect(detectCodeFenceStart('```c_plus_plus')).toEqual({ language: 'c_plus_plus' });
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe('isHorizontalRule', () => {
|
|
279
|
+
describe('dashes', () => {
|
|
280
|
+
it('detects three dashes', () => {
|
|
281
|
+
expect(isHorizontalRule('---')).toBe(true);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('detects four dashes', () => {
|
|
285
|
+
expect(isHorizontalRule('----')).toBe(true);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('detects dashes with spaces between', () => {
|
|
289
|
+
expect(isHorizontalRule('- - -')).toBe(true);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('detects many dashes', () => {
|
|
293
|
+
expect(isHorizontalRule('----------')).toBe(true);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe('asterisks', () => {
|
|
298
|
+
it('detects three asterisks', () => {
|
|
299
|
+
expect(isHorizontalRule('***')).toBe(true);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('detects four asterisks', () => {
|
|
303
|
+
expect(isHorizontalRule('****')).toBe(true);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('detects asterisks with spaces between', () => {
|
|
307
|
+
expect(isHorizontalRule('* * *')).toBe(true);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('detects many asterisks', () => {
|
|
311
|
+
expect(isHorizontalRule('**********')).toBe(true);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe('underscores', () => {
|
|
316
|
+
it('detects three underscores', () => {
|
|
317
|
+
expect(isHorizontalRule('___')).toBe(true);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('detects four underscores', () => {
|
|
321
|
+
expect(isHorizontalRule('____')).toBe(true);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('detects underscores with spaces between', () => {
|
|
325
|
+
expect(isHorizontalRule('_ _ _')).toBe(true);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('detects many underscores', () => {
|
|
329
|
+
expect(isHorizontalRule('__________')).toBe(true);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe('edge cases', () => {
|
|
334
|
+
it('returns false for two dashes', () => {
|
|
335
|
+
expect(isHorizontalRule('--')).toBe(false);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('returns false for two asterisks', () => {
|
|
339
|
+
expect(isHorizontalRule('**')).toBe(false);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('returns false for two underscores', () => {
|
|
343
|
+
expect(isHorizontalRule('__')).toBe(false);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('returns true for mixed characters (allowed by regex)', () => {
|
|
347
|
+
// The regex ^([-*_]\s*){3,}$ allows mixed characters as long as each is -, *, or _
|
|
348
|
+
expect(isHorizontalRule('-*_')).toBe(true);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('returns false for dashes with text', () => {
|
|
352
|
+
expect(isHorizontalRule('---text')).toBe(false);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('returns false for empty string', () => {
|
|
356
|
+
expect(isHorizontalRule('')).toBe(false);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('returns false for plain text', () => {
|
|
360
|
+
expect(isHorizontalRule('abc')).toBe(false);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('handles leading whitespace', () => {
|
|
364
|
+
expect(isHorizontalRule(' ---')).toBe(true);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('handles trailing whitespace', () => {
|
|
368
|
+
expect(isHorizontalRule('--- ')).toBe(true);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('handles both leading and trailing whitespace', () => {
|
|
372
|
+
expect(isHorizontalRule(' --- ')).toBe(true);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('returns false for single dash', () => {
|
|
376
|
+
expect(isHorizontalRule('-')).toBe(false);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
describe('detectLinePattern', () => {
|
|
382
|
+
describe('heading patterns', () => {
|
|
383
|
+
it('detects H1 as heading pattern', () => {
|
|
384
|
+
expect(detectLinePattern('# Hello')).toEqual({ type: 'heading', level: 1 });
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('detects H2 as heading pattern', () => {
|
|
388
|
+
expect(detectLinePattern('## Title')).toEqual({ type: 'heading', level: 2 });
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('detects H6 as heading pattern', () => {
|
|
392
|
+
expect(detectLinePattern('###### Small')).toEqual({ type: 'heading', level: 6 });
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
describe('list patterns', () => {
|
|
397
|
+
it('detects unordered list with dash', () => {
|
|
398
|
+
expect(detectLinePattern('- item')).toEqual({ type: 'unordered-list' });
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('detects unordered list with asterisk', () => {
|
|
402
|
+
expect(detectLinePattern('* item')).toEqual({ type: 'unordered-list' });
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('detects unordered list with plus', () => {
|
|
406
|
+
expect(detectLinePattern('+ item')).toEqual({ type: 'unordered-list' });
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('detects ordered list', () => {
|
|
410
|
+
expect(detectLinePattern('1. item')).toEqual({ type: 'ordered-list' });
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('detects ordered list with large number', () => {
|
|
414
|
+
expect(detectLinePattern('99. item')).toEqual({ type: 'ordered-list' });
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
describe('blockquote patterns', () => {
|
|
419
|
+
it('detects blockquote', () => {
|
|
420
|
+
expect(detectLinePattern('> quote')).toEqual({ type: 'blockquote' });
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('detects empty blockquote', () => {
|
|
424
|
+
expect(detectLinePattern('>')).toEqual({ type: 'blockquote' });
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
describe('code block patterns', () => {
|
|
429
|
+
it('detects code block without language', () => {
|
|
430
|
+
expect(detectLinePattern('```')).toEqual({ type: 'code-block', language: '' });
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('detects code block with language', () => {
|
|
434
|
+
expect(detectLinePattern('```javascript')).toEqual({ type: 'code-block', language: 'javascript' });
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
describe('horizontal rule patterns', () => {
|
|
439
|
+
it('detects hr with dashes', () => {
|
|
440
|
+
expect(detectLinePattern('---')).toEqual({ type: 'hr' });
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('detects hr with asterisks', () => {
|
|
444
|
+
expect(detectLinePattern('***')).toEqual({ type: 'hr' });
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('detects hr with underscores', () => {
|
|
448
|
+
expect(detectLinePattern('___')).toEqual({ type: 'hr' });
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it('detects hr with spaces between dashes', () => {
|
|
452
|
+
expect(detectLinePattern('- - -')).toEqual({ type: 'hr' });
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
describe('no pattern detected', () => {
|
|
457
|
+
it('returns null for empty string', () => {
|
|
458
|
+
expect(detectLinePattern('')).toBeNull();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('returns null for plain text', () => {
|
|
462
|
+
expect(detectLinePattern('Hello World')).toBeNull();
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('returns null for text starting with number but no dot', () => {
|
|
466
|
+
expect(detectLinePattern('123 items')).toBeNull();
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('returns null for hash without space', () => {
|
|
470
|
+
expect(detectLinePattern('#nospace')).toBeNull();
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('returns null for incomplete code fence', () => {
|
|
474
|
+
expect(detectLinePattern('``')).toBeNull();
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
describe('pattern priority', () => {
|
|
479
|
+
it('detects hr before unordered list for dashes', () => {
|
|
480
|
+
// "---" should be hr, not list
|
|
481
|
+
expect(detectLinePattern('---')).toEqual({ type: 'hr' });
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('detects hr before unordered list for asterisks', () => {
|
|
485
|
+
// "***" should be hr, not list
|
|
486
|
+
expect(detectLinePattern('***')).toEqual({ type: 'hr' });
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('detects list when dash has content', () => {
|
|
490
|
+
// "- item" should be list, not hr
|
|
491
|
+
expect(detectLinePattern('- item')).toEqual({ type: 'unordered-list' });
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line pattern detection for markdown
|
|
3
|
+
* Detects markdown patterns at the start of lines
|
|
4
|
+
* Used for character sequence triggering in live editing
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface LinePattern {
|
|
8
|
+
type: "heading" | "unordered-list" | "ordered-list" | "blockquote" | "code-block" | "hr";
|
|
9
|
+
level?: number; // For headings (1-6)
|
|
10
|
+
language?: string; // For code blocks
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface HeadingPattern {
|
|
14
|
+
level: number;
|
|
15
|
+
content: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ListPattern {
|
|
19
|
+
type: "unordered" | "ordered";
|
|
20
|
+
content: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface BlockquotePattern {
|
|
24
|
+
content: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CodeFencePattern {
|
|
28
|
+
language: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if a line starts with a heading pattern (# to ######)
|
|
33
|
+
* Requires at least one space after the hash marks AND at least one character of content.
|
|
34
|
+
* This ensures conversion happens after content is typed, not just on "# ".
|
|
35
|
+
* @param line - The line to check
|
|
36
|
+
* @returns Pattern info with level and content, or null if no pattern
|
|
37
|
+
*/
|
|
38
|
+
export function detectHeadingPattern(line: string): HeadingPattern | null {
|
|
39
|
+
// Require at least one non-whitespace character after "# " to avoid cursor issues
|
|
40
|
+
const match = line.match(/^(#{1,6})\s+(\S.*)$/);
|
|
41
|
+
if (!match) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
level: match[1].length,
|
|
47
|
+
content: match[2]
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if a line starts with a list pattern (-, *, +, or 1.)
|
|
53
|
+
* Requires a space after the marker
|
|
54
|
+
* @param line - The line to check
|
|
55
|
+
* @returns Pattern info with type and content, or null if no pattern
|
|
56
|
+
*/
|
|
57
|
+
export function detectListPattern(line: string): ListPattern | null {
|
|
58
|
+
// Unordered list: -, *, + followed by space
|
|
59
|
+
const unorderedMatch = line.match(/^[-*+]\s+(.*)$/);
|
|
60
|
+
if (unorderedMatch) {
|
|
61
|
+
return {
|
|
62
|
+
type: "unordered",
|
|
63
|
+
content: unorderedMatch[1]
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Ordered list: number followed by . and space
|
|
68
|
+
const orderedMatch = line.match(/^\d+\.\s+(.*)$/);
|
|
69
|
+
if (orderedMatch) {
|
|
70
|
+
return {
|
|
71
|
+
type: "ordered",
|
|
72
|
+
content: orderedMatch[1]
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check if a line starts with a blockquote pattern (>)
|
|
81
|
+
* @param line - The line to check
|
|
82
|
+
* @returns Pattern info with content, or null if no pattern
|
|
83
|
+
*/
|
|
84
|
+
export function detectBlockquotePattern(line: string): BlockquotePattern | null {
|
|
85
|
+
// Blockquote: > optionally followed by space and content
|
|
86
|
+
const match = line.match(/^>\s?(.*)$/);
|
|
87
|
+
if (!match) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
content: match[1]
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check if a line is a code fence start (```) with a language identifier
|
|
98
|
+
* Requires at least one character in the language identifier to avoid triggering
|
|
99
|
+
* on just "```" before the user finishes typing the language.
|
|
100
|
+
* @param line - The line to check
|
|
101
|
+
* @returns Pattern info with language, or null if not a code fence with language
|
|
102
|
+
*/
|
|
103
|
+
export function detectCodeFenceStart(line: string): CodeFencePattern | null {
|
|
104
|
+
// Code fence: ``` followed by required language identifier (at least 1 char)
|
|
105
|
+
// This prevents triggering on just "```" before user types the language
|
|
106
|
+
const match = line.match(/^```(\w+)$/);
|
|
107
|
+
if (!match) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
language: match[1]
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Check if a line is a horizontal rule (---, ***, ___)
|
|
118
|
+
* Must be at least 3 characters of the same type
|
|
119
|
+
* @param line - The line to check (should be trimmed)
|
|
120
|
+
* @returns True if the line is a horizontal rule
|
|
121
|
+
*/
|
|
122
|
+
export function isHorizontalRule(line: string): boolean {
|
|
123
|
+
const trimmed = line.trim();
|
|
124
|
+
// Must be 3+ of same character (-, *, _) with optional spaces between
|
|
125
|
+
return /^([-*_]\s*){3,}$/.test(trimmed) && /^[-*_\s]+$/.test(trimmed);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Detect if a line starts with a markdown pattern
|
|
130
|
+
* @param line - The line to check
|
|
131
|
+
* @returns Pattern info or null if no pattern detected
|
|
132
|
+
*/
|
|
133
|
+
export function detectLinePattern(line: string): LinePattern | null {
|
|
134
|
+
// Check for horizontal rule first (before list patterns)
|
|
135
|
+
if (isHorizontalRule(line)) {
|
|
136
|
+
return { type: "hr" };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check heading
|
|
140
|
+
const headingPattern = detectHeadingPattern(line);
|
|
141
|
+
if (headingPattern) {
|
|
142
|
+
return {
|
|
143
|
+
type: "heading",
|
|
144
|
+
level: headingPattern.level
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check list
|
|
149
|
+
const listPattern = detectListPattern(line);
|
|
150
|
+
if (listPattern) {
|
|
151
|
+
return {
|
|
152
|
+
type: listPattern.type === "unordered" ? "unordered-list" : "ordered-list"
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Check blockquote
|
|
157
|
+
const blockquotePattern = detectBlockquotePattern(line);
|
|
158
|
+
if (blockquotePattern) {
|
|
159
|
+
return { type: "blockquote" };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Check code fence
|
|
163
|
+
const codeFencePattern = detectCodeFenceStart(line);
|
|
164
|
+
if (codeFencePattern) {
|
|
165
|
+
return {
|
|
166
|
+
type: "code-block",
|
|
167
|
+
language: codeFencePattern.language
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return null;
|
|
172
|
+
}
|