@pep/term-deck 1.0.10
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/LICENSE +21 -0
- package/README.md +356 -0
- package/bin/term-deck.ts +45 -0
- package/examples/slides/01-welcome.md +9 -0
- package/examples/slides/02-features.md +12 -0
- package/examples/slides/03-colors.md +17 -0
- package/examples/slides/04-ascii-art.md +11 -0
- package/examples/slides/05-gradients.md +14 -0
- package/examples/slides/06-themes.md +13 -0
- package/examples/slides/07-markdown.md +13 -0
- package/examples/slides/08-controls.md +13 -0
- package/examples/slides/09-thanks.md +11 -0
- package/examples/slides/deck.config.ts +13 -0
- package/examples/slides-hacker/01-welcome.md +9 -0
- package/examples/slides-hacker/02-features.md +12 -0
- package/examples/slides-hacker/03-colors.md +17 -0
- package/examples/slides-hacker/04-ascii-art.md +11 -0
- package/examples/slides-hacker/05-gradients.md +14 -0
- package/examples/slides-hacker/06-themes.md +13 -0
- package/examples/slides-hacker/07-markdown.md +13 -0
- package/examples/slides-hacker/08-controls.md +13 -0
- package/examples/slides-hacker/09-thanks.md +11 -0
- package/examples/slides-hacker/deck.config.ts +13 -0
- package/examples/slides-matrix/01-welcome.md +9 -0
- package/examples/slides-matrix/02-features.md +12 -0
- package/examples/slides-matrix/03-colors.md +17 -0
- package/examples/slides-matrix/04-ascii-art.md +11 -0
- package/examples/slides-matrix/05-gradients.md +14 -0
- package/examples/slides-matrix/06-themes.md +13 -0
- package/examples/slides-matrix/07-markdown.md +13 -0
- package/examples/slides-matrix/08-controls.md +13 -0
- package/examples/slides-matrix/09-thanks.md +11 -0
- package/examples/slides-matrix/deck.config.ts +13 -0
- package/examples/slides-minimal/01-welcome.md +9 -0
- package/examples/slides-minimal/02-features.md +12 -0
- package/examples/slides-minimal/03-colors.md +17 -0
- package/examples/slides-minimal/04-ascii-art.md +11 -0
- package/examples/slides-minimal/05-gradients.md +14 -0
- package/examples/slides-minimal/06-themes.md +13 -0
- package/examples/slides-minimal/07-markdown.md +13 -0
- package/examples/slides-minimal/08-controls.md +13 -0
- package/examples/slides-minimal/09-thanks.md +11 -0
- package/examples/slides-minimal/deck.config.ts +13 -0
- package/examples/slides-neon/01-welcome.md +9 -0
- package/examples/slides-neon/02-features.md +12 -0
- package/examples/slides-neon/03-colors.md +17 -0
- package/examples/slides-neon/04-ascii-art.md +11 -0
- package/examples/slides-neon/05-gradients.md +14 -0
- package/examples/slides-neon/06-themes.md +13 -0
- package/examples/slides-neon/07-markdown.md +13 -0
- package/examples/slides-neon/08-controls.md +13 -0
- package/examples/slides-neon/09-thanks.md +11 -0
- package/examples/slides-neon/deck.config.ts +13 -0
- package/examples/slides-retro/01-welcome.md +9 -0
- package/examples/slides-retro/02-features.md +12 -0
- package/examples/slides-retro/03-colors.md +17 -0
- package/examples/slides-retro/04-ascii-art.md +11 -0
- package/examples/slides-retro/05-gradients.md +14 -0
- package/examples/slides-retro/06-themes.md +13 -0
- package/examples/slides-retro/07-markdown.md +13 -0
- package/examples/slides-retro/08-controls.md +13 -0
- package/examples/slides-retro/09-thanks.md +11 -0
- package/examples/slides-retro/deck.config.ts +13 -0
- package/package.json +66 -0
- package/src/cli/__tests__/errors.test.ts +201 -0
- package/src/cli/__tests__/help.test.ts +157 -0
- package/src/cli/__tests__/init.test.ts +110 -0
- package/src/cli/commands/export.ts +33 -0
- package/src/cli/commands/init.ts +125 -0
- package/src/cli/commands/present.ts +29 -0
- package/src/cli/errors.ts +77 -0
- package/src/core/__tests__/slide.test.ts +1759 -0
- package/src/core/__tests__/theme.test.ts +1103 -0
- package/src/core/slide.ts +509 -0
- package/src/core/theme.ts +388 -0
- package/src/export/__tests__/recorder.test.ts +566 -0
- package/src/export/recorder.ts +639 -0
- package/src/index.ts +36 -0
- package/src/presenter/__tests__/main.test.ts +244 -0
- package/src/presenter/main.ts +658 -0
- package/src/renderer/__tests__/screen-extended.test.ts +801 -0
- package/src/renderer/__tests__/screen.test.ts +525 -0
- package/src/renderer/screen.ts +671 -0
- package/src/schemas/__tests__/config.test.ts +429 -0
- package/src/schemas/__tests__/slide.test.ts +349 -0
- package/src/schemas/__tests__/theme.test.ts +970 -0
- package/src/schemas/__tests__/validation.test.ts +256 -0
- package/src/schemas/config.ts +58 -0
- package/src/schemas/slide.ts +56 -0
- package/src/schemas/theme.ts +203 -0
- package/src/schemas/validation.ts +64 -0
- package/src/themes/matrix/index.ts +53 -0
- package/themes/hacker.ts +53 -0
- package/themes/minimal.ts +53 -0
- package/themes/neon.ts +53 -0
- package/themes/retro.ts +53 -0
|
@@ -0,0 +1,1759 @@
|
|
|
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
|
+
})
|