@pep/term-deck 1.0.13 → 1.0.15

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 (36) hide show
  1. package/dist/bin/term-deck.d.ts +1 -0
  2. package/dist/bin/term-deck.js +1720 -0
  3. package/dist/bin/term-deck.js.map +1 -0
  4. package/dist/index.d.ts +670 -0
  5. package/dist/index.js +159 -0
  6. package/dist/index.js.map +1 -0
  7. package/package.json +16 -13
  8. package/bin/term-deck.ts +0 -45
  9. package/src/cli/__tests__/errors.test.ts +0 -201
  10. package/src/cli/__tests__/help.test.ts +0 -157
  11. package/src/cli/__tests__/init.test.ts +0 -110
  12. package/src/cli/commands/export.ts +0 -33
  13. package/src/cli/commands/init.ts +0 -125
  14. package/src/cli/commands/present.ts +0 -29
  15. package/src/cli/errors.ts +0 -77
  16. package/src/core/__tests__/slide.test.ts +0 -1759
  17. package/src/core/__tests__/theme.test.ts +0 -1103
  18. package/src/core/slide.ts +0 -509
  19. package/src/core/theme.ts +0 -388
  20. package/src/export/__tests__/recorder.test.ts +0 -566
  21. package/src/export/recorder.ts +0 -639
  22. package/src/index.ts +0 -36
  23. package/src/presenter/__tests__/main.test.ts +0 -244
  24. package/src/presenter/main.ts +0 -658
  25. package/src/renderer/__tests__/screen-extended.test.ts +0 -801
  26. package/src/renderer/__tests__/screen.test.ts +0 -525
  27. package/src/renderer/screen.ts +0 -671
  28. package/src/schemas/__tests__/config.test.ts +0 -429
  29. package/src/schemas/__tests__/slide.test.ts +0 -349
  30. package/src/schemas/__tests__/theme.test.ts +0 -970
  31. package/src/schemas/__tests__/validation.test.ts +0 -256
  32. package/src/schemas/config.ts +0 -58
  33. package/src/schemas/slide.ts +0 -56
  34. package/src/schemas/theme.ts +0 -203
  35. package/src/schemas/validation.ts +0 -64
  36. package/src/themes/matrix/index.ts +0 -53
