@pep/term-deck 1.0.14 → 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.
- package/dist/bin/term-deck.d.ts +1 -0
- package/dist/bin/term-deck.js +1720 -0
- package/dist/bin/term-deck.js.map +1 -0
- package/dist/index.d.ts +670 -0
- package/dist/index.js +159 -0
- package/dist/index.js.map +1 -0
- package/package.json +16 -13
- package/bin/term-deck.js +0 -14
- package/bin/term-deck.ts +0 -45
- package/src/cli/__tests__/errors.test.ts +0 -201
- package/src/cli/__tests__/help.test.ts +0 -157
- package/src/cli/__tests__/init.test.ts +0 -110
- package/src/cli/commands/export.ts +0 -33
- package/src/cli/commands/init.ts +0 -125
- package/src/cli/commands/present.ts +0 -29
- package/src/cli/errors.ts +0 -77
- package/src/core/__tests__/slide.test.ts +0 -1759
- package/src/core/__tests__/theme.test.ts +0 -1103
- package/src/core/slide.ts +0 -509
- package/src/core/theme.ts +0 -388
- package/src/export/__tests__/recorder.test.ts +0 -566
- package/src/export/recorder.ts +0 -639
- package/src/index.ts +0 -36
- package/src/presenter/__tests__/main.test.ts +0 -244
- package/src/presenter/main.ts +0 -658
- package/src/renderer/__tests__/screen-extended.test.ts +0 -801
- package/src/renderer/__tests__/screen.test.ts +0 -525
- package/src/renderer/screen.ts +0 -671
- package/src/schemas/__tests__/config.test.ts +0 -429
- package/src/schemas/__tests__/slide.test.ts +0 -349
- package/src/schemas/__tests__/theme.test.ts +0 -970
- package/src/schemas/__tests__/validation.test.ts +0 -256
- package/src/schemas/config.ts +0 -58
- package/src/schemas/slide.ts +0 -56
- package/src/schemas/theme.ts +0 -203
- package/src/schemas/validation.ts +0 -64
- package/src/themes/matrix/index.ts +0 -53
package/dist/index.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { parse } from 'yaml';
|
|
3
|
+
import deepmerge from 'deepmerge';
|
|
4
|
+
import 'gradient-string';
|
|
5
|
+
|
|
6
|
+
// src/schemas/config.ts
|
|
7
|
+
var HexColorSchema = z.string().regex(/^#[0-9a-fA-F]{6}$/, {
|
|
8
|
+
message: "Color must be a valid hex color (e.g., #ff0066)"
|
|
9
|
+
});
|
|
10
|
+
var GradientSchema = z.array(HexColorSchema).min(2, {
|
|
11
|
+
message: "Gradient must have at least 2 colors"
|
|
12
|
+
});
|
|
13
|
+
var ThemeSchema = z.object({
|
|
14
|
+
// Theme metadata
|
|
15
|
+
name: z.string().min(1, { message: "Theme name is required" }),
|
|
16
|
+
description: z.string().optional(),
|
|
17
|
+
author: z.string().optional(),
|
|
18
|
+
version: z.string().optional(),
|
|
19
|
+
// Color palette
|
|
20
|
+
colors: z.object({
|
|
21
|
+
primary: HexColorSchema,
|
|
22
|
+
secondary: HexColorSchema.optional(),
|
|
23
|
+
accent: HexColorSchema,
|
|
24
|
+
background: HexColorSchema,
|
|
25
|
+
text: HexColorSchema,
|
|
26
|
+
muted: HexColorSchema,
|
|
27
|
+
success: HexColorSchema.optional(),
|
|
28
|
+
warning: HexColorSchema.optional(),
|
|
29
|
+
error: HexColorSchema.optional()
|
|
30
|
+
}),
|
|
31
|
+
// Named gradients for bigText
|
|
32
|
+
gradients: z.record(z.string(), GradientSchema).refine(
|
|
33
|
+
(g) => Object.keys(g).length >= 1,
|
|
34
|
+
{ message: "At least one gradient must be defined" }
|
|
35
|
+
),
|
|
36
|
+
// Glyph set for matrix rain background
|
|
37
|
+
glyphs: z.string().min(10, {
|
|
38
|
+
message: "Glyph set must have at least 10 characters"
|
|
39
|
+
}),
|
|
40
|
+
// Animation settings
|
|
41
|
+
animations: z.object({
|
|
42
|
+
// Speed multiplier (1.0 = normal, 0.5 = half speed, 2.0 = double speed)
|
|
43
|
+
revealSpeed: z.number().min(0.1).max(5).default(1),
|
|
44
|
+
// Matrix rain density (number of drops)
|
|
45
|
+
matrixDensity: z.number().min(10).max(200).default(50),
|
|
46
|
+
// Glitch effect iterations
|
|
47
|
+
glitchIterations: z.number().min(1).max(20).default(5),
|
|
48
|
+
// Delay between lines during reveal (ms)
|
|
49
|
+
lineDelay: z.number().min(0).max(500).default(30),
|
|
50
|
+
// Matrix rain update interval (ms)
|
|
51
|
+
matrixInterval: z.number().min(20).max(200).default(80)
|
|
52
|
+
}),
|
|
53
|
+
// Window appearance
|
|
54
|
+
window: z.object({
|
|
55
|
+
// Border style
|
|
56
|
+
borderStyle: z.enum(["line", "double", "rounded", "none"]).default("line"),
|
|
57
|
+
// Shadow effect
|
|
58
|
+
shadow: z.boolean().default(true),
|
|
59
|
+
// Padding inside windows
|
|
60
|
+
padding: z.object({
|
|
61
|
+
top: z.number().min(0).max(5).default(1),
|
|
62
|
+
bottom: z.number().min(0).max(5).default(1),
|
|
63
|
+
left: z.number().min(0).max(10).default(2),
|
|
64
|
+
right: z.number().min(0).max(10).default(2)
|
|
65
|
+
}).optional()
|
|
66
|
+
}).optional()
|
|
67
|
+
});
|
|
68
|
+
ThemeSchema.deepPartial();
|
|
69
|
+
|
|
70
|
+
// src/schemas/config.ts
|
|
71
|
+
var SettingsSchema = z.object({
|
|
72
|
+
// Start slide (0-indexed)
|
|
73
|
+
startSlide: z.number().min(0).default(0),
|
|
74
|
+
// Loop back to first slide after last
|
|
75
|
+
loop: z.boolean().default(false),
|
|
76
|
+
// Auto-advance slides (ms, 0 = disabled)
|
|
77
|
+
autoAdvance: z.number().min(0).default(0),
|
|
78
|
+
// Show slide numbers
|
|
79
|
+
showSlideNumbers: z.boolean().default(false),
|
|
80
|
+
// Show progress bar
|
|
81
|
+
showProgress: z.boolean().default(false)
|
|
82
|
+
});
|
|
83
|
+
var ExportSettingsSchema = z.object({
|
|
84
|
+
// Output width in characters (min 80, max 400)
|
|
85
|
+
width: z.number().min(80).max(400).default(120),
|
|
86
|
+
// Output height in characters (min 24, max 100)
|
|
87
|
+
height: z.number().min(24).max(100).default(40),
|
|
88
|
+
// Frames per second for video (min 10, max 60)
|
|
89
|
+
fps: z.number().min(10).max(60).default(30)
|
|
90
|
+
});
|
|
91
|
+
var DeckConfigSchema = z.object({
|
|
92
|
+
// Presentation metadata
|
|
93
|
+
title: z.string().optional(),
|
|
94
|
+
author: z.string().optional(),
|
|
95
|
+
date: z.string().optional(),
|
|
96
|
+
// Theme (already validated Theme object)
|
|
97
|
+
theme: ThemeSchema,
|
|
98
|
+
// Presentation settings
|
|
99
|
+
settings: SettingsSchema.optional(),
|
|
100
|
+
// Export settings
|
|
101
|
+
export: ExportSettingsSchema.optional()
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// src/schemas/validation.ts
|
|
105
|
+
var ValidationError = class extends Error {
|
|
106
|
+
constructor(message) {
|
|
107
|
+
super(message);
|
|
108
|
+
this.name = "ValidationError";
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
function formatZodError(error, context) {
|
|
112
|
+
const issues = error.issues.map((issue) => {
|
|
113
|
+
const path = issue.path.join(".");
|
|
114
|
+
return ` - ${path ? `${path}: ` : ""}${issue.message}`;
|
|
115
|
+
});
|
|
116
|
+
return `Invalid ${context}:
|
|
117
|
+
${issues.join("\n")}`;
|
|
118
|
+
}
|
|
119
|
+
function safeParse(schema, data, context) {
|
|
120
|
+
const result = schema.safeParse(data);
|
|
121
|
+
if (!result.success) {
|
|
122
|
+
throw new ValidationError(formatZodError(result.error, context));
|
|
123
|
+
}
|
|
124
|
+
return result.data;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/core/theme.ts
|
|
128
|
+
function createThemeFromMerge(base, overrides) {
|
|
129
|
+
const merged = deepmerge(base, overrides, {
|
|
130
|
+
// Arrays should be replaced, not concatenated
|
|
131
|
+
arrayMerge: (_, source) => source
|
|
132
|
+
});
|
|
133
|
+
const validated = safeParse(ThemeSchema, merged, "merged theme");
|
|
134
|
+
return {
|
|
135
|
+
...validated,
|
|
136
|
+
extend(newOverrides) {
|
|
137
|
+
return createThemeFromMerge(validated, newOverrides);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function createTheme(yaml) {
|
|
142
|
+
const parsed = parse(yaml);
|
|
143
|
+
const validated = safeParse(ThemeSchema, parsed, "theme");
|
|
144
|
+
return {
|
|
145
|
+
...validated,
|
|
146
|
+
extend(overrides) {
|
|
147
|
+
return createThemeFromMerge(validated, overrides);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/index.ts
|
|
153
|
+
function defineConfig(config) {
|
|
154
|
+
return DeckConfigSchema.parse(config);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export { createTheme, defineConfig };
|
|
158
|
+
//# sourceMappingURL=index.js.map
|
|
159
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/schemas/theme.ts","../src/schemas/config.ts","../src/schemas/validation.ts","../src/core/theme.ts","../src/index.ts"],"names":["z","parseYaml"],"mappings":"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","file":"index.js","sourcesContent":["import { z } from 'zod'\n\n/**\n * Schema for validating hex color strings.\n * Accepts 6-digit hex colors with # prefix (e.g., #ff0066)\n */\nexport const HexColorSchema = z.string().regex(/^#[0-9a-fA-F]{6}$/, {\n message: 'Color must be a valid hex color (e.g., #ff0066)',\n})\n\nexport type HexColor = z.infer<typeof HexColorSchema>\n\n/**\n * Schema for validating gradient color arrays.\n * A gradient requires at least 2 hex colors.\n */\nexport const GradientSchema = z.array(HexColorSchema).min(2, {\n message: 'Gradient must have at least 2 colors',\n})\n\nexport type Gradient = z.infer<typeof GradientSchema>\n\n/**\n * Schema for validating theme objects.\n * Defines the visual appearance of the presentation deck.\n */\nexport const ThemeSchema = z.object({\n // Theme metadata\n name: z.string().min(1, { message: 'Theme name is required' }),\n description: z.string().optional(),\n author: z.string().optional(),\n version: z.string().optional(),\n\n // Color palette\n colors: z.object({\n primary: HexColorSchema,\n secondary: HexColorSchema.optional(),\n accent: HexColorSchema,\n background: HexColorSchema,\n text: HexColorSchema,\n muted: HexColorSchema,\n success: HexColorSchema.optional(),\n warning: HexColorSchema.optional(),\n error: HexColorSchema.optional(),\n }),\n\n // Named gradients for bigText\n gradients: z.record(z.string(), GradientSchema).refine(\n (g) => Object.keys(g).length >= 1,\n { message: 'At least one gradient must be defined' }\n ),\n\n // Glyph set for matrix rain background\n glyphs: z.string().min(10, {\n message: 'Glyph set must have at least 10 characters',\n }),\n\n // Animation settings\n animations: z.object({\n // Speed multiplier (1.0 = normal, 0.5 = half speed, 2.0 = double speed)\n revealSpeed: z.number().min(0.1).max(5.0).default(1.0),\n // Matrix rain density (number of drops)\n matrixDensity: z.number().min(10).max(200).default(50),\n // Glitch effect iterations\n glitchIterations: z.number().min(1).max(20).default(5),\n // Delay between lines during reveal (ms)\n lineDelay: z.number().min(0).max(500).default(30),\n // Matrix rain update interval (ms)\n matrixInterval: z.number().min(20).max(200).default(80),\n }),\n\n // Window appearance\n window: z.object({\n // Border style\n borderStyle: z.enum(['line', 'double', 'rounded', 'none']).default('line'),\n // Shadow effect\n shadow: z.boolean().default(true),\n // Padding inside windows\n padding: z.object({\n top: z.number().min(0).max(5).default(1),\n bottom: z.number().min(0).max(5).default(1),\n left: z.number().min(0).max(10).default(2),\n right: z.number().min(0).max(10).default(2),\n }).optional(),\n }).optional(),\n})\n\nexport type Theme = z.infer<typeof ThemeSchema>\n\n// ============================================================================\n// Color Token System\n// ============================================================================\n\n/**\n * Valid color tokens for inline styling in slide body content.\n * Tokens can be either:\n * - Built-in colors: GREEN, ORANGE, CYAN, PINK, WHITE, GRAY\n * - Theme-mapped colors: PRIMARY, SECONDARY, ACCENT, MUTED, TEXT, BACKGROUND\n *\n * Usage in slides: {GREEN}colored text{/}\n */\nexport const ColorTokens = [\n 'GREEN',\n 'ORANGE',\n 'CYAN',\n 'PINK',\n 'WHITE',\n 'GRAY',\n 'PRIMARY', // Maps to theme.colors.primary\n 'SECONDARY', // Maps to theme.colors.secondary\n 'ACCENT', // Maps to theme.colors.accent\n 'MUTED', // Maps to theme.colors.muted\n 'TEXT', // Maps to theme.colors.text\n 'BACKGROUND', // Maps to theme.colors.background\n] as const\n\n/**\n * Type for valid color token names.\n */\nexport type ColorToken = typeof ColorTokens[number]\n\n/**\n * Pattern for matching color tokens in slide content.\n * Matches: {GREEN}, {ORANGE}, {CYAN}, {PINK}, {WHITE}, {GRAY},\n * {PRIMARY}, {SECONDARY}, {ACCENT}, {MUTED}, {TEXT}, {BACKGROUND}, {/}\n *\n * The {/} token closes any open color tag.\n */\nexport const COLOR_TOKEN_PATTERN = /\\{(GREEN|ORANGE|CYAN|PINK|WHITE|GRAY|PRIMARY|SECONDARY|ACCENT|MUTED|TEXT|BACKGROUND|\\/)\\}/g\n\n// ============================================================================\n// Partial Theme for Extension\n// ============================================================================\n\n/**\n * Deep partial utility type that makes all nested properties optional.\n * Used for theme extension where only specific fields need to be overridden.\n */\nexport type DeepPartial<T> = T extends object ? {\n [P in keyof T]?: DeepPartial<T[P]>\n} : T\n\n/**\n * Partial theme type for use with theme.extend() functionality.\n * All fields (including nested) become optional.\n */\nexport type PartialTheme = DeepPartial<Theme>\n\n/**\n * Schema for validating partial theme objects.\n * All fields become optional recursively, allowing partial overrides.\n * Used for theme extension validation.\n */\nexport const PartialThemeSchema = ThemeSchema.deepPartial()\n\n// ============================================================================\n// Default Theme\n// ============================================================================\n\n/**\n * Default matrix/cyberpunk theme.\n * Used when no theme is specified or as a base for theme extension.\n */\nexport const DEFAULT_THEME: Theme = {\n name: 'matrix',\n description: 'Default cyberpunk/matrix theme',\n\n colors: {\n primary: '#00cc66',\n accent: '#ff6600',\n background: '#0a0a0a',\n text: '#ffffff',\n muted: '#666666',\n },\n\n gradients: {\n fire: ['#ff6600', '#ff3300', '#ff0066'],\n cool: ['#00ccff', '#0066ff', '#6600ff'],\n pink: ['#ff0066', '#ff0099', '#cc00ff'],\n hf: ['#99cc00', '#00cc66', '#00cccc'],\n },\n\n glyphs: 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン0123456789',\n\n animations: {\n revealSpeed: 1.0,\n matrixDensity: 50,\n glitchIterations: 5,\n lineDelay: 30,\n matrixInterval: 80,\n },\n\n window: {\n borderStyle: 'line',\n shadow: true,\n padding: {\n top: 1,\n bottom: 1,\n left: 2,\n right: 2,\n },\n },\n}\n","import { z } from 'zod'\nimport { ThemeSchema } from './theme'\n\n/**\n * Schema for presentation settings.\n * Controls how the presentation behaves during runtime.\n */\nexport const SettingsSchema = z.object({\n // Start slide (0-indexed)\n startSlide: z.number().min(0).default(0),\n // Loop back to first slide after last\n loop: z.boolean().default(false),\n // Auto-advance slides (ms, 0 = disabled)\n autoAdvance: z.number().min(0).default(0),\n // Show slide numbers\n showSlideNumbers: z.boolean().default(false),\n // Show progress bar\n showProgress: z.boolean().default(false),\n})\n\nexport type Settings = z.infer<typeof SettingsSchema>\n\n/**\n * Schema for export settings.\n * Controls the output dimensions and quality of exported videos/GIFs.\n */\nexport const ExportSettingsSchema = z.object({\n // Output width in characters (min 80, max 400)\n width: z.number().min(80).max(400).default(120),\n // Output height in characters (min 24, max 100)\n height: z.number().min(24).max(100).default(40),\n // Frames per second for video (min 10, max 60)\n fps: z.number().min(10).max(60).default(30),\n})\n\nexport type ExportSettings = z.infer<typeof ExportSettingsSchema>\n\n/**\n * Schema for validating deck configuration (deck.config.ts).\n * Defines the complete configuration for a presentation deck.\n */\nexport const DeckConfigSchema = z.object({\n // Presentation metadata\n title: z.string().optional(),\n author: z.string().optional(),\n date: z.string().optional(),\n\n // Theme (already validated Theme object)\n theme: ThemeSchema,\n\n // Presentation settings\n settings: SettingsSchema.optional(),\n\n // Export settings\n export: ExportSettingsSchema.optional(),\n})\n\nexport type DeckConfig = z.infer<typeof DeckConfigSchema>\n","import { z, ZodError } from 'zod'\n\n/**\n * Custom error class for validation failures.\n * Extends Error with a specific name for easy identification.\n */\nexport class ValidationError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'ValidationError'\n }\n}\n\n/**\n * Format Zod errors into user-friendly messages.\n * Formats each issue with its field path and message.\n *\n * @param error - The ZodError to format\n * @param context - A description of what was being validated (e.g., \"theme\", \"slide frontmatter\")\n * @returns A formatted string with all validation issues\n *\n * @example\n * const error = new ZodError([...])\n * formatZodError(error, 'theme')\n * // Returns:\n * // \"Invalid theme:\n * // - colors.primary: Color must be a valid hex color (e.g., #ff0066)\n * // - name: Theme name is required\"\n */\nexport function formatZodError(error: ZodError, context: string): string {\n const issues = error.issues.map((issue) => {\n const path = issue.path.join('.')\n return ` - ${path ? `${path}: ` : ''}${issue.message}`\n })\n\n return `Invalid ${context}:\\n${issues.join('\\n')}`\n}\n\n/**\n * Parse data with a Zod schema and throw a ValidationError with friendly messages on failure.\n *\n * @param schema - The Zod schema to validate against\n * @param data - The data to validate\n * @param context - A description of what's being validated (e.g., \"theme\", \"slide frontmatter\")\n * @returns The parsed and validated data\n * @throws {ValidationError} If validation fails\n *\n * @example\n * const theme = safeParse(ThemeSchema, rawData, 'theme')\n * // Throws ValidationError with formatted message if invalid\n */\nexport function safeParse<T>(\n schema: z.ZodSchema<T>,\n data: unknown,\n context: string\n): T {\n const result = schema.safeParse(data)\n\n if (!result.success) {\n throw new ValidationError(formatZodError(result.error, context))\n }\n\n return result.data\n}\n","import { parse as parseYaml } from 'yaml'\nimport deepmerge from 'deepmerge'\nimport gradient from 'gradient-string'\nimport { readFile, access } from 'fs/promises'\nimport type { Theme, PartialTheme } from '../schemas/theme'\nimport { ThemeSchema } from '../schemas/theme'\nimport { safeParse, ValidationError } from '../schemas/validation'\n\n/**\n * Error class for theme-related failures.\n * Includes optional source information (theme name and file path) for better debugging.\n */\nexport class ThemeError extends Error {\n /**\n * @param message - The error message\n * @param themeName - Optional name of the theme that caused the error\n * @param path - Optional path to the theme file or package\n */\n constructor(\n message: string,\n public readonly themeName?: string,\n public readonly path?: string\n ) {\n super(message)\n this.name = 'ThemeError'\n }\n}\n\n/**\n * Format any error into a user-friendly ThemeError.\n * Handles ValidationError, generic Error, and unknown error types.\n *\n * @param error - The error to format\n * @param source - Description of the theme source (e.g., file path or package name)\n * @returns A ThemeError with a user-friendly message\n *\n * @example\n * try {\n * await loadThemeFromFile('./theme.yml')\n * } catch (error) {\n * throw formatThemeError(error, './theme.yml')\n * }\n */\nexport function formatThemeError(error: unknown, source: string): ThemeError {\n if (error instanceof ValidationError) {\n return new ThemeError(\n `Invalid theme from ${source}:\\n${error.message}`,\n undefined,\n source\n )\n }\n\n if (error instanceof Error) {\n return new ThemeError(\n `Failed to load theme from ${source}: ${error.message}`,\n undefined,\n source\n )\n }\n\n return new ThemeError(`Unknown error loading theme from ${source}`)\n}\n\n/**\n * Theme object with extension capability.\n * Extends the base Theme type with an extend() method that allows\n * Tailwind-style theme customization.\n */\nexport interface ThemeObject extends Theme {\n /**\n * Create a new theme by merging overrides into this theme.\n * Uses deep merge with array replacement strategy.\n *\n * @param overrides - Partial theme object with values to override\n * @returns A new ThemeObject with the merged values\n *\n * @example\n * const custom = matrix.extend({\n * colors: { primary: '#ff0066' }\n * })\n *\n * @example\n * // Chained extensions\n * const custom = matrix\n * .extend({ colors: { primary: '#ff0066' } })\n * .extend({ animations: { revealSpeed: 0.5 } })\n */\n extend(overrides: PartialTheme): ThemeObject\n}\n\n/**\n * Theme package structure for npm packages or local theme files.\n * Contains the raw YAML source, parsed theme, and package metadata.\n */\nexport interface ThemePackage {\n /** Raw YAML content of the theme file */\n yaml: string\n\n /** Parsed and validated theme object */\n theme: ThemeObject\n\n /** Package metadata */\n meta: {\n /** Package or theme name */\n name: string\n /** Package version */\n version: string\n /** Path to the theme file or package */\n path: string\n }\n}\n\n/**\n * Create a ThemeObject from a validated Theme.\n * Internal helper that adds the extend() method to a Theme.\n *\n * @param validated - A validated Theme object\n * @returns A ThemeObject with extension capability\n */\nfunction createThemeFromMerge(base: Theme, overrides: PartialTheme): ThemeObject {\n // Deep merge, with overrides taking precedence\n const merged = deepmerge(base, overrides, {\n // Arrays should be replaced, not concatenated\n arrayMerge: (_, source) => source,\n }) as Theme\n\n // Re-validate the merged result\n const validated = safeParse(ThemeSchema, merged, 'merged theme')\n\n return {\n ...validated,\n extend(newOverrides: PartialTheme): ThemeObject {\n return createThemeFromMerge(validated, newOverrides)\n },\n }\n}\n\n/**\n * Create a theme from a YAML string.\n * Parses the YAML, validates it against ThemeSchema, and returns a ThemeObject\n * with extension capability.\n *\n * @param yaml - The YAML string containing the theme definition\n * @returns A validated ThemeObject with extend() method\n * @throws {Error} If the YAML syntax is invalid\n * @throws {ValidationError} If the parsed data doesn't match ThemeSchema\n *\n * @example\n * const theme = createTheme(`\n * name: custom\n * colors:\n * primary: \"#ff0066\"\n * accent: \"#00ff66\"\n * background: \"#000000\"\n * text: \"#ffffff\"\n * muted: \"#666666\"\n * gradients:\n * main:\n * - \"#ff0066\"\n * - \"#00ff66\"\n * glyphs: \"0123456789ABCDEF\"\n * animations:\n * revealSpeed: 1.0\n * matrixDensity: 30\n * glitchIterations: 3\n * lineDelay: 20\n * matrixInterval: 100\n * `)\n */\nexport function createTheme(yaml: string): ThemeObject {\n const parsed = parseYaml(yaml)\n const validated = safeParse(ThemeSchema, parsed, 'theme')\n\n return {\n ...validated,\n extend(overrides: PartialTheme): ThemeObject {\n return createThemeFromMerge(validated, overrides)\n },\n }\n}\n\n/**\n * Load a theme from a YAML file on the filesystem.\n * Reads the file contents and passes them to createTheme for parsing and validation.\n *\n * @param path - The filesystem path to the YAML theme file\n * @returns A validated ThemeObject with extend() method\n * @throws {Error} If the file cannot be read (e.g., file not found, permission denied)\n * @throws {Error} If the YAML syntax is invalid\n * @throws {ValidationError} If the parsed data doesn't match ThemeSchema\n *\n * @example\n * const theme = await loadThemeFromFile('./themes/matrix.yml')\n * const customTheme = theme.extend({ colors: { primary: '#ff0066' } })\n */\nexport async function loadThemeFromFile(path: string): Promise<ThemeObject> {\n try {\n await access(path)\n } catch {\n throw new Error(`Theme file not found: ${path}`)\n }\n\n const content = await readFile(path, 'utf-8')\n return createTheme(content)\n}\n\n/**\n * Load a theme from an npm package.\n * Packages must export a default ThemeObject.\n *\n * @param name - The npm package name (e.g., '@term-deck/theme-retro')\n * @returns A validated ThemeObject with extend() method\n * @throws {Error} If the package doesn't export a default theme\n * @throws {Error} If the package is not installed (with helpful install message)\n * @throws {ValidationError} If the exported theme doesn't match ThemeSchema\n *\n * @example\n * const theme = await loadThemeFromPackage('@term-deck/theme-retro')\n * const customTheme = theme.extend({ colors: { primary: '#ff0066' } })\n */\nexport async function loadThemeFromPackage(name: string): Promise<ThemeObject> {\n try {\n // Dynamic import of npm package\n const pkg = await import(name)\n\n if (!pkg.default) {\n throw new Error(`Theme package \"${name}\" must export a default theme`)\n }\n\n // Validate the exported theme\n const validated = safeParse(ThemeSchema, pkg.default, `theme from ${name}`)\n\n return {\n ...validated,\n extend(overrides: PartialTheme): ThemeObject {\n return createThemeFromMerge(validated, overrides)\n },\n }\n } catch (error) {\n // Handle module not found with helpful message\n // Bun's module resolution errors are not instanceof Error but have message/code properties\n const err = error as { message?: string; code?: string }\n const isModuleNotFound =\n err.message?.includes('Cannot find package') ||\n err.message?.includes('Cannot find module') ||\n err.code === 'MODULE_NOT_FOUND' ||\n err.code === 'ERR_MODULE_NOT_FOUND'\n\n if (isModuleNotFound) {\n throw new Error(\n `Theme package \"${name}\" not found. Install it with: bun add ${name}`\n )\n }\n throw error\n }\n}\n\n/**\n * Function type for applying a gradient to text.\n * Returns ANSI-colored text with the gradient applied.\n */\nexport interface GradientFunction {\n (text: string): string\n}\n\n/**\n * Create gradient functions from theme gradients.\n * Returns an object mapping gradient names to gradient functions that can be\n * applied to text to produce ANSI-colored output.\n *\n * @param theme - The theme containing gradient definitions\n * @returns Record mapping gradient names to gradient functions\n *\n * @example\n * const gradients = createGradients(theme)\n * const styledText = gradients.fire('Hello World')\n */\nexport function createGradients(theme: Theme): Record<string, GradientFunction> {\n const gradients: Record<string, GradientFunction> = {}\n\n for (const [name, colors] of Object.entries(theme.gradients)) {\n gradients[name] = gradient(colors)\n }\n\n return gradients\n}\n\n/**\n * Apply a gradient to text by name.\n * Looks up the gradient in the theme and applies it to the text.\n * Falls back gracefully if the gradient doesn't exist.\n *\n * @param text - The text to apply the gradient to\n * @param gradientName - The name of the gradient to use\n * @param theme - The theme containing gradient definitions\n * @returns The text with gradient applied, or unstyled text if gradient not found\n *\n * @example\n * const styledText = applyGradient('Hello World', 'fire', theme)\n */\nexport function applyGradient(\n text: string,\n gradientName: string,\n theme: Theme\n): string {\n const colors = theme.gradients[gradientName]\n\n if (!colors) {\n // Fall back gracefully - return unstyled text\n return text\n }\n\n return gradient(colors)(text)\n}\n\n/**\n * Built-in color mappings for color tokens in slide content.\n * These are fixed colors that don't change with the theme.\n */\nexport const BUILTIN_COLORS: Record<string, string> = {\n GREEN: '#00cc66',\n ORANGE: '#ff6600',\n CYAN: '#00ccff',\n PINK: '#ff0066',\n WHITE: '#ffffff',\n GRAY: '#666666',\n}\n\n/**\n * Resolve a color token to its hex value.\n * Theme colors (PRIMARY, ACCENT, etc.) are resolved from the theme.\n * Built-in colors (GREEN, ORANGE, etc.) use fixed values.\n *\n * @param token - The color token to resolve (e.g., 'PRIMARY', 'GREEN')\n * @param theme - The theme to resolve theme-specific tokens from\n * @returns The hex color value\n *\n * @example\n * const color = resolveColorToken('PRIMARY', theme) // '#00cc66'\n * const color = resolveColorToken('GREEN', theme) // '#00cc66'\n */\nexport function resolveColorToken(token: string, theme: Theme): string {\n // Check theme colors first\n switch (token) {\n case 'PRIMARY':\n return theme.colors.primary\n case 'SECONDARY':\n return theme.colors.secondary ?? theme.colors.primary\n case 'ACCENT':\n return theme.colors.accent\n case 'MUTED':\n return theme.colors.muted\n case 'TEXT':\n return theme.colors.text\n case 'BACKGROUND':\n return theme.colors.background\n }\n\n // Fall back to built-in colors\n return BUILTIN_COLORS[token] ?? theme.colors.text\n}\n\n/**\n * Convert color tokens in content to blessed tags.\n * Transforms tokens like {GREEN} to blessed color tags like {#00cc66-fg}.\n * Preserves closing tags {/} as-is.\n *\n * @param content - The content with color tokens\n * @param theme - The theme to resolve theme-specific tokens from\n * @returns Content with color tokens converted to blessed tags\n *\n * @example\n * const content = '{GREEN}Hello{/} {ORANGE}World{/}'\n * const result = colorTokensToBlessedTags(content, theme)\n * // '{#00cc66-fg}Hello{/} {#ff6600-fg}World{/}'\n */\nexport function colorTokensToBlessedTags(content: string, theme: Theme): string {\n return content.replace(\n /\\{(GREEN|ORANGE|CYAN|PINK|WHITE|GRAY|PRIMARY|SECONDARY|ACCENT|MUTED|TEXT|BACKGROUND|\\/)\\}/g,\n (_, token) => {\n if (token === '/') {\n return '{/}' // Close tag\n }\n const color = resolveColorToken(token, theme)\n return `{${color}-fg}`\n }\n )\n}\n","/**\n * Public API for term-deck\n *\n * Exports functions and types for use in deck.config.ts files\n * and external integrations.\n */\n\nimport { DeckConfigSchema, type DeckConfig } from './schemas/config.js';\nimport { type Theme } from './schemas/theme.js';\nimport { type Slide, type SlideFrontmatter } from './schemas/slide.js';\nimport { createTheme, type ThemeObject } from './core/theme.js';\n\n/**\n * Define deck configuration with validation\n *\n * Usage in deck.config.ts:\n * ```typescript\n * import { defineConfig } from 'term-deck'\n * import matrix from '@term-deck/theme-matrix'\n *\n * export default defineConfig({\n * title: 'My Presentation',\n * theme: matrix,\n * })\n * ```\n */\nexport function defineConfig(config: DeckConfig): DeckConfig {\n return DeckConfigSchema.parse(config);\n}\n\n// Re-export theme creation\nexport { createTheme };\nexport type { ThemeObject };\n\n// Re-export types for consumers\nexport type { DeckConfig, Theme, Slide, SlideFrontmatter };\n"]}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pep/term-deck",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.15",
|
|
4
4
|
"description": "Terminal presentation tool with a cyberpunk aesthetic",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"term-deck": "./bin/term-deck.js"
|
|
7
|
+
"term-deck": "./dist/bin/term-deck.js"
|
|
8
8
|
},
|
|
9
|
-
"main": "
|
|
10
|
-
"types": "
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
11
|
"keywords": [
|
|
12
12
|
"presentation",
|
|
13
13
|
"terminal",
|
|
@@ -32,34 +32,37 @@
|
|
|
32
32
|
"node": ">=18.0.0"
|
|
33
33
|
},
|
|
34
34
|
"files": [
|
|
35
|
-
"
|
|
36
|
-
"bin/",
|
|
35
|
+
"dist/",
|
|
37
36
|
"themes/",
|
|
38
37
|
"examples/",
|
|
39
38
|
"LICENSE",
|
|
40
39
|
"README.md"
|
|
41
40
|
],
|
|
42
41
|
"dependencies": {
|
|
42
|
+
"canvas": "^3.2.1",
|
|
43
43
|
"commander": "^14.0.2",
|
|
44
|
-
"
|
|
44
|
+
"deepmerge": "^4.3.1",
|
|
45
|
+
"execa": "^9.6.1",
|
|
46
|
+
"fast-glob": "^3.3.3",
|
|
45
47
|
"figlet": "^1.9.4",
|
|
46
48
|
"gradient-string": "^3.0.0",
|
|
47
49
|
"gray-matter": "^4.0.3",
|
|
48
|
-
"yaml": "^2.7.0",
|
|
49
|
-
"zod": "^3.22.4",
|
|
50
|
-
"deepmerge": "^4.3.1",
|
|
51
50
|
"mermaid-ascii": "^1.0.0",
|
|
52
|
-
"
|
|
53
|
-
"
|
|
51
|
+
"neo-blessed": "^0.2.0",
|
|
52
|
+
"yaml": "^2.7.0",
|
|
53
|
+
"zod": "^3.22.4"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@types/node": "^22.0.0",
|
|
57
56
|
"@types/figlet": "^1.5.8",
|
|
57
|
+
"@types/node": "^22.0.0",
|
|
58
|
+
"tsup": "^8.5.1",
|
|
59
|
+
"tsx": "^4.19.2",
|
|
58
60
|
"typescript": "^5.9.3",
|
|
59
61
|
"vitest": "^2.1.8"
|
|
60
62
|
},
|
|
61
63
|
"scripts": {
|
|
62
64
|
"dev": "tsx bin/term-deck.ts",
|
|
65
|
+
"build": "tsup",
|
|
63
66
|
"test": "vitest",
|
|
64
67
|
"typecheck": "tsc --noEmit"
|
|
65
68
|
}
|
package/bin/term-deck.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* JavaScript wrapper for term-deck CLI
|
|
4
|
-
* Uses tsx to execute the TypeScript entry point
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { register } from 'node:module';
|
|
8
|
-
import { pathToFileURL } from 'node:url';
|
|
9
|
-
|
|
10
|
-
// Register tsx for TypeScript execution
|
|
11
|
-
register('tsx/esm', pathToFileURL('./'));
|
|
12
|
-
|
|
13
|
-
// Import and run the TypeScript CLI
|
|
14
|
-
await import('./term-deck.ts');
|
package/bin/term-deck.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* CLI Entry Point for term-deck
|
|
4
|
-
*
|
|
5
|
-
* Terminal presentation tool with a cyberpunk aesthetic.
|
|
6
|
-
* Provides commands for presenting, exporting, and initializing decks.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { Command } from 'commander';
|
|
10
|
-
import { version } from '../package.json';
|
|
11
|
-
import { presentCommand } from '../src/cli/commands/present.js';
|
|
12
|
-
import { exportCommand } from '../src/cli/commands/export.js';
|
|
13
|
-
import { initCommand } from '../src/cli/commands/init.js';
|
|
14
|
-
import { handleError } from '../src/cli/errors.js';
|
|
15
|
-
|
|
16
|
-
const program = new Command();
|
|
17
|
-
|
|
18
|
-
program
|
|
19
|
-
.name('term-deck')
|
|
20
|
-
.description('Terminal presentation tool with a cyberpunk aesthetic')
|
|
21
|
-
.version(version);
|
|
22
|
-
|
|
23
|
-
// Register commands
|
|
24
|
-
program.addCommand(presentCommand);
|
|
25
|
-
program.addCommand(exportCommand);
|
|
26
|
-
program.addCommand(initCommand);
|
|
27
|
-
|
|
28
|
-
// Default action: present if directory given, else show help
|
|
29
|
-
program
|
|
30
|
-
.argument('[dir]', 'Slides directory to present')
|
|
31
|
-
.action(async (dir) => {
|
|
32
|
-
if (dir) {
|
|
33
|
-
// Default action: present the deck
|
|
34
|
-
try {
|
|
35
|
-
const { present } = await import('../src/presenter/main.js');
|
|
36
|
-
await present(dir, {});
|
|
37
|
-
} catch (error) {
|
|
38
|
-
handleError(error);
|
|
39
|
-
}
|
|
40
|
-
} else {
|
|
41
|
-
program.help();
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
program.parse();
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for CLI error handling
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, test, expect, mock, spyOn, beforeEach, afterEach } from 'bun:test';
|
|
6
|
-
import { handleError } from '../errors.js';
|
|
7
|
-
import { ValidationError } from '../../schemas/validation.js';
|
|
8
|
-
import { SlideParseError, DeckLoadError } from '../../core/slide.js';
|
|
9
|
-
import { ThemeError } from '../../core/theme.js';
|
|
10
|
-
|
|
11
|
-
describe('handleError', () => {
|
|
12
|
-
let consoleErrorSpy: ReturnType<typeof spyOn>;
|
|
13
|
-
let processExitSpy: ReturnType<typeof spyOn>;
|
|
14
|
-
let originalDebug: string | undefined;
|
|
15
|
-
|
|
16
|
-
beforeEach(() => {
|
|
17
|
-
consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {});
|
|
18
|
-
processExitSpy = spyOn(process, 'exit').mockImplementation(
|
|
19
|
-
(() => {}) as never,
|
|
20
|
-
);
|
|
21
|
-
originalDebug = process.env.DEBUG;
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
afterEach(() => {
|
|
25
|
-
consoleErrorSpy.mockRestore();
|
|
26
|
-
processExitSpy.mockRestore();
|
|
27
|
-
process.env.DEBUG = originalDebug;
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test('handles ValidationError', () => {
|
|
31
|
-
const error = new ValidationError('Invalid theme configuration');
|
|
32
|
-
|
|
33
|
-
handleError(error);
|
|
34
|
-
|
|
35
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
36
|
-
expect.stringContaining('Invalid theme configuration'),
|
|
37
|
-
);
|
|
38
|
-
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test('handles SlideParseError', () => {
|
|
42
|
-
const cause = new Error('Missing title field');
|
|
43
|
-
const error = new SlideParseError('Invalid frontmatter', 'slides/01.md', cause);
|
|
44
|
-
|
|
45
|
-
handleError(error);
|
|
46
|
-
|
|
47
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
48
|
-
expect.stringContaining('slides/01.md'),
|
|
49
|
-
);
|
|
50
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
51
|
-
expect.stringContaining('Invalid frontmatter'),
|
|
52
|
-
);
|
|
53
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
54
|
-
expect.stringContaining('Missing title field'),
|
|
55
|
-
);
|
|
56
|
-
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test('handles SlideParseError without cause', () => {
|
|
60
|
-
const error = new SlideParseError('Invalid frontmatter', 'slides/02.md');
|
|
61
|
-
|
|
62
|
-
handleError(error);
|
|
63
|
-
|
|
64
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
65
|
-
expect.stringContaining('slides/02.md'),
|
|
66
|
-
);
|
|
67
|
-
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test('handles DeckLoadError', () => {
|
|
71
|
-
const error = new DeckLoadError('No slides found', './slides');
|
|
72
|
-
|
|
73
|
-
handleError(error);
|
|
74
|
-
|
|
75
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
76
|
-
expect.stringContaining('./slides'),
|
|
77
|
-
);
|
|
78
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
79
|
-
expect.stringContaining('No slides found'),
|
|
80
|
-
);
|
|
81
|
-
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test('handles ThemeError', () => {
|
|
85
|
-
const error = new ThemeError('Invalid color format', 'custom-theme');
|
|
86
|
-
|
|
87
|
-
handleError(error);
|
|
88
|
-
|
|
89
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
90
|
-
expect.stringContaining('Theme error'),
|
|
91
|
-
);
|
|
92
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
93
|
-
expect.stringContaining('Invalid color format'),
|
|
94
|
-
);
|
|
95
|
-
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test('handles ENOENT errors', () => {
|
|
99
|
-
const error = new Error('ENOENT: no such file or directory');
|
|
100
|
-
|
|
101
|
-
handleError(error);
|
|
102
|
-
|
|
103
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
104
|
-
expect.stringContaining('File or directory not found'),
|
|
105
|
-
);
|
|
106
|
-
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test('handles ffmpeg errors', () => {
|
|
110
|
-
const error = new Error('ffmpeg not found in PATH');
|
|
111
|
-
|
|
112
|
-
handleError(error);
|
|
113
|
-
|
|
114
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
115
|
-
expect.stringContaining('ffmpeg error'),
|
|
116
|
-
);
|
|
117
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
118
|
-
expect.stringContaining('Make sure ffmpeg is installed'),
|
|
119
|
-
);
|
|
120
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
121
|
-
expect.stringContaining('brew install ffmpeg'),
|
|
122
|
-
);
|
|
123
|
-
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test('handles generic errors', () => {
|
|
127
|
-
const error = new Error('Something went wrong');
|
|
128
|
-
|
|
129
|
-
handleError(error);
|
|
130
|
-
|
|
131
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
132
|
-
expect.stringContaining('Something went wrong'),
|
|
133
|
-
);
|
|
134
|
-
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test('shows stack trace when DEBUG is set', () => {
|
|
138
|
-
process.env.DEBUG = '1';
|
|
139
|
-
const error = new Error('Debug error');
|
|
140
|
-
error.stack = 'Error: Debug error\n at test.ts:10:20';
|
|
141
|
-
|
|
142
|
-
handleError(error);
|
|
143
|
-
|
|
144
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
145
|
-
expect.stringContaining('at test.ts'),
|
|
146
|
-
);
|
|
147
|
-
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
test('hides stack trace when DEBUG is not set', () => {
|
|
151
|
-
delete process.env.DEBUG;
|
|
152
|
-
const error = new Error('Regular error');
|
|
153
|
-
error.stack = 'Error: Regular error\n at test.ts:10:20';
|
|
154
|
-
|
|
155
|
-
handleError(error);
|
|
156
|
-
|
|
157
|
-
// Should show error message
|
|
158
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
159
|
-
expect.stringContaining('Regular error'),
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
// Should NOT show stack trace
|
|
163
|
-
const calls = consoleErrorSpy.mock.calls;
|
|
164
|
-
const hasStackTrace = calls.some((call) =>
|
|
165
|
-
call.some((arg) => String(arg).includes('at test.ts')),
|
|
166
|
-
);
|
|
167
|
-
expect(hasStackTrace).toBe(false);
|
|
168
|
-
|
|
169
|
-
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test('handles unknown errors', () => {
|
|
173
|
-
const error = 'string error';
|
|
174
|
-
|
|
175
|
-
handleError(error);
|
|
176
|
-
|
|
177
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
178
|
-
expect.stringContaining('Unknown error'),
|
|
179
|
-
);
|
|
180
|
-
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
test('exits with code 1 for all errors', () => {
|
|
184
|
-
const errors = [
|
|
185
|
-
new ValidationError('test'),
|
|
186
|
-
new SlideParseError('test', 'test.md'),
|
|
187
|
-
new DeckLoadError('test', './slides'),
|
|
188
|
-
new ThemeError('test', 'theme'),
|
|
189
|
-
new Error('ENOENT'),
|
|
190
|
-
new Error('ffmpeg'),
|
|
191
|
-
new Error('generic'),
|
|
192
|
-
'unknown',
|
|
193
|
-
];
|
|
194
|
-
|
|
195
|
-
for (const error of errors) {
|
|
196
|
-
processExitSpy.mockClear();
|
|
197
|
-
handleError(error);
|
|
198
|
-
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
});
|