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.
Files changed (90) hide show
  1. package/dist/danx.es.js +17884 -12732
  2. package/dist/danx.es.js.map +1 -1
  3. package/dist/danx.umd.js +192 -118
  4. package/dist/danx.umd.js.map +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +11 -2
  7. package/scripts/publish.sh +76 -0
  8. package/src/components/Utility/Code/CodeViewer.vue +31 -14
  9. package/src/components/Utility/Code/CodeViewerCollapsed.vue +2 -0
  10. package/src/components/Utility/Code/CodeViewerFooter.vue +1 -1
  11. package/src/components/Utility/Code/LanguageBadge.vue +278 -5
  12. package/src/components/Utility/Code/MarkdownContent.vue +160 -6
  13. package/src/components/Utility/Code/index.ts +3 -0
  14. package/src/components/Utility/Markdown/ContextMenu.vue +314 -0
  15. package/src/components/Utility/Markdown/HotkeyHelpPopover.vue +259 -0
  16. package/src/components/Utility/Markdown/LineTypeMenu.vue +226 -0
  17. package/src/components/Utility/Markdown/LinkPopover.vue +331 -0
  18. package/src/components/Utility/Markdown/MarkdownEditor.vue +228 -0
  19. package/src/components/Utility/Markdown/MarkdownEditorContent.vue +235 -0
  20. package/src/components/Utility/Markdown/MarkdownEditorFooter.vue +50 -0
  21. package/src/components/Utility/Markdown/TablePopover.vue +420 -0
  22. package/src/components/Utility/Markdown/index.ts +11 -0
  23. package/src/components/Utility/Markdown/types.ts +27 -0
  24. package/src/components/Utility/index.ts +1 -0
  25. package/src/composables/index.ts +1 -0
  26. package/src/composables/markdown/features/useBlockquotes.spec.ts +428 -0
  27. package/src/composables/markdown/features/useBlockquotes.ts +248 -0
  28. package/src/composables/markdown/features/useCodeBlockManager.ts +369 -0
  29. package/src/composables/markdown/features/useCodeBlocks.spec.ts +779 -0
  30. package/src/composables/markdown/features/useCodeBlocks.ts +774 -0
  31. package/src/composables/markdown/features/useContextMenu.ts +444 -0
  32. package/src/composables/markdown/features/useFocusTracking.ts +116 -0
  33. package/src/composables/markdown/features/useHeadings.spec.ts +834 -0
  34. package/src/composables/markdown/features/useHeadings.ts +290 -0
  35. package/src/composables/markdown/features/useInlineFormatting.spec.ts +705 -0
  36. package/src/composables/markdown/features/useInlineFormatting.ts +402 -0
  37. package/src/composables/markdown/features/useLineTypeMenu.ts +285 -0
  38. package/src/composables/markdown/features/useLinks.spec.ts +369 -0
  39. package/src/composables/markdown/features/useLinks.ts +374 -0
  40. package/src/composables/markdown/features/useLists.spec.ts +834 -0
  41. package/src/composables/markdown/features/useLists.ts +747 -0
  42. package/src/composables/markdown/features/usePopoverManager.ts +181 -0
  43. package/src/composables/markdown/features/useTables.spec.ts +1601 -0
  44. package/src/composables/markdown/features/useTables.ts +1107 -0
  45. package/src/composables/markdown/index.ts +16 -0
  46. package/src/composables/markdown/useMarkdownEditor.spec.ts +332 -0
  47. package/src/composables/markdown/useMarkdownEditor.ts +1068 -0
  48. package/src/composables/markdown/useMarkdownHotkeys.spec.ts +791 -0
  49. package/src/composables/markdown/useMarkdownHotkeys.ts +266 -0
  50. package/src/composables/markdown/useMarkdownSelection.ts +219 -0
  51. package/src/composables/markdown/useMarkdownSync.ts +549 -0
  52. package/src/composables/useCodeViewerEditor.spec.ts +655 -0
  53. package/src/composables/useCodeViewerEditor.ts +174 -20
  54. package/src/helpers/formats/index.ts +1 -1
  55. package/src/helpers/formats/markdown/escapeHtml.ts +15 -0
  56. package/src/helpers/formats/markdown/escapeSequences.ts +60 -0
  57. package/src/helpers/formats/markdown/htmlToMarkdown/convertHeadings.ts +41 -0
  58. package/src/helpers/formats/markdown/htmlToMarkdown/index.spec.ts +489 -0
  59. package/src/helpers/formats/markdown/htmlToMarkdown/index.ts +412 -0
  60. package/src/helpers/formats/markdown/index.ts +92 -0
  61. package/src/helpers/formats/markdown/linePatterns.spec.ts +495 -0
  62. package/src/helpers/formats/markdown/linePatterns.ts +172 -0
  63. package/src/helpers/formats/markdown/parseInline.ts +124 -0
  64. package/src/helpers/formats/markdown/render/index.ts +92 -0
  65. package/src/helpers/formats/markdown/render/renderFootnotes.ts +30 -0
  66. package/src/helpers/formats/markdown/render/renderList.ts +69 -0
  67. package/src/helpers/formats/markdown/render/renderTable.ts +38 -0
  68. package/src/helpers/formats/markdown/state.ts +58 -0
  69. package/src/helpers/formats/markdown/tokenize/extractDefinitions.ts +39 -0
  70. package/src/helpers/formats/markdown/tokenize/index.ts +139 -0
  71. package/src/helpers/formats/markdown/tokenize/parseBlockquote.ts +34 -0
  72. package/src/helpers/formats/markdown/tokenize/parseCodeBlock.ts +85 -0
  73. package/src/helpers/formats/markdown/tokenize/parseDefinitionList.ts +88 -0
  74. package/src/helpers/formats/markdown/tokenize/parseHeading.ts +65 -0
  75. package/src/helpers/formats/markdown/tokenize/parseHorizontalRule.ts +22 -0
  76. package/src/helpers/formats/markdown/tokenize/parseList.ts +119 -0
  77. package/src/helpers/formats/markdown/tokenize/parseParagraph.ts +59 -0
  78. package/src/helpers/formats/markdown/tokenize/parseTable.ts +70 -0
  79. package/src/helpers/formats/markdown/tokenize/parseTaskList.ts +47 -0
  80. package/src/helpers/formats/markdown/tokenize/utils.ts +25 -0
  81. package/src/helpers/formats/markdown/types.ts +63 -0
  82. package/src/styles/danx.scss +1 -0
  83. package/src/styles/themes/danx/markdown.scss +96 -0
  84. package/src/test/helpers/editorTestUtils.spec.ts +296 -0
  85. package/src/test/helpers/editorTestUtils.ts +253 -0
  86. package/src/test/helpers/index.ts +1 -0
  87. package/src/test/setup.test.ts +12 -0
  88. package/src/test/setup.ts +12 -0
  89. package/vitest.config.ts +19 -0
  90. 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
+ }