@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
package/src/core/slide.ts DELETED
@@ -1,509 +0,0 @@
1
- import matter from 'gray-matter'
2
- import { join } from 'path'
3
- import { Glob } from 'bun'
4
- import { mermaidToAscii as convertMermaid } from 'mermaid-ascii'
5
- import type { Slide } from '../schemas/slide.js'
6
- import type { DeckConfig } from '../schemas/config.js'
7
- import type { Theme } from '../schemas/theme.js'
8
- import { SlideFrontmatterSchema, SlideSchema } from '../schemas/slide.js'
9
- import { DeckConfigSchema } from '../schemas/config.js'
10
- import { safeParse } from '../schemas/validation.js'
11
- import { DEFAULT_THEME } from '../schemas/theme.js'
12
- import { colorTokensToBlessedTags } from './theme.js'
13
-
14
- /**
15
- * Raw parsed slide before validation.
16
- * This is the intermediate structure after parsing frontmatter
17
- * but before Zod validation.
18
- */
19
- export interface RawSlide {
20
- frontmatter: Record<string, unknown>
21
- body: string
22
- notes?: string
23
- sourcePath: string
24
- }
25
-
26
- /**
27
- * Deck structure containing all slides and configuration.
28
- * Represents a complete presentation loaded from disk.
29
- */
30
- export interface Deck {
31
- slides: Slide[]
32
- config: DeckConfig
33
- basePath: string
34
- }
35
-
36
- /**
37
- * Slide file info for sorting and loading.
38
- * Used during the slide discovery phase.
39
- */
40
- export interface SlideFile {
41
- path: string
42
- name: string
43
- index: number
44
- }
45
-
46
- /**
47
- * Result of extracting notes from slide content.
48
- */
49
- export interface ExtractedNotes {
50
- body: string
51
- notes?: string
52
- }
53
-
54
- const NOTES_MARKER = '<!-- notes -->'
55
- const NOTES_END_MARKER = '<!-- /notes -->'
56
-
57
- /**
58
- * Pattern to match mermaid code blocks in markdown content.
59
- * Captures the diagram code inside the block.
60
- *
61
- * Matches: ```mermaid\n<content>```
62
- * Group 1: The mermaid diagram code
63
- */
64
- const MERMAID_BLOCK_PATTERN = /```mermaid\n([\s\S]*?)```/g
65
-
66
- /**
67
- * Check if content contains mermaid diagrams.
68
- *
69
- * @param content - The markdown content to check
70
- * @returns true if the content contains at least one mermaid code block
71
- */
72
- export function hasMermaidDiagrams(content: string): boolean {
73
- // Reset pattern since we use global flag
74
- MERMAID_BLOCK_PATTERN.lastIndex = 0
75
- return MERMAID_BLOCK_PATTERN.test(content)
76
- }
77
-
78
- /**
79
- * Extract all mermaid blocks from content.
80
- *
81
- * Finds all mermaid code blocks and extracts the diagram code
82
- * (without the ```mermaid and ``` delimiters).
83
- *
84
- * @param content - The markdown content to search
85
- * @returns Array of mermaid diagram code strings (trimmed)
86
- */
87
- export function extractMermaidBlocks(content: string): string[] {
88
- const blocks: string[] = []
89
- let match: RegExpExecArray | null
90
-
91
- // Reset pattern before use
92
- MERMAID_BLOCK_PATTERN.lastIndex = 0
93
-
94
- while ((match = MERMAID_BLOCK_PATTERN.exec(content)) !== null) {
95
- blocks.push(match[1].trim())
96
- }
97
-
98
- return blocks
99
- }
100
-
101
- /**
102
- * Format a mermaid parsing error as ASCII.
103
- *
104
- * Creates a visually recognizable error block showing the
105
- * first few lines of the diagram with a border.
106
- *
107
- * @param code - The mermaid diagram code that failed to parse
108
- * @param _error - The error that occurred (unused but kept for signature)
109
- * @returns ASCII art error block
110
- */
111
- export function formatMermaidError(code: string, _error: unknown): string {
112
- const lines = [
113
- '┌─ Diagram (parse error) ─┐',
114
- '│ │',
115
- ]
116
-
117
- // Show first few lines of the diagram
118
- const codeLines = code.split('\n').slice(0, 5)
119
- for (const line of codeLines) {
120
- const truncated = line.slice(0, 23).padEnd(23)
121
- lines.push(`│ ${truncated} │`)
122
- }
123
-
124
- if (code.split('\n').length > 5) {
125
- lines.push('│ ... │')
126
- }
127
-
128
- lines.push('│ │')
129
- lines.push('└─────────────────────────┘')
130
-
131
- return lines.join('\n')
132
- }
133
-
134
- /**
135
- * Convert mermaid diagram to ASCII art.
136
- *
137
- * Uses the mermaid-ascii library to convert mermaid diagram
138
- * syntax to ASCII art representation. Falls back to an error
139
- * block if parsing fails.
140
- *
141
- * @param mermaidCode - Raw mermaid diagram code
142
- * @returns ASCII art representation or error block
143
- */
144
- export function mermaidToAscii(mermaidCode: string): string {
145
- try {
146
- return convertMermaid(mermaidCode)
147
- } catch (error) {
148
- // If parsing fails, return a formatted error block
149
- return formatMermaidError(mermaidCode, error)
150
- }
151
- }
152
-
153
- /**
154
- * Escape special regex characters in a string.
155
- *
156
- * @param str - String to escape
157
- * @returns Escaped string safe for use in RegExp
158
- */
159
- function escapeRegex(str: string): string {
160
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
161
- }
162
-
163
- /**
164
- * Process all mermaid blocks in slide content.
165
- *
166
- * Finds all mermaid code blocks in the content and replaces them
167
- * with their ASCII art representation.
168
- *
169
- * @param content - The slide content potentially containing mermaid blocks
170
- * @returns Content with mermaid blocks replaced by ASCII art
171
- */
172
- export function processMermaidDiagrams(content: string): string {
173
- if (!hasMermaidDiagrams(content)) {
174
- return content
175
- }
176
-
177
- // Find all blocks and convert
178
- let result = content
179
- const blocks = extractMermaidBlocks(content)
180
-
181
- for (const block of blocks) {
182
- const ascii = mermaidToAscii(block)
183
-
184
- // Replace mermaid block with ASCII
185
- // The pattern matches the code block including newlines
186
- result = result.replace(
187
- new RegExp('```mermaid\\n' + escapeRegex(block) + '\\n?```', 'g'),
188
- ascii
189
- )
190
- }
191
-
192
- return result
193
- }
194
-
195
- /**
196
- * Process slide body content.
197
- *
198
- * Applies the full content processing pipeline:
199
- * 1. Process mermaid diagrams (convert to ASCII)
200
- * 2. Apply color tokens (convert to blessed tags)
201
- *
202
- * The order is important: mermaid diagrams are processed first so that
203
- * any color tokens they might contain are then converted to blessed tags.
204
- *
205
- * @param body - The slide body content to process
206
- * @param theme - The theme to use for color token resolution
207
- * @returns Processed content with mermaid converted and color tokens applied
208
- *
209
- * @example
210
- * const processed = await processSlideContent(
211
- * '{GREEN}Hello{/}\n\n```mermaid\ngraph LR\nA-->B\n```',
212
- * theme
213
- * )
214
- * // Returns: '{#00cc66-fg}Hello{/}\n\n<ascii art>'
215
- */
216
- export async function processSlideContent(
217
- body: string,
218
- theme: Theme
219
- ): Promise<string> {
220
- // Process mermaid diagrams first
221
- let processed = processMermaidDiagrams(body)
222
-
223
- // Apply color tokens
224
- processed = colorTokensToBlessedTags(processed, theme)
225
-
226
- return processed
227
- }
228
-
229
- /**
230
- * Extract presenter notes from body content.
231
- *
232
- * Notes are delimited by `<!-- notes -->` marker.
233
- * An optional `<!-- /notes -->` end marker can be used to include
234
- * content after notes in the body.
235
- *
236
- * @param content - Raw body content from markdown file
237
- * @returns Object containing separated body and notes
238
- */
239
- export function extractNotes(content: string): ExtractedNotes {
240
- const notesStart = content.indexOf(NOTES_MARKER)
241
-
242
- if (notesStart === -1) {
243
- return { body: content }
244
- }
245
-
246
- const body = content.slice(0, notesStart).trim()
247
-
248
- // Check for explicit end marker
249
- const notesEnd = content.indexOf(NOTES_END_MARKER, notesStart)
250
-
251
- let notes: string
252
- if (notesEnd !== -1) {
253
- notes = content.slice(notesStart + NOTES_MARKER.length, notesEnd).trim()
254
- } else {
255
- // Everything after marker is notes
256
- notes = content.slice(notesStart + NOTES_MARKER.length).trim()
257
- }
258
-
259
- return { body, notes: notes || undefined }
260
- }
261
-
262
- /**
263
- * Parse a single slide file.
264
- *
265
- * Reads the markdown file, extracts frontmatter using gray-matter,
266
- * extracts presenter notes, and validates the result against the schema.
267
- *
268
- * @param filePath - Path to the markdown slide file
269
- * @param index - The slide index in the deck (0-indexed)
270
- * @returns Validated Slide object
271
- * @throws {ValidationError} If frontmatter or slide validation fails
272
- */
273
- export async function parseSlide(
274
- filePath: string,
275
- index: number
276
- ): Promise<Slide> {
277
- // Read file content
278
- const content = await Bun.file(filePath).text()
279
-
280
- // Parse frontmatter with gray-matter
281
- const { data, content: rawBody } = matter(content)
282
-
283
- // Extract notes from body
284
- const { body, notes } = extractNotes(rawBody)
285
-
286
- // Validate frontmatter
287
- const frontmatter = safeParse(
288
- SlideFrontmatterSchema,
289
- data,
290
- `frontmatter in ${filePath}`
291
- )
292
-
293
- // Build full slide object
294
- const slide = {
295
- frontmatter,
296
- body: body.trim(),
297
- notes: notes?.trim(),
298
- sourcePath: filePath,
299
- index,
300
- }
301
-
302
- // Validate complete slide and return
303
- return safeParse(SlideSchema, slide, `slide ${filePath}`)
304
- }
305
-
306
- /**
307
- * Find and sort slide files in a directory.
308
- *
309
- * Finds all markdown files (*.md), excludes non-slide files like
310
- * README.md and files starting with underscore, and sorts them
311
- * numerically by filename (e.g., 01-intro.md, 02-content.md).
312
- *
313
- * @param dir - Directory to search for slide files
314
- * @returns Array of SlideFile objects sorted by filename
315
- */
316
- export async function findSlideFiles(dir: string): Promise<SlideFile[]> {
317
- const glob = new Glob('*.md')
318
- const files: SlideFile[] = []
319
-
320
- for await (const file of glob.scan(dir)) {
321
- // Skip non-slide files
322
- if (file === 'README.md' || file.startsWith('_')) {
323
- continue
324
- }
325
-
326
- files.push({
327
- path: join(dir, file),
328
- name: file,
329
- index: 0, // Will be set after sorting
330
- })
331
- }
332
-
333
- // Sort by filename numerically (01-intro.md, 02-problem.md, etc.)
334
- files.sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }))
335
-
336
- // Assign indices after sorting
337
- files.forEach((file, i) => {
338
- file.index = i
339
- })
340
-
341
- return files
342
- }
343
-
344
- /**
345
- * Load deck configuration from deck.config.ts in slides directory.
346
- *
347
- * Looks for a deck.config.ts file in the specified directory.
348
- * If found, dynamically imports and validates it against DeckConfigSchema.
349
- * If not found, returns a default config with the DEFAULT_THEME.
350
- *
351
- * @param slidesDir - Directory to search for deck.config.ts
352
- * @returns Validated DeckConfig object
353
- * @throws {ValidationError} If config file exists but fails validation
354
- */
355
- export async function loadDeckConfig(slidesDir: string): Promise<DeckConfig> {
356
- const configPath = join(slidesDir, 'deck.config.ts')
357
-
358
- try {
359
- // Check if config file exists
360
- const configFile = Bun.file(configPath)
361
- if (!(await configFile.exists())) {
362
- // Return default config with DEFAULT_THEME
363
- return {
364
- theme: DEFAULT_THEME,
365
- }
366
- }
367
-
368
- // Dynamic import of TypeScript config
369
- // Add cache buster to prevent module caching in tests
370
- const configModule = await import(configPath + '?t=' + Date.now())
371
-
372
- if (!configModule.default) {
373
- throw new Error('deck.config.ts must export default config')
374
- }
375
-
376
- // Validate config against schema
377
- return safeParse(DeckConfigSchema, configModule.default, 'deck.config.ts')
378
- } catch (error) {
379
- if ((error as NodeJS.ErrnoException).code === 'MODULE_NOT_FOUND') {
380
- // No config file found, use defaults
381
- return { theme: DEFAULT_THEME }
382
- }
383
- throw error
384
- }
385
- }
386
-
387
- /**
388
- * Load a complete deck from a directory.
389
- *
390
- * Loads the deck configuration, finds all slide files, and parses
391
- * them in parallel. Returns a Deck object containing all slides,
392
- * the configuration, and the base path.
393
- *
394
- * @param slidesDir - Directory containing slide files and optional deck.config.ts
395
- * @returns Complete Deck object with slides, config, and basePath
396
- * @throws {ValidationError} If any slide fails to parse or validate
397
- */
398
- export async function loadDeck(slidesDir: string): Promise<Deck> {
399
- // Load config first
400
- const config = await loadDeckConfig(slidesDir)
401
-
402
- // Find all markdown files
403
- const slideFiles = await findSlideFiles(slidesDir)
404
-
405
- // Parse all slides in parallel
406
- const slides = await Promise.all(
407
- slideFiles.map((file) => parseSlide(file.path, file.index))
408
- )
409
-
410
- return {
411
- slides,
412
- config,
413
- basePath: slidesDir,
414
- }
415
- }
416
-
417
- /**
418
- * Normalize bigText to array.
419
- *
420
- * Converts the bigText frontmatter field to a consistent array format
421
- * for use by the renderer. Handles:
422
- * - undefined → empty array
423
- * - string → single-element array
424
- * - string[] → pass through unchanged
425
- *
426
- * @param bigText - The bigText value from slide frontmatter
427
- * @returns Array of strings for rendering
428
- *
429
- * @example
430
- * normalizeBigText(undefined) // []
431
- * normalizeBigText('HELLO') // ['HELLO']
432
- * normalizeBigText(['A', 'B']) // ['A', 'B']
433
- */
434
- export function normalizeBigText(bigText: string | string[] | undefined): string[] {
435
- if (!bigText) return []
436
- return Array.isArray(bigText) ? bigText : [bigText]
437
- }
438
-
439
- /**
440
- * Error class for slide parsing failures.
441
- * Includes the file path of the slide that failed to parse and
442
- * optionally the underlying cause for error chaining.
443
- */
444
- export class SlideParseError extends Error {
445
- /**
446
- * @param message - The error message describing what went wrong
447
- * @param filePath - Path to the slide file that failed to parse
448
- * @param cause - Optional underlying error that caused this failure
449
- */
450
- constructor(
451
- message: string,
452
- public readonly filePath: string,
453
- public override readonly cause?: Error
454
- ) {
455
- super(message)
456
- this.name = 'SlideParseError'
457
- }
458
- }
459
-
460
- /**
461
- * Error class for deck loading failures.
462
- * Includes the directory path of the slides and optionally
463
- * the underlying cause for error chaining.
464
- */
465
- export class DeckLoadError extends Error {
466
- /**
467
- * @param message - The error message describing what went wrong
468
- * @param slidesDir - Path to the directory that was being loaded
469
- * @param cause - Optional underlying error that caused this failure
470
- */
471
- constructor(
472
- message: string,
473
- public readonly slidesDir: string,
474
- public override readonly cause?: Error
475
- ) {
476
- super(message)
477
- this.name = 'DeckLoadError'
478
- }
479
- }
480
-
481
- /**
482
- * Format a slide parse error for user-friendly display.
483
- * Creates a multi-line message with the file path, error message,
484
- * and optional cause chain.
485
- *
486
- * @param error - The SlideParseError to format
487
- * @returns A formatted string suitable for console output
488
- *
489
- * @example
490
- * const error = new SlideParseError(
491
- * 'Missing required field: title',
492
- * '/slides/01-intro.md',
493
- * new Error('Validation failed')
494
- * )
495
- * console.log(formatSlideError(error))
496
- * // Error parsing slide: /slides/01-intro.md
497
- * // Missing required field: title
498
- * // Caused by: Validation failed
499
- */
500
- export function formatSlideError(error: SlideParseError): string {
501
- let msg = `Error parsing slide: ${error.filePath}\n`
502
- msg += ` ${error.message}\n`
503
-
504
- if (error.cause) {
505
- msg += ` Caused by: ${error.cause.message}\n`
506
- }
507
-
508
- return msg
509
- }