@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.
- package/CHANGELOG.md +56 -0
- package/LICENSE +21 -0
- package/README.md +248 -0
- package/README_ja.md +248 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +2994 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +999 -0
- package/dist/index.js +1469 -0
- package/dist/index.js.map +1 -0
- package/icons/custom/.gitkeep +3 -0
- package/icons/registry.yaml +77 -0
- package/package.json +84 -0
- package/templates/basic/bullet-list.yaml +39 -0
- package/templates/basic/numbered-list.yaml +39 -0
- package/templates/basic/section.yaml +26 -0
- package/templates/basic/title.yaml +46 -0
- package/templates/data/comparison-table.yaml +107 -0
- package/templates/data/table.yaml +94 -0
- package/templates/diagrams/cycle-diagram.yaml +71 -0
- package/templates/diagrams/flow-chart.yaml +141 -0
- package/templates/diagrams/hierarchy.yaml +117 -0
- package/templates/diagrams/matrix.yaml +163 -0
- package/templates/diagrams/timeline.yaml +167 -0
- package/templates/layouts/gallery.yaml +94 -0
- package/templates/layouts/image-text.yaml +105 -0
- package/templates/layouts/three-column.yaml +101 -0
- package/templates/layouts/two-column.yaml +85 -0
- package/templates/special/bibliography.yaml +132 -0
- package/templates/special/code-block.yaml +79 -0
- package/templates/special/custom.yaml +45 -0
- package/templates/special/quote.yaml +76 -0
- package/themes/default.css +122 -0
|
@@ -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
|