@@ -1,1759 +0,0 @@
1
- import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'bun:test'
2
- import { extractNotes, parseSlide, findSlideFiles, loadDeckConfig, loadDeck, hasMermaidDiagrams, extractMermaidBlocks, mermaidToAscii, formatMermaidError, processMermaidDiagrams, normalizeBigText } from '../slide'
3
- import { DEFAULT_THEME } from '../../schemas/theme'
4
- import { join } from 'path'
5
- import { rmSync, mkdirSync } from 'fs'
6
-
7
- // Temp directory for test files
8
- const TEST_DIR = join(import.meta.dir, '.test-slides')
9
-
10
- // Helper to create test slide files
11
- async function createTestSlide(name: string, content: string): Promise<string> {
12
- const path = join(TEST_DIR, name)
13
- await Bun.write(path, content)
14
- return path
15
- }
16
-
17
- describe('extractNotes', () => {
18
- describe('basic extraction', () => {
19
- it('extracts notes from content with marker', () => {
20
- const content = `
21
- This is the body.
22
-
23
- <!-- notes -->
24
- These are presenter notes.
25
- They can span multiple lines.
26
- `
27
- const { body, notes } = extractNotes(content)
28
-
29
- expect(body).toBe('This is the body.')
30
- expect(notes).toBe('These are presenter notes.\nThey can span multiple lines.')
31
- })
32
-
33
- it('returns undefined notes if no marker', () => {
34
- const content = 'Just body content'
35
- const { body, notes } = extractNotes(content)
36
-
37
- expect(body).toBe('Just body content')
38
- expect(notes).toBeUndefined()
39
- })
40
-
41
- it('handles empty content', () => {
42
- const { body, notes } = extractNotes('')
43
-
44
- expect(body).toBe('')
45
- expect(notes).toBeUndefined()
46
- })
47
-
48
- it('handles only notes marker with no content', () => {
49
- const content = '<!-- notes -->'
50
- const { body, notes } = extractNotes(content)
51
-
52
- expect(body).toBe('')
53
- expect(notes).toBeUndefined()
54
- })
55
- })
56
-
57
- describe('explicit end marker', () => {
58
- it('handles explicit end marker', () => {
59
- const content = `
60
- Body content.
61
-
62
- <!-- notes -->
63
- Notes here.
64
- <!-- /notes -->
65
-
66
- More body content.
67
- `
68
- const { body, notes } = extractNotes(content)
69
-
70
- expect(body).toBe('Body content.')
71
- expect(notes).toBe('Notes here.')
72
- })
73
-
74
- it('extracts only content between markers', () => {
75
- const content = `First part.
76
-
77
- <!-- notes -->
78
- Secret speaker notes.
79
- <!-- /notes -->
80
-
81
- This comes after notes.`
82
-
83
- const { body, notes } = extractNotes(content)
84
-
85
- expect(body).toBe('First part.')
86
- expect(notes).toBe('Secret speaker notes.')
87
- })
88
-
89
- it('handles empty notes with end marker', () => {
90
- const content = `Body.
91
- <!-- notes -->
92
- <!-- /notes -->
93
- After.`
94
-
95
- const { body, notes } = extractNotes(content)
96
-
97
- expect(body).toBe('Body.')
98
- expect(notes).toBeUndefined()
99
- })
100
- })
101
-
102
- describe('edge cases', () => {
103
- it('handles notes at the very start', () => {
104
- const content = `<!-- notes -->
105
- Notes first.`
106
-
107
- const { body, notes } = extractNotes(content)
108
-
109
- expect(body).toBe('')
110
- expect(notes).toBe('Notes first.')
111
- })
112
-
113
- it('preserves whitespace in notes', () => {
114
- const content = `Body.
115
-
116
- <!-- notes -->
117
- Line 1
118
-
119
- Line 3
120
-
121
- Line 5`
122
-
123
- const { body, notes } = extractNotes(content)
124
-
125
- expect(notes).toBe('Line 1\n\nLine 3\n\nLine 5')
126
- })
127
-
128
- it('handles multiline body content', () => {
129
- const content = `# Title
130
-
131
- Paragraph one.
132
-
133
- Paragraph two.
134
-
135
- - List item 1
136
- - List item 2
137
-
138
- <!-- notes -->
139
- Speaker notes.`
140
-
141
- const { body, notes } = extractNotes(content)
142
-
143
- expect(body).toBe(`# Title
144
-
145
- Paragraph one.
146
-
147
- Paragraph two.
148
-
149
- - List item 1
150
- - List item 2`)
151
- expect(notes).toBe('Speaker notes.')
152
- })
153
-
154
- it('handles code blocks in body', () => {
155
- const content = `Some code:
156
-
157
- \`\`\`typescript
158
- const x = 1;
159
- \`\`\`
160
-
161
- <!-- notes -->
162
- Explain the code.`
163
-
164
- const { body, notes } = extractNotes(content)
165
-
166
- expect(body).toContain('```typescript')
167
- expect(notes).toBe('Explain the code.')
168
- })
169
-
170
- it('handles multiple notes markers (uses first)', () => {
171
- const content = `Body.
172
-
173
- <!-- notes -->
174
- First notes.
175
-
176
- <!-- notes -->
177
- Second notes section.`
178
-
179
- const { body, notes } = extractNotes(content)
180
-
181
- expect(body).toBe('Body.')
182
- expect(notes).toBe('First notes.\n\n<!-- notes -->\nSecond notes section.')
183
- })
184
-
185
- it('handles marker as part of text (not at line start)', () => {
186
- const content = `The marker is <!-- notes --> in the middle.`
187
-
188
- const { body, notes } = extractNotes(content)
189
-
190
- expect(body).toBe('The marker is')
191
- expect(notes).toBe('in the middle.')
192
- })
193
- })
194
-
195
- describe('trimming behavior', () => {
196
- it('trims whitespace from body', () => {
197
- const content = `
198
-
199
- Body with leading/trailing space.
200
-
201
- <!-- notes -->
202
- Notes.`
203
-
204
- const { body, notes } = extractNotes(content)
205
-
206
- expect(body).toBe('Body with leading/trailing space.')
207
- })
208
-
209
- it('trims whitespace from notes', () => {
210
- const content = `Body.
211
-
212
- <!-- notes -->
213
-
214
- Notes with extra whitespace.
215
-
216
- `
217
-
218
- const { body, notes } = extractNotes(content)
219
-
220
- expect(notes).toBe('Notes with extra whitespace.')
221
- })
222
- })
223
- })
224
-
225
- describe('hasMermaidDiagrams', () => {
226
- describe('detection', () => {
227
- it('returns true when content contains a mermaid block', () => {
228
- const content = `
229
- Some text
230
-
231
- \`\`\`mermaid
232
- graph LR
233
- A --> B
234
- \`\`\`
235
-
236
- More text
237
- `
238
- expect(hasMermaidDiagrams(content)).toBe(true)
239
- })
240
-
241
- it('returns false when content has no mermaid blocks', () => {
242
- const content = `
243
- # Regular Markdown
244
-
245
- Some text without diagrams.
246
-
247
- \`\`\`typescript
248
- const x = 1;
249
- \`\`\`
250
- `
251
- expect(hasMermaidDiagrams(content)).toBe(false)
252
- })
253
-
254
- it('returns false for empty content', () => {
255
- expect(hasMermaidDiagrams('')).toBe(false)
256
- })
257
-
258
- it('returns false for content with only regular code blocks', () => {
259
- const content = `
260
- \`\`\`javascript
261
- console.log('hello');
262
- \`\`\`
263
-
264
- \`\`\`python
265
- print('hello')
266
- \`\`\`
267
- `
268
- expect(hasMermaidDiagrams(content)).toBe(false)
269
- })
270
-
271
- it('returns true for multiple mermaid blocks', () => {
272
- const content = `
273
- \`\`\`mermaid
274
- graph LR
275
- A --> B
276
- \`\`\`
277
-
278
- Some text
279
-
280
- \`\`\`mermaid
281
- sequenceDiagram
282
- Alice->>Bob: Hello
283
- \`\`\`
284
- `
285
- expect(hasMermaidDiagrams(content)).toBe(true)
286
- })
287
- })
288
-
289
- describe('edge cases', () => {
290
- it('does not match mermaid without newline after language identifier', () => {
291
- // The pattern requires a newline after ```mermaid
292
- const content = '\`\`\`mermaidgraph LR\`\`\`'
293
- expect(hasMermaidDiagrams(content)).toBe(false)
294
- })
295
-
296
- it('handles mermaid block at start of content', () => {
297
- const content = `\`\`\`mermaid
298
- graph LR
299
- A --> B
300
- \`\`\``
301
- expect(hasMermaidDiagrams(content)).toBe(true)
302
- })
303
-
304
- it('handles mermaid block at end of content', () => {
305
- const content = `Text before
306
-
307
- \`\`\`mermaid
308
- graph TD
309
- A --> B
310
- \`\`\``
311
- expect(hasMermaidDiagrams(content)).toBe(true)
312
- })
313
-
314
- it('is case-sensitive (Mermaid uppercase not matched)', () => {
315
- const content = `
316
- \`\`\`Mermaid
317
- graph LR
318
- A --> B
319
- \`\`\`
320
- `
321
- expect(hasMermaidDiagrams(content)).toBe(false)
322
- })
323
-
324
- it('can be called multiple times correctly (handles lastIndex reset)', () => {
325
- const content = `
326
- \`\`\`mermaid
327
- graph LR
328
- A --> B
329
- \`\`\`
330
- `
331
- // Call multiple times to ensure lastIndex is properly reset
332
- expect(hasMermaidDiagrams(content)).toBe(true)
333
- expect(hasMermaidDiagrams(content)).toBe(true)
334
- expect(hasMermaidDiagrams(content)).toBe(true)
335
- })
336
- })
337
- })
338
-
339
- describe('extractMermaidBlocks', () => {
340
- describe('single block extraction', () => {
341
- it('extracts a single mermaid block', () => {
342
- const content = `
343
- Some text
344
-
345
- \`\`\`mermaid
346
- graph LR
347
- A --> B
348
- \`\`\`
349
-
350
- More text
351
- `
352
- const blocks = extractMermaidBlocks(content)
353
-
354
- expect(blocks).toHaveLength(1)
355
- expect(blocks[0]).toBe('graph LR\n A --> B')
356
- })
357
-
358
- it('trims whitespace from extracted blocks', () => {
359
- const content = `
360
- \`\`\`mermaid
361
-
362
- graph TD
363
- A --> B
364
-
365
- \`\`\`
366
- `
367
- const blocks = extractMermaidBlocks(content)
368
-
369
- expect(blocks).toHaveLength(1)
370
- expect(blocks[0]).toBe('graph TD\n A --> B')
371
- })
372
- })
373
-
374
- describe('multiple block extraction', () => {
375
- it('extracts multiple mermaid blocks', () => {
376
- const content = `
377
- Some text
378
-
379
- \`\`\`mermaid
380
- graph LR
381
- A --> B
382
- \`\`\`
383
-
384
- More text
385
-
386
- \`\`\`mermaid
387
- sequenceDiagram
388
- Alice->>Bob: Hello
389
- \`\`\`
390
-
391
- Even more text
392
- `
393
- const blocks = extractMermaidBlocks(content)
394
-
395
- expect(blocks).toHaveLength(2)
396
- expect(blocks[0]).toContain('graph LR')
397
- expect(blocks[0]).toContain('A --> B')
398
- expect(blocks[1]).toContain('sequenceDiagram')
399
- expect(blocks[1]).toContain('Alice->>Bob: Hello')
400
- })
401
-
402
- it('extracts three mermaid blocks', () => {
403
- const content = `
404
- \`\`\`mermaid
405
- graph LR
406
- A --> B
407
- \`\`\`
408
-
409
- \`\`\`mermaid
410
- pie
411
- title Pets
412
- "Dogs" : 386
413
- \`\`\`
414
-
415
- \`\`\`mermaid
416
- classDiagram
417
- class Animal
418
- \`\`\`
419
- `
420
- const blocks = extractMermaidBlocks(content)
421
-
422
- expect(blocks).toHaveLength(3)
423
- expect(blocks[0]).toContain('graph LR')
424
- expect(blocks[1]).toContain('pie')
425
- expect(blocks[2]).toContain('classDiagram')
426
- })
427
- })
428
-
429
- describe('no blocks', () => {
430
- it('returns empty array when no mermaid blocks', () => {
431
- const content = 'Just regular text'
432
- const blocks = extractMermaidBlocks(content)
433
-
434
- expect(blocks).toHaveLength(0)
435
- expect(blocks).toEqual([])
436
- })
437
-
438
- it('returns empty array for empty content', () => {
439
- const blocks = extractMermaidBlocks('')
440
-
441
- expect(blocks).toHaveLength(0)
442
- })
443
-
444
- it('ignores non-mermaid code blocks', () => {
445
- const content = `
446
- \`\`\`typescript
447
- const x = 1;
448
- \`\`\`
449
-
450
- \`\`\`javascript
451
- console.log('hello');
452
- \`\`\`
453
- `
454
- const blocks = extractMermaidBlocks(content)
455
-
456
- expect(blocks).toHaveLength(0)
457
- })
458
- })
459
-
460
- describe('edge cases', () => {
461
- it('handles empty mermaid block', () => {
462
- const content = `
463
- \`\`\`mermaid
464
- \`\`\`
465
- `
466
- const blocks = extractMermaidBlocks(content)
467
-
468
- expect(blocks).toHaveLength(1)
469
- expect(blocks[0]).toBe('')
470
- })
471
-
472
- it('handles mermaid block with only whitespace', () => {
473
- const content = `
474
- \`\`\`mermaid
475
-
476
-
477
-
478
- \`\`\`
479
- `
480
- const blocks = extractMermaidBlocks(content)
481
-
482
- expect(blocks).toHaveLength(1)
483
- expect(blocks[0]).toBe('')
484
- })
485
-
486
- it('preserves complex diagram syntax', () => {
487
- const content = `
488
- \`\`\`mermaid
489
- graph TB
490
- subgraph "Group One"
491
- A[Square] --> B((Circle))
492
- B --> C{Diamond}
493
- end
494
- C -->|Yes| D[Result]
495
- C -->|No| E[Other]
496
- \`\`\`
497
- `
498
- const blocks = extractMermaidBlocks(content)
499
-
500
- expect(blocks).toHaveLength(1)
501
- expect(blocks[0]).toContain('subgraph "Group One"')
502
- expect(blocks[0]).toContain('A[Square]')
503
- expect(blocks[0]).toContain('B((Circle))')
504
- expect(blocks[0]).toContain('C{Diamond}')
505
- expect(blocks[0]).toContain('-->|Yes|')
506
- })
507
-
508
- it('handles flowchart with special characters', () => {
509
- const content = `
510
- \`\`\`mermaid
511
- flowchart LR
512
- A["Input with (parens)"] --> B["Output with {braces}"]
513
- B --> C["Text with [brackets]"]
514
- \`\`\`
515
- `
516
- const blocks = extractMermaidBlocks(content)
517
-
518
- expect(blocks).toHaveLength(1)
519
- expect(blocks[0]).toContain('flowchart LR')
520
- expect(blocks[0]).toContain('"Input with (parens)"')
521
- })
522
-
523
- it('handles sequence diagram syntax', () => {
524
- const content = `
525
- \`\`\`mermaid
526
- sequenceDiagram
527
- participant A as Alice
528
- participant B as Bob
529
- A->>B: Hello Bob
530
- B-->>A: Hi Alice
531
- A->>B: How are you?
532
- Note over A,B: This is a note
533
- \`\`\`
534
- `
535
- const blocks = extractMermaidBlocks(content)
536
-
537
- expect(blocks).toHaveLength(1)
538
- expect(blocks[0]).toContain('sequenceDiagram')
539
- expect(blocks[0]).toContain('participant A as Alice')
540
- expect(blocks[0]).toContain('A->>B:')
541
- expect(blocks[0]).toContain('Note over A,B:')
542
- })
543
-
544
- it('can be called multiple times correctly (handles lastIndex reset)', () => {
545
- const content = `
546
- \`\`\`mermaid
547
- graph LR
548
- A --> B
549
- \`\`\`
550
- `
551
- // Call multiple times to ensure lastIndex is properly reset
552
- expect(extractMermaidBlocks(content)).toHaveLength(1)
553
- expect(extractMermaidBlocks(content)).toHaveLength(1)
554
- expect(extractMermaidBlocks(content)).toHaveLength(1)
555
- })
556
-
557
- it('handles mixed mermaid and other code blocks', () => {
558
- const content = `
559
- \`\`\`typescript
560
- const x = 1;
561
- \`\`\`
562
-
563
- \`\`\`mermaid
564
- graph LR
565
- A --> B
566
- \`\`\`
567
-
568
- \`\`\`python
569
- print('hello')
570
- \`\`\`
571
-
572
- \`\`\`mermaid
573
- pie title Votes
574
- "A" : 50
575
- "B" : 30
576
- \`\`\`
577
-
578
- \`\`\`bash
579
- echo "hello"
580
- \`\`\`
581
- `
582
- const blocks = extractMermaidBlocks(content)
583
-
584
- expect(blocks).toHaveLength(2)
585
- expect(blocks[0]).toContain('graph LR')
586
- expect(blocks[1]).toContain('pie title Votes')
587
- })
588
- })
589
- })
590
-
591
- describe('parseSlide', () => {
592
- beforeAll(() => {
593
- // Create temp directory
594
- mkdirSync(TEST_DIR, { recursive: true })
595
- })
596
-
597
- afterAll(() => {
598
- // Cleanup temp directory
599
- rmSync(TEST_DIR, { recursive: true, force: true })
600
- })
601
-
602
- describe('valid slides', () => {
603
- it('parses a complete slide with all fields', async () => {
604
- const path = await createTestSlide('01-complete.md', `---
605
- title: Test Slide
606
- bigText: TEST
607
- gradient: fire
608
- transition: glitch
609
- ---
610
-
611
- {GREEN}Hello{/} World
612
-
613
- <!-- notes -->
614
- Test notes here.
615
- `)
616
-
617
- const slide = await parseSlide(path, 0)
618
-
619
- expect(slide.frontmatter.title).toBe('Test Slide')
620
- expect(slide.frontmatter.bigText).toBe('TEST')
621
- expect(slide.frontmatter.gradient).toBe('fire')
622
- expect(slide.frontmatter.transition).toBe('glitch')
623
- expect(slide.body).toBe('{GREEN}Hello{/} World')
624
- expect(slide.notes).toBe('Test notes here.')
625
- expect(slide.sourcePath).toBe(path)
626
- expect(slide.index).toBe(0)
627
- })
628
-
629
- it('parses a minimal slide with only title', async () => {
630
- const path = await createTestSlide('02-minimal.md', `---
631
- title: Minimal Slide
632
- ---
633
-
634
- Just body content.
635
- `)
636
-
637
- const slide = await parseSlide(path, 5)
638
-
639
- expect(slide.frontmatter.title).toBe('Minimal Slide')
640
- expect(slide.frontmatter.bigText).toBeUndefined()
641
- expect(slide.frontmatter.transition).toBe('glitch') // default
642
- expect(slide.body).toBe('Just body content.')
643
- expect(slide.notes).toBeUndefined()
644
- expect(slide.index).toBe(5)
645
- })
646
-
647
- it('parses slide with bigText as array', async () => {
648
- const path = await createTestSlide('03-array-bigtext.md', `---
649
- title: Multi-line Title
650
- bigText:
651
- - LINE ONE
652
- - LINE TWO
653
- ---
654
-
655
- Content.
656
- `)
657
-
658
- const slide = await parseSlide(path, 0)
659
-
660
- expect(slide.frontmatter.bigText).toEqual(['LINE ONE', 'LINE TWO'])
661
- })
662
-
663
- it('parses slide with different transitions', async () => {
664
- const transitions = ['fade', 'instant', 'typewriter'] as const
665
-
666
- for (const transition of transitions) {
667
- const path = await createTestSlide(`transition-${transition}.md`, `---
668
- title: ${transition} Test
669
- transition: ${transition}
670
- ---
671
-
672
- Content.
673
- `)
674
- const slide = await parseSlide(path, 0)
675
- expect(slide.frontmatter.transition).toBe(transition)
676
- }
677
- })
678
-
679
- it('parses slide with meta field', async () => {
680
- const path = await createTestSlide('04-meta.md', `---
681
- title: With Meta
682
- meta:
683
- author: Test Author
684
- customField: value
685
- ---
686
-
687
- Body.
688
- `)
689
-
690
- const slide = await parseSlide(path, 0)
691
-
692
- expect(slide.frontmatter.meta).toEqual({
693
- author: 'Test Author',
694
- customField: 'value',
695
- })
696
- })
697
-
698
- it('parses slide with explicit notes end marker', async () => {
699
- const path = await createTestSlide('05-notes-end.md', `---
700
- title: Notes End Test
701
- ---
702
-
703
- Before notes.
704
-
705
- <!-- notes -->
706
- The notes.
707
- <!-- /notes -->
708
-
709
- After notes.
710
- `)
711
-
712
- const slide = await parseSlide(path, 0)
713
-
714
- expect(slide.body).toBe('Before notes.')
715
- expect(slide.notes).toBe('The notes.')
716
- })
717
- })
718
-
719
- describe('invalid slides', () => {
720
- it('throws on missing title', async () => {
721
- const path = await createTestSlide('invalid-no-title.md', `---
722
- bigText: TEST
723
- ---
724
-
725
- Content without title.
726
- `)
727
-
728
- await expect(parseSlide(path, 0)).rejects.toThrow(/title/)
729
- })
730
-
731
- it('throws on empty title', async () => {
732
- const path = await createTestSlide('invalid-empty-title.md', `---
733
- title: ""
734
- ---
735
-
736
- Content.
737
- `)
738
-
739
- await expect(parseSlide(path, 0)).rejects.toThrow(/title/)
740
- })
741
-
742
- it('throws on invalid transition type', async () => {
743
- const path = await createTestSlide('invalid-transition.md', `---
744
- title: Invalid Transition
745
- transition: bounce
746
- ---
747
-
748
- Content.
749
- `)
750
-
751
- await expect(parseSlide(path, 0)).rejects.toThrow()
752
- })
753
- })
754
-
755
- describe('edge cases', () => {
756
- it('handles empty body', async () => {
757
- const path = await createTestSlide('empty-body.md', `---
758
- title: Empty Body
759
- ---
760
- `)
761
-
762
- const slide = await parseSlide(path, 0)
763
-
764
- expect(slide.body).toBe('')
765
- })
766
-
767
- it('handles body with only whitespace', async () => {
768
- const path = await createTestSlide('whitespace-body.md', `---
769
- title: Whitespace Body
770
- ---
771
-
772
-
773
-
774
- `)
775
-
776
- const slide = await parseSlide(path, 0)
777
-
778
- expect(slide.body).toBe('')
779
- })
780
-
781
- it('preserves code blocks in body', async () => {
782
- const path = await createTestSlide('code-block.md', `---
783
- title: Code Example
784
- ---
785
-
786
- \`\`\`typescript
787
- const x = 1;
788
- console.log(x);
789
- \`\`\`
790
- `)
791
-
792
- const slide = await parseSlide(path, 0)
793
-
794
- expect(slide.body).toContain('```typescript')
795
- expect(slide.body).toContain('const x = 1;')
796
- })
797
-
798
- it('handles multiline content with various markdown', async () => {
799
- const path = await createTestSlide('multiline.md', `---
800
- title: Multi-line
801
- ---
802
-
803
- # Header
804
-
805
- Paragraph with **bold** and *italic*.
806
-
807
- - List item 1
808
- - List item 2
809
-
810
- > Blockquote
811
-
812
- <!-- notes -->
813
- Notes at the end.
814
- `)
815
-
816
- const slide = await parseSlide(path, 0)
817
-
818
- expect(slide.body).toContain('# Header')
819
- expect(slide.body).toContain('**bold**')
820
- expect(slide.body).toContain('- List item 1')
821
- expect(slide.body).toContain('> Blockquote')
822
- expect(slide.notes).toBe('Notes at the end.')
823
- })
824
- })
825
- })
826
-
827
- describe('findSlideFiles', () => {
828
- const SLIDES_DIR = join(import.meta.dir, '.test-slides-find')
829
-
830
- beforeEach(() => {
831
- mkdirSync(SLIDES_DIR, { recursive: true })
832
- })
833
-
834
- afterEach(() => {
835
- rmSync(SLIDES_DIR, { recursive: true, force: true })
836
- })
837
-
838
- // Helper to create a test file
839
- async function createFile(name: string): Promise<void> {
840
- await Bun.write(join(SLIDES_DIR, name), `---\ntitle: ${name}\n---\n`)
841
- }
842
-
843
- describe('file discovery', () => {
844
- it('finds all markdown files in directory', async () => {
845
- await createFile('01-intro.md')
846
- await createFile('02-content.md')
847
- await createFile('03-end.md')
848
-
849
- const files = await findSlideFiles(SLIDES_DIR)
850
-
851
- expect(files).toHaveLength(3)
852
- expect(files.map(f => f.name)).toEqual(['01-intro.md', '02-content.md', '03-end.md'])
853
- })
854
-
855
- it('returns empty array for empty directory', async () => {
856
- const files = await findSlideFiles(SLIDES_DIR)
857
-
858
- expect(files).toHaveLength(0)
859
- })
860
-
861
- it('returns full path for each file', async () => {
862
- await createFile('slide.md')
863
-
864
- const files = await findSlideFiles(SLIDES_DIR)
865
-
866
- expect(files[0].path).toBe(join(SLIDES_DIR, 'slide.md'))
867
- })
868
- })
869
-
870
- describe('exclusions', () => {
871
- it('excludes README.md', async () => {
872
- await createFile('01-intro.md')
873
- await createFile('README.md')
874
- await createFile('02-end.md')
875
-
876
- const files = await findSlideFiles(SLIDES_DIR)
877
-
878
- expect(files).toHaveLength(2)
879
- expect(files.map(f => f.name)).not.toContain('README.md')
880
- })
881
-
882
- it('excludes files starting with underscore', async () => {
883
- await createFile('01-intro.md')
884
- await createFile('_draft.md')
885
- await createFile('_template.md')
886
- await createFile('02-end.md')
887
-
888
- const files = await findSlideFiles(SLIDES_DIR)
889
-
890
- expect(files).toHaveLength(2)
891
- expect(files.map(f => f.name)).toEqual(['01-intro.md', '02-end.md'])
892
- })
893
-
894
- it('excludes both README.md and underscore files', async () => {
895
- await createFile('README.md')
896
- await createFile('_draft.md')
897
- await createFile('01-actual.md')
898
-
899
- const files = await findSlideFiles(SLIDES_DIR)
900
-
901
- expect(files).toHaveLength(1)
902
- expect(files[0].name).toBe('01-actual.md')
903
- })
904
- })
905
-
906
- describe('numeric sorting', () => {
907
- it('sorts files numerically (1, 2, 10 not 1, 10, 2)', async () => {
908
- // Create files in wrong order
909
- await createFile('10-tenth.md')
910
- await createFile('1-first.md')
911
- await createFile('2-second.md')
912
-
913
- const files = await findSlideFiles(SLIDES_DIR)
914
-
915
- expect(files.map(f => f.name)).toEqual([
916
- '1-first.md',
917
- '2-second.md',
918
- '10-tenth.md',
919
- ])
920
- })
921
-
922
- it('sorts with zero-padded numbers', async () => {
923
- await createFile('03-three.md')
924
- await createFile('01-one.md')
925
- await createFile('02-two.md')
926
-
927
- const files = await findSlideFiles(SLIDES_DIR)
928
-
929
- expect(files.map(f => f.name)).toEqual([
930
- '01-one.md',
931
- '02-two.md',
932
- '03-three.md',
933
- ])
934
- })
935
-
936
- it('handles mixed naming conventions', async () => {
937
- await createFile('a-alpha.md')
938
- await createFile('01-first.md')
939
- await createFile('b-beta.md')
940
-
941
- const files = await findSlideFiles(SLIDES_DIR)
942
-
943
- // Numeric prefixes come before letters
944
- expect(files.map(f => f.name)).toEqual([
945
- '01-first.md',
946
- 'a-alpha.md',
947
- 'b-beta.md',
948
- ])
949
- })
950
- })
951
-
952
- describe('index assignment', () => {
953
- it('assigns sequential indices starting from 0', async () => {
954
- await createFile('01-intro.md')
955
- await createFile('02-middle.md')
956
- await createFile('03-end.md')
957
-
958
- const files = await findSlideFiles(SLIDES_DIR)
959
-
960
- expect(files[0].index).toBe(0)
961
- expect(files[1].index).toBe(1)
962
- expect(files[2].index).toBe(2)
963
- })
964
-
965
- it('assigns indices after sorting', async () => {
966
- // Create in reverse order
967
- await createFile('03-c.md')
968
- await createFile('01-a.md')
969
- await createFile('02-b.md')
970
-
971
- const files = await findSlideFiles(SLIDES_DIR)
972
-
973
- // 01-a.md should have index 0 (sorted first)
974
- expect(files[0].name).toBe('01-a.md')
975
- expect(files[0].index).toBe(0)
976
-
977
- // 02-b.md should have index 1
978
- expect(files[1].name).toBe('02-b.md')
979
- expect(files[1].index).toBe(1)
980
-
981
- // 03-c.md should have index 2
982
- expect(files[2].name).toBe('03-c.md')
983
- expect(files[2].index).toBe(2)
984
- })
985
- })
986
-
987
- describe('edge cases', () => {
988
- it('handles single file', async () => {
989
- await createFile('only-slide.md')
990
-
991
- const files = await findSlideFiles(SLIDES_DIR)
992
-
993
- expect(files).toHaveLength(1)
994
- expect(files[0].name).toBe('only-slide.md')
995
- expect(files[0].index).toBe(0)
996
- })
997
-
998
- it('handles files with dots in name', async () => {
999
- await createFile('01-intro.v2.md')
1000
- await createFile('02-content.final.md')
1001
-
1002
- const files = await findSlideFiles(SLIDES_DIR)
1003
-
1004
- expect(files).toHaveLength(2)
1005
- expect(files.map(f => f.name)).toEqual([
1006
- '01-intro.v2.md',
1007
- '02-content.final.md',
1008
- ])
1009
- })
1010
-
1011
- it('handles files with spaces in name', async () => {
1012
- await createFile('01 intro.md')
1013
- await createFile('02 content.md')
1014
-
1015
- const files = await findSlideFiles(SLIDES_DIR)
1016
-
1017
- expect(files).toHaveLength(2)
1018
- })
1019
- })
1020
- })
1021
-
1022
- describe('loadDeckConfig', () => {
1023
- const CONFIG_DIR = join(import.meta.dir, '.test-config')
1024
-
1025
- beforeEach(() => {
1026
- mkdirSync(CONFIG_DIR, { recursive: true })
1027
- })
1028
-
1029
- afterEach(() => {
1030
- rmSync(CONFIG_DIR, { recursive: true, force: true })
1031
- })
1032
-
1033
- describe('default config fallback', () => {
1034
- it('returns default config when no deck.config.ts exists', async () => {
1035
- const config = await loadDeckConfig(CONFIG_DIR)
1036
-
1037
- expect(config.theme).toEqual(DEFAULT_THEME)
1038
- })
1039
-
1040
- it('returns default config with DEFAULT_THEME properties', async () => {
1041
- const config = await loadDeckConfig(CONFIG_DIR)
1042
-
1043
- expect(config.theme.name).toBe('matrix')
1044
- expect(config.theme.colors.primary).toBe('#00cc66')
1045
- expect(config.theme.colors.accent).toBe('#ff6600')
1046
- })
1047
- })
1048
-
1049
- describe('config file loading', () => {
1050
- it('loads and validates a valid deck.config.ts', async () => {
1051
- const configContent = `
1052
- export default {
1053
- title: 'Test Presentation',
1054
- author: 'Test Author',
1055
- theme: ${JSON.stringify(DEFAULT_THEME, null, 2)},
1056
- }
1057
- `
1058
- await Bun.write(join(CONFIG_DIR, 'deck.config.ts'), configContent)
1059
-
1060
- const config = await loadDeckConfig(CONFIG_DIR)
1061
-
1062
- expect(config.title).toBe('Test Presentation')
1063
- expect(config.author).toBe('Test Author')
1064
- expect(config.theme).toEqual(DEFAULT_THEME)
1065
- })
1066
-
1067
- it('loads config with custom theme', async () => {
1068
- const configContent = `
1069
- export default {
1070
- theme: {
1071
- name: 'custom',
1072
- colors: {
1073
- primary: '#ff0000',
1074
- accent: '#00ff00',
1075
- background: '#000000',
1076
- text: '#ffffff',
1077
- muted: '#888888',
1078
- },
1079
- gradients: {
1080
- custom: ['#ff0000', '#00ff00'],
1081
- },
1082
- glyphs: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
1083
- animations: {
1084
- revealSpeed: 1.0,
1085
- matrixDensity: 50,
1086
- glitchIterations: 5,
1087
- lineDelay: 30,
1088
- matrixInterval: 80,
1089
- },
1090
- },
1091
- }
1092
- `
1093
- await Bun.write(join(CONFIG_DIR, 'deck.config.ts'), configContent)
1094
-
1095
- const config = await loadDeckConfig(CONFIG_DIR)
1096
-
1097
- expect(config.theme.name).toBe('custom')
1098
- expect(config.theme.colors.primary).toBe('#ff0000')
1099
- })
1100
-
1101
- it('loads config with settings', async () => {
1102
- const configContent = `
1103
- export default {
1104
- theme: ${JSON.stringify(DEFAULT_THEME, null, 2)},
1105
- settings: {
1106
- startSlide: 2,
1107
- loop: true,
1108
- showProgress: true,
1109
- },
1110
- }
1111
- `
1112
- await Bun.write(join(CONFIG_DIR, 'deck.config.ts'), configContent)
1113
-
1114
- const config = await loadDeckConfig(CONFIG_DIR)
1115
-
1116
- expect(config.settings?.startSlide).toBe(2)
1117
- expect(config.settings?.loop).toBe(true)
1118
- expect(config.settings?.showProgress).toBe(true)
1119
- })
1120
-
1121
- it('loads config with export settings', async () => {
1122
- const configContent = `
1123
- export default {
1124
- theme: ${JSON.stringify(DEFAULT_THEME, null, 2)},
1125
- export: {
1126
- width: 160,
1127
- height: 50,
1128
- fps: 24,
1129
- },
1130
- }
1131
- `
1132
- await Bun.write(join(CONFIG_DIR, 'deck.config.ts'), configContent)
1133
-
1134
- const config = await loadDeckConfig(CONFIG_DIR)
1135
-
1136
- expect(config.export?.width).toBe(160)
1137
- expect(config.export?.height).toBe(50)
1138
- expect(config.export?.fps).toBe(24)
1139
- })
1140
- })
1141
-
1142
- describe('validation errors', () => {
1143
- it('throws on missing default export', async () => {
1144
- const configContent = `
1145
- export const config = { theme: {} }
1146
- `
1147
- await Bun.write(join(CONFIG_DIR, 'deck.config.ts'), configContent)
1148
-
1149
- await expect(loadDeckConfig(CONFIG_DIR)).rejects.toThrow(
1150
- 'deck.config.ts must export default config'
1151
- )
1152
- })
1153
-
1154
- it('throws on invalid theme schema', async () => {
1155
- const configContent = `
1156
- export default {
1157
- theme: {
1158
- name: 'invalid',
1159
- // Missing required fields
1160
- },
1161
- }
1162
- `
1163
- await Bun.write(join(CONFIG_DIR, 'deck.config.ts'), configContent)
1164
-
1165
- await expect(loadDeckConfig(CONFIG_DIR)).rejects.toThrow()
1166
- })
1167
-
1168
- it('throws on invalid color format', async () => {
1169
- const configContent = `
1170
- export default {
1171
- theme: {
1172
- name: 'bad-colors',
1173
- colors: {
1174
- primary: 'not-a-hex',
1175
- accent: '#00ff00',
1176
- background: '#000000',
1177
- text: '#ffffff',
1178
- muted: '#888888',
1179
- },
1180
- gradients: { test: ['#ff0000', '#00ff00'] },
1181
- glyphs: 'ABCDEFGHIJ',
1182
- animations: {},
1183
- },
1184
- }
1185
- `
1186
- await Bun.write(join(CONFIG_DIR, 'deck.config.ts'), configContent)
1187
-
1188
- await expect(loadDeckConfig(CONFIG_DIR)).rejects.toThrow(/hex color/)
1189
- })
1190
- })
1191
- })
1192
-
1193
- describe('loadDeck', () => {
1194
- const DECK_DIR = join(import.meta.dir, '.test-deck')
1195
-
1196
- beforeEach(() => {
1197
- mkdirSync(DECK_DIR, { recursive: true })
1198
- })
1199
-
1200
- afterEach(() => {
1201
- rmSync(DECK_DIR, { recursive: true, force: true })
1202
- })
1203
-
1204
- // Helper to create a slide file
1205
- async function createSlide(name: string, title: string, body: string = 'Content'): Promise<void> {
1206
- await Bun.write(join(DECK_DIR, name), `---
1207
- title: ${title}
1208
- ---
1209
-
1210
- ${body}
1211
- `)
1212
- }
1213
-
1214
- describe('complete deck loading', () => {
1215
- it('loads a complete deck with slides and default config', async () => {
1216
- await createSlide('01-intro.md', 'Introduction')
1217
- await createSlide('02-content.md', 'Main Content')
1218
- await createSlide('03-end.md', 'Conclusion')
1219
-
1220
- const deck = await loadDeck(DECK_DIR)
1221
-
1222
- expect(deck.slides).toHaveLength(3)
1223
- expect(deck.config.theme).toEqual(DEFAULT_THEME)
1224
- expect(deck.basePath).toBe(DECK_DIR)
1225
- })
1226
-
1227
- it('loads slides in correct numeric order', async () => {
1228
- // Create slides out of order
1229
- await createSlide('10-last.md', 'Last')
1230
- await createSlide('01-first.md', 'First')
1231
- await createSlide('02-second.md', 'Second')
1232
-
1233
- const deck = await loadDeck(DECK_DIR)
1234
-
1235
- expect(deck.slides[0].frontmatter.title).toBe('First')
1236
- expect(deck.slides[1].frontmatter.title).toBe('Second')
1237
- expect(deck.slides[2].frontmatter.title).toBe('Last')
1238
- })
1239
-
1240
- it('assigns correct indices to slides', async () => {
1241
- await createSlide('01-intro.md', 'Intro')
1242
- await createSlide('02-middle.md', 'Middle')
1243
- await createSlide('03-end.md', 'End')
1244
-
1245
- const deck = await loadDeck(DECK_DIR)
1246
-
1247
- expect(deck.slides[0].index).toBe(0)
1248
- expect(deck.slides[1].index).toBe(1)
1249
- expect(deck.slides[2].index).toBe(2)
1250
- })
1251
-
1252
- it('preserves slide source paths', async () => {
1253
- await createSlide('01-intro.md', 'Intro')
1254
-
1255
- const deck = await loadDeck(DECK_DIR)
1256
-
1257
- expect(deck.slides[0].sourcePath).toBe(join(DECK_DIR, '01-intro.md'))
1258
- })
1259
- })
1260
-
1261
- // Note: Custom config tests are prone to Bun's dynamic import caching
1262
- // when using temp directories. Config file loading is tested in loadDeckConfig.
1263
- // loadDeck correctly delegates to loadDeckConfig, and the integration is
1264
- // tested via the 'complete deck loading' tests that use default config.
1265
-
1266
- describe('parallel slide parsing', () => {
1267
- it('parses multiple slides successfully', async () => {
1268
- // Create many slides to test parallel parsing
1269
- for (let i = 1; i <= 10; i++) {
1270
- const num = i.toString().padStart(2, '0')
1271
- await createSlide(`${num}-slide.md`, `Slide ${i}`, `Content for slide ${i}`)
1272
- }
1273
-
1274
- const deck = await loadDeck(DECK_DIR)
1275
-
1276
- expect(deck.slides).toHaveLength(10)
1277
- deck.slides.forEach((slide, i) => {
1278
- expect(slide.frontmatter.title).toBe(`Slide ${i + 1}`)
1279
- expect(slide.body).toBe(`Content for slide ${i + 1}`)
1280
- })
1281
- })
1282
-
1283
- it('preserves slide content and notes', async () => {
1284
- await Bun.write(join(DECK_DIR, '01-intro.md'), `---
1285
- title: Introduction
1286
- bigText: INTRO
1287
- gradient: fire
1288
- ---
1289
-
1290
- {GREEN}Hello{/} World
1291
-
1292
- - Point one
1293
- - Point two
1294
-
1295
- <!-- notes -->
1296
- Speaker notes here.
1297
- `)
1298
-
1299
- const deck = await loadDeck(DECK_DIR)
1300
-
1301
- const slide = deck.slides[0]
1302
- expect(slide.frontmatter.title).toBe('Introduction')
1303
- expect(slide.frontmatter.bigText).toBe('INTRO')
1304
- expect(slide.frontmatter.gradient).toBe('fire')
1305
- expect(slide.body).toContain('{GREEN}Hello{/} World')
1306
- expect(slide.body).toContain('- Point one')
1307
- expect(slide.notes).toBe('Speaker notes here.')
1308
- })
1309
- })
1310
-
1311
- describe('edge cases', () => {
1312
- it('returns empty slides array for empty directory', async () => {
1313
- const deck = await loadDeck(DECK_DIR)
1314
-
1315
- expect(deck.slides).toHaveLength(0)
1316
- expect(deck.config.theme).toEqual(DEFAULT_THEME)
1317
- expect(deck.basePath).toBe(DECK_DIR)
1318
- })
1319
-
1320
- it('excludes README.md and underscore files from slides', async () => {
1321
- await createSlide('01-intro.md', 'Intro')
1322
- await Bun.write(join(DECK_DIR, 'README.md'), `# README\n\nNot a slide.`)
1323
- await Bun.write(join(DECK_DIR, '_draft.md'), `---\ntitle: Draft\n---\n\nDraft content.`)
1324
-
1325
- const deck = await loadDeck(DECK_DIR)
1326
-
1327
- expect(deck.slides).toHaveLength(1)
1328
- expect(deck.slides[0].frontmatter.title).toBe('Intro')
1329
- })
1330
-
1331
- it('handles single slide deck', async () => {
1332
- await createSlide('01-only.md', 'Only Slide')
1333
-
1334
- const deck = await loadDeck(DECK_DIR)
1335
-
1336
- expect(deck.slides).toHaveLength(1)
1337
- expect(deck.slides[0].index).toBe(0)
1338
- })
1339
- })
1340
-
1341
- describe('error handling', () => {
1342
- it('throws on invalid slide frontmatter', async () => {
1343
- // Create a slide without required title
1344
- await Bun.write(join(DECK_DIR, '01-invalid.md'), `---
1345
- bigText: TEST
1346
- ---
1347
-
1348
- No title here.
1349
- `)
1350
-
1351
- await expect(loadDeck(DECK_DIR)).rejects.toThrow(/title/)
1352
- })
1353
-
1354
- // Note: Config file validation tests are covered in loadDeckConfig tests.
1355
- // Testing config errors through loadDeck is problematic due to Bun's
1356
- // dynamic import caching and temp directory path resolution.
1357
- })
1358
-
1359
- describe('basePath', () => {
1360
- it('returns the correct basePath', async () => {
1361
- await createSlide('01-intro.md', 'Intro')
1362
-
1363
- const deck = await loadDeck(DECK_DIR)
1364
-
1365
- expect(deck.basePath).toBe(DECK_DIR)
1366
- })
1367
-
1368
- it('returns absolute path as basePath', async () => {
1369
- await createSlide('01-intro.md', 'Intro')
1370
-
1371
- const deck = await loadDeck(DECK_DIR)
1372
-
1373
- // Path should be absolute (starts with /)
1374
- expect(deck.basePath.startsWith('/')).toBe(true)
1375
- })
1376
- })
1377
- })
1378
-
1379
- describe('formatMermaidError', () => {
1380
- describe('error block formatting', () => {
1381
- it('creates bordered error block', () => {
1382
- const code = 'invalid diagram'
1383
- const error = new Error('parse error')
1384
- const result = formatMermaidError(code, error)
1385
-
1386
- expect(result).toContain('┌─ Diagram (parse error) ─┐')
1387
- expect(result).toContain('└─────────────────────────┘')
1388
- })
1389
-
1390
- it('includes diagram code in error block', () => {
1391
- const code = 'graph LR\n A --> B'
1392
- const error = new Error('parse error')
1393
- const result = formatMermaidError(code, error)
1394
-
1395
- expect(result).toContain('graph LR')
1396
- })
1397
-
1398
- it('truncates long lines to fit border width', () => {
1399
- const code = 'This is a very very very very long line that exceeds the box width'
1400
- const error = new Error('parse error')
1401
- const result = formatMermaidError(code, error)
1402
-
1403
- // Each content line should be exactly 23 chars truncated then padded
1404
- const lines = result.split('\n')
1405
- const contentLine = lines.find(l => l.includes('This is'))
1406
- // slice(0, 23) gives 23 chars: "This is a very very ver"
1407
- expect(contentLine).toBe('│ This is a very very ver │')
1408
- })
1409
-
1410
- it('limits to first 5 lines', () => {
1411
- const code = 'line1\nline2\nline3\nline4\nline5\nline6\nline7'
1412
- const error = new Error('parse error')
1413
- const result = formatMermaidError(code, error)
1414
-
1415
- expect(result).toContain('line1')
1416
- expect(result).toContain('line5')
1417
- expect(result).toContain('...')
1418
- expect(result).not.toContain('line7')
1419
- })
1420
-
1421
- it('does not show ellipsis for 5 or fewer lines', () => {
1422
- const code = 'line1\nline2\nline3'
1423
- const error = new Error('parse error')
1424
- const result = formatMermaidError(code, error)
1425
-
1426
- expect(result).not.toContain('...')
1427
- })
1428
-
1429
- it('handles single line code', () => {
1430
- const code = 'single line'
1431
- const error = new Error('parse error')
1432
- const result = formatMermaidError(code, error)
1433
-
1434
- expect(result).toContain('single line')
1435
- expect(result).toContain('┌─ Diagram (parse error) ─┐')
1436
- })
1437
-
1438
- it('handles empty code', () => {
1439
- const code = ''
1440
- const error = new Error('parse error')
1441
- const result = formatMermaidError(code, error)
1442
-
1443
- expect(result).toContain('┌─ Diagram (parse error) ─┐')
1444
- expect(result).toContain('└─────────────────────────┘')
1445
- })
1446
- })
1447
- })
1448
-
1449
- describe('mermaidToAscii', () => {
1450
- describe('valid diagrams', () => {
1451
- it('converts simple graph LR diagram', () => {
1452
- const code = `graph LR
1453
- A --> B`
1454
- const result = mermaidToAscii(code)
1455
-
1456
- // Should contain ASCII box drawing characters
1457
- expect(result).toContain('┌')
1458
- expect(result).toContain('└')
1459
- expect(result).toContain('─')
1460
- })
1461
-
1462
- it('converts graph TD diagram', () => {
1463
- const code = `graph TD
1464
- A --> B`
1465
- const result = mermaidToAscii(code)
1466
-
1467
- expect(result).toContain('┌')
1468
- expect(result).toContain('└')
1469
- })
1470
-
1471
- it('converts flowchart diagram', () => {
1472
- const code = `flowchart LR
1473
- Start --> End`
1474
- const result = mermaidToAscii(code)
1475
-
1476
- expect(result).toContain('Start')
1477
- expect(result).toContain('End')
1478
- })
1479
- })
1480
-
1481
- describe('invalid diagrams', () => {
1482
- it('returns error block for invalid diagram', () => {
1483
- const code = 'not a valid diagram'
1484
- const result = mermaidToAscii(code)
1485
-
1486
- expect(result).toContain('┌─ Diagram (parse error) ─┐')
1487
- expect(result).toContain('not a valid diagram')
1488
- })
1489
-
1490
- it('returns error block for incomplete diagram', () => {
1491
- const code = 'graph'
1492
- const result = mermaidToAscii(code)
1493
-
1494
- expect(result).toContain('┌─ Diagram (parse error) ─┐')
1495
- })
1496
-
1497
- it('returns error block for empty content', () => {
1498
- const code = ''
1499
- const result = mermaidToAscii(code)
1500
-
1501
- expect(result).toContain('┌─ Diagram (parse error) ─┐')
1502
- })
1503
- })
1504
- })
1505
-
1506
- describe('processMermaidDiagrams', () => {
1507
- describe('content without mermaid', () => {
1508
- it('returns content unchanged when no mermaid blocks', () => {
1509
- const content = 'Just some text\n\nMore text'
1510
- const result = processMermaidDiagrams(content)
1511
-
1512
- expect(result).toBe(content)
1513
- })
1514
-
1515
- it('returns empty string unchanged', () => {
1516
- const content = ''
1517
- const result = processMermaidDiagrams(content)
1518
-
1519
- expect(result).toBe('')
1520
- })
1521
-
1522
- it('returns content with other code blocks unchanged', () => {
1523
- const content = `Some text
1524
-
1525
- \`\`\`typescript
1526
- const x = 1;
1527
- \`\`\`
1528
-
1529
- More text`
1530
- const result = processMermaidDiagrams(content)
1531
-
1532
- expect(result).toBe(content)
1533
- })
1534
- })
1535
-
1536
- describe('content with mermaid blocks', () => {
1537
- it('replaces single mermaid block with ASCII', () => {
1538
- const content = `Before
1539
-
1540
- \`\`\`mermaid
1541
- graph LR
1542
- A --> B
1543
- \`\`\`
1544
-
1545
- After`
1546
- const result = processMermaidDiagrams(content)
1547
-
1548
- // Should not contain mermaid markers
1549
- expect(result).not.toContain('```mermaid')
1550
- expect(result).not.toContain('```')
1551
- // Should contain ASCII art
1552
- expect(result).toContain('┌')
1553
- // Should preserve surrounding text
1554
- expect(result).toContain('Before')
1555
- expect(result).toContain('After')
1556
- })
1557
-
1558
- it('replaces multiple mermaid blocks', () => {
1559
- const content = `Text 1
1560
-
1561
- \`\`\`mermaid
1562
- graph LR
1563
- A --> B
1564
- \`\`\`
1565
-
1566
- Text 2
1567
-
1568
- \`\`\`mermaid
1569
- graph TD
1570
- C --> D
1571
- \`\`\`
1572
-
1573
- Text 3`
1574
- const result = processMermaidDiagrams(content)
1575
-
1576
- // Should not contain any mermaid markers
1577
- expect(result).not.toContain('```mermaid')
1578
- // Should preserve all text sections
1579
- expect(result).toContain('Text 1')
1580
- expect(result).toContain('Text 2')
1581
- expect(result).toContain('Text 3')
1582
- })
1583
-
1584
- it('handles invalid mermaid by showing error block', () => {
1585
- const content = `Before
1586
-
1587
- \`\`\`mermaid
1588
- invalid content
1589
- \`\`\`
1590
-
1591
- After`
1592
- const result = processMermaidDiagrams(content)
1593
-
1594
- expect(result).not.toContain('```mermaid')
1595
- expect(result).toContain('┌─ Diagram (parse error) ─┐')
1596
- expect(result).toContain('Before')
1597
- expect(result).toContain('After')
1598
- })
1599
-
1600
- it('preserves non-mermaid code blocks', () => {
1601
- const content = `\`\`\`typescript
1602
- const x = 1;
1603
- \`\`\`
1604
-
1605
- \`\`\`mermaid
1606
- graph LR
1607
- A --> B
1608
- \`\`\`
1609
-
1610
- \`\`\`python
1611
- print('hello')
1612
- \`\`\``
1613
- const result = processMermaidDiagrams(content)
1614
-
1615
- // Mermaid should be converted
1616
- expect(result).not.toContain('```mermaid')
1617
- // Other code blocks should remain
1618
- expect(result).toContain('```typescript')
1619
- expect(result).toContain('```python')
1620
- })
1621
- })
1622
-
1623
- describe('edge cases', () => {
1624
- it('handles mermaid block at start of content', () => {
1625
- const content = `\`\`\`mermaid
1626
- graph LR
1627
- A --> B
1628
- \`\`\`
1629
- After`
1630
- const result = processMermaidDiagrams(content)
1631
-
1632
- expect(result).not.toContain('```mermaid')
1633
- expect(result).toContain('After')
1634
- })
1635
-
1636
- it('handles mermaid block at end of content', () => {
1637
- const content = `Before
1638
- \`\`\`mermaid
1639
- graph LR
1640
- A --> B
1641
- \`\`\``
1642
- const result = processMermaidDiagrams(content)
1643
-
1644
- expect(result).not.toContain('```mermaid')
1645
- expect(result).toContain('Before')
1646
- })
1647
-
1648
- it('handles mermaid with special regex characters in content', () => {
1649
- const content = `Before
1650
-
1651
- \`\`\`mermaid
1652
- graph LR
1653
- A[Input $100] --> B[Output (result)]
1654
- \`\`\`
1655
-
1656
- After`
1657
- const result = processMermaidDiagrams(content)
1658
-
1659
- // Should not throw and should process
1660
- expect(result).not.toContain('```mermaid')
1661
- expect(result).toContain('Before')
1662
- expect(result).toContain('After')
1663
- })
1664
- })
1665
- })
1666
-
1667
- describe('normalizeBigText', () => {
1668
- describe('undefined input', () => {
1669
- it('converts undefined to empty array', () => {
1670
- const result = normalizeBigText(undefined)
1671
-
1672
- expect(result).toEqual([])
1673
- })
1674
- })
1675
-
1676
- describe('string input', () => {
1677
- it('wraps string in array', () => {
1678
- const result = normalizeBigText('HELLO')
1679
-
1680
- expect(result).toEqual(['HELLO'])
1681
- })
1682
-
1683
- it('wraps multi-word string in array', () => {
1684
- const result = normalizeBigText('HELLO WORLD')
1685
-
1686
- expect(result).toEqual(['HELLO WORLD'])
1687
- })
1688
-
1689
- it('wraps empty string in array', () => {
1690
- const result = normalizeBigText('')
1691
-
1692
- // Empty string is falsy, so returns empty array
1693
- expect(result).toEqual([])
1694
- })
1695
- })
1696
-
1697
- describe('array input', () => {
1698
- it('passes through array unchanged', () => {
1699
- const result = normalizeBigText(['LINE ONE', 'LINE TWO'])
1700
-
1701
- expect(result).toEqual(['LINE ONE', 'LINE TWO'])
1702
- })
1703
-
1704
- it('passes through single-element array unchanged', () => {
1705
- const result = normalizeBigText(['HELLO'])
1706
-
1707
- expect(result).toEqual(['HELLO'])
1708
- })
1709
-
1710
- it('passes through empty array unchanged', () => {
1711
- const result = normalizeBigText([])
1712
-
1713
- // Empty array is falsy (length 0), but Array.isArray([]) is true
1714
- // So it returns [] directly
1715
- expect(result).toEqual([])
1716
- })
1717
-
1718
- it('passes through array with multiple lines unchanged', () => {
1719
- const result = normalizeBigText(['A', 'B', 'C', 'D'])
1720
-
1721
- expect(result).toEqual(['A', 'B', 'C', 'D'])
1722
- })
1723
-
1724
- it('preserves order of array elements', () => {
1725
- const result = normalizeBigText(['FIRST', 'SECOND', 'THIRD'])
1726
-
1727
- expect(result[0]).toBe('FIRST')
1728
- expect(result[1]).toBe('SECOND')
1729
- expect(result[2]).toBe('THIRD')
1730
- })
1731
- })
1732
-
1733
- describe('edge cases', () => {
1734
- it('handles whitespace-only string', () => {
1735
- const result = normalizeBigText(' ')
1736
-
1737
- // Whitespace string is truthy, so it wraps in array
1738
- expect(result).toEqual([' '])
1739
- })
1740
-
1741
- it('handles array with empty strings', () => {
1742
- const result = normalizeBigText(['', 'HELLO', ''])
1743
-
1744
- expect(result).toEqual(['', 'HELLO', ''])
1745
- })
1746
-
1747
- it('handles special characters', () => {
1748
- const result = normalizeBigText('HELLO!@#$%')
1749
-
1750
- expect(result).toEqual(['HELLO!@#$%'])
1751
- })
1752
-
1753
- it('handles unicode characters', () => {
1754
- const result = normalizeBigText('HÉLLO WÖRLD')
1755
-
1756
- expect(result).toEqual(['HÉLLO WÖRLD'])
1757
- })
1758
- })
1759
- })