@ncukondo/slide-generation 0.1.0

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.
@@ -0,0 +1,2994 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { Command as Command8 } from "commander";
5
+
6
+ // src/core/parser.ts
7
+ import { z } from "zod";
8
+ import { parse as parseYaml } from "yaml";
9
+ import { readFile } from "fs/promises";
10
+ var referencesConfigSchema = z.object({
11
+ enabled: z.boolean().default(true),
12
+ style: z.string().default("author-year-pmid")
13
+ });
14
+ var metaSchema = z.object({
15
+ title: z.string(),
16
+ author: z.string().optional(),
17
+ date: z.string().optional(),
18
+ theme: z.string().default("default"),
19
+ references: referencesConfigSchema.optional()
20
+ });
21
+ var slideSchema = z.object({
22
+ template: z.string(),
23
+ content: z.record(z.unknown()).default({}),
24
+ class: z.string().optional(),
25
+ notes: z.string().optional(),
26
+ raw: z.string().optional()
27
+ });
28
+ var presentationSchema = z.object({
29
+ meta: metaSchema,
30
+ slides: z.array(slideSchema).default([])
31
+ });
32
+ var ParseError = class extends Error {
33
+ constructor(message, details) {
34
+ super(message);
35
+ this.details = details;
36
+ this.name = "ParseError";
37
+ }
38
+ };
39
+ var ValidationError = class extends Error {
40
+ constructor(message, details) {
41
+ super(message);
42
+ this.details = details;
43
+ this.name = "ValidationError";
44
+ }
45
+ };
46
+ var Parser = class {
47
+ parse(yamlContent) {
48
+ let rawData;
49
+ try {
50
+ rawData = parseYaml(yamlContent);
51
+ } catch (error) {
52
+ throw new ParseError("Failed to parse YAML", error);
53
+ }
54
+ const result = presentationSchema.safeParse(rawData);
55
+ if (!result.success) {
56
+ throw new ValidationError(
57
+ "Schema validation failed",
58
+ result.error.format()
59
+ );
60
+ }
61
+ return result.data;
62
+ }
63
+ async parseFile(filePath) {
64
+ let content;
65
+ try {
66
+ content = await readFile(filePath, "utf-8");
67
+ } catch (error) {
68
+ if (error.code === "ENOENT") {
69
+ throw new ParseError(`File not found: ${filePath}`);
70
+ }
71
+ throw new ParseError(`Failed to read file: ${filePath}`, error);
72
+ }
73
+ return this.parse(content);
74
+ }
75
+ };
76
+
77
+ // src/core/transformer.ts
78
+ var ICON_PLACEHOLDER_PREFIX = "___ICON_PLACEHOLDER_";
79
+ var ICON_PLACEHOLDER_SUFFIX = "___";
80
+ var REFS_CITE_PLACEHOLDER_PREFIX = "___REFS_CITE_PLACEHOLDER_";
81
+ var REFS_CITE_PLACEHOLDER_SUFFIX = "___";
82
+ var REFS_EXPAND_PLACEHOLDER_PREFIX = "___REFS_EXPAND_PLACEHOLDER_";
83
+ var REFS_EXPAND_PLACEHOLDER_SUFFIX = "___";
84
+ var TransformError = class extends Error {
85
+ constructor(message, slide, details) {
86
+ super(message);
87
+ this.slide = slide;
88
+ this.details = details;
89
+ this.name = "TransformError";
90
+ }
91
+ };
92
+ var Transformer = class {
93
+ constructor(templateEngine, templateLoader, iconResolver, citationFormatter) {
94
+ this.templateEngine = templateEngine;
95
+ this.templateLoader = templateLoader;
96
+ this.iconResolver = iconResolver;
97
+ this.citationFormatter = citationFormatter;
98
+ }
99
+ /**
100
+ * Transform a single slide using its template
101
+ */
102
+ async transform(slide, context) {
103
+ if (slide.template === "raw") {
104
+ return slide.raw ?? "";
105
+ }
106
+ const template = this.templateLoader.get(slide.template);
107
+ if (!template) {
108
+ throw new TransformError(
109
+ `Template "${slide.template}" not found`,
110
+ slide
111
+ );
112
+ }
113
+ const validationResult = this.templateLoader.validateContent(
114
+ slide.template,
115
+ slide.content
116
+ );
117
+ if (!validationResult.valid) {
118
+ throw new TransformError(
119
+ `Slide content validation failed: ${validationResult.errors?.join(", ")}`,
120
+ slide,
121
+ validationResult.errors
122
+ );
123
+ }
124
+ const pending = {
125
+ icons: /* @__PURE__ */ new Map(),
126
+ cites: /* @__PURE__ */ new Map(),
127
+ expands: /* @__PURE__ */ new Map()
128
+ };
129
+ const templateContext = this.buildTemplateContext(slide, context, pending);
130
+ let output = this.templateEngine.render(template.output, templateContext);
131
+ output = await this.resolvePlaceholders(output, pending);
132
+ if (slide.class) {
133
+ output = `<!-- _class: ${slide.class} -->
134
+ ${output}`;
135
+ }
136
+ return output.trim();
137
+ }
138
+ /**
139
+ * Transform all slides in a presentation
140
+ */
141
+ async transformAll(presentation) {
142
+ const results = [];
143
+ const totalSlides = presentation.slides.length;
144
+ for (let i = 0; i < presentation.slides.length; i++) {
145
+ const slide = presentation.slides[i];
146
+ const context = {
147
+ meta: presentation.meta,
148
+ slideIndex: i,
149
+ totalSlides
150
+ };
151
+ const transformed = await this.transform(slide, context);
152
+ results.push(transformed);
153
+ }
154
+ return results;
155
+ }
156
+ /**
157
+ * Build the full template context with helpers that collect async operations
158
+ */
159
+ buildTemplateContext(slide, context, pending) {
160
+ let iconCounter = 0;
161
+ let citeCounter = 0;
162
+ let expandCounter = 0;
163
+ const icons = {
164
+ render: (name, options) => {
165
+ const id = `${iconCounter++}`;
166
+ const placeholder = `${ICON_PLACEHOLDER_PREFIX}${id}${ICON_PLACEHOLDER_SUFFIX}`;
167
+ pending.icons.set(id, { name, options });
168
+ return placeholder;
169
+ }
170
+ };
171
+ const refs = {
172
+ cite: (id) => {
173
+ const counterId = `${citeCounter++}`;
174
+ const placeholder = `${REFS_CITE_PLACEHOLDER_PREFIX}${counterId}${REFS_CITE_PLACEHOLDER_SUFFIX}`;
175
+ pending.cites.set(counterId, id);
176
+ return placeholder;
177
+ },
178
+ expand: (text) => {
179
+ const counterId = `${expandCounter++}`;
180
+ const placeholder = `${REFS_EXPAND_PLACEHOLDER_PREFIX}${counterId}${REFS_EXPAND_PLACEHOLDER_SUFFIX}`;
181
+ pending.expands.set(counterId, text);
182
+ return placeholder;
183
+ }
184
+ };
185
+ return {
186
+ content: slide.content,
187
+ meta: {
188
+ title: context.meta.title,
189
+ author: context.meta.author,
190
+ theme: context.meta.theme
191
+ },
192
+ slide: {
193
+ index: context.slideIndex,
194
+ total: context.totalSlides
195
+ },
196
+ icons,
197
+ refs
198
+ };
199
+ }
200
+ /**
201
+ * Resolve all placeholders by executing async operations
202
+ */
203
+ async resolvePlaceholders(output, pending) {
204
+ const iconResults = /* @__PURE__ */ new Map();
205
+ for (const [id, { name, options }] of pending.icons) {
206
+ const rendered = await this.iconResolver.render(
207
+ name,
208
+ options
209
+ );
210
+ iconResults.set(id, rendered);
211
+ }
212
+ const citeResults = /* @__PURE__ */ new Map();
213
+ for (const [counterId, id] of pending.cites) {
214
+ const formatted = await this.citationFormatter.formatInline(id);
215
+ citeResults.set(counterId, formatted);
216
+ }
217
+ const expandResults = /* @__PURE__ */ new Map();
218
+ for (const [counterId, text] of pending.expands) {
219
+ const expanded = await this.citationFormatter.expandCitations(text);
220
+ expandResults.set(counterId, expanded);
221
+ }
222
+ let result = output;
223
+ for (const [id, rendered] of iconResults) {
224
+ const placeholder = `${ICON_PLACEHOLDER_PREFIX}${id}${ICON_PLACEHOLDER_SUFFIX}`;
225
+ result = result.replace(placeholder, rendered);
226
+ }
227
+ for (const [counterId, formatted] of citeResults) {
228
+ const placeholder = `${REFS_CITE_PLACEHOLDER_PREFIX}${counterId}${REFS_CITE_PLACEHOLDER_SUFFIX}`;
229
+ result = result.replace(placeholder, formatted);
230
+ }
231
+ for (const [counterId, expanded] of expandResults) {
232
+ const placeholder = `${REFS_EXPAND_PLACEHOLDER_PREFIX}${counterId}${REFS_EXPAND_PLACEHOLDER_SUFFIX}`;
233
+ result = result.replace(placeholder, expanded);
234
+ }
235
+ return result;
236
+ }
237
+ };
238
+
239
+ // src/core/renderer.ts
240
+ var Renderer = class {
241
+ /**
242
+ * Render slides and metadata into final Marp markdown
243
+ */
244
+ render(slides, meta, options) {
245
+ const frontMatter = this.renderFrontMatter(meta, options);
246
+ const slidesContent = this.joinSlides(slides, options?.notes);
247
+ if (slides.length === 0) {
248
+ return frontMatter;
249
+ }
250
+ return `${frontMatter}
251
+
252
+ ${slidesContent}`;
253
+ }
254
+ /**
255
+ * Render the YAML front matter block
256
+ */
257
+ renderFrontMatter(meta, options) {
258
+ const lines = ["---", "marp: true"];
259
+ lines.push(`title: ${meta.title}`);
260
+ if (meta.author) {
261
+ lines.push(`author: ${meta.author}`);
262
+ }
263
+ if (meta.date) {
264
+ lines.push(`date: ${meta.date}`);
265
+ }
266
+ const includeTheme = options?.includeTheme ?? true;
267
+ if (includeTheme && meta.theme) {
268
+ lines.push(`theme: ${meta.theme}`);
269
+ }
270
+ if (options?.additionalFrontMatter) {
271
+ for (const [key, value] of Object.entries(options.additionalFrontMatter)) {
272
+ lines.push(`${key}: ${this.formatFrontMatterValue(value)}`);
273
+ }
274
+ }
275
+ lines.push("---");
276
+ return lines.join("\n");
277
+ }
278
+ /**
279
+ * Format a front matter value for YAML
280
+ */
281
+ formatFrontMatterValue(value) {
282
+ if (typeof value === "boolean") {
283
+ return value ? "true" : "false";
284
+ }
285
+ if (typeof value === "number") {
286
+ return String(value);
287
+ }
288
+ if (typeof value === "string") {
289
+ if (/[:#[\]{}|>]/.test(value)) {
290
+ return `"${value.replace(/"/g, '\\"')}"`;
291
+ }
292
+ return value;
293
+ }
294
+ return String(value);
295
+ }
296
+ /**
297
+ * Join slides with Marp slide separator
298
+ */
299
+ joinSlides(slides, notes) {
300
+ const parts = [];
301
+ for (let i = 0; i < slides.length; i++) {
302
+ let slideContent = slides[i];
303
+ const note = notes?.[i];
304
+ if (note && note.trim()) {
305
+ slideContent = `${slideContent}
306
+
307
+ ${this.renderSpeakerNotes(note)}`;
308
+ }
309
+ parts.push(slideContent);
310
+ }
311
+ return parts.map((slide) => `---
312
+
313
+ ${slide}`).join("\n\n");
314
+ }
315
+ /**
316
+ * Render speaker notes as HTML comment
317
+ */
318
+ renderSpeakerNotes(notes) {
319
+ return `<!--
320
+ ${notes}
321
+ -->`;
322
+ }
323
+ };
324
+
325
+ // src/core/pipeline.ts
326
+ import { writeFile } from "fs/promises";
327
+
328
+ // src/templates/engine.ts
329
+ import nunjucks from "nunjucks";
330
+ var TemplateEngine = class {
331
+ env;
332
+ constructor() {
333
+ this.env = new nunjucks.Environment(null, {
334
+ autoescape: false,
335
+ // HTML output for Marp
336
+ throwOnUndefined: false
337
+ });
338
+ this.registerFilters();
339
+ this.registerGlobals();
340
+ }
341
+ render(template, context) {
342
+ return this.env.renderString(template, context);
343
+ }
344
+ registerFilters() {
345
+ }
346
+ registerGlobals() {
347
+ const icons = {
348
+ render: (name, options) => {
349
+ const size = options?.["size"] ?? "24px";
350
+ const color = options?.["color"] ?? "currentColor";
351
+ return `<span class="icon icon-${name}" style="font-size: ${size}; color: ${color};">[${name}]</span>`;
352
+ }
353
+ };
354
+ this.env.addGlobal("icons", icons);
355
+ const refs = {
356
+ cite: (id) => {
357
+ const cleanId = id.replace("@", "");
358
+ return `(${cleanId})`;
359
+ },
360
+ expand: (text) => {
361
+ return text.replace(/\[@(\w+)\]/g, "($1)");
362
+ }
363
+ };
364
+ this.env.addGlobal("refs", refs);
365
+ }
366
+ };
367
+
368
+ // src/templates/loader.ts
369
+ import { z as z3 } from "zod";
370
+ import * as yaml from "yaml";
371
+ import * as fs from "fs/promises";
372
+ import * as path from "path";
373
+
374
+ // src/templates/validators.ts
375
+ import { z as z2 } from "zod";
376
+ function jsonSchemaToZod(schema) {
377
+ if (schema.oneOf && schema.oneOf.length > 0) {
378
+ const schemas = schema.oneOf.map((s) => jsonSchemaToZod(s));
379
+ if (schemas.length === 1) {
380
+ return schemas[0];
381
+ }
382
+ return z2.union(schemas);
383
+ }
384
+ const type = schema.type ?? "object";
385
+ switch (type) {
386
+ case "string": {
387
+ if (schema.enum && schema.enum.length > 0) {
388
+ const enumValues = schema.enum;
389
+ return z2.enum(enumValues);
390
+ }
391
+ let zodSchema = z2.string();
392
+ if (schema.pattern) {
393
+ zodSchema = zodSchema.regex(new RegExp(schema.pattern));
394
+ }
395
+ return zodSchema;
396
+ }
397
+ case "number":
398
+ return z2.number();
399
+ case "integer":
400
+ return z2.number().int();
401
+ case "boolean":
402
+ return z2.boolean();
403
+ case "array": {
404
+ const itemSchema = schema.items ? jsonSchemaToZod(schema.items) : z2.unknown();
405
+ let arraySchema = z2.array(itemSchema);
406
+ if (schema.minItems !== void 0) {
407
+ arraySchema = arraySchema.min(schema.minItems);
408
+ }
409
+ if (schema.maxItems !== void 0) {
410
+ arraySchema = arraySchema.max(schema.maxItems);
411
+ }
412
+ return arraySchema;
413
+ }
414
+ case "object": {
415
+ if (!schema.properties) {
416
+ return z2.record(z2.unknown());
417
+ }
418
+ const shape = {};
419
+ const required = new Set(schema.required ?? []);
420
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
421
+ const propZod = jsonSchemaToZod(propSchema);
422
+ shape[key] = required.has(key) ? propZod : propZod.optional();
423
+ }
424
+ return z2.object(shape).passthrough();
425
+ }
426
+ default:
427
+ return z2.unknown();
428
+ }
429
+ }
430
+ function validateWithJsonSchema(schema, content) {
431
+ const zodSchema = jsonSchemaToZod(schema);
432
+ const result = zodSchema.safeParse(content);
433
+ if (result.success) {
434
+ return { valid: true, errors: [] };
435
+ }
436
+ const errors = result.error.errors.map((issue) => {
437
+ const path3 = issue.path.join(".");
438
+ return path3 ? `${path3}: ${issue.message}` : issue.message;
439
+ });
440
+ return { valid: false, errors };
441
+ }
442
+
443
+ // src/templates/loader.ts
444
+ var jsonSchemaSchema = z3.record(z3.unknown());
445
+ var templateDefSchema = z3.object({
446
+ name: z3.string().min(1, "Template name is required"),
447
+ description: z3.string(),
448
+ category: z3.string(),
449
+ schema: jsonSchemaSchema,
450
+ example: z3.record(z3.unknown()).optional(),
451
+ output: z3.string().min(1, "Template output is required"),
452
+ css: z3.string().optional()
453
+ });
454
+ var TemplateLoader = class {
455
+ templates;
456
+ constructor() {
457
+ this.templates = /* @__PURE__ */ new Map();
458
+ }
459
+ /**
460
+ * Load a template from a YAML string
461
+ */
462
+ async loadFromString(yamlContent) {
463
+ const parsed = yaml.parse(yamlContent);
464
+ const result = templateDefSchema.safeParse(parsed);
465
+ if (!result.success) {
466
+ const errors = result.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ");
467
+ throw new Error(`Invalid template definition: ${errors}`);
468
+ }
469
+ this.templates.set(result.data.name, result.data);
470
+ }
471
+ /**
472
+ * Load a template from a file
473
+ */
474
+ async loadFromFile(filePath) {
475
+ const content = await fs.readFile(filePath, "utf-8");
476
+ await this.loadFromString(content);
477
+ }
478
+ /**
479
+ * Load all templates from a directory (recursively)
480
+ */
481
+ async loadBuiltIn(directory) {
482
+ await this.loadDirectory(directory);
483
+ }
484
+ /**
485
+ * Load custom templates from a directory (can override built-in)
486
+ */
487
+ async loadCustom(directory) {
488
+ await this.loadDirectory(directory);
489
+ }
490
+ /**
491
+ * Internal method to load templates from a directory
492
+ */
493
+ async loadDirectory(directory) {
494
+ const entries = await fs.readdir(directory, { withFileTypes: true });
495
+ for (const entry of entries) {
496
+ const fullPath = path.join(directory, entry.name);
497
+ if (entry.isDirectory()) {
498
+ await this.loadDirectory(fullPath);
499
+ } else if (entry.isFile() && (entry.name.endsWith(".yaml") || entry.name.endsWith(".yml"))) {
500
+ await this.loadFromFile(fullPath);
501
+ }
502
+ }
503
+ }
504
+ /**
505
+ * Get a template by name
506
+ */
507
+ get(name) {
508
+ return this.templates.get(name);
509
+ }
510
+ /**
511
+ * List all loaded templates
512
+ */
513
+ list() {
514
+ return Array.from(this.templates.values());
515
+ }
516
+ /**
517
+ * List templates filtered by category
518
+ */
519
+ listByCategory(category) {
520
+ return this.list().filter((t) => t.category === category);
521
+ }
522
+ /**
523
+ * Validate content against a template's schema
524
+ */
525
+ validateContent(templateName, content) {
526
+ const template = this.get(templateName);
527
+ if (!template) {
528
+ return {
529
+ valid: false,
530
+ errors: [`Template "${templateName}" not found`]
531
+ };
532
+ }
533
+ return validateWithJsonSchema(template.schema, content);
534
+ }
535
+ };
536
+
537
+ // src/icons/registry.ts
538
+ import * as fs2 from "fs/promises";
539
+ import { parse as parseYaml2 } from "yaml";
540
+
541
+ // src/icons/schema.ts
542
+ import { z as z4 } from "zod";
543
+ var iconSourceTypeSchema = z4.enum([
544
+ "web-font",
545
+ "svg-inline",
546
+ "svg-sprite",
547
+ "local-svg"
548
+ ]);
549
+ var iconSourceSchema = z4.object({
550
+ name: z4.string(),
551
+ type: iconSourceTypeSchema,
552
+ prefix: z4.string(),
553
+ url: z4.string().optional(),
554
+ path: z4.string().optional(),
555
+ render: z4.string().optional()
556
+ });
557
+ var iconDefaultsSchema = z4.object({
558
+ size: z4.string().default("24px"),
559
+ color: z4.string().default("currentColor")
560
+ });
561
+ var iconRegistrySchema = z4.object({
562
+ sources: z4.array(iconSourceSchema),
563
+ aliases: z4.record(z4.string()).default({}),
564
+ colors: z4.record(z4.string()).optional(),
565
+ defaults: iconDefaultsSchema.default({})
566
+ });
567
+
568
+ // src/icons/registry.ts
569
+ var IconRegistryLoader = class {
570
+ registry = null;
571
+ sourcesByPrefix = /* @__PURE__ */ new Map();
572
+ aliasMap = /* @__PURE__ */ new Map();
573
+ colorMap = /* @__PURE__ */ new Map();
574
+ /**
575
+ * Load registry from YAML file
576
+ */
577
+ async load(configPath) {
578
+ const content = await fs2.readFile(configPath, "utf-8");
579
+ const parsed = parseYaml2(content);
580
+ const validated = iconRegistrySchema.parse(parsed);
581
+ this.registry = validated;
582
+ this.buildMaps();
583
+ return validated;
584
+ }
585
+ /**
586
+ * Resolve an alias to its icon reference
587
+ * @returns The resolved icon reference or the original name if not an alias
588
+ */
589
+ resolveAlias(nameOrAlias) {
590
+ return this.aliasMap.get(nameOrAlias) ?? nameOrAlias;
591
+ }
592
+ /**
593
+ * Get icon source by prefix
594
+ */
595
+ getSource(prefix) {
596
+ return this.sourcesByPrefix.get(prefix);
597
+ }
598
+ /**
599
+ * Parse an icon reference string (e.g., "mi:home" or "iconify:mdi:account")
600
+ * @returns Parsed reference or null if invalid format
601
+ */
602
+ parseIconReference(reference) {
603
+ const colonIndex = reference.indexOf(":");
604
+ if (colonIndex === -1) {
605
+ return null;
606
+ }
607
+ const prefix = reference.substring(0, colonIndex);
608
+ const name = reference.substring(colonIndex + 1);
609
+ return { prefix, name };
610
+ }
611
+ /**
612
+ * Get registry defaults
613
+ */
614
+ getDefaults() {
615
+ if (!this.registry) {
616
+ return { size: "24px", color: "currentColor" };
617
+ }
618
+ return this.registry.defaults;
619
+ }
620
+ /**
621
+ * Get color by name from color palette
622
+ */
623
+ getColor(name) {
624
+ return this.colorMap.get(name);
625
+ }
626
+ /**
627
+ * Get all sources
628
+ */
629
+ getSources() {
630
+ return this.registry?.sources ?? [];
631
+ }
632
+ /**
633
+ * Get all aliases
634
+ */
635
+ getAliases() {
636
+ return this.registry?.aliases ?? {};
637
+ }
638
+ /**
639
+ * Check if registry is loaded
640
+ */
641
+ isLoaded() {
642
+ return this.registry !== null;
643
+ }
644
+ /**
645
+ * Build internal lookup maps from registry
646
+ */
647
+ buildMaps() {
648
+ this.sourcesByPrefix.clear();
649
+ this.aliasMap.clear();
650
+ this.colorMap.clear();
651
+ if (!this.registry) {
652
+ return;
653
+ }
654
+ for (const source of this.registry.sources) {
655
+ this.sourcesByPrefix.set(source.prefix, source);
656
+ }
657
+ for (const [alias, target] of Object.entries(this.registry.aliases)) {
658
+ this.aliasMap.set(alias, target);
659
+ }
660
+ if (this.registry.colors) {
661
+ for (const [name, color] of Object.entries(this.registry.colors)) {
662
+ this.colorMap.set(name, color);
663
+ }
664
+ }
665
+ }
666
+ };
667
+
668
+ // src/icons/resolver.ts
669
+ import * as fs3 from "fs/promises";
670
+ import * as path2 from "path";
671
+ import nunjucks2 from "nunjucks";
672
+ var IconResolver = class {
673
+ constructor(registry) {
674
+ this.registry = registry;
675
+ this.nunjucksEnv = new nunjucks2.Environment(null, {
676
+ autoescape: false
677
+ });
678
+ }
679
+ nunjucksEnv;
680
+ /**
681
+ * Render an icon by name or alias
682
+ */
683
+ async render(nameOrAlias, options) {
684
+ const resolved = this.registry.resolveAlias(nameOrAlias);
685
+ const parsed = this.registry.parseIconReference(resolved);
686
+ if (!parsed) {
687
+ throw new Error(
688
+ `Invalid icon reference format: "${resolved}". Expected format: "prefix:name"`
689
+ );
690
+ }
691
+ const source = this.registry.getSource(parsed.prefix);
692
+ if (!source) {
693
+ throw new Error(`Unknown icon source prefix: "${parsed.prefix}"`);
694
+ }
695
+ const defaults = this.registry.getDefaults();
696
+ const mergedOptions = {
697
+ size: options?.size ?? defaults.size,
698
+ color: options?.color ?? defaults.color,
699
+ ...options?.class !== void 0 ? { class: options.class } : {}
700
+ };
701
+ switch (source.type) {
702
+ case "web-font":
703
+ return this.renderWebFont(source, parsed.name, mergedOptions);
704
+ case "local-svg":
705
+ return await this.renderLocalSvg(source, parsed.name, mergedOptions);
706
+ case "svg-inline":
707
+ return await this.renderSvgInline(source, parsed.name, mergedOptions);
708
+ case "svg-sprite":
709
+ return this.renderSvgSprite(source, parsed.name, mergedOptions);
710
+ default:
711
+ throw new Error(`Unsupported icon source type: "${source.type}"`);
712
+ }
713
+ }
714
+ /**
715
+ * Render a web-font icon
716
+ */
717
+ renderWebFont(source, name, options) {
718
+ const style = this.buildStyle(options);
719
+ const className = this.buildClassName(name, options);
720
+ if (source.render) {
721
+ return this.nunjucksEnv.renderString(source.render, {
722
+ name,
723
+ style,
724
+ class: className,
725
+ size: options.size,
726
+ color: options.color
727
+ });
728
+ }
729
+ return `<span class="${className}" style="${style}">${name}</span>`;
730
+ }
731
+ /**
732
+ * Render a local SVG icon
733
+ */
734
+ async renderLocalSvg(source, name, options) {
735
+ if (!source.path) {
736
+ throw new Error(`Local SVG source "${source.name}" has no path defined`);
737
+ }
738
+ const svgPath = path2.join(source.path, `${name}.svg`);
739
+ try {
740
+ const svgContent = await fs3.readFile(svgPath, "utf-8");
741
+ return this.processSvg(svgContent, name, options);
742
+ } catch (error) {
743
+ if (error.code === "ENOENT") {
744
+ throw new Error(`Icon file not found: ${svgPath}`);
745
+ }
746
+ throw error;
747
+ }
748
+ }
749
+ /**
750
+ * Render an SVG from external URL (placeholder without cache)
751
+ */
752
+ async renderSvgInline(source, name, options) {
753
+ const className = this.buildClassName(name, options);
754
+ const style = this.buildStyle(options);
755
+ return `<span class="${className}" style="${style}" data-icon-source="${source.name}" data-icon-name="${name}">[${name}]</span>`;
756
+ }
757
+ /**
758
+ * Render an SVG sprite reference
759
+ */
760
+ renderSvgSprite(source, name, options) {
761
+ const className = this.buildClassName(name, options);
762
+ const size = options.size ?? "24px";
763
+ const color = options.color ?? "currentColor";
764
+ const spriteUrl = source.url ?? "";
765
+ return `<svg class="${className}" width="${size}" height="${size}" fill="${color}">
766
+ <use xlink:href="${spriteUrl}#${name}"/>
767
+ </svg>`;
768
+ }
769
+ /**
770
+ * Process SVG content and apply options
771
+ */
772
+ processSvg(svgContent, name, options) {
773
+ const className = this.buildClassName(name, options);
774
+ const size = options.size ?? "24px";
775
+ const color = options.color ?? "currentColor";
776
+ let processed = svgContent.trim();
777
+ if (processed.includes("class=")) {
778
+ processed = processed.replace(/class="[^"]*"/, `class="${className}"`);
779
+ } else {
780
+ processed = processed.replace("<svg", `<svg class="${className}"`);
781
+ }
782
+ if (processed.includes("width=")) {
783
+ processed = processed.replace(/width="[^"]*"/, `width="${size}"`);
784
+ } else {
785
+ processed = processed.replace("<svg", `<svg width="${size}"`);
786
+ }
787
+ if (processed.includes("height=")) {
788
+ processed = processed.replace(/height="[^"]*"/, `height="${size}"`);
789
+ } else {
790
+ processed = processed.replace("<svg", `<svg height="${size}"`);
791
+ }
792
+ if (color !== "currentColor") {
793
+ processed = processed.replace(/fill="currentColor"/g, `fill="${color}"`);
794
+ }
795
+ return processed;
796
+ }
797
+ /**
798
+ * Build CSS style string
799
+ */
800
+ buildStyle(options) {
801
+ const styles = [];
802
+ if (options.size) {
803
+ styles.push(`font-size: ${options.size}`);
804
+ }
805
+ if (options.color) {
806
+ styles.push(`color: ${options.color}`);
807
+ }
808
+ return styles.join("; ");
809
+ }
810
+ /**
811
+ * Build class name string
812
+ */
813
+ buildClassName(name, options) {
814
+ const classes = ["icon", `icon-${name}`];
815
+ if (options.class) {
816
+ classes.push(options.class);
817
+ }
818
+ return classes.join(" ");
819
+ }
820
+ };
821
+
822
+ // src/references/manager.ts
823
+ import { exec } from "child_process";
824
+ var ReferenceManagerError = class extends Error {
825
+ constructor(message, cause) {
826
+ super(message);
827
+ this.cause = cause;
828
+ this.name = "ReferenceManagerError";
829
+ }
830
+ };
831
+ var ReferenceManager = class {
832
+ constructor(command = "ref") {
833
+ this.command = command;
834
+ }
835
+ /**
836
+ * Check if reference-manager CLI is available
837
+ */
838
+ async isAvailable() {
839
+ try {
840
+ await this.execCommand(`${this.command} --version`);
841
+ return true;
842
+ } catch {
843
+ return false;
844
+ }
845
+ }
846
+ /**
847
+ * Get all references from the library
848
+ */
849
+ async getAll() {
850
+ const result = await this.execCommand(`${this.command} list --format json`);
851
+ return this.parseJSON(result);
852
+ }
853
+ /**
854
+ * Get a single reference by ID
855
+ */
856
+ async getById(id) {
857
+ const result = await this.execCommand(
858
+ `${this.command} list --id ${id} --format json`
859
+ );
860
+ const items = this.parseJSON(result);
861
+ return items[0] || null;
862
+ }
863
+ /**
864
+ * Get multiple references by IDs
865
+ */
866
+ async getByIds(ids) {
867
+ if (ids.length === 0) {
868
+ return /* @__PURE__ */ new Map();
869
+ }
870
+ const result = await this.execCommand(`${this.command} list --format json`);
871
+ const allItems = this.parseJSON(result);
872
+ const idSet = new Set(ids);
873
+ const map = /* @__PURE__ */ new Map();
874
+ for (const item of allItems) {
875
+ if (idSet.has(item.id)) {
876
+ map.set(item.id, item);
877
+ }
878
+ }
879
+ return map;
880
+ }
881
+ execCommand(cmd) {
882
+ return new Promise((resolve2, reject) => {
883
+ exec(cmd, (error, stdout) => {
884
+ if (error) {
885
+ reject(
886
+ new ReferenceManagerError(`Failed to execute: ${cmd}`, error)
887
+ );
888
+ return;
889
+ }
890
+ resolve2(stdout.toString());
891
+ });
892
+ });
893
+ }
894
+ parseJSON(data) {
895
+ try {
896
+ return JSON.parse(data);
897
+ } catch (error) {
898
+ throw new ReferenceManagerError(
899
+ "Failed to parse reference-manager output as JSON",
900
+ error
901
+ );
902
+ }
903
+ }
904
+ };
905
+
906
+ // src/references/extractor.ts
907
+ var SINGLE_CITATION_PATTERN = /@([\w-]+)(?:,\s*([^;\]]+))?/g;
908
+ var CITATION_BRACKET_PATTERN = /\[(@[\w-]+(?:,\s*[^;\]]+)?(?:;\s*@[\w-]+(?:,\s*[^;\]]+)?)*)\]/g;
909
+ var SOURCE_CITATION_PATTERN = /^@([\w-]+)$/;
910
+ var CitationExtractor = class {
911
+ /**
912
+ * Extract citations from a text string
913
+ */
914
+ extract(text) {
915
+ const citations = [];
916
+ let bracketMatch;
917
+ CITATION_BRACKET_PATTERN.lastIndex = 0;
918
+ while ((bracketMatch = CITATION_BRACKET_PATTERN.exec(text)) !== null) {
919
+ const bracketStart = bracketMatch.index;
920
+ const bracketContent = bracketMatch[1];
921
+ SINGLE_CITATION_PATTERN.lastIndex = 0;
922
+ let singleMatch;
923
+ while ((singleMatch = SINGLE_CITATION_PATTERN.exec(bracketContent)) !== null) {
924
+ const id = singleMatch[1];
925
+ const locator = singleMatch[2]?.trim();
926
+ citations.push({
927
+ id,
928
+ locator: locator ?? void 0,
929
+ position: {
930
+ start: bracketStart,
931
+ end: bracketStart + bracketMatch[0].length
932
+ }
933
+ });
934
+ }
935
+ }
936
+ return citations;
937
+ }
938
+ /**
939
+ * Extract citations from a slide's content
940
+ */
941
+ extractFromSlide(slide) {
942
+ const allCitations = [];
943
+ const seenIds = /* @__PURE__ */ new Set();
944
+ const extractFromValue = (value) => {
945
+ if (typeof value === "string") {
946
+ const sourceMatch = SOURCE_CITATION_PATTERN.exec(value);
947
+ if (sourceMatch && sourceMatch[1]) {
948
+ const id = sourceMatch[1];
949
+ if (!seenIds.has(id)) {
950
+ seenIds.add(id);
951
+ allCitations.push({
952
+ id,
953
+ locator: void 0,
954
+ position: { start: 0, end: value.length }
955
+ });
956
+ }
957
+ return;
958
+ }
959
+ const citations = this.extract(value);
960
+ for (const citation of citations) {
961
+ if (!seenIds.has(citation.id)) {
962
+ seenIds.add(citation.id);
963
+ allCitations.push(citation);
964
+ }
965
+ }
966
+ } else if (Array.isArray(value)) {
967
+ for (const item of value) {
968
+ extractFromValue(item);
969
+ }
970
+ } else if (value && typeof value === "object") {
971
+ for (const v of Object.values(value)) {
972
+ extractFromValue(v);
973
+ }
974
+ }
975
+ };
976
+ extractFromValue(slide.content);
977
+ if (slide.notes) {
978
+ const notesCitations = this.extract(slide.notes);
979
+ for (const citation of notesCitations) {
980
+ if (!seenIds.has(citation.id)) {
981
+ seenIds.add(citation.id);
982
+ allCitations.push(citation);
983
+ }
984
+ }
985
+ }
986
+ return allCitations;
987
+ }
988
+ /**
989
+ * Extract citations from all slides in a presentation
990
+ */
991
+ extractFromPresentation(presentation) {
992
+ const allCitations = [];
993
+ const seenIds = /* @__PURE__ */ new Set();
994
+ for (const slide of presentation.slides) {
995
+ const slideCitations = this.extractFromSlide(slide);
996
+ for (const citation of slideCitations) {
997
+ if (!seenIds.has(citation.id)) {
998
+ seenIds.add(citation.id);
999
+ allCitations.push(citation);
1000
+ }
1001
+ }
1002
+ }
1003
+ return allCitations;
1004
+ }
1005
+ /**
1006
+ * Get unique citation IDs in order of first appearance
1007
+ */
1008
+ getUniqueIds(citations) {
1009
+ const seen = /* @__PURE__ */ new Set();
1010
+ const uniqueIds = [];
1011
+ for (const citation of citations) {
1012
+ if (!seen.has(citation.id)) {
1013
+ seen.add(citation.id);
1014
+ uniqueIds.push(citation.id);
1015
+ }
1016
+ }
1017
+ return uniqueIds;
1018
+ }
1019
+ };
1020
+
1021
+ // src/references/formatter.ts
1022
+ var DEFAULT_CONFIG = {
1023
+ author: {
1024
+ maxAuthors: 2,
1025
+ etAl: "et al.",
1026
+ etAlJa: "\u307B\u304B",
1027
+ separatorJa: "\u30FB"
1028
+ },
1029
+ inline: {
1030
+ authorSep: ", ",
1031
+ identifierSep: "; ",
1032
+ multiSep: "), ("
1033
+ }
1034
+ };
1035
+ var JAPANESE_PATTERN = /[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]/;
1036
+ var CITATION_BRACKET_PATTERN2 = /\[(@[\w-]+(?:,\s*[^;\]]+)?(?:;\s*@[\w-]+(?:,\s*[^;\]]+)?)*)\]/g;
1037
+ var SINGLE_CITATION_PATTERN2 = /@([\w-]+)(?:,\s*([^;\]]+))?/g;
1038
+ var CitationFormatter = class {
1039
+ constructor(manager, config) {
1040
+ this.manager = manager;
1041
+ this.config = {
1042
+ author: { ...DEFAULT_CONFIG.author, ...config?.author },
1043
+ inline: { ...DEFAULT_CONFIG.inline, ...config?.inline }
1044
+ };
1045
+ }
1046
+ config;
1047
+ /**
1048
+ * Format an inline citation
1049
+ * e.g., "(Smith et al., 2024; PMID: 12345678)"
1050
+ */
1051
+ async formatInline(id) {
1052
+ const item = await this.manager.getById(id);
1053
+ if (!item) {
1054
+ return `[${id}]`;
1055
+ }
1056
+ return this.formatInlineItem(item);
1057
+ }
1058
+ /**
1059
+ * Format a full bibliography citation
1060
+ */
1061
+ async formatFull(id) {
1062
+ const item = await this.manager.getById(id);
1063
+ if (!item) {
1064
+ return `[${id}]`;
1065
+ }
1066
+ return this.formatFullItem(item);
1067
+ }
1068
+ /**
1069
+ * Expand all citations in text
1070
+ * e.g., "[@smith2024]" -> "(Smith et al., 2024; PMID: 12345678)"
1071
+ */
1072
+ async expandCitations(text) {
1073
+ const ids = /* @__PURE__ */ new Set();
1074
+ CITATION_BRACKET_PATTERN2.lastIndex = 0;
1075
+ let match;
1076
+ while ((match = CITATION_BRACKET_PATTERN2.exec(text)) !== null) {
1077
+ const content = match[1];
1078
+ SINGLE_CITATION_PATTERN2.lastIndex = 0;
1079
+ let singleMatch;
1080
+ while ((singleMatch = SINGLE_CITATION_PATTERN2.exec(content)) !== null) {
1081
+ ids.add(singleMatch[1]);
1082
+ }
1083
+ }
1084
+ const items = await this.manager.getByIds([...ids]);
1085
+ CITATION_BRACKET_PATTERN2.lastIndex = 0;
1086
+ let result = text;
1087
+ const matches = [];
1088
+ CITATION_BRACKET_PATTERN2.lastIndex = 0;
1089
+ while ((match = CITATION_BRACKET_PATTERN2.exec(text)) !== null) {
1090
+ const content = match[1];
1091
+ const replacements = [];
1092
+ SINGLE_CITATION_PATTERN2.lastIndex = 0;
1093
+ let singleMatch;
1094
+ while ((singleMatch = SINGLE_CITATION_PATTERN2.exec(content)) !== null) {
1095
+ const id = singleMatch[1];
1096
+ const item = items.get(id);
1097
+ if (item) {
1098
+ replacements.push(this.formatInlineItem(item));
1099
+ } else {
1100
+ replacements.push(`[${id}]`);
1101
+ }
1102
+ }
1103
+ matches.push({
1104
+ start: match.index,
1105
+ end: match.index + match[0].length,
1106
+ replacement: replacements.join(", ")
1107
+ });
1108
+ }
1109
+ for (const m of [...matches].reverse()) {
1110
+ result = result.slice(0, m.start) + m.replacement + result.slice(m.end);
1111
+ }
1112
+ return result;
1113
+ }
1114
+ /**
1115
+ * Generate bibliography entries
1116
+ */
1117
+ async generateBibliography(ids, sort = "citation-order") {
1118
+ if (ids.length === 0) {
1119
+ return [];
1120
+ }
1121
+ const items = await this.manager.getByIds(ids);
1122
+ let sortedItems;
1123
+ if (sort === "citation-order") {
1124
+ sortedItems = ids.map((id) => items.get(id)).filter((item) => item !== void 0);
1125
+ } else if (sort === "author") {
1126
+ sortedItems = [...items.values()].sort((a, b) => {
1127
+ const authorA = this.getFirstAuthorFamily(a);
1128
+ const authorB = this.getFirstAuthorFamily(b);
1129
+ return authorA.localeCompare(authorB);
1130
+ });
1131
+ } else {
1132
+ sortedItems = [...items.values()].sort((a, b) => {
1133
+ const yearA = this.getYear(a);
1134
+ const yearB = this.getYear(b);
1135
+ return yearA - yearB;
1136
+ });
1137
+ }
1138
+ return sortedItems.map((item) => this.formatFullItem(item));
1139
+ }
1140
+ formatInlineItem(item) {
1141
+ const author = this.formatAuthorInline(item.author);
1142
+ const year = this.getYear(item);
1143
+ const identifier = this.getIdentifier(item);
1144
+ if (identifier) {
1145
+ return `(${author}, ${year}; ${identifier})`;
1146
+ }
1147
+ return `(${author}, ${year})`;
1148
+ }
1149
+ formatFullItem(item) {
1150
+ const parts = [];
1151
+ const isJapanese = this.isJapaneseAuthors(item.author);
1152
+ const authors = this.formatAuthorsFull(item.author, isJapanese);
1153
+ parts.push(authors);
1154
+ const year = this.getYear(item);
1155
+ parts.push(`(${year}).`);
1156
+ if (item.title) {
1157
+ parts.push(`${item.title}.`);
1158
+ }
1159
+ if (item["container-title"]) {
1160
+ const journal = isJapanese ? item["container-title"] : `*${item["container-title"]}*`;
1161
+ let location = "";
1162
+ if (item.volume) {
1163
+ location = item.issue ? `${item.volume}(${item.issue})` : item.volume;
1164
+ }
1165
+ if (item.page) {
1166
+ location = location ? `${location}, ${item.page}` : item.page;
1167
+ }
1168
+ parts.push(location ? `${journal}, ${location}.` : `${journal}.`);
1169
+ }
1170
+ const identifier = this.getIdentifier(item);
1171
+ if (identifier) {
1172
+ parts.push(identifier);
1173
+ }
1174
+ return parts.join(" ");
1175
+ }
1176
+ formatAuthorInline(authors) {
1177
+ if (!authors || authors.length === 0) {
1178
+ return "Unknown";
1179
+ }
1180
+ const isJapanese = this.isJapaneseAuthors(authors);
1181
+ const { etAl, etAlJa, separatorJa } = this.config.author;
1182
+ const firstAuthor = authors[0];
1183
+ if (authors.length === 1) {
1184
+ return firstAuthor.family;
1185
+ }
1186
+ if (authors.length === 2) {
1187
+ const separator = isJapanese ? separatorJa : " & ";
1188
+ return `${firstAuthor.family}${separator}${authors[1].family}`;
1189
+ }
1190
+ const suffix = isJapanese ? etAlJa : ` ${etAl}`;
1191
+ return `${firstAuthor.family}${suffix}`;
1192
+ }
1193
+ formatAuthorsFull(authors, isJapanese) {
1194
+ if (!authors || authors.length === 0) {
1195
+ return "Unknown";
1196
+ }
1197
+ if (isJapanese) {
1198
+ return authors.map((a) => `${a.family}${a.given || ""}`).join(", ");
1199
+ }
1200
+ if (authors.length === 1) {
1201
+ const a = authors[0];
1202
+ const initial = a.given ? `${a.given.charAt(0)}.` : "";
1203
+ return `${a.family}, ${initial}`;
1204
+ }
1205
+ const formatted = authors.map((a, i) => {
1206
+ const initial = a.given ? `${a.given.charAt(0)}.` : "";
1207
+ if (i === authors.length - 1) {
1208
+ return `& ${a.family}, ${initial}`;
1209
+ }
1210
+ return `${a.family}, ${initial}`;
1211
+ });
1212
+ return formatted.join(", ").replace(", &", ", &");
1213
+ }
1214
+ isJapaneseAuthors(authors) {
1215
+ if (!authors || authors.length === 0) {
1216
+ return false;
1217
+ }
1218
+ return JAPANESE_PATTERN.test(authors[0].family);
1219
+ }
1220
+ getFirstAuthorFamily(item) {
1221
+ return item.author?.[0]?.family || "";
1222
+ }
1223
+ getYear(item) {
1224
+ const dateParts = item.issued?.["date-parts"];
1225
+ if (dateParts && dateParts[0] && dateParts[0][0]) {
1226
+ return dateParts[0][0];
1227
+ }
1228
+ return 0;
1229
+ }
1230
+ getIdentifier(item) {
1231
+ if (item.PMID) {
1232
+ return `PMID: ${item.PMID}`;
1233
+ }
1234
+ if (item.DOI) {
1235
+ return `DOI: ${item.DOI}`;
1236
+ }
1237
+ return null;
1238
+ }
1239
+ };
1240
+
1241
+ // src/core/pipeline.ts
1242
+ var PipelineError = class extends Error {
1243
+ constructor(message, stage, details) {
1244
+ super(message);
1245
+ this.stage = stage;
1246
+ this.details = details;
1247
+ this.name = "PipelineError";
1248
+ }
1249
+ };
1250
+ var Pipeline = class {
1251
+ constructor(config) {
1252
+ this.config = config;
1253
+ this.parser = new Parser();
1254
+ this.templateEngine = new TemplateEngine();
1255
+ this.templateLoader = new TemplateLoader();
1256
+ this.iconRegistry = new IconRegistryLoader();
1257
+ this.iconResolver = new IconResolver(this.iconRegistry);
1258
+ this.referenceManager = new ReferenceManager(
1259
+ config.references.connection.command
1260
+ );
1261
+ this.citationExtractor = new CitationExtractor();
1262
+ this.citationFormatter = new CitationFormatter(
1263
+ this.referenceManager,
1264
+ {
1265
+ author: {
1266
+ maxAuthors: config.references.format.maxAuthors,
1267
+ etAl: config.references.format.etAl,
1268
+ etAlJa: config.references.format.etAlJa
1269
+ },
1270
+ inline: {
1271
+ authorSep: config.references.format.authorSep,
1272
+ identifierSep: config.references.format.identifierSep
1273
+ }
1274
+ }
1275
+ );
1276
+ this.transformer = new Transformer(
1277
+ this.templateEngine,
1278
+ this.templateLoader,
1279
+ this.iconResolver,
1280
+ this.citationFormatter
1281
+ );
1282
+ this.renderer = new Renderer();
1283
+ }
1284
+ parser;
1285
+ templateEngine;
1286
+ templateLoader;
1287
+ iconRegistry;
1288
+ iconResolver;
1289
+ referenceManager;
1290
+ citationExtractor;
1291
+ citationFormatter;
1292
+ transformer;
1293
+ renderer;
1294
+ warnings = [];
1295
+ /**
1296
+ * Run the full conversion pipeline
1297
+ */
1298
+ async run(inputPath, options) {
1299
+ this.warnings = [];
1300
+ try {
1301
+ const presentation = await this.parseSource(inputPath);
1302
+ const citationIds = this.collectCitations(presentation);
1303
+ await this.resolveReferences(citationIds);
1304
+ const transformedSlides = await this.transformSlides(presentation);
1305
+ const output = this.render(transformedSlides, presentation);
1306
+ if (options?.outputPath) {
1307
+ await writeFile(options.outputPath, output, "utf-8");
1308
+ }
1309
+ return output;
1310
+ } catch (error) {
1311
+ if (error instanceof PipelineError) {
1312
+ throw error;
1313
+ }
1314
+ throw new PipelineError(
1315
+ error instanceof Error ? error.message : "Unknown error",
1316
+ "unknown",
1317
+ error
1318
+ );
1319
+ }
1320
+ }
1321
+ /**
1322
+ * Run the full pipeline with detailed result
1323
+ */
1324
+ async runWithResult(inputPath, options) {
1325
+ this.warnings = [];
1326
+ try {
1327
+ const presentation = await this.parseSource(inputPath);
1328
+ const citationIds = this.collectCitations(presentation);
1329
+ await this.resolveReferences(citationIds);
1330
+ const transformedSlides = await this.transformSlides(presentation);
1331
+ const output = this.render(transformedSlides, presentation);
1332
+ if (options?.outputPath) {
1333
+ await writeFile(options.outputPath, output, "utf-8");
1334
+ }
1335
+ return {
1336
+ output,
1337
+ citations: citationIds,
1338
+ warnings: this.warnings,
1339
+ slideCount: presentation.slides.length
1340
+ };
1341
+ } catch (error) {
1342
+ if (error instanceof PipelineError) {
1343
+ throw error;
1344
+ }
1345
+ throw new PipelineError(
1346
+ error instanceof Error ? error.message : "Unknown error",
1347
+ "unknown",
1348
+ error
1349
+ );
1350
+ }
1351
+ }
1352
+ /**
1353
+ * Initialize the pipeline by loading templates and icon registry
1354
+ */
1355
+ async initialize() {
1356
+ try {
1357
+ await this.templateLoader.loadBuiltIn(this.config.templates.builtin);
1358
+ if (this.config.templates.custom) {
1359
+ await this.templateLoader.loadCustom(this.config.templates.custom);
1360
+ }
1361
+ await this.iconRegistry.load(this.config.icons.registry);
1362
+ } catch (error) {
1363
+ throw new PipelineError(
1364
+ `Failed to initialize pipeline: ${error instanceof Error ? error.message : "Unknown error"}`,
1365
+ "initialize",
1366
+ error
1367
+ );
1368
+ }
1369
+ }
1370
+ /**
1371
+ * Get collected warnings
1372
+ */
1373
+ getWarnings() {
1374
+ return [...this.warnings];
1375
+ }
1376
+ // --- Private Stage Methods ---
1377
+ /**
1378
+ * Stage 1: Parse the YAML source file
1379
+ */
1380
+ async parseSource(inputPath) {
1381
+ try {
1382
+ return await this.parser.parseFile(inputPath);
1383
+ } catch (error) {
1384
+ throw new PipelineError(
1385
+ `Failed to parse source file: ${error instanceof Error ? error.message : "Unknown error"}`,
1386
+ "parse",
1387
+ error
1388
+ );
1389
+ }
1390
+ }
1391
+ /**
1392
+ * Stage 2: Collect all citation IDs from the presentation
1393
+ */
1394
+ collectCitations(presentation) {
1395
+ const citations = this.citationExtractor.extractFromPresentation(presentation);
1396
+ return this.citationExtractor.getUniqueIds(citations);
1397
+ }
1398
+ /**
1399
+ * Stage 3: Resolve references from the reference manager
1400
+ */
1401
+ async resolveReferences(ids) {
1402
+ if (!this.config.references.enabled || ids.length === 0) {
1403
+ return /* @__PURE__ */ new Map();
1404
+ }
1405
+ try {
1406
+ const items = await this.referenceManager.getByIds(ids);
1407
+ for (const id of ids) {
1408
+ if (!items.has(id)) {
1409
+ this.warnings.push(`Reference not found: ${id}`);
1410
+ }
1411
+ }
1412
+ return items;
1413
+ } catch (error) {
1414
+ this.warnings.push(
1415
+ `Failed to resolve references: ${error instanceof Error ? error.message : "Unknown error"}`
1416
+ );
1417
+ return /* @__PURE__ */ new Map();
1418
+ }
1419
+ }
1420
+ /**
1421
+ * Stage 4: Transform all slides using templates
1422
+ */
1423
+ async transformSlides(presentation) {
1424
+ try {
1425
+ return await this.transformer.transformAll(presentation);
1426
+ } catch (error) {
1427
+ throw new PipelineError(
1428
+ `Failed to transform slides: ${error instanceof Error ? error.message : "Unknown error"}`,
1429
+ "transform",
1430
+ error
1431
+ );
1432
+ }
1433
+ }
1434
+ /**
1435
+ * Stage 5: Render the final Marp markdown
1436
+ */
1437
+ render(slides, presentation) {
1438
+ try {
1439
+ const notes = presentation.slides.map((slide) => slide.notes);
1440
+ const renderOptions = {
1441
+ includeTheme: true,
1442
+ notes
1443
+ };
1444
+ return this.renderer.render(slides, presentation.meta, renderOptions);
1445
+ } catch (error) {
1446
+ throw new PipelineError(
1447
+ `Failed to render output: ${error instanceof Error ? error.message : "Unknown error"}`,
1448
+ "render",
1449
+ error
1450
+ );
1451
+ }
1452
+ }
1453
+ };
1454
+
1455
+ // src/index.ts
1456
+ var VERSION = "0.1.0";
1457
+
1458
+ // src/cli/commands/convert.ts
1459
+ import { Command } from "commander";
1460
+ import { access as access2 } from "fs/promises";
1461
+ import { basename, dirname, join as join4 } from "path";
1462
+ import chalk from "chalk";
1463
+ import ora from "ora";
1464
+
1465
+ // src/config/loader.ts
1466
+ import { access, readFile as readFile5 } from "fs/promises";
1467
+ import { join as join3 } from "path";
1468
+ import { parse as parseYaml3 } from "yaml";
1469
+
1470
+ // src/config/schema.ts
1471
+ import { z as z5 } from "zod";
1472
+ var configSchema = z5.object({
1473
+ templates: z5.object({
1474
+ builtin: z5.string().default("./templates"),
1475
+ custom: z5.string().optional()
1476
+ }).default({}),
1477
+ icons: z5.object({
1478
+ registry: z5.string().default("./icons/registry.yaml"),
1479
+ cache: z5.object({
1480
+ enabled: z5.boolean().default(true),
1481
+ directory: z5.string().default(".cache/icons"),
1482
+ ttl: z5.number().default(86400)
1483
+ }).default({})
1484
+ }).default({}),
1485
+ references: z5.object({
1486
+ enabled: z5.boolean().default(true),
1487
+ connection: z5.object({
1488
+ type: z5.literal("cli").default("cli"),
1489
+ command: z5.string().default("ref")
1490
+ }).default({}),
1491
+ format: z5.object({
1492
+ locale: z5.string().default("ja-JP"),
1493
+ authorSep: z5.string().default(", "),
1494
+ identifierSep: z5.string().default("; "),
1495
+ maxAuthors: z5.number().default(2),
1496
+ etAl: z5.string().default("et al."),
1497
+ etAlJa: z5.string().default("\u307B\u304B")
1498
+ }).default({})
1499
+ }).default({}),
1500
+ output: z5.object({
1501
+ theme: z5.string().default("default"),
1502
+ inlineStyles: z5.boolean().default(false)
1503
+ }).default({})
1504
+ });
1505
+
1506
+ // src/config/loader.ts
1507
+ var CONFIG_NAMES = ["config.yaml", "slide-gen.yaml"];
1508
+ var ConfigLoader = class {
1509
+ async load(configPath) {
1510
+ const fileConfig = await this.loadFile(configPath);
1511
+ return configSchema.parse(fileConfig);
1512
+ }
1513
+ async findConfig(directory) {
1514
+ for (const name of CONFIG_NAMES) {
1515
+ const path3 = join3(directory, name);
1516
+ try {
1517
+ await access(path3);
1518
+ return path3;
1519
+ } catch {
1520
+ }
1521
+ }
1522
+ return void 0;
1523
+ }
1524
+ async loadFile(configPath) {
1525
+ if (!configPath) return {};
1526
+ try {
1527
+ const content = await readFile5(configPath, "utf-8");
1528
+ return parseYaml3(content) ?? {};
1529
+ } catch (error) {
1530
+ if (error.code === "ENOENT") {
1531
+ return {};
1532
+ }
1533
+ throw error;
1534
+ }
1535
+ }
1536
+ };
1537
+
1538
+ // src/cli/commands/convert.ts
1539
+ var ExitCode = {
1540
+ Success: 0,
1541
+ GeneralError: 1,
1542
+ ArgumentError: 2,
1543
+ FileReadError: 3,
1544
+ ValidationError: 4,
1545
+ ConversionError: 5,
1546
+ ReferenceError: 6
1547
+ };
1548
+ function getDefaultOutputPath(inputPath) {
1549
+ const dir = dirname(inputPath);
1550
+ const base = basename(inputPath, ".yaml");
1551
+ return join4(dir, `${base}.md`);
1552
+ }
1553
+ function createConvertCommand() {
1554
+ return new Command("convert").description("Convert YAML source to Marp Markdown").argument("<input>", "Input YAML file").option("-o, --output <path>", "Output file path").option("-c, --config <path>", "Config file path").option("-t, --theme <name>", "Theme name").option("--no-references", "Disable reference processing").option("-v, --verbose", "Verbose output").action(async (input, options) => {
1555
+ await executeConvert(input, options);
1556
+ });
1557
+ }
1558
+ async function executeConvert(inputPath, options) {
1559
+ const spinner = options.verbose ? null : ora();
1560
+ const verbose = options.verbose ?? false;
1561
+ const updateSpinner = (text) => {
1562
+ if (spinner) {
1563
+ spinner.text = text;
1564
+ }
1565
+ };
1566
+ try {
1567
+ spinner?.start(`Reading ${inputPath}...`);
1568
+ try {
1569
+ await access2(inputPath);
1570
+ } catch {
1571
+ spinner?.fail(`File not found: ${inputPath}`);
1572
+ console.error(chalk.red(`Error: Input file not found: ${inputPath}`));
1573
+ process.exitCode = ExitCode.FileReadError;
1574
+ return;
1575
+ }
1576
+ const outputPath = options.output ?? getDefaultOutputPath(inputPath);
1577
+ updateSpinner("Loading configuration...");
1578
+ const configLoader = new ConfigLoader();
1579
+ let configPath = options.config;
1580
+ if (!configPath) {
1581
+ configPath = await configLoader.findConfig(dirname(inputPath));
1582
+ }
1583
+ const config = await configLoader.load(configPath);
1584
+ if (options.references === false) {
1585
+ config.references.enabled = false;
1586
+ }
1587
+ if (options.theme) {
1588
+ config.output.theme = options.theme;
1589
+ }
1590
+ updateSpinner("Initializing pipeline...");
1591
+ const pipeline = new Pipeline(config);
1592
+ try {
1593
+ await pipeline.initialize();
1594
+ } catch (error) {
1595
+ spinner?.fail("Failed to initialize pipeline");
1596
+ if (error instanceof PipelineError) {
1597
+ console.error(chalk.red(`Error: ${error.message}`));
1598
+ } else {
1599
+ console.error(
1600
+ chalk.red(
1601
+ `Error: ${error instanceof Error ? error.message : "Unknown error"}`
1602
+ )
1603
+ );
1604
+ }
1605
+ process.exitCode = ExitCode.ConversionError;
1606
+ return;
1607
+ }
1608
+ updateSpinner(`Converting ${inputPath}...`);
1609
+ const result = await pipeline.runWithResult(inputPath, { outputPath });
1610
+ spinner?.succeed(`Converted ${inputPath}`);
1611
+ if (verbose) {
1612
+ console.log("");
1613
+ console.log(chalk.green(" \u2713") + ` Parsed ${result.slideCount} slides`);
1614
+ if (result.citations.length > 0) {
1615
+ console.log(
1616
+ chalk.green(" \u2713") + ` Resolved ${result.citations.length} references`
1617
+ );
1618
+ }
1619
+ console.log(chalk.green(" \u2713") + " Generated output");
1620
+ }
1621
+ for (const warning of result.warnings) {
1622
+ console.log(chalk.yellow(" \u26A0") + ` ${warning}`);
1623
+ }
1624
+ console.log("");
1625
+ console.log(`Output: ${chalk.cyan(outputPath)}`);
1626
+ } catch (error) {
1627
+ spinner?.fail("Conversion failed");
1628
+ if (error instanceof PipelineError) {
1629
+ console.error(chalk.red(`
1630
+ Error (${error.stage}): ${error.message}`));
1631
+ switch (error.stage) {
1632
+ case "parse":
1633
+ process.exitCode = ExitCode.FileReadError;
1634
+ break;
1635
+ case "transform":
1636
+ process.exitCode = ExitCode.ValidationError;
1637
+ break;
1638
+ case "render":
1639
+ process.exitCode = ExitCode.ConversionError;
1640
+ break;
1641
+ default:
1642
+ process.exitCode = ExitCode.GeneralError;
1643
+ }
1644
+ } else {
1645
+ console.error(
1646
+ chalk.red(
1647
+ `
1648
+ Error: ${error instanceof Error ? error.message : "Unknown error"}`
1649
+ )
1650
+ );
1651
+ process.exitCode = ExitCode.GeneralError;
1652
+ }
1653
+ }
1654
+ }
1655
+
1656
+ // src/cli/commands/validate.ts
1657
+ import { Command as Command2 } from "commander";
1658
+ import { access as access3, readFile as readFile6 } from "fs/promises";
1659
+ import { dirname as dirname2 } from "path";
1660
+ import chalk2 from "chalk";
1661
+ import ora2 from "ora";
1662
+ import { parse as parseYaml4 } from "yaml";
1663
+ function createValidateCommand() {
1664
+ return new Command2("validate").description("Validate source file without conversion").argument("<input>", "Input YAML file").option("-c, --config <path>", "Config file path").option("--strict", "Treat warnings as errors").option("--format <fmt>", "Output format (text/json)", "text").action(async (input, options) => {
1665
+ await executeValidate(input, options);
1666
+ });
1667
+ }
1668
+ async function executeValidate(inputPath, options) {
1669
+ const isJsonFormat = options.format === "json";
1670
+ const spinner = isJsonFormat ? null : ora2();
1671
+ const result = {
1672
+ valid: true,
1673
+ errors: [],
1674
+ warnings: [],
1675
+ stats: {
1676
+ yamlSyntax: false,
1677
+ metaValid: false,
1678
+ slideCount: 0,
1679
+ templatesFound: false,
1680
+ iconsResolved: false,
1681
+ referencesCount: 0
1682
+ }
1683
+ };
1684
+ try {
1685
+ spinner?.start(`Validating ${inputPath}...`);
1686
+ try {
1687
+ await access3(inputPath);
1688
+ } catch {
1689
+ spinner?.fail(`File not found: ${inputPath}`);
1690
+ result.errors.push(`File not found: ${inputPath}`);
1691
+ result.valid = false;
1692
+ outputResult(result, options);
1693
+ process.exitCode = ExitCode.FileReadError;
1694
+ return;
1695
+ }
1696
+ const content = await readFile6(inputPath, "utf-8");
1697
+ try {
1698
+ parseYaml4(content);
1699
+ result.stats.yamlSyntax = true;
1700
+ } catch (error) {
1701
+ const message = error instanceof Error ? error.message : "Unknown error";
1702
+ result.errors.push(`YAML syntax error: ${message}`);
1703
+ result.valid = false;
1704
+ spinner?.fail("YAML syntax invalid");
1705
+ outputResult(result, options);
1706
+ process.exitCode = ExitCode.ValidationError;
1707
+ return;
1708
+ }
1709
+ const parser = new Parser();
1710
+ let presentation;
1711
+ try {
1712
+ presentation = parser.parse(content);
1713
+ result.stats.metaValid = true;
1714
+ result.stats.slideCount = presentation.slides.length;
1715
+ } catch (error) {
1716
+ if (error instanceof ParseError) {
1717
+ result.errors.push(`Parse error: ${error.message}`);
1718
+ } else if (error instanceof ValidationError) {
1719
+ result.errors.push(`Schema validation error: ${error.message}`);
1720
+ } else {
1721
+ const message = error instanceof Error ? error.message : "Unknown error";
1722
+ result.errors.push(`Schema error: ${message}`);
1723
+ }
1724
+ result.valid = false;
1725
+ spinner?.fail("Schema validation failed");
1726
+ outputResult(result, options);
1727
+ process.exitCode = ExitCode.ValidationError;
1728
+ return;
1729
+ }
1730
+ const configLoader = new ConfigLoader();
1731
+ let configPath = options.config;
1732
+ if (!configPath) {
1733
+ configPath = await configLoader.findConfig(dirname2(inputPath));
1734
+ }
1735
+ const config = await configLoader.load(configPath);
1736
+ const templateLoader = new TemplateLoader();
1737
+ try {
1738
+ await templateLoader.loadBuiltIn(config.templates.builtin);
1739
+ if (config.templates.custom) {
1740
+ try {
1741
+ await templateLoader.loadCustom(config.templates.custom);
1742
+ } catch {
1743
+ }
1744
+ }
1745
+ } catch (error) {
1746
+ result.warnings.push(
1747
+ `Could not load templates: ${error instanceof Error ? error.message : "Unknown error"}`
1748
+ );
1749
+ }
1750
+ const missingTemplates = [];
1751
+ for (let i = 0; i < presentation.slides.length; i++) {
1752
+ const slide = presentation.slides[i];
1753
+ const template = templateLoader.get(slide.template);
1754
+ if (!template) {
1755
+ missingTemplates.push(`Slide ${i + 1}: Template "${slide.template}" not found`);
1756
+ } else {
1757
+ const validationResult = templateLoader.validateContent(
1758
+ slide.template,
1759
+ slide.content
1760
+ );
1761
+ if (!validationResult.valid) {
1762
+ for (const err of validationResult.errors) {
1763
+ result.errors.push(`Slide ${i + 1} (${slide.template}): ${err}`);
1764
+ }
1765
+ }
1766
+ }
1767
+ }
1768
+ if (missingTemplates.length > 0) {
1769
+ for (const msg of missingTemplates) {
1770
+ result.errors.push(msg);
1771
+ }
1772
+ result.valid = false;
1773
+ } else {
1774
+ result.stats.templatesFound = true;
1775
+ }
1776
+ const iconRegistry = new IconRegistryLoader();
1777
+ try {
1778
+ await iconRegistry.load(config.icons.registry);
1779
+ result.stats.iconsResolved = true;
1780
+ } catch (error) {
1781
+ result.warnings.push(
1782
+ `Could not load icon registry: ${error instanceof Error ? error.message : "Unknown error"}`
1783
+ );
1784
+ }
1785
+ const iconPattern = /icon\(['"]([^'"]+)['"]\)/g;
1786
+ for (let i = 0; i < presentation.slides.length; i++) {
1787
+ const slide = presentation.slides[i];
1788
+ const contentStr = JSON.stringify(slide.content);
1789
+ let match;
1790
+ while ((match = iconPattern.exec(contentStr)) !== null) {
1791
+ const iconRef = match[1];
1792
+ const resolved = iconRegistry.resolveAlias(iconRef);
1793
+ const parsed = iconRegistry.parseIconReference(resolved);
1794
+ if (parsed) {
1795
+ const source = iconRegistry.getSource(parsed.prefix);
1796
+ if (!source) {
1797
+ result.warnings.push(
1798
+ `Slide ${i + 1}: Unknown icon source "${parsed.prefix}" in "${iconRef}"`
1799
+ );
1800
+ }
1801
+ } else if (!iconRegistry.resolveAlias(iconRef)) {
1802
+ result.warnings.push(`Slide ${i + 1}: Unknown icon "${iconRef}"`);
1803
+ }
1804
+ }
1805
+ }
1806
+ const citationPattern = /@([a-zA-Z0-9_-]+)/g;
1807
+ const references = /* @__PURE__ */ new Set();
1808
+ for (const slide of presentation.slides) {
1809
+ const contentStr = JSON.stringify(slide.content);
1810
+ let match;
1811
+ while ((match = citationPattern.exec(contentStr)) !== null) {
1812
+ references.add(match[1]);
1813
+ }
1814
+ }
1815
+ result.stats.referencesCount = references.size;
1816
+ if (result.errors.length > 0) {
1817
+ result.valid = false;
1818
+ }
1819
+ if (options.strict && result.warnings.length > 0) {
1820
+ result.valid = false;
1821
+ result.errors.push(...result.warnings);
1822
+ result.warnings = [];
1823
+ }
1824
+ if (result.valid) {
1825
+ spinner?.succeed(`Validated ${inputPath}`);
1826
+ } else {
1827
+ spinner?.fail(`Validation failed for ${inputPath}`);
1828
+ }
1829
+ outputResult(result, options);
1830
+ if (!result.valid) {
1831
+ process.exitCode = ExitCode.ValidationError;
1832
+ }
1833
+ } catch (error) {
1834
+ spinner?.fail("Validation failed");
1835
+ result.errors.push(
1836
+ `Unexpected error: ${error instanceof Error ? error.message : "Unknown error"}`
1837
+ );
1838
+ result.valid = false;
1839
+ outputResult(result, options);
1840
+ process.exitCode = ExitCode.GeneralError;
1841
+ }
1842
+ }
1843
+ function outputResult(result, options) {
1844
+ if (options.format === "json") {
1845
+ console.log(
1846
+ JSON.stringify(
1847
+ {
1848
+ valid: result.valid,
1849
+ errors: result.errors,
1850
+ warnings: result.warnings,
1851
+ stats: result.stats
1852
+ },
1853
+ null,
1854
+ 2
1855
+ )
1856
+ );
1857
+ return;
1858
+ }
1859
+ console.log("");
1860
+ if (result.stats.yamlSyntax) {
1861
+ console.log(chalk2.green("\u2713") + " YAML syntax valid");
1862
+ } else {
1863
+ console.log(chalk2.red("\u2717") + " YAML syntax invalid");
1864
+ }
1865
+ if (result.stats.metaValid) {
1866
+ console.log(chalk2.green("\u2713") + " Meta section valid");
1867
+ }
1868
+ if (result.stats.slideCount > 0) {
1869
+ console.log(chalk2.green("\u2713") + ` ${result.stats.slideCount} slides validated`);
1870
+ }
1871
+ if (result.stats.templatesFound) {
1872
+ console.log(chalk2.green("\u2713") + " All templates found");
1873
+ }
1874
+ if (result.stats.iconsResolved) {
1875
+ console.log(chalk2.green("\u2713") + " All icons resolved");
1876
+ }
1877
+ if (result.stats.referencesCount > 0) {
1878
+ console.log(chalk2.green("\u2713") + ` ${result.stats.referencesCount} references found`);
1879
+ }
1880
+ for (const error of result.errors) {
1881
+ console.log(chalk2.red("\u2717") + ` ${error}`);
1882
+ }
1883
+ for (const warning of result.warnings) {
1884
+ console.log(chalk2.yellow("\u26A0") + ` ${warning}`);
1885
+ }
1886
+ console.log("");
1887
+ if (result.valid) {
1888
+ console.log(chalk2.green("Validation passed!"));
1889
+ } else {
1890
+ const errorCount = result.errors.length;
1891
+ const warningCount = result.warnings.length;
1892
+ let summary = `Validation failed with ${errorCount} error${errorCount !== 1 ? "s" : ""}`;
1893
+ if (warningCount > 0) {
1894
+ summary += ` and ${warningCount} warning${warningCount !== 1 ? "s" : ""}`;
1895
+ }
1896
+ console.log(chalk2.red(summary));
1897
+ }
1898
+ }
1899
+
1900
+ // src/cli/commands/templates.ts
1901
+ import { Command as Command3 } from "commander";
1902
+ import chalk3 from "chalk";
1903
+ import * as yaml2 from "yaml";
1904
+ function formatTableList(templates) {
1905
+ const byCategory = /* @__PURE__ */ new Map();
1906
+ for (const template of templates) {
1907
+ const cat = template.category;
1908
+ if (!byCategory.has(cat)) {
1909
+ byCategory.set(cat, []);
1910
+ }
1911
+ byCategory.get(cat).push(template);
1912
+ }
1913
+ const lines = ["Templates:", ""];
1914
+ const sortedCategories = Array.from(byCategory.keys()).sort();
1915
+ for (const category of sortedCategories) {
1916
+ lines.push(`${category}/`);
1917
+ const categoryTemplates = byCategory.get(category);
1918
+ categoryTemplates.sort((a, b) => a.name.localeCompare(b.name));
1919
+ for (const template of categoryTemplates) {
1920
+ const paddedName = template.name.padEnd(16);
1921
+ lines.push(` ${paddedName}${template.description}`);
1922
+ }
1923
+ lines.push("");
1924
+ }
1925
+ return lines.join("\n");
1926
+ }
1927
+ function formatJsonList(templates) {
1928
+ const output = templates.map((t) => ({
1929
+ name: t.name,
1930
+ description: t.description,
1931
+ category: t.category
1932
+ }));
1933
+ return JSON.stringify(output, null, 2);
1934
+ }
1935
+ function formatLlmList(templates) {
1936
+ const byCategory = /* @__PURE__ */ new Map();
1937
+ for (const template of templates) {
1938
+ const cat = template.category;
1939
+ if (!byCategory.has(cat)) {
1940
+ byCategory.set(cat, []);
1941
+ }
1942
+ byCategory.get(cat).push(template);
1943
+ }
1944
+ const lines = [];
1945
+ const sortedCategories = Array.from(byCategory.keys()).sort();
1946
+ for (const category of sortedCategories) {
1947
+ lines.push(`[${category}]`);
1948
+ const categoryTemplates = byCategory.get(category);
1949
+ categoryTemplates.sort((a, b) => a.name.localeCompare(b.name));
1950
+ for (const template of categoryTemplates) {
1951
+ lines.push(`${template.name}: ${template.description}`);
1952
+ }
1953
+ lines.push("");
1954
+ }
1955
+ return lines.join("\n").trim();
1956
+ }
1957
+ function formatTemplateList(templates, format) {
1958
+ switch (format) {
1959
+ case "json":
1960
+ return formatJsonList(templates);
1961
+ case "llm":
1962
+ return formatLlmList(templates);
1963
+ case "table":
1964
+ default:
1965
+ return formatTableList(templates);
1966
+ }
1967
+ }
1968
+ function formatSchemaProperty(name, prop, required, indent) {
1969
+ const lines = [];
1970
+ const type = prop["type"] ?? "unknown";
1971
+ const requiredStr = required ? ", required" : "";
1972
+ const description = prop["description"];
1973
+ lines.push(`${indent}${name} (${type}${requiredStr})`);
1974
+ if (description) {
1975
+ lines.push(`${indent} ${description}`);
1976
+ }
1977
+ if (type === "object" && prop["properties"]) {
1978
+ const nestedRequired = prop["required"] ?? [];
1979
+ const properties = prop["properties"];
1980
+ for (const [propName, propDef] of Object.entries(properties)) {
1981
+ lines.push(
1982
+ ...formatSchemaProperty(propName, propDef, nestedRequired.includes(propName), indent + " ")
1983
+ );
1984
+ }
1985
+ }
1986
+ if (type === "array" && prop["items"]) {
1987
+ const items = prop["items"];
1988
+ if (items["type"] === "object" && items["properties"]) {
1989
+ const itemRequired = items["required"] ?? [];
1990
+ const itemProps = items["properties"];
1991
+ lines.push(`${indent} Items:`);
1992
+ for (const [propName, propDef] of Object.entries(itemProps)) {
1993
+ lines.push(
1994
+ ...formatSchemaProperty(propName, propDef, itemRequired.includes(propName), indent + " ")
1995
+ );
1996
+ }
1997
+ }
1998
+ }
1999
+ return lines;
2000
+ }
2001
+ function formatTextInfo(template) {
2002
+ const lines = [
2003
+ `Template: ${template.name}`,
2004
+ `Description: ${template.description}`,
2005
+ `Category: ${template.category}`,
2006
+ "",
2007
+ "Schema:"
2008
+ ];
2009
+ const schema = template.schema;
2010
+ if (schema.properties) {
2011
+ const required = schema.required ?? [];
2012
+ for (const [name, prop] of Object.entries(schema.properties)) {
2013
+ lines.push(...formatSchemaProperty(name, prop, required.includes(name), " "));
2014
+ }
2015
+ }
2016
+ if (template.example) {
2017
+ lines.push("", "Example:");
2018
+ const exampleYaml = yaml2.stringify(template.example, { indent: 2 });
2019
+ lines.push(
2020
+ ...exampleYaml.split("\n").filter((l) => l.trim()).map((l) => ` ${l}`)
2021
+ );
2022
+ }
2023
+ return lines.join("\n");
2024
+ }
2025
+ function formatJsonInfo(template) {
2026
+ return JSON.stringify(
2027
+ {
2028
+ name: template.name,
2029
+ description: template.description,
2030
+ category: template.category,
2031
+ schema: template.schema,
2032
+ example: template.example
2033
+ },
2034
+ null,
2035
+ 2
2036
+ );
2037
+ }
2038
+ function formatLlmInfo(template) {
2039
+ const lines = [
2040
+ `Template: ${template.name}`,
2041
+ `Description: ${template.description}`,
2042
+ `Category: ${template.category}`,
2043
+ "",
2044
+ "Schema:"
2045
+ ];
2046
+ const schema = template.schema;
2047
+ if (schema.properties) {
2048
+ const required = schema.required ?? [];
2049
+ for (const [name, prop] of Object.entries(schema.properties)) {
2050
+ const type = prop["type"] ?? "unknown";
2051
+ const reqStr = required.includes(name) ? ", required" : "";
2052
+ lines.push(` ${name} (${type}${reqStr})`);
2053
+ const description = prop["description"];
2054
+ if (description) {
2055
+ lines.push(` ${description}`);
2056
+ }
2057
+ }
2058
+ }
2059
+ if (template.example) {
2060
+ lines.push("", "Example:");
2061
+ const exampleYaml = yaml2.stringify(
2062
+ { template: template.name, content: template.example },
2063
+ { indent: 2 }
2064
+ );
2065
+ lines.push(
2066
+ ...exampleYaml.split("\n").filter((l) => l.trim()).map((l) => ` ${l}`)
2067
+ );
2068
+ }
2069
+ return lines.join("\n");
2070
+ }
2071
+ function formatTemplateInfo(template, format) {
2072
+ switch (format) {
2073
+ case "json":
2074
+ return formatJsonInfo(template);
2075
+ case "llm":
2076
+ return formatLlmInfo(template);
2077
+ case "text":
2078
+ default:
2079
+ return formatTextInfo(template);
2080
+ }
2081
+ }
2082
+ function formatTemplateExample(template) {
2083
+ const slideYaml = yaml2.stringify(
2084
+ [{ template: template.name, content: template.example ?? {} }],
2085
+ { indent: 2 }
2086
+ );
2087
+ return slideYaml;
2088
+ }
2089
+ async function loadTemplates(configPath) {
2090
+ const configLoader = new ConfigLoader();
2091
+ if (!configPath) {
2092
+ configPath = await configLoader.findConfig(process.cwd());
2093
+ }
2094
+ const config = await configLoader.load(configPath);
2095
+ const templateLoader = new TemplateLoader();
2096
+ try {
2097
+ await templateLoader.loadBuiltIn(config.templates.builtin);
2098
+ } catch {
2099
+ }
2100
+ if (config.templates.custom) {
2101
+ try {
2102
+ await templateLoader.loadCustom(config.templates.custom);
2103
+ } catch {
2104
+ }
2105
+ }
2106
+ return templateLoader;
2107
+ }
2108
+ function createListCommand() {
2109
+ return new Command3("list").description("List available templates").option("--category <cat>", "Filter by category").option("--format <fmt>", "Output format (table/json/llm)", "table").option("-c, --config <path>", "Config file path").action(async (options) => {
2110
+ try {
2111
+ const templateLoader = await loadTemplates(options.config);
2112
+ let templates = templateLoader.list();
2113
+ if (options.category) {
2114
+ templates = templateLoader.listByCategory(options.category);
2115
+ }
2116
+ if (templates.length === 0) {
2117
+ if (options.format === "json") {
2118
+ console.log("[]");
2119
+ } else {
2120
+ console.log("No templates found.");
2121
+ }
2122
+ return;
2123
+ }
2124
+ const format = options.format ?? "table";
2125
+ const output = formatTemplateList(templates, format);
2126
+ console.log(output);
2127
+ } catch (error) {
2128
+ console.error(
2129
+ chalk3.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`)
2130
+ );
2131
+ process.exitCode = ExitCode.GeneralError;
2132
+ }
2133
+ });
2134
+ }
2135
+ function createInfoCommand() {
2136
+ return new Command3("info").description("Show template details").argument("<name>", "Template name").option("--format <fmt>", "Output format (text/json/llm)", "text").option("-c, --config <path>", "Config file path").action(async (name, options) => {
2137
+ try {
2138
+ const templateLoader = await loadTemplates(options.config);
2139
+ const template = templateLoader.get(name);
2140
+ if (!template) {
2141
+ console.error(chalk3.red(`Error: Template "${name}" not found`));
2142
+ process.exitCode = ExitCode.GeneralError;
2143
+ return;
2144
+ }
2145
+ const format = options.format ?? "text";
2146
+ const output = formatTemplateInfo(template, format);
2147
+ console.log(output);
2148
+ } catch (error) {
2149
+ console.error(
2150
+ chalk3.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`)
2151
+ );
2152
+ process.exitCode = ExitCode.GeneralError;
2153
+ }
2154
+ });
2155
+ }
2156
+ function createExampleCommand() {
2157
+ return new Command3("example").description("Output example YAML for a template").argument("<name>", "Template name").option("-c, --config <path>", "Config file path").action(async (name, options) => {
2158
+ try {
2159
+ const templateLoader = await loadTemplates(options.config);
2160
+ const template = templateLoader.get(name);
2161
+ if (!template) {
2162
+ console.error(chalk3.red(`Error: Template "${name}" not found`));
2163
+ process.exitCode = ExitCode.GeneralError;
2164
+ return;
2165
+ }
2166
+ const output = formatTemplateExample(template);
2167
+ console.log(output);
2168
+ } catch (error) {
2169
+ console.error(
2170
+ chalk3.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`)
2171
+ );
2172
+ process.exitCode = ExitCode.GeneralError;
2173
+ }
2174
+ });
2175
+ }
2176
+ function createTemplatesCommand() {
2177
+ const cmd = new Command3("templates").description("Manage and list templates");
2178
+ cmd.addCommand(createListCommand());
2179
+ cmd.addCommand(createInfoCommand());
2180
+ cmd.addCommand(createExampleCommand());
2181
+ return cmd;
2182
+ }
2183
+
2184
+ // src/cli/commands/icons.ts
2185
+ import { Command as Command4 } from "commander";
2186
+ import chalk4 from "chalk";
2187
+ function formatTableSourceList(sources) {
2188
+ const lines = ["Icon Sources:", ""];
2189
+ const maxNameLen = Math.max(...sources.map((s) => s.name.length), 4);
2190
+ const maxPrefixLen = Math.max(...sources.map((s) => s.prefix.length), 6);
2191
+ const maxTypeLen = Math.max(...sources.map((s) => s.type.length), 4);
2192
+ const namePad = "Name".padEnd(maxNameLen);
2193
+ const prefixPad = "Prefix".padEnd(maxPrefixLen);
2194
+ const typePad = "Type".padEnd(maxTypeLen);
2195
+ lines.push(` ${namePad} ${prefixPad} ${typePad}`);
2196
+ lines.push(` ${"\u2500".repeat(maxNameLen)} ${"\u2500".repeat(maxPrefixLen)} ${"\u2500".repeat(maxTypeLen)}`);
2197
+ for (const source of sources) {
2198
+ const name = source.name.padEnd(maxNameLen);
2199
+ const prefix = source.prefix.padEnd(maxPrefixLen);
2200
+ const type = source.type.padEnd(maxTypeLen);
2201
+ lines.push(` ${name} ${prefix} ${type}`);
2202
+ }
2203
+ return lines.join("\n");
2204
+ }
2205
+ function formatJsonSourceList(sources) {
2206
+ const output = sources.map((s) => ({
2207
+ name: s.name,
2208
+ type: s.type,
2209
+ prefix: s.prefix,
2210
+ ...s.url ? { url: s.url } : {},
2211
+ ...s.path ? { path: s.path } : {}
2212
+ }));
2213
+ return JSON.stringify(output, null, 2);
2214
+ }
2215
+ function formatIconSourceList(sources, format) {
2216
+ switch (format) {
2217
+ case "json":
2218
+ return formatJsonSourceList(sources);
2219
+ case "table":
2220
+ default:
2221
+ return formatTableSourceList(sources);
2222
+ }
2223
+ }
2224
+ function formatTableAliases(aliases) {
2225
+ const entries = Object.entries(aliases);
2226
+ if (entries.length === 0) {
2227
+ return "No aliases defined.";
2228
+ }
2229
+ const lines = ["Icon Aliases:", ""];
2230
+ const maxAliasLen = Math.max(...entries.map(([a]) => a.length), 5);
2231
+ const maxTargetLen = Math.max(...entries.map(([, t]) => t.length), 6);
2232
+ const aliasPad = "Alias".padEnd(maxAliasLen);
2233
+ const targetPad = "Target".padEnd(maxTargetLen);
2234
+ lines.push(` ${aliasPad} ${targetPad}`);
2235
+ lines.push(` ${"\u2500".repeat(maxAliasLen)} ${"\u2500".repeat(maxTargetLen)}`);
2236
+ entries.sort((a, b) => a[0].localeCompare(b[0]));
2237
+ for (const [alias, target] of entries) {
2238
+ const aliasStr = alias.padEnd(maxAliasLen);
2239
+ lines.push(` ${aliasStr} ${target}`);
2240
+ }
2241
+ return lines.join("\n");
2242
+ }
2243
+ function formatJsonAliases(aliases) {
2244
+ return JSON.stringify(aliases, null, 2);
2245
+ }
2246
+ function formatAliasesList(aliases, format) {
2247
+ switch (format) {
2248
+ case "json":
2249
+ return formatJsonAliases(aliases);
2250
+ case "table":
2251
+ default:
2252
+ return formatTableAliases(aliases);
2253
+ }
2254
+ }
2255
+ function formatTableSearchResults(results) {
2256
+ const lines = [`Search results for "${results.query}"`, ""];
2257
+ const hasResults = results.aliases.length > 0 || results.sources.length > 0;
2258
+ if (!hasResults) {
2259
+ lines.push("No results found.");
2260
+ return lines.join("\n");
2261
+ }
2262
+ if (results.aliases.length > 0) {
2263
+ lines.push("Aliases:");
2264
+ for (const { alias, target } of results.aliases) {
2265
+ lines.push(` ${alias} \u2192 ${target}`);
2266
+ }
2267
+ lines.push("");
2268
+ }
2269
+ if (results.sources.length > 0) {
2270
+ lines.push("Sources:");
2271
+ for (const source of results.sources) {
2272
+ lines.push(` ${source.name} (${source.prefix}:) [${source.type}]`);
2273
+ }
2274
+ }
2275
+ return lines.join("\n").trim();
2276
+ }
2277
+ function formatJsonSearchResults(results) {
2278
+ return JSON.stringify(results, null, 2);
2279
+ }
2280
+ function formatSearchResults(results, format) {
2281
+ switch (format) {
2282
+ case "json":
2283
+ return formatJsonSearchResults(results);
2284
+ case "table":
2285
+ default:
2286
+ return formatTableSearchResults(results);
2287
+ }
2288
+ }
2289
+ async function loadRegistry(configPath) {
2290
+ const configLoader = new ConfigLoader();
2291
+ if (!configPath) {
2292
+ configPath = await configLoader.findConfig(process.cwd());
2293
+ }
2294
+ const config = await configLoader.load(configPath);
2295
+ const registry = new IconRegistryLoader();
2296
+ if (config.icons?.registry) {
2297
+ await registry.load(config.icons.registry);
2298
+ }
2299
+ return registry;
2300
+ }
2301
+ function searchIcons(registry, query) {
2302
+ const lowerQuery = query.toLowerCase();
2303
+ const aliases = Object.entries(registry.getAliases()).filter(
2304
+ ([alias, target]) => alias.toLowerCase().includes(lowerQuery) || target.toLowerCase().includes(lowerQuery)
2305
+ ).map(([alias, target]) => ({ alias, target }));
2306
+ const sources = registry.getSources().filter(
2307
+ (source) => source.name.toLowerCase().includes(lowerQuery) || source.prefix.toLowerCase().includes(lowerQuery)
2308
+ ).map((s) => ({ name: s.name, prefix: s.prefix, type: s.type }));
2309
+ return { query, aliases, sources };
2310
+ }
2311
+ function createListCommand2() {
2312
+ return new Command4("list").description("List icon sources and aliases").option("--source <name>", "Filter by source name").option("--aliases", "Show only aliases").option("--format <fmt>", "Output format (table/json)", "table").option("-c, --config <path>", "Config file path").action(async (options) => {
2313
+ try {
2314
+ const registry = await loadRegistry(options.config);
2315
+ if (!registry.isLoaded()) {
2316
+ console.log("No icon registry found.");
2317
+ return;
2318
+ }
2319
+ const format = options.format ?? "table";
2320
+ if (options.aliases) {
2321
+ const aliases = registry.getAliases();
2322
+ const output2 = formatAliasesList(aliases, format);
2323
+ console.log(output2);
2324
+ return;
2325
+ }
2326
+ if (options.source) {
2327
+ const sources2 = registry.getSources().filter(
2328
+ (s) => s.name === options.source || s.prefix === options.source
2329
+ );
2330
+ if (sources2.length === 0) {
2331
+ console.error(chalk4.yellow(`No source found matching "${options.source}"`));
2332
+ return;
2333
+ }
2334
+ const output2 = formatIconSourceList(sources2, format);
2335
+ console.log(output2);
2336
+ return;
2337
+ }
2338
+ const sources = registry.getSources();
2339
+ const output = formatIconSourceList(sources, format);
2340
+ console.log(output);
2341
+ } catch (error) {
2342
+ console.error(
2343
+ chalk4.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`)
2344
+ );
2345
+ process.exitCode = ExitCode.GeneralError;
2346
+ }
2347
+ });
2348
+ }
2349
+ function createSearchCommand() {
2350
+ return new Command4("search").description("Search for icons by name or keyword").argument("<query>", "Search query").option("--format <fmt>", "Output format (table/json)", "table").option("-c, --config <path>", "Config file path").action(async (query, options) => {
2351
+ try {
2352
+ const registry = await loadRegistry(options.config);
2353
+ if (!registry.isLoaded()) {
2354
+ console.log("No icon registry found.");
2355
+ return;
2356
+ }
2357
+ const format = options.format ?? "table";
2358
+ const results = searchIcons(registry, query);
2359
+ const output = formatSearchResults(results, format);
2360
+ console.log(output);
2361
+ } catch (error) {
2362
+ console.error(
2363
+ chalk4.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`)
2364
+ );
2365
+ process.exitCode = ExitCode.GeneralError;
2366
+ }
2367
+ });
2368
+ }
2369
+ function createPreviewCommand() {
2370
+ return new Command4("preview").description("Preview an icon").argument("<name>", "Icon name or alias").option("--format <fmt>", "Output format (svg/html)", "html").option("--size <size>", "Icon size", "24px").option("--color <color>", "Icon color", "currentColor").option("-c, --config <path>", "Config file path").action(async (name, options) => {
2371
+ try {
2372
+ const registry = await loadRegistry(options.config);
2373
+ if (!registry.isLoaded()) {
2374
+ console.error(chalk4.red("Error: No icon registry found"));
2375
+ process.exitCode = ExitCode.GeneralError;
2376
+ return;
2377
+ }
2378
+ const resolver = new IconResolver(registry);
2379
+ const iconOptions = {};
2380
+ if (options.size) {
2381
+ iconOptions.size = options.size;
2382
+ }
2383
+ if (options.color) {
2384
+ iconOptions.color = options.color;
2385
+ }
2386
+ const rendered = await resolver.render(name, iconOptions);
2387
+ if (options.format === "svg") {
2388
+ console.log(rendered);
2389
+ } else {
2390
+ const html = `<!DOCTYPE html>
2391
+ <html>
2392
+ <head>
2393
+ <meta charset="utf-8">
2394
+ <title>Icon Preview: ${name}</title>
2395
+ <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
2396
+ <style>
2397
+ body {
2398
+ display: flex;
2399
+ align-items: center;
2400
+ justify-content: center;
2401
+ min-height: 100vh;
2402
+ margin: 0;
2403
+ font-family: system-ui, sans-serif;
2404
+ background: #f5f5f5;
2405
+ }
2406
+ .preview {
2407
+ padding: 2rem;
2408
+ background: white;
2409
+ border-radius: 8px;
2410
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
2411
+ text-align: center;
2412
+ }
2413
+ .icon-container {
2414
+ margin-bottom: 1rem;
2415
+ }
2416
+ .icon-name {
2417
+ color: #666;
2418
+ font-size: 14px;
2419
+ }
2420
+ .material-icons {
2421
+ font-family: 'Material Icons';
2422
+ font-weight: normal;
2423
+ font-style: normal;
2424
+ display: inline-block;
2425
+ line-height: 1;
2426
+ text-transform: none;
2427
+ letter-spacing: normal;
2428
+ word-wrap: normal;
2429
+ white-space: nowrap;
2430
+ direction: ltr;
2431
+ -webkit-font-smoothing: antialiased;
2432
+ }
2433
+ </style>
2434
+ </head>
2435
+ <body>
2436
+ <div class="preview">
2437
+ <div class="icon-container">
2438
+ ${rendered}
2439
+ </div>
2440
+ <div class="icon-name">${name}</div>
2441
+ </div>
2442
+ </body>
2443
+ </html>`;
2444
+ console.log(html);
2445
+ }
2446
+ } catch (error) {
2447
+ console.error(
2448
+ chalk4.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`)
2449
+ );
2450
+ process.exitCode = ExitCode.GeneralError;
2451
+ }
2452
+ });
2453
+ }
2454
+ function createIconsCommand() {
2455
+ const cmd = new Command4("icons").description("Manage and search icons");
2456
+ cmd.addCommand(createListCommand2());
2457
+ cmd.addCommand(createSearchCommand());
2458
+ cmd.addCommand(createPreviewCommand());
2459
+ return cmd;
2460
+ }
2461
+
2462
+ // src/cli/commands/init.ts
2463
+ import { Command as Command5 } from "commander";
2464
+ import { mkdir, writeFile as writeFile2, access as access4, readdir as readdir2 } from "fs/promises";
2465
+ import { join as join5, resolve } from "path";
2466
+ import chalk5 from "chalk";
2467
+ import ora3 from "ora";
2468
+ function createInitCommand() {
2469
+ return new Command5("init").description("Initialize a new project").argument("[directory]", "Target directory", ".").option("--template <name>", "Initial template").option("--no-examples", "Do not create sample files").action(async (directory, options) => {
2470
+ await executeInit(directory, options);
2471
+ });
2472
+ }
2473
+ async function executeInit(directory, options) {
2474
+ const spinner = ora3();
2475
+ const targetDir = resolve(directory);
2476
+ const includeExamples = options.examples !== false;
2477
+ try {
2478
+ spinner.start(`Initializing project in ${targetDir}...`);
2479
+ try {
2480
+ await access4(targetDir);
2481
+ const entries = await readdir2(targetDir);
2482
+ if (entries.length > 0) {
2483
+ spinner.info(`Directory ${targetDir} already exists, adding files...`);
2484
+ }
2485
+ } catch {
2486
+ }
2487
+ await mkdir(targetDir, { recursive: true });
2488
+ await mkdir(join5(targetDir, "themes"), { recursive: true });
2489
+ await mkdir(join5(targetDir, "icons", "custom"), { recursive: true });
2490
+ const configContent = generateConfigContent();
2491
+ await writeFileIfNotExists(join5(targetDir, "config.yaml"), configContent);
2492
+ const customCssContent = generateCustomCssContent();
2493
+ await writeFileIfNotExists(join5(targetDir, "themes", "custom.css"), customCssContent);
2494
+ if (includeExamples) {
2495
+ const presentationContent = generatePresentationContent(options.template);
2496
+ await writeFileIfNotExists(join5(targetDir, "presentation.yaml"), presentationContent);
2497
+ }
2498
+ spinner.succeed(`Project initialized in ${targetDir}`);
2499
+ console.log("");
2500
+ console.log(chalk5.green("Created files:"));
2501
+ console.log(` ${chalk5.cyan("config.yaml")} - Project configuration`);
2502
+ console.log(` ${chalk5.cyan("themes/custom.css")} - Custom theme styles`);
2503
+ console.log(` ${chalk5.cyan("icons/custom/")} - Custom icons directory`);
2504
+ if (includeExamples) {
2505
+ console.log(` ${chalk5.cyan("presentation.yaml")} - Sample presentation`);
2506
+ }
2507
+ console.log("");
2508
+ console.log(chalk5.blue("Next steps:"));
2509
+ console.log(` 1. Edit ${chalk5.yellow("presentation.yaml")} to add your slides`);
2510
+ console.log(` 2. Run ${chalk5.yellow("slide-gen convert presentation.yaml")} to generate markdown`);
2511
+ } catch (error) {
2512
+ spinner.fail("Failed to initialize project");
2513
+ console.error(
2514
+ chalk5.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`)
2515
+ );
2516
+ process.exitCode = ExitCode.GeneralError;
2517
+ }
2518
+ }
2519
+ async function writeFileIfNotExists(filePath, content) {
2520
+ try {
2521
+ await access4(filePath);
2522
+ } catch {
2523
+ await writeFile2(filePath, content, "utf-8");
2524
+ }
2525
+ }
2526
+ function generateConfigContent() {
2527
+ return `# slide-gen configuration
2528
+ # See https://github.com/example/slide-generation for documentation
2529
+
2530
+ templates:
2531
+ # Path to built-in templates (relative to project root)
2532
+ builtin: ./templates
2533
+ # Path to custom templates (optional)
2534
+ # custom: ./my-templates
2535
+
2536
+ icons:
2537
+ # Path to icon registry file
2538
+ registry: ./icons/registry.yaml
2539
+ cache:
2540
+ enabled: true
2541
+ directory: .cache/icons
2542
+ ttl: 86400
2543
+
2544
+ references:
2545
+ enabled: true
2546
+ connection:
2547
+ type: cli
2548
+ command: ref
2549
+ format:
2550
+ locale: ja-JP
2551
+
2552
+ output:
2553
+ theme: default
2554
+ inlineStyles: false
2555
+ `;
2556
+ }
2557
+ function generateCustomCssContent() {
2558
+ return `/* Custom Marp theme styles */
2559
+ /* See https://marpit.marp.app/theme-css for documentation */
2560
+
2561
+ /*
2562
+ section {
2563
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
2564
+ color: #fff;
2565
+ }
2566
+
2567
+ h1 {
2568
+ color: #fff;
2569
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
2570
+ }
2571
+ */
2572
+ `;
2573
+ }
2574
+ function generatePresentationContent(_template) {
2575
+ const baseContent = `# Sample Presentation
2576
+ # Generated by slide-gen init
2577
+
2578
+ meta:
2579
+ title: My Presentation
2580
+ author: Your Name
2581
+ date: "${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}"
2582
+ theme: default
2583
+
2584
+ slides:
2585
+ - template: title
2586
+ content:
2587
+ title: My Presentation
2588
+ subtitle: A sample slide deck
2589
+ author: Your Name
2590
+
2591
+ - template: content
2592
+ content:
2593
+ title: Introduction
2594
+ body: |
2595
+ Welcome to this presentation!
2596
+
2597
+ - Point one
2598
+ - Point two
2599
+ - Point three
2600
+
2601
+ - template: section
2602
+ content:
2603
+ title: Section Title
2604
+ subtitle: Section description
2605
+
2606
+ - template: content
2607
+ content:
2608
+ title: Main Content
2609
+ body: |
2610
+ Here's the main content of your presentation.
2611
+
2612
+ You can use **markdown** formatting in the body text.
2613
+
2614
+ - template: end
2615
+ content:
2616
+ title: Thank You
2617
+ subtitle: Questions?
2618
+ `;
2619
+ return baseContent;
2620
+ }
2621
+
2622
+ // src/cli/commands/watch.ts
2623
+ import { Command as Command6 } from "commander";
2624
+ import { access as access5 } from "fs/promises";
2625
+ import { basename as basename2, dirname as dirname3, join as join6 } from "path";
2626
+ import chalk6 from "chalk";
2627
+ import { watch as chokidarWatch } from "chokidar";
2628
+ var WatchState = class {
2629
+ _isRunning = false;
2630
+ _conversionCount = 0;
2631
+ _lastError = null;
2632
+ get isRunning() {
2633
+ return this._isRunning;
2634
+ }
2635
+ get conversionCount() {
2636
+ return this._conversionCount;
2637
+ }
2638
+ get lastError() {
2639
+ return this._lastError;
2640
+ }
2641
+ start() {
2642
+ this._isRunning = true;
2643
+ }
2644
+ stop() {
2645
+ this._isRunning = false;
2646
+ }
2647
+ incrementConversion() {
2648
+ this._conversionCount++;
2649
+ }
2650
+ setError(error) {
2651
+ this._lastError = error;
2652
+ }
2653
+ clearError() {
2654
+ this._lastError = null;
2655
+ }
2656
+ };
2657
+ function getDefaultOutputPath2(inputPath) {
2658
+ const dir = dirname3(inputPath);
2659
+ const base = basename2(inputPath, ".yaml");
2660
+ return join6(dir, `${base}.md`);
2661
+ }
2662
+ function formatTime() {
2663
+ const now = /* @__PURE__ */ new Date();
2664
+ return `[${now.toLocaleTimeString("en-GB")}]`;
2665
+ }
2666
+ function createWatchCommand() {
2667
+ return new Command6("watch").description("Watch source file and auto-convert on changes").argument("<input>", "Input YAML file to watch").option("-o, --output <path>", "Output file path").option("-c, --config <path>", "Config file path").option("--debounce <ms>", "Debounce delay in milliseconds", "300").option("-v, --verbose", "Verbose output").action(async (input, options) => {
2668
+ await executeWatch(input, options);
2669
+ });
2670
+ }
2671
+ async function runConversion(inputPath, outputPath, pipeline, verbose) {
2672
+ try {
2673
+ const result = await pipeline.runWithResult(inputPath, { outputPath });
2674
+ console.log(
2675
+ `${formatTime()} ${chalk6.green("\u2713")} Output: ${chalk6.cyan(outputPath)}`
2676
+ );
2677
+ if (verbose) {
2678
+ console.log(` Parsed ${result.slideCount} slides`);
2679
+ if (result.warnings.length > 0) {
2680
+ for (const warning of result.warnings) {
2681
+ console.log(chalk6.yellow(` \u26A0 ${warning}`));
2682
+ }
2683
+ }
2684
+ }
2685
+ return { success: true };
2686
+ } catch (error) {
2687
+ const message = error instanceof PipelineError ? `${error.stage}: ${error.message}` : error instanceof Error ? error.message : "Unknown error";
2688
+ console.log(
2689
+ `${formatTime()} ${chalk6.red("\u2717")} Conversion failed: ${message}`
2690
+ );
2691
+ return { success: false, error: message };
2692
+ }
2693
+ }
2694
+ async function executeWatch(inputPath, options) {
2695
+ const state = new WatchState();
2696
+ const errors = [];
2697
+ const debounceMs = Number(options.debounce) || 300;
2698
+ const verbose = options.verbose ?? false;
2699
+ if (options.signal?.aborted) {
2700
+ try {
2701
+ await access5(inputPath);
2702
+ } catch {
2703
+ errors.push(`File not found: ${inputPath}`);
2704
+ return { success: false, conversionCount: 0, errors };
2705
+ }
2706
+ return { success: true, conversionCount: 0, errors };
2707
+ }
2708
+ try {
2709
+ await access5(inputPath);
2710
+ } catch {
2711
+ console.error(chalk6.red(`Error: File not found: ${inputPath}`));
2712
+ errors.push(`File not found: ${inputPath}`);
2713
+ process.exitCode = ExitCode.FileReadError;
2714
+ return { success: false, conversionCount: 0, errors };
2715
+ }
2716
+ const outputPath = options.output ?? getDefaultOutputPath2(inputPath);
2717
+ const configLoader = new ConfigLoader();
2718
+ let configPath = options.config;
2719
+ if (!configPath) {
2720
+ configPath = await configLoader.findConfig(dirname3(inputPath));
2721
+ }
2722
+ const config = await configLoader.load(configPath);
2723
+ const pipeline = new Pipeline(config);
2724
+ try {
2725
+ await pipeline.initialize();
2726
+ } catch (error) {
2727
+ const message = error instanceof Error ? error.message : "Unknown initialization error";
2728
+ console.error(chalk6.red(`Error: Failed to initialize pipeline: ${message}`));
2729
+ errors.push(message);
2730
+ process.exitCode = ExitCode.ConversionError;
2731
+ return { success: false, conversionCount: 0, errors };
2732
+ }
2733
+ console.log(`Watching ${chalk6.cyan(inputPath)}...`);
2734
+ console.log(`Output: ${chalk6.cyan(outputPath)}`);
2735
+ console.log("");
2736
+ console.log(`${formatTime()} Initial conversion...`);
2737
+ const initialResult = await runConversion(inputPath, outputPath, pipeline, verbose);
2738
+ if (initialResult.success) {
2739
+ state.incrementConversion();
2740
+ } else if (initialResult.error) {
2741
+ errors.push(initialResult.error);
2742
+ }
2743
+ let debounceTimer = null;
2744
+ let watcher = null;
2745
+ const handleChange = () => {
2746
+ if (debounceTimer) {
2747
+ clearTimeout(debounceTimer);
2748
+ }
2749
+ debounceTimer = setTimeout(async () => {
2750
+ console.log(`${formatTime()} Changed: ${inputPath}`);
2751
+ console.log(`${formatTime()} Converting...`);
2752
+ const result = await runConversion(inputPath, outputPath, pipeline, verbose);
2753
+ if (result.success) {
2754
+ state.incrementConversion();
2755
+ state.clearError();
2756
+ } else if (result.error) {
2757
+ errors.push(result.error);
2758
+ state.setError(new Error(result.error));
2759
+ }
2760
+ }, debounceMs);
2761
+ };
2762
+ state.start();
2763
+ watcher = chokidarWatch(inputPath, {
2764
+ persistent: true,
2765
+ ignoreInitial: true,
2766
+ awaitWriteFinish: {
2767
+ stabilityThreshold: 100,
2768
+ pollInterval: 50
2769
+ }
2770
+ });
2771
+ watcher.on("change", handleChange);
2772
+ const cleanup = () => {
2773
+ state.stop();
2774
+ if (debounceTimer) {
2775
+ clearTimeout(debounceTimer);
2776
+ }
2777
+ watcher?.close();
2778
+ };
2779
+ if (options.signal) {
2780
+ options.signal.addEventListener("abort", cleanup);
2781
+ }
2782
+ const signalHandler = () => {
2783
+ console.log("\n" + chalk6.yellow("Watch stopped."));
2784
+ cleanup();
2785
+ process.exit(0);
2786
+ };
2787
+ process.on("SIGINT", signalHandler);
2788
+ process.on("SIGTERM", signalHandler);
2789
+ await new Promise((resolve2) => {
2790
+ if (options.signal) {
2791
+ options.signal.addEventListener("abort", () => resolve2());
2792
+ }
2793
+ });
2794
+ return {
2795
+ success: errors.length === 0,
2796
+ conversionCount: state.conversionCount,
2797
+ errors
2798
+ };
2799
+ }
2800
+
2801
+ // src/cli/commands/preview.ts
2802
+ import { Command as Command7 } from "commander";
2803
+ import { access as access6, unlink } from "fs/promises";
2804
+ import { basename as basename3, dirname as dirname4, join as join7 } from "path";
2805
+ import { tmpdir } from "os";
2806
+ import { spawn, execSync } from "child_process";
2807
+ import chalk7 from "chalk";
2808
+ import { watch as chokidarWatch2 } from "chokidar";
2809
+ async function checkMarpCliAvailable() {
2810
+ try {
2811
+ execSync("npx marp --version", { stdio: "ignore" });
2812
+ return true;
2813
+ } catch {
2814
+ return false;
2815
+ }
2816
+ }
2817
+ function getTempOutputPath(inputPath) {
2818
+ const base = basename3(inputPath, ".yaml");
2819
+ return join7(tmpdir(), `slide-gen-preview-${base}-${Date.now()}.md`);
2820
+ }
2821
+ function buildMarpCommand(markdownPath, options) {
2822
+ const parts = ["npx", "marp", "--preview"];
2823
+ if (options.port) {
2824
+ parts.push("-p", String(options.port));
2825
+ }
2826
+ if (options.watch) {
2827
+ parts.push("--watch");
2828
+ }
2829
+ parts.push(markdownPath);
2830
+ return parts.join(" ");
2831
+ }
2832
+ function createPreviewCommand2() {
2833
+ return new Command7("preview").description("Preview the generated slides in browser (requires Marp CLI)").argument("<input>", "Input YAML file").option("-p, --port <number>", "Preview server port", "8080").option("-w, --watch", "Watch for changes and auto-reload").option("-c, --config <path>", "Config file path").option("-v, --verbose", "Verbose output").action(async (input, options) => {
2834
+ await executePreview(input, options);
2835
+ });
2836
+ }
2837
+ async function executePreview(inputPath, options) {
2838
+ const errors = [];
2839
+ const verbose = options.verbose ?? false;
2840
+ const port = Number(options.port) || 8080;
2841
+ if (options.signal?.aborted) {
2842
+ try {
2843
+ await access6(inputPath);
2844
+ } catch {
2845
+ errors.push(`File not found: ${inputPath}`);
2846
+ return { success: false, errors };
2847
+ }
2848
+ return { success: true, errors };
2849
+ }
2850
+ try {
2851
+ await access6(inputPath);
2852
+ } catch {
2853
+ console.error(chalk7.red(`Error: File not found: ${inputPath}`));
2854
+ errors.push(`File not found: ${inputPath}`);
2855
+ process.exitCode = ExitCode.FileReadError;
2856
+ return { success: false, errors };
2857
+ }
2858
+ console.log("Checking for Marp CLI...");
2859
+ const marpAvailable = await checkMarpCliAvailable();
2860
+ if (!marpAvailable) {
2861
+ console.error(
2862
+ chalk7.red(
2863
+ "Error: Marp CLI not found. Install it with: npm install -g @marp-team/marp-cli"
2864
+ )
2865
+ );
2866
+ errors.push("Marp CLI not available");
2867
+ process.exitCode = ExitCode.GeneralError;
2868
+ return { success: false, errors };
2869
+ }
2870
+ console.log(chalk7.green("\u2713") + " Marp CLI found");
2871
+ const configLoader = new ConfigLoader();
2872
+ let configPath = options.config;
2873
+ if (!configPath) {
2874
+ configPath = await configLoader.findConfig(dirname4(inputPath));
2875
+ }
2876
+ const config = await configLoader.load(configPath);
2877
+ console.log("Initializing pipeline...");
2878
+ const pipeline = new Pipeline(config);
2879
+ try {
2880
+ await pipeline.initialize();
2881
+ } catch (error) {
2882
+ const message = error instanceof Error ? error.message : "Unknown initialization error";
2883
+ console.error(chalk7.red(`Error: Failed to initialize pipeline: ${message}`));
2884
+ errors.push(message);
2885
+ process.exitCode = ExitCode.ConversionError;
2886
+ return { success: false, errors };
2887
+ }
2888
+ const tempMarkdownPath = getTempOutputPath(inputPath);
2889
+ console.log(`Converting ${chalk7.cyan(inputPath)}...`);
2890
+ try {
2891
+ await pipeline.runWithResult(inputPath, { outputPath: tempMarkdownPath });
2892
+ console.log(chalk7.green("\u2713") + " Initial conversion complete");
2893
+ } catch (error) {
2894
+ const message = error instanceof PipelineError ? `${error.stage}: ${error.message}` : error instanceof Error ? error.message : "Unknown error";
2895
+ console.error(chalk7.red(`Error: Conversion failed: ${message}`));
2896
+ errors.push(message);
2897
+ process.exitCode = ExitCode.ConversionError;
2898
+ return { success: false, errors };
2899
+ }
2900
+ console.log(`
2901
+ Starting preview server on port ${chalk7.cyan(port)}...`);
2902
+ const marpCommand = buildMarpCommand(tempMarkdownPath, {
2903
+ ...options,
2904
+ port,
2905
+ watch: false
2906
+ // We handle watch ourselves if needed
2907
+ });
2908
+ if (verbose) {
2909
+ console.log(`Running: ${marpCommand}`);
2910
+ }
2911
+ const marpProcess = spawn("npx", ["marp", "--preview", "-p", String(port), tempMarkdownPath], {
2912
+ stdio: "inherit",
2913
+ shell: true
2914
+ });
2915
+ let watcher = null;
2916
+ let debounceTimer = null;
2917
+ if (options.watch) {
2918
+ console.log(`
2919
+ Watching ${chalk7.cyan(inputPath)} for changes...`);
2920
+ watcher = chokidarWatch2(inputPath, {
2921
+ persistent: true,
2922
+ ignoreInitial: true,
2923
+ awaitWriteFinish: {
2924
+ stabilityThreshold: 100,
2925
+ pollInterval: 50
2926
+ }
2927
+ });
2928
+ watcher.on("change", () => {
2929
+ if (debounceTimer) {
2930
+ clearTimeout(debounceTimer);
2931
+ }
2932
+ debounceTimer = setTimeout(async () => {
2933
+ console.log(`
2934
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString("en-GB")}] File changed, reconverting...`);
2935
+ try {
2936
+ await pipeline.runWithResult(inputPath, { outputPath: tempMarkdownPath });
2937
+ console.log(chalk7.green("\u2713") + " Reconversion complete");
2938
+ } catch (error) {
2939
+ const message = error instanceof PipelineError ? `${error.stage}: ${error.message}` : error instanceof Error ? error.message : "Unknown error";
2940
+ console.error(chalk7.red(`\u2717 Reconversion failed: ${message}`));
2941
+ }
2942
+ }, 300);
2943
+ });
2944
+ }
2945
+ const cleanup = async () => {
2946
+ if (debounceTimer) {
2947
+ clearTimeout(debounceTimer);
2948
+ }
2949
+ watcher?.close();
2950
+ marpProcess.kill();
2951
+ try {
2952
+ await unlink(tempMarkdownPath);
2953
+ } catch {
2954
+ }
2955
+ };
2956
+ if (options.signal) {
2957
+ options.signal.addEventListener("abort", () => {
2958
+ cleanup();
2959
+ });
2960
+ }
2961
+ const signalHandler = async () => {
2962
+ console.log("\n" + chalk7.yellow("Preview stopped."));
2963
+ await cleanup();
2964
+ process.exit(0);
2965
+ };
2966
+ process.on("SIGINT", signalHandler);
2967
+ process.on("SIGTERM", signalHandler);
2968
+ await new Promise((resolve2) => {
2969
+ marpProcess.on("exit", () => {
2970
+ cleanup();
2971
+ resolve2();
2972
+ });
2973
+ if (options.signal) {
2974
+ options.signal.addEventListener("abort", () => resolve2());
2975
+ }
2976
+ });
2977
+ return {
2978
+ success: errors.length === 0,
2979
+ errors
2980
+ };
2981
+ }
2982
+
2983
+ // src/cli/index.ts
2984
+ var program = new Command8();
2985
+ program.name("slide-gen").description("Generate Marp-compatible Markdown from YAML source files").version(VERSION);
2986
+ program.addCommand(createConvertCommand());
2987
+ program.addCommand(createValidateCommand());
2988
+ program.addCommand(createTemplatesCommand());
2989
+ program.addCommand(createIconsCommand());
2990
+ program.addCommand(createInitCommand());
2991
+ program.addCommand(createWatchCommand());
2992
+ program.addCommand(createPreviewCommand2());
2993
+ program.parse();
2994
+ //# sourceMappingURL=index.js.map