markdownfly 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/dist/cli.js ADDED
@@ -0,0 +1,2756 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+ import chalk2 from "chalk";
6
+ import { readFileSync as readFileSync3 } from "fs";
7
+
8
+ // src/index.ts
9
+ import { readFileSync as readFileSync2 } from "fs";
10
+ import { resolve as resolve3 } from "path";
11
+
12
+ // src/parser/index.ts
13
+ import { unified } from "unified";
14
+ import remarkParse from "remark-parse";
15
+ import remarkFrontmatter from "remark-frontmatter";
16
+ import remarkGfm from "remark-gfm";
17
+
18
+ // src/parser/frontmatter.ts
19
+ import { parse as parseYaml } from "yaml";
20
+
21
+ // src/config/defaults.ts
22
+ var DEFAULT_CONFIG = {
23
+ theme: "clean"
24
+ };
25
+
26
+ // src/utils/progress.ts
27
+ import ora from "ora";
28
+ import chalk from "chalk";
29
+ var quiet = false;
30
+ function setQuiet(value) {
31
+ quiet = value;
32
+ }
33
+ var ProgressReporter = class {
34
+ spinner;
35
+ constructor() {
36
+ this.spinner = ora();
37
+ }
38
+ start(text) {
39
+ if (quiet) return;
40
+ this.spinner.start(text);
41
+ }
42
+ update(text) {
43
+ if (quiet) return;
44
+ this.spinner.text = text;
45
+ }
46
+ succeed(text) {
47
+ if (quiet) return;
48
+ this.spinner.succeed(text);
49
+ }
50
+ fail(text) {
51
+ if (quiet) return;
52
+ this.spinner.fail(text);
53
+ }
54
+ info(text) {
55
+ if (quiet) return;
56
+ this.spinner.info(text);
57
+ }
58
+ };
59
+ var log = {
60
+ info: (msg) => console.log(chalk.blue("\u2139"), msg),
61
+ success: (msg) => console.log(chalk.green("\u2714"), msg),
62
+ warn: (msg) => console.error(chalk.yellow("\u26A0"), msg),
63
+ error: (msg) => console.error(chalk.red("\u2716"), msg)
64
+ };
65
+
66
+ // src/parser/frontmatter.ts
67
+ function extractFrontmatter(tree) {
68
+ const yamlNode = tree.children.find((node) => node.type === "yaml");
69
+ if (!yamlNode) {
70
+ return { ...DEFAULT_CONFIG };
71
+ }
72
+ try {
73
+ const parsed = parseYaml(yamlNode.value);
74
+ const { resource_dir, ...parsedOnly } = parsed;
75
+ const merged = {
76
+ ...DEFAULT_CONFIG,
77
+ ...parsedOnly
78
+ };
79
+ if (resource_dir && !merged.resourceDir) {
80
+ merged.resourceDir = resource_dir;
81
+ }
82
+ return merged;
83
+ } catch {
84
+ log.warn("Failed to parse frontmatter YAML, using defaults");
85
+ return { ...DEFAULT_CONFIG };
86
+ }
87
+ }
88
+
89
+ // src/parser/splitter.ts
90
+ var DIAGRAM_LANGUAGES = /* @__PURE__ */ new Set(["mermaid", "dot", "graphviz", "echarts"]);
91
+ var CALLOUT_VARIANTS = /* @__PURE__ */ new Set([
92
+ "note",
93
+ "info",
94
+ "tip",
95
+ "success",
96
+ "warning",
97
+ "caution",
98
+ "danger"
99
+ ]);
100
+ function extractText(node) {
101
+ if ("value" in node && typeof node.value === "string") {
102
+ return node.value;
103
+ }
104
+ if (node.type === "softbreak") {
105
+ return "\n";
106
+ }
107
+ if ("children" in node && Array.isArray(node.children)) {
108
+ return node.children.map(extractText).join("");
109
+ }
110
+ return "";
111
+ }
112
+ function parseComment(value) {
113
+ const match = value.match(/<!--\s*mfly:(row|col|dir)\s*(.*?)\s*-->/s);
114
+ if (!match) return null;
115
+ if (match[1] === "row" || match[1] === "col") {
116
+ return { type: "break", direction: match[1] };
117
+ }
118
+ if (match[1] === "dir") {
119
+ const raw = decodeURIComponent(match[2]);
120
+ return { directives: parseDirectiveString(raw) };
121
+ }
122
+ return null;
123
+ }
124
+ function parseDirectiveString(input) {
125
+ const inner = input.replace(/^@\(/, "").replace(/\)$/, "").trim();
126
+ const result = {};
127
+ if (!inner) return result;
128
+ const re = /([A-Za-z][A-Za-z0-9_-]*)\s*=\s*("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^,)]+)/g;
129
+ let m;
130
+ while ((m = re.exec(inner)) !== null) {
131
+ let value = m[2].trim();
132
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
133
+ value = value.slice(1, -1);
134
+ }
135
+ result[m[1]] = value;
136
+ }
137
+ return result;
138
+ }
139
+ function toDirectives(raw) {
140
+ const d = {};
141
+ if (raw.layout) d.layout = raw.layout;
142
+ if (raw.notes) d.notes = raw.notes;
143
+ if (raw.chart) d.chart = raw.chart;
144
+ if (raw.highlight) d.highlight = raw.highlight;
145
+ if (raw.background) d.background = raw.background;
146
+ if (raw.steps) d.steps = raw.steps === "true" || raw.steps === "yes" || raw.steps === "1";
147
+ return d;
148
+ }
149
+ function parseHighlightRanges(spec) {
150
+ const lines = /* @__PURE__ */ new Set();
151
+ for (const part of spec.split(",")) {
152
+ const range = part.trim().match(/^(\d+)\s*-\s*(\d+)$/);
153
+ if (range) {
154
+ const from = Number(range[1]);
155
+ const to = Number(range[2]);
156
+ for (let n = Math.min(from, to); n <= Math.max(from, to); n++) lines.add(n);
157
+ } else {
158
+ const single = part.trim().match(/^\d+$/);
159
+ if (single) lines.add(Number(single));
160
+ }
161
+ }
162
+ return [...lines].sort((a, b) => a - b);
163
+ }
164
+ function parseCallout(content) {
165
+ const lines = content.split("\n");
166
+ const markerMatch = lines[0]?.match(/^\s*\[!(.*?)\]\s*(.*?)\s*$/);
167
+ if (!markerMatch) return null;
168
+ const variant = markerMatch[1].toLowerCase();
169
+ if (!CALLOUT_VARIANTS.has(variant)) return null;
170
+ const title = markerMatch[2].trim() || void 0;
171
+ return {
172
+ type: "callout",
173
+ variant,
174
+ title,
175
+ content: lines.slice(1).join("\n").trim()
176
+ };
177
+ }
178
+ function parseImageParams(text) {
179
+ const result = {};
180
+ const body = text.replace(/^\{\s*|\s*\}$/g, "");
181
+ for (const part of body.split(",")) {
182
+ const eq = part.indexOf("=");
183
+ if (eq === -1) continue;
184
+ const key = part.slice(0, eq).trim().toLowerCase();
185
+ const value = part.slice(eq + 1).trim();
186
+ switch (key) {
187
+ case "w":
188
+ case "width": {
189
+ const normalized = normalizeImageSize(value);
190
+ if (normalized) result.width = normalized;
191
+ break;
192
+ }
193
+ case "h":
194
+ case "height": {
195
+ const normalized = normalizeImageSize(value);
196
+ if (normalized) result.height = normalized;
197
+ break;
198
+ }
199
+ case "align": {
200
+ if (value === "left" || value === "center" || value === "right") {
201
+ result.align = value;
202
+ }
203
+ break;
204
+ }
205
+ }
206
+ }
207
+ return result;
208
+ }
209
+ function normalizeImageSize(value) {
210
+ const match = value.match(/^(\d+(?:\.\d+)?)(px|pt|cm|mm|in|inch|%)?$/i);
211
+ if (!match) return void 0;
212
+ const n = parseFloat(match[1]);
213
+ if (!(n > 0)) return void 0;
214
+ const unit = (match[2] || "px").toLowerCase();
215
+ if (unit === "%") return `${n}%`;
216
+ const inches = unit === "px" ? n / 96 : unit === "pt" ? n / 72 : unit === "cm" ? n / 2.54 : unit === "mm" ? n / 25.4 : n;
217
+ return `${Math.round(inches * 100) / 100}in`;
218
+ }
219
+ function nodeToElement(node) {
220
+ switch (node.type) {
221
+ case "heading": {
222
+ return {
223
+ type: "heading",
224
+ content: node.children.map(extractText).join(""),
225
+ level: node.depth
226
+ };
227
+ }
228
+ case "paragraph": {
229
+ const first = node.children[0];
230
+ if (first?.type === "image") {
231
+ const rest = node.children.slice(1);
232
+ const paramsFromText = (() => {
233
+ if (rest.length === 0) return {};
234
+ if (!rest.every((c) => c.type === "text")) return null;
235
+ const suffix = rest.map((c) => c.type === "text" ? c.value : "").join("").trim();
236
+ const match = suffix.match(/^\{.*\}$/);
237
+ return match ? parseImageParams(match[0]) : null;
238
+ })();
239
+ if (paramsFromText !== null) {
240
+ return {
241
+ type: "image",
242
+ src: first.url,
243
+ alt: first.alt ?? void 0,
244
+ ...paramsFromText
245
+ };
246
+ }
247
+ }
248
+ return {
249
+ type: "text",
250
+ content: node.children.map(extractText).join("")
251
+ };
252
+ }
253
+ case "list": {
254
+ const hasCheckboxes = node.children.some(
255
+ (item) => typeof item.checked === "boolean"
256
+ );
257
+ const items = node.children.map((item) => {
258
+ return item.children.map((child) => extractText(child)).join("");
259
+ });
260
+ const result = {
261
+ type: "list",
262
+ items,
263
+ ordered: node.ordered ?? false
264
+ };
265
+ if (hasCheckboxes) {
266
+ result.checked = node.children.map(
267
+ (item) => typeof item.checked === "boolean" ? item.checked : false
268
+ );
269
+ }
270
+ return result;
271
+ }
272
+ case "code": {
273
+ const lang = node.lang?.toLowerCase() ?? "";
274
+ if (DIAGRAM_LANGUAGES.has(lang)) {
275
+ return {
276
+ type: "diagram",
277
+ diagramType: lang === "graphviz" ? "dot" : lang,
278
+ content: node.value
279
+ };
280
+ }
281
+ const element = {
282
+ type: "code",
283
+ content: node.value,
284
+ language: node.lang ?? void 0
285
+ };
286
+ return element;
287
+ }
288
+ case "image": {
289
+ return {
290
+ type: "image",
291
+ src: node.url,
292
+ alt: node.alt ?? void 0
293
+ };
294
+ }
295
+ case "table": {
296
+ const rows = node.children.map(
297
+ (row) => row.children.map(
298
+ (cell) => cell.children.map(extractText).join("")
299
+ )
300
+ );
301
+ const headers = rows.length > 0 ? rows[0] : [];
302
+ const bodyRows = rows.slice(1);
303
+ return {
304
+ type: "table",
305
+ headers,
306
+ rows: bodyRows
307
+ };
308
+ }
309
+ case "blockquote": {
310
+ const content = node.children.map((child) => extractText(child)).join("\n");
311
+ const callout = parseCallout(content);
312
+ if (callout) return callout;
313
+ return {
314
+ type: "blockquote",
315
+ content
316
+ };
317
+ }
318
+ case "html": {
319
+ const parsed = parseComment(node.value);
320
+ if (parsed === null) return null;
321
+ if ("direction" in parsed) return parsed;
322
+ return null;
323
+ }
324
+ default:
325
+ return null;
326
+ }
327
+ }
328
+ function detectLayout(title, elements, isFirstSlide, headingLevel) {
329
+ if (isFirstSlide && headingLevel === 1) {
330
+ return "title";
331
+ }
332
+ if (headingLevel === 2 && elements.length === 0) {
333
+ return "section";
334
+ }
335
+ const nonEmptyElements = elements.filter(
336
+ (e) => e.type !== "text" || e.content.trim() !== ""
337
+ );
338
+ if (nonEmptyElements.length > 0 && nonEmptyElements.every((e) => e.type === "code")) {
339
+ return "code";
340
+ }
341
+ if (nonEmptyElements.length > 0 && nonEmptyElements.every((e) => e.type === "blockquote")) {
342
+ return "quote";
343
+ }
344
+ return "content";
345
+ }
346
+ function splitIntoSlides(tree, defaultLayout) {
347
+ const slides = [];
348
+ let currentTitle;
349
+ let currentSubtitle;
350
+ let currentElements = [];
351
+ let currentDirectives = {};
352
+ let currentHeadingLevel = 0;
353
+ let isFirstSlide = true;
354
+ function flushSlide() {
355
+ if (!currentTitle && currentElements.length === 0 && Object.keys(currentDirectives).length === 0) {
356
+ return;
357
+ }
358
+ if (currentDirectives.highlight && currentElements.length > 0) {
359
+ const ranges = parseHighlightRanges(currentDirectives.highlight);
360
+ if (ranges.length > 0) {
361
+ const code = currentElements.find(
362
+ (e) => e.type === "code" && !e.highlightLines
363
+ );
364
+ if (code) code.highlightLines = ranges;
365
+ }
366
+ }
367
+ const autoLayout = detectLayout(
368
+ currentTitle,
369
+ currentElements,
370
+ isFirstSlide && slides.length === 0,
371
+ currentHeadingLevel
372
+ );
373
+ const layout = currentDirectives.layout ?? (autoLayout === "content" ? defaultLayout : autoLayout) ?? "content";
374
+ slides.push({
375
+ layout,
376
+ title: currentTitle,
377
+ subtitle: currentSubtitle,
378
+ elements: currentElements,
379
+ notes: currentDirectives.notes,
380
+ directives: toDirectives(currentDirectives)
381
+ });
382
+ currentTitle = void 0;
383
+ currentSubtitle = void 0;
384
+ currentElements = [];
385
+ currentDirectives = {};
386
+ currentHeadingLevel = 0;
387
+ isFirstSlide = false;
388
+ }
389
+ for (const node of tree.children) {
390
+ if (node.type === "yaml") continue;
391
+ if (node.type === "thematicBreak") {
392
+ flushSlide();
393
+ continue;
394
+ }
395
+ if (node.type === "heading" && (node.depth === 1 || node.depth === 2)) {
396
+ flushSlide();
397
+ currentTitle = node.children.map(extractText).join("");
398
+ currentHeadingLevel = node.depth;
399
+ continue;
400
+ }
401
+ if (node.type === "html") {
402
+ const parsed = parseComment(node.value);
403
+ if (parsed !== null) {
404
+ if ("direction" in parsed) {
405
+ currentElements.push(parsed);
406
+ } else {
407
+ Object.assign(currentDirectives, parsed.directives);
408
+ }
409
+ }
410
+ continue;
411
+ }
412
+ const element = nodeToElement(node);
413
+ if (element) {
414
+ currentElements.push(element);
415
+ }
416
+ }
417
+ flushSlide();
418
+ return slides;
419
+ }
420
+
421
+ // src/parser/preprocess.ts
422
+ var ROW_RE = /^={3,}$/;
423
+ var COL_RE = /^<->$/;
424
+ var DIR_RE = /^@\(.+\)$/;
425
+ var COMMENT_RE = /^%%/;
426
+ function preprocessMarkdown(markdown) {
427
+ const lines = markdown.split("\n");
428
+ const out = [];
429
+ let inFence = false;
430
+ let fenceChar = "";
431
+ for (const line of lines) {
432
+ const trimmed = line.trim();
433
+ const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
434
+ if (!inFence) {
435
+ if (fenceMatch) {
436
+ inFence = true;
437
+ fenceChar = fenceMatch[1][0];
438
+ out.push(line);
439
+ continue;
440
+ }
441
+ if (COMMENT_RE.test(trimmed)) {
442
+ out.push("");
443
+ continue;
444
+ }
445
+ if (COL_RE.test(trimmed)) {
446
+ out.push("<!-- mfly:col -->");
447
+ continue;
448
+ }
449
+ if (ROW_RE.test(trimmed)) {
450
+ out.push("<!-- mfly:row -->");
451
+ continue;
452
+ }
453
+ if (DIR_RE.test(trimmed)) {
454
+ out.push(`<!-- mfly:dir:${encodeURIComponent(trimmed)} -->`);
455
+ continue;
456
+ }
457
+ out.push(line);
458
+ continue;
459
+ }
460
+ out.push(line);
461
+ if (fenceMatch && trimmed.startsWith(fenceChar)) {
462
+ inFence = false;
463
+ }
464
+ }
465
+ return out.join("\n");
466
+ }
467
+
468
+ // src/parser/index.ts
469
+ function parseMarkdown(markdown) {
470
+ const processor = unified().use(remarkParse).use(remarkFrontmatter, ["yaml"]).use(remarkGfm);
471
+ const tree = processor.parse(preprocessMarkdown(markdown));
472
+ const config = extractFrontmatter(tree);
473
+ const slides = splitIntoSlides(tree, config.layout);
474
+ return { config, slides };
475
+ }
476
+
477
+ // src/renderer/pptx-renderer.ts
478
+ import PptxGenJS from "pptxgenjs";
479
+
480
+ // src/renderer/layouts/title.ts
481
+ function renderTitleSlide(slide, node, theme) {
482
+ const textColor = theme.colors.titleText ?? "FFFFFF";
483
+ const subtitleColor = textColor === "FFFFFF" ? "FFFFFFCC" : theme.colors.secondary;
484
+ const metaColor = textColor === "FFFFFF" ? "FFFFFF99" : theme.colors.secondary;
485
+ const bgColor = theme.colors.backgroundGradient ? void 0 : theme.colors.titleBackground ?? theme.colors.primary;
486
+ if (bgColor) {
487
+ slide.background = { color: bgColor };
488
+ }
489
+ if (node.title) {
490
+ slide.addText(node.title, {
491
+ x: 0.8,
492
+ y: 2,
493
+ w: 11.7,
494
+ h: 1.5,
495
+ fontSize: theme.fontSize.title,
496
+ fontFace: theme.fonts.heading,
497
+ color: textColor,
498
+ bold: true,
499
+ align: "center",
500
+ valign: "middle"
501
+ });
502
+ }
503
+ if (node.subtitle) {
504
+ slide.addText(node.subtitle, {
505
+ x: 0.8,
506
+ y: 3.8,
507
+ w: 11.7,
508
+ h: 0.8,
509
+ fontSize: theme.fontSize.body,
510
+ fontFace: theme.fonts.body,
511
+ color: subtitleColor,
512
+ align: "center",
513
+ valign: "middle"
514
+ });
515
+ }
516
+ const textElements = node.elements.filter((e) => e.type === "text");
517
+ if (textElements.length > 0) {
518
+ const metaText = textElements.map((e) => e.content).join(" \xB7 ");
519
+ slide.addText(metaText, {
520
+ x: 0.8,
521
+ y: 5.5,
522
+ w: 11.7,
523
+ h: 0.6,
524
+ fontSize: theme.fontSize.small,
525
+ fontFace: theme.fonts.body,
526
+ color: metaColor,
527
+ align: "center"
528
+ });
529
+ }
530
+ }
531
+
532
+ // src/renderer/layouts/section.ts
533
+ function renderSectionSlide(slide, node, theme) {
534
+ slide.addShape("rect", {
535
+ x: 0,
536
+ y: 0,
537
+ w: 0.15,
538
+ h: 7.5,
539
+ fill: { color: theme.colors.accent }
540
+ });
541
+ if (node.title) {
542
+ slide.addText(node.title, {
543
+ x: 1,
544
+ y: 2.5,
545
+ w: 11.3,
546
+ h: 2,
547
+ fontSize: theme.fontSize.heading + 4,
548
+ fontFace: theme.fonts.heading,
549
+ color: theme.colors.text,
550
+ bold: true,
551
+ align: "left",
552
+ valign: "middle"
553
+ });
554
+ }
555
+ }
556
+
557
+ // src/utils/png-size.ts
558
+ function getPngSize(data) {
559
+ if (data.length < 24) return null;
560
+ if (data.readUInt32BE(0) !== 2303741511) return null;
561
+ const width = data.readUInt32BE(16);
562
+ const height = data.readUInt32BE(20);
563
+ if (!(width > 0 && height > 0)) return null;
564
+ return { width, height };
565
+ }
566
+
567
+ // src/utils/image-fit.ts
568
+ function fitInBox(img, maxWidth, maxHeight) {
569
+ if (!(img.width > 0 && img.height > 0) || !(maxWidth > 0 && maxHeight > 0)) {
570
+ return { width: maxWidth, height: maxHeight };
571
+ }
572
+ const ar = img.height / img.width;
573
+ let width = maxWidth;
574
+ let height = width * ar;
575
+ if (height > maxHeight) {
576
+ height = maxHeight;
577
+ width = height / ar;
578
+ }
579
+ return { width, height };
580
+ }
581
+ function resolveImageSize(value, of) {
582
+ if (!value) return void 0;
583
+ if (value.endsWith("%")) {
584
+ const pct = parseFloat(value);
585
+ if (Number.isNaN(pct) || pct <= 0) return void 0;
586
+ return of * pct / 100;
587
+ }
588
+ const n = parseFloat(value);
589
+ if (Number.isNaN(n) || n <= 0) return void 0;
590
+ return n;
591
+ }
592
+ function resolveSizes(img, box, widthStr, heightStr) {
593
+ const width = resolveImageSize(widthStr, box.width);
594
+ const height = resolveImageSize(heightStr, box.height);
595
+ const ar = img.height > 0 && img.width > 0 ? img.height / img.width : 0.5;
596
+ if (width === void 0 && height === void 0) {
597
+ return fitInBox(
598
+ { width: img.width > 0 ? img.width : 6, height: img.height > 0 ? img.height : 3 },
599
+ box.width,
600
+ box.height
601
+ );
602
+ }
603
+ if (height === void 0) {
604
+ return { width, height: width * ar };
605
+ }
606
+ if (width === void 0) {
607
+ return { width: height / ar, height };
608
+ }
609
+ return { width, height };
610
+ }
611
+ function fitImageWithOptions(img, box, options = {}) {
612
+ let { width, height } = resolveSizes(img, box, options.width, options.height);
613
+ if (width > box.width || height > box.height) {
614
+ const fitted = fitInBox({ width, height }, box.width, box.height);
615
+ width = fitted.width;
616
+ height = fitted.height;
617
+ }
618
+ const align = options.align ?? "center";
619
+ const x = align === "left" ? 0 : align === "right" ? box.width - width : (box.width - width) / 2;
620
+ return { x, width, height };
621
+ }
622
+
623
+ // src/utils/table-chart.ts
624
+ function tableToChartOption(table, type) {
625
+ const { headers, rows } = table;
626
+ if (headers.length < 2 || rows.length === 0) {
627
+ throw new Error("chart: table needs at least 2 columns and 1 data row");
628
+ }
629
+ if (type === "pie") {
630
+ const series2 = {
631
+ name: headers[1],
632
+ type: "pie",
633
+ radius: "55%",
634
+ data: rows.map((r) => ({ name: r[0], value: Number(r[1]) || 0 }))
635
+ };
636
+ return JSON.stringify({
637
+ toolbox: { show: false },
638
+ series: [series2]
639
+ });
640
+ }
641
+ const categories = rows.map((r) => r[0]);
642
+ const series = headers.slice(1).map((name, i) => ({
643
+ name,
644
+ type,
645
+ data: rows.map((r) => Number(r[i + 1]) || 0)
646
+ }));
647
+ return JSON.stringify({
648
+ xAxis: { type: "category", data: categories, axisLabel: { hideOverlap: true } },
649
+ yAxis: { type: "value" },
650
+ series
651
+ });
652
+ }
653
+
654
+ // src/renderer/layouts/content.ts
655
+ import { readFileSync } from "fs";
656
+ var SLIDE_W = 13.33;
657
+ var SLIDE_H = 7.5;
658
+ var MARGIN = 0.6;
659
+ var CONTENT_W = SLIDE_W - MARGIN * 2;
660
+ var TITLE_H = 0.9;
661
+ var BOTTOM_MARGIN = 0.45;
662
+ var GRID_GAP = 0.25;
663
+ var IMAGE_MIME = {
664
+ png: "image/png",
665
+ jpg: "image/jpeg",
666
+ jpeg: "image/jpeg",
667
+ gif: "image/gif",
668
+ webp: "image/webp",
669
+ bmp: "image/bmp",
670
+ svg: "image/svg+xml"
671
+ };
672
+ function contentAreaInsets(node) {
673
+ return {
674
+ top: node.title ? 0.3 + TITLE_H + 0.2 : 0.3,
675
+ bottom: BOTTOM_MARGIN
676
+ };
677
+ }
678
+ function splitGrid(elements) {
679
+ const rows = [];
680
+ let currentRow = [];
681
+ for (const el of elements) {
682
+ if (el.type === "break" && el.direction === "row") {
683
+ rows.push(currentRow);
684
+ currentRow = [];
685
+ continue;
686
+ }
687
+ currentRow.push(el);
688
+ }
689
+ rows.push(currentRow);
690
+ return rows.map((row) => {
691
+ const columns = [];
692
+ let currentCol = [];
693
+ for (const el of row) {
694
+ if (el.type === "break" && el.direction === "col") {
695
+ columns.push(currentCol);
696
+ currentCol = [];
697
+ continue;
698
+ }
699
+ currentCol.push(el);
700
+ }
701
+ columns.push(currentCol);
702
+ return columns;
703
+ });
704
+ }
705
+ async function renderContentSlide(slide, node, theme, ctx) {
706
+ const { top } = contentAreaInsets(node);
707
+ if (node.title) {
708
+ slide.addShape("rect", {
709
+ x: MARGIN,
710
+ y: 0.3 + TITLE_H - 0.05,
711
+ w: CONTENT_W,
712
+ h: 0.04,
713
+ fill: { color: theme.colors.primary }
714
+ });
715
+ slide.addText(node.title, {
716
+ x: MARGIN,
717
+ y: 0.3,
718
+ w: CONTENT_W,
719
+ h: TITLE_H,
720
+ fontSize: theme.fontSize.heading,
721
+ fontFace: theme.fonts.heading,
722
+ color: theme.colors.primary,
723
+ bold: true,
724
+ valign: "bottom"
725
+ });
726
+ }
727
+ const grid = splitGrid(prepareElements(node));
728
+ const rows = grid.length;
729
+ const availH = SLIDE_H - top - BOTTOM_MARGIN - GRID_GAP * (rows - 1);
730
+ const ests = grid.map(
731
+ (columns) => Math.max(
732
+ 0.8,
733
+ ...columns.map((c) => estimateColumnHeight(c, (CONTENT_W - GRID_GAP * (columns.length - 1)) / columns.length))
734
+ )
735
+ );
736
+ const totalEst = ests.reduce((a, b) => a + b, 0);
737
+ const scale = totalEst > availH ? availH / totalEst : 1;
738
+ let y = top;
739
+ for (let r = 0; r < rows; r++) {
740
+ const columns = grid[r];
741
+ const cols = columns.length;
742
+ const colW = (CONTENT_W - GRID_GAP * (cols - 1)) / cols;
743
+ const rowH = ests[r] * scale;
744
+ for (let c = 0; c < cols; c++) {
745
+ await renderColumn(
746
+ slide,
747
+ columns[c],
748
+ { x: MARGIN + c * (colW + GRID_GAP), y, w: colW, h: rowH },
749
+ node,
750
+ theme,
751
+ ctx
752
+ );
753
+ }
754
+ y += rowH + GRID_GAP;
755
+ }
756
+ }
757
+ function prepareElements(node) {
758
+ const chart = node.directives?.chart;
759
+ if (!chart) return node.elements;
760
+ const elements = [...node.elements];
761
+ const idx = elements.findIndex((e) => e.type === "table");
762
+ if (idx === -1) return elements;
763
+ const table = elements[idx];
764
+ try {
765
+ elements[idx] = {
766
+ type: "diagram",
767
+ diagramType: "echarts",
768
+ content: tableToChartOption(table, chart)
769
+ };
770
+ } catch {
771
+ }
772
+ return elements;
773
+ }
774
+ function estimateTextLines(content, w) {
775
+ const charsPerLine = Math.max(20, Math.floor(w * 60));
776
+ return Math.max(1, Math.ceil(content.length / charsPerLine));
777
+ }
778
+ function estimateColumnHeight(elements, w) {
779
+ let h = 0;
780
+ for (const el of elements) {
781
+ switch (el.type) {
782
+ case "text":
783
+ h += Math.max(0.5, Math.ceil(el.content.length / Math.max(20, Math.floor(w * 60))) * 0.35);
784
+ break;
785
+ case "heading":
786
+ h += 0.7;
787
+ break;
788
+ case "list":
789
+ h += el.items.length * 0.5;
790
+ break;
791
+ case "code":
792
+ h += Math.max(1, el.content.split("\n").length * 0.25);
793
+ break;
794
+ case "diagram":
795
+ case "image":
796
+ if (el.type === "image" && el.height && !el.height.endsWith("%")) {
797
+ h += parseFloat(el.height) + 0.2;
798
+ } else if (el.type === "image" && el.width && !el.width.endsWith("%")) {
799
+ h += parseFloat(el.width) * 0.5 + 0.2;
800
+ } else {
801
+ h += Math.min(4.5, Math.max(1.2, w * 0.5));
802
+ }
803
+ break;
804
+ case "table":
805
+ h += (el.rows.length + 1) * 0.4;
806
+ break;
807
+ case "callout":
808
+ h += Math.min(4.5, 0.72 + estimateTextLines(el.content, w) * 0.3);
809
+ break;
810
+ case "blockquote":
811
+ h += 1;
812
+ break;
813
+ case "break":
814
+ break;
815
+ default:
816
+ h += 0.5;
817
+ }
818
+ }
819
+ return h;
820
+ }
821
+ async function renderColumn(slide, elements, box, node, theme, ctx) {
822
+ const est = estimateColumnHeight(elements, box.w);
823
+ const yPos = box.h > est ? box.y + (box.h - est) / 2 : box.y;
824
+ let cursor = yPos;
825
+ for (const element of elements) {
826
+ const consumed = await renderElement(slide, element, cursor, box, node, theme, ctx);
827
+ cursor += consumed;
828
+ }
829
+ }
830
+ async function renderElement(slide, element, yPos, box, node, theme, ctx) {
831
+ const { x, w } = box;
832
+ const maxH = Math.max(0.5, box.y + box.h - yPos);
833
+ switch (element.type) {
834
+ case "text": {
835
+ if (!element.content.trim()) return 0;
836
+ slide.addText(element.content, {
837
+ x,
838
+ y: yPos,
839
+ w,
840
+ h: 0.6,
841
+ fontSize: theme.fontSize.body,
842
+ fontFace: theme.fonts.body,
843
+ color: theme.colors.text,
844
+ bold: element.bold,
845
+ italic: element.italic,
846
+ valign: "top",
847
+ wrap: true
848
+ });
849
+ const charsPerLine = Math.max(20, Math.floor(w * 60));
850
+ const lines = Math.ceil(element.content.length / charsPerLine);
851
+ return Math.max(0.5, lines * 0.35);
852
+ }
853
+ case "heading": {
854
+ slide.addText(element.content, {
855
+ x,
856
+ y: yPos,
857
+ w,
858
+ h: 0.6,
859
+ fontSize: theme.fontSize.heading - (element.level - 2) * 4,
860
+ fontFace: theme.fonts.heading,
861
+ color: theme.colors.primary,
862
+ bold: true
863
+ });
864
+ return 0.7;
865
+ }
866
+ case "list": {
867
+ const items = element.items.map((item, i) => {
868
+ const checked = element.checked?.[i];
869
+ const isTask = checked !== void 0;
870
+ let text = item;
871
+ const options = {
872
+ // Task items have no bullet, so they need an explicit break line;
873
+ // bulleted items break lines on their own.
874
+ breakLine: isTask ? true : void 0,
875
+ bullet: element.ordered ? { type: "number", startAt: i + 1 } : isTask ? false : true,
876
+ color: theme.colors.text,
877
+ fontSize: theme.fontSize.body,
878
+ fontFace: theme.fonts.body,
879
+ paraSpaceBefore: 4,
880
+ paraSpaceAfter: 4
881
+ };
882
+ if (isTask) {
883
+ text = `${checked ? "\u2611" : "\u2610"} ${item}`;
884
+ if (checked) options.color = theme.colors.secondary;
885
+ }
886
+ return { text, options };
887
+ });
888
+ const height = Math.max(0.8, Math.min(maxH, items.length * 0.5));
889
+ slide.addText(items, {
890
+ x,
891
+ y: yPos,
892
+ w,
893
+ h: height,
894
+ valign: "top"
895
+ });
896
+ return height + 0.15;
897
+ }
898
+ case "code": {
899
+ const runs = await ctx.highlightCode(
900
+ element.content,
901
+ element.language ?? "text",
902
+ element.highlightLines
903
+ );
904
+ const lines = element.content.split("\n").length;
905
+ const height = Math.min(maxH, Math.max(1, lines * 0.25));
906
+ slide.addText(runs, {
907
+ x,
908
+ y: yPos,
909
+ w,
910
+ h: height,
911
+ fill: { color: theme.colors.codeBackground },
912
+ fontFace: theme.fonts.code,
913
+ fontSize: theme.fontSize.code,
914
+ color: theme.colors.codeText,
915
+ valign: "top",
916
+ margin: [8, 12, 8, 12]
917
+ });
918
+ return height + 0.2;
919
+ }
920
+ case "diagram": {
921
+ try {
922
+ const pngBuffer = await ctx.renderDiagram(element.diagramType, element.content);
923
+ const imgSize = getPngSize(pngBuffer) ?? { width: 8, height: 3.5 };
924
+ const boxH = Math.max(1.2, maxH);
925
+ const fitted = fitInBox(imgSize, w, boxH);
926
+ const base64 = pngBuffer.toString("base64");
927
+ slide.addImage({
928
+ data: `image/png;base64,${base64}`,
929
+ x: x + (w - fitted.width) / 2,
930
+ y: yPos,
931
+ w: fitted.width,
932
+ h: fitted.height
933
+ });
934
+ return fitted.height + 0.2;
935
+ } catch (err) {
936
+ slide.addText(`[Diagram render error: ${err instanceof Error ? err.message : "unknown"}]`, {
937
+ x,
938
+ y: yPos,
939
+ w,
940
+ h: 0.5,
941
+ fontSize: theme.fontSize.small,
942
+ color: "FF0000",
943
+ italic: true
944
+ });
945
+ return 0.6;
946
+ }
947
+ }
948
+ case "image": {
949
+ try {
950
+ const resolved = await ctx.resolveImage(element.src);
951
+ if (!resolved.ok) {
952
+ log.warn(`[${node.title ?? "slide"}] ${resolved.error}`);
953
+ return 0;
954
+ }
955
+ const imgSize = getPngSize(readFileSync(resolved.path)) ?? { width: 6, height: 3 };
956
+ const boxH = Math.max(1.2, maxH);
957
+ const placed = fitImageWithOptions(
958
+ imgSize,
959
+ { width: w, height: boxH },
960
+ { width: element.width, height: element.height, align: element.align }
961
+ );
962
+ const ext = resolved.path.split(".").pop()?.toLowerCase() ?? "png";
963
+ const mime = IMAGE_MIME[ext] ?? "image/png";
964
+ const data = `${mime};base64,${readFileSync(resolved.path).toString("base64")}`;
965
+ slide.addImage({
966
+ data,
967
+ path: "preencoded.png",
968
+ x: x + placed.x,
969
+ y: yPos,
970
+ w: placed.width,
971
+ h: placed.height
972
+ });
973
+ return placed.height + 0.2;
974
+ } catch (err) {
975
+ log.warn(
976
+ `Failed to embed image ${element.src}: ${err instanceof Error ? err.message : String(err)}`
977
+ );
978
+ }
979
+ return 0;
980
+ }
981
+ case "table": {
982
+ const headerRow = element.headers.map((h) => ({
983
+ text: h,
984
+ options: {
985
+ bold: true,
986
+ color: "FFFFFF",
987
+ fill: { color: theme.colors.primary },
988
+ fontSize: theme.fontSize.body - 2,
989
+ fontFace: theme.fonts.body,
990
+ align: "center",
991
+ border: { type: "solid", pt: 0.5, color: theme.colors.primary }
992
+ }
993
+ }));
994
+ const bodyRows = element.rows.map(
995
+ (row) => row.map((cell) => ({
996
+ text: cell,
997
+ options: {
998
+ fontSize: theme.fontSize.body - 2,
999
+ fontFace: theme.fonts.body,
1000
+ color: theme.colors.text,
1001
+ border: { type: "solid", pt: 0.5, color: "D1D5DB" }
1002
+ }
1003
+ }))
1004
+ );
1005
+ const allRows = [headerRow, ...bodyRows];
1006
+ const tableH = Math.min(maxH, allRows.length * 0.4);
1007
+ slide.addTable(allRows, {
1008
+ x,
1009
+ y: yPos,
1010
+ w,
1011
+ h: tableH,
1012
+ colW: Array(element.headers.length).fill(w / element.headers.length),
1013
+ margin: [4, 6, 4, 6],
1014
+ border: { type: "solid", pt: 0.5, color: "D1D5DB" },
1015
+ autoPage: false
1016
+ });
1017
+ return tableH + 0.2;
1018
+ }
1019
+ case "blockquote": {
1020
+ slide.addShape("rect", {
1021
+ x,
1022
+ y: yPos,
1023
+ w: 0.06,
1024
+ h: Math.min(0.8, maxH),
1025
+ fill: { color: theme.colors.accent }
1026
+ });
1027
+ slide.addText(element.content, {
1028
+ x: x + 0.2,
1029
+ y: yPos,
1030
+ w: w - 0.2,
1031
+ h: Math.min(0.8, maxH),
1032
+ fontSize: theme.fontSize.body,
1033
+ fontFace: theme.fonts.body,
1034
+ color: theme.colors.secondary,
1035
+ italic: true,
1036
+ valign: "middle"
1037
+ });
1038
+ return 1;
1039
+ }
1040
+ case "callout": {
1041
+ const palette = {
1042
+ note: theme.colors.primary,
1043
+ info: theme.colors.primary,
1044
+ tip: theme.colors.accent,
1045
+ success: theme.colors.accent,
1046
+ warning: "EAB308",
1047
+ caution: "EAB308",
1048
+ danger: "DC2626"
1049
+ };
1050
+ const color = palette[element.variant] ?? theme.colors.primary;
1051
+ const label = element.title ?? element.variant.toUpperCase();
1052
+ const textLines = estimateTextLines(element.content, w);
1053
+ const cardH = Math.min(maxH, 0.72 + textLines * 0.3);
1054
+ const textH = cardH - 0.44;
1055
+ slide.addShape("roundRect", {
1056
+ x,
1057
+ y: yPos,
1058
+ w,
1059
+ h: cardH,
1060
+ fill: { color, transparency: 88 },
1061
+ line: { color, width: 0.75, transparency: 70 },
1062
+ rectRadius: 0.08
1063
+ });
1064
+ slide.addShape("rect", {
1065
+ x,
1066
+ y: yPos + 0.12,
1067
+ w: 0.07,
1068
+ h: cardH - 0.24,
1069
+ fill: { color }
1070
+ });
1071
+ slide.addText(label, {
1072
+ x: x + 0.2,
1073
+ y: yPos + 0.08,
1074
+ w: w - 0.4,
1075
+ h: 0.3,
1076
+ fontSize: theme.fontSize.small,
1077
+ fontFace: theme.fonts.heading,
1078
+ color,
1079
+ bold: true
1080
+ });
1081
+ slide.addText(element.content, {
1082
+ x: x + 0.2,
1083
+ y: yPos + 0.4,
1084
+ w: w - 0.4,
1085
+ h: textH,
1086
+ fontSize: theme.fontSize.body - 1,
1087
+ fontFace: theme.fonts.body,
1088
+ color: theme.colors.text,
1089
+ valign: "top",
1090
+ wrap: true
1091
+ });
1092
+ return cardH + 0.1;
1093
+ }
1094
+ default:
1095
+ return 0;
1096
+ }
1097
+ }
1098
+
1099
+ // src/renderer/layouts/code.ts
1100
+ async function renderCodeSlide(slide, node, theme, ctx) {
1101
+ let yPos = 0.3;
1102
+ if (node.title) {
1103
+ slide.addText(node.title, {
1104
+ x: 0.5,
1105
+ y: yPos,
1106
+ w: 12.3,
1107
+ h: 0.7,
1108
+ fontSize: theme.fontSize.heading,
1109
+ fontFace: theme.fonts.heading,
1110
+ color: theme.colors.primary,
1111
+ bold: true
1112
+ });
1113
+ yPos += 0.9;
1114
+ }
1115
+ for (const element of node.elements) {
1116
+ if (element.type === "code") {
1117
+ const runs = await ctx.highlightCode(
1118
+ element.content,
1119
+ element.language ?? "text",
1120
+ element.highlightLines
1121
+ );
1122
+ slide.addText(runs, {
1123
+ x: 0.5,
1124
+ y: yPos,
1125
+ w: 12.3,
1126
+ h: 7.5 - yPos - 0.5,
1127
+ fill: { color: theme.colors.codeBackground },
1128
+ color: theme.colors.codeText,
1129
+ fontFace: theme.fonts.code,
1130
+ fontSize: theme.fontSize.code,
1131
+ valign: "top",
1132
+ paraSpaceAfter: 2,
1133
+ margin: [10, 15, 10, 15]
1134
+ });
1135
+ yPos += 5;
1136
+ }
1137
+ }
1138
+ }
1139
+
1140
+ // src/renderer/layouts/quote.ts
1141
+ function renderQuoteSlide(slide, node, theme) {
1142
+ slide.addText("\u201C", {
1143
+ x: 1.5,
1144
+ y: 1,
1145
+ w: 2,
1146
+ h: 1.5,
1147
+ fontSize: 72,
1148
+ fontFace: "Georgia",
1149
+ color: theme.colors.accent,
1150
+ bold: true
1151
+ });
1152
+ const quoteElement = node.elements.find((e) => e.type === "blockquote");
1153
+ if (quoteElement && quoteElement.type === "blockquote") {
1154
+ let quoteText = quoteElement.content;
1155
+ let attribution = "";
1156
+ const attrMatch = quoteText.match(/\n?\s*[—\u2014-]\s*(.+)$/);
1157
+ if (attrMatch) {
1158
+ attribution = attrMatch[1].trim();
1159
+ quoteText = quoteText.slice(0, attrMatch.index).trim();
1160
+ }
1161
+ slide.addText(quoteText, {
1162
+ x: 1.8,
1163
+ y: 2.5,
1164
+ w: 9.7,
1165
+ h: 2.5,
1166
+ fontSize: theme.fontSize.heading,
1167
+ fontFace: theme.fonts.body,
1168
+ color: theme.colors.text,
1169
+ italic: true,
1170
+ align: "center",
1171
+ valign: "middle"
1172
+ });
1173
+ if (attribution) {
1174
+ slide.addText(`\u2014 ${attribution}`, {
1175
+ x: 1.8,
1176
+ y: 5.2,
1177
+ w: 9.7,
1178
+ h: 0.6,
1179
+ fontSize: theme.fontSize.body,
1180
+ fontFace: theme.fonts.body,
1181
+ color: theme.colors.secondary,
1182
+ align: "right"
1183
+ });
1184
+ }
1185
+ }
1186
+ }
1187
+
1188
+ // src/renderer/layouts/index.ts
1189
+ async function renderSlideLayout(slide, node, theme, ctx) {
1190
+ if (node.directives?.background) {
1191
+ const resolved = await ctx.resolveImage(node.directives.background);
1192
+ if (resolved.ok && resolved.path) {
1193
+ slide.background = { path: resolved.path };
1194
+ } else if (node.directives.background.startsWith("#")) {
1195
+ slide.background = { color: node.directives.background.replace(/^#/, "") };
1196
+ } else if (!resolved.ok) {
1197
+ log.warn(`[${node.title ?? "slide"}] ${resolved.error}`);
1198
+ }
1199
+ }
1200
+ switch (node.layout) {
1201
+ case "title":
1202
+ renderTitleSlide(slide, node, theme);
1203
+ break;
1204
+ case "section":
1205
+ renderSectionSlide(slide, node, theme);
1206
+ break;
1207
+ case "code":
1208
+ await renderCodeSlide(slide, node, theme, ctx);
1209
+ break;
1210
+ case "quote":
1211
+ renderQuoteSlide(slide, node, theme);
1212
+ break;
1213
+ case "content":
1214
+ default:
1215
+ await renderContentSlide(slide, node, theme, ctx);
1216
+ break;
1217
+ }
1218
+ if (node.notes) {
1219
+ slide.addNotes(node.notes);
1220
+ }
1221
+ if (node.layout !== "title" && node.layout !== "closing" && node.layout !== "blank") {
1222
+ const footerText = renderFooter(node, theme, ctx);
1223
+ if (footerText) {
1224
+ slide.addText(footerText, {
1225
+ x: 0.6,
1226
+ y: 7.16,
1227
+ w: 12.13,
1228
+ h: 0.28,
1229
+ fontSize: theme.fontSize.small - 1,
1230
+ fontFace: theme.fonts.body,
1231
+ color: theme.colors.secondary,
1232
+ align: "right",
1233
+ valign: "middle"
1234
+ });
1235
+ }
1236
+ }
1237
+ }
1238
+ function renderFooter(node, theme, ctx) {
1239
+ const template = ctx.footerTemplate;
1240
+ if (!template) return "";
1241
+ return template.replace("{page}", String(ctx.pageNumber)).replace("{total}", String(ctx.totalSlides)).replace("{section}", ctx.currentSection || "").replace("{title}", node.title ?? "");
1242
+ }
1243
+
1244
+ // src/renderer/code-highlighter.ts
1245
+ import { createHighlighter } from "shiki";
1246
+ var highlighterInstance = null;
1247
+ async function getHighlighter() {
1248
+ if (!highlighterInstance) {
1249
+ highlighterInstance = await createHighlighter({
1250
+ themes: ["github-dark", "dracula"],
1251
+ langs: [
1252
+ "javascript",
1253
+ "typescript",
1254
+ "python",
1255
+ "java",
1256
+ "c",
1257
+ "cpp",
1258
+ "csharp",
1259
+ "go",
1260
+ "rust",
1261
+ "ruby",
1262
+ "php",
1263
+ "swift",
1264
+ "kotlin",
1265
+ "bash",
1266
+ "shell",
1267
+ "sql",
1268
+ "html",
1269
+ "css",
1270
+ "json",
1271
+ "yaml",
1272
+ "markdown",
1273
+ "xml",
1274
+ "docker"
1275
+ ]
1276
+ });
1277
+ }
1278
+ return highlighterInstance;
1279
+ }
1280
+ function cleanColor(color) {
1281
+ if (!color) return "E2E8F0";
1282
+ return color.replace(/^#/, "");
1283
+ }
1284
+ async function highlightCode(code, language, theme, highlightLines = []) {
1285
+ const highlighter = await getHighlighter();
1286
+ const runs = [];
1287
+ const shikiThemeName = theme.shikiTheme ?? "github-dark";
1288
+ const highlightColor = theme.colors.highlightBackground ?? "FFF3C4";
1289
+ try {
1290
+ const loadedLangs = highlighter.getLoadedLanguages();
1291
+ if (!loadedLangs.includes(language)) {
1292
+ try {
1293
+ await highlighter.loadLanguage(language);
1294
+ } catch {
1295
+ return [
1296
+ {
1297
+ text: code,
1298
+ options: {
1299
+ color: theme.colors.codeText,
1300
+ fontFace: theme.fonts.code,
1301
+ fontSize: theme.fontSize.code
1302
+ }
1303
+ }
1304
+ ];
1305
+ }
1306
+ }
1307
+ const loadedThemes = highlighter.getLoadedThemes();
1308
+ if (!loadedThemes.includes(shikiThemeName)) {
1309
+ try {
1310
+ await highlighter.loadTheme(shikiThemeName);
1311
+ } catch {
1312
+ }
1313
+ }
1314
+ const effectiveTheme = highlighter.getLoadedThemes().includes(shikiThemeName) ? shikiThemeName : "github-dark";
1315
+ const result = highlighter.codeToTokens(code, {
1316
+ lang: language,
1317
+ theme: effectiveTheme
1318
+ });
1319
+ for (let i = 0; i < result.tokens.length; i++) {
1320
+ const lineNumber = i + 1;
1321
+ const line = result.tokens[i];
1322
+ const isHighlighted = highlightLines.includes(lineNumber);
1323
+ for (const token of line) {
1324
+ const options = {
1325
+ color: cleanColor(token.color),
1326
+ fontFace: theme.fonts.code,
1327
+ fontSize: theme.fontSize.code
1328
+ };
1329
+ if (isHighlighted) {
1330
+ options.highlight = highlightColor;
1331
+ }
1332
+ runs.push({ text: token.content, options });
1333
+ }
1334
+ if (i < result.tokens.length - 1) {
1335
+ runs.push({
1336
+ text: "\n",
1337
+ options: {
1338
+ fontFace: theme.fonts.code,
1339
+ fontSize: theme.fontSize.code
1340
+ }
1341
+ });
1342
+ }
1343
+ }
1344
+ } catch {
1345
+ runs.push({
1346
+ text: code,
1347
+ options: {
1348
+ color: theme.colors.codeText,
1349
+ fontFace: theme.fonts.code,
1350
+ fontSize: theme.fontSize.code
1351
+ }
1352
+ });
1353
+ }
1354
+ return runs;
1355
+ }
1356
+
1357
+ // src/renderer/image-handler.ts
1358
+ import { existsSync, writeFileSync } from "fs";
1359
+ import { resolve } from "path";
1360
+ import { tmpdir } from "os";
1361
+ import { join } from "path";
1362
+ import { randomUUID } from "crypto";
1363
+ var REMOTE_FETCH_TIMEOUT_MS = 1e4;
1364
+ async function resolveImage(src, basePath) {
1365
+ try {
1366
+ if (src.startsWith("data:")) {
1367
+ const match = src.match(/^data:image\/(\w+);base64,(.+)/);
1368
+ if (match) {
1369
+ const ext = match[1];
1370
+ const base64 = match[2];
1371
+ const tmpPath = join(tmpdir(), `mfly-${randomUUID().slice(0, 8)}.${ext}`);
1372
+ writeFileSync(tmpPath, Buffer.from(base64, "base64"));
1373
+ return { ok: true, path: tmpPath };
1374
+ }
1375
+ return { ok: false, error: `Unsupported data URI: ${src.slice(0, 48)}` };
1376
+ }
1377
+ if (src.startsWith("http://") || src.startsWith("https://")) {
1378
+ let response;
1379
+ try {
1380
+ response = await fetch(src, { signal: AbortSignal.timeout(REMOTE_FETCH_TIMEOUT_MS) });
1381
+ } catch (err) {
1382
+ const name = err instanceof Error ? err.name : "";
1383
+ const reason = name === "TimeoutError" || name === "AbortError" ? `request timed out after ${REMOTE_FETCH_TIMEOUT_MS / 1e3}s` : err instanceof Error ? err.message : String(err);
1384
+ return { ok: false, error: `Failed to fetch ${src}: ${reason}` };
1385
+ }
1386
+ if (!response.ok) {
1387
+ return { ok: false, error: `Failed to fetch ${src}: HTTP ${response.status}` };
1388
+ }
1389
+ const buffer = Buffer.from(await response.arrayBuffer());
1390
+ const ext = src.split(".").pop()?.split("?")[0] ?? "png";
1391
+ const tmpPath = join(tmpdir(), `mfly-${randomUUID().slice(0, 8)}.${ext}`);
1392
+ writeFileSync(tmpPath, buffer);
1393
+ return { ok: true, path: tmpPath };
1394
+ }
1395
+ const absolutePath = resolve(basePath, src);
1396
+ if (existsSync(absolutePath)) {
1397
+ return { ok: true, path: absolutePath };
1398
+ }
1399
+ return { ok: false, error: `Image not found: ${src} (resolved to ${absolutePath})` };
1400
+ } catch (err) {
1401
+ return {
1402
+ ok: false,
1403
+ error: err instanceof Error ? err.message : String(err)
1404
+ };
1405
+ }
1406
+ }
1407
+
1408
+ // src/diagrams/svg-to-png.ts
1409
+ function normalizeSvg(svg) {
1410
+ let clean = svg.trim();
1411
+ if (!clean.includes("xmlns=")) {
1412
+ clean = clean.replace(/<svg\b([^>]*)>/i, '<svg xmlns="http://www.w3.org/2000/svg" $1>');
1413
+ }
1414
+ clean = clean.replace(/(<svg\b[^>]*)\s+width=["'][^"']*["']/gi, "$1");
1415
+ clean = clean.replace(/(<svg\b[^>]*)\s+height=["'][^"']*["']/gi, "$1");
1416
+ clean = clean.replace(/<marker\b[\s\S]*?<\/marker>/gi, "");
1417
+ clean = clean.replace(/\s+marker-(end|start|mid)=["'][^"']*["']/gi, "");
1418
+ const viewBoxMatch = clean.match(/viewBox\s*=\s*["']([^"']+)["']/i);
1419
+ if (viewBoxMatch) {
1420
+ const parts = viewBoxMatch[1].trim().split(/[\s,]+/).map(Number);
1421
+ if (parts.length === 4 && parts[2] > 0 && parts[3] > 0) {
1422
+ const w = parts[2];
1423
+ const h = parts[3];
1424
+ clean = clean.replace(/<svg\b([^>]*)>/i, `<svg width="${w}" height="${h}" $1>`);
1425
+ }
1426
+ } else {
1427
+ clean = clean.replace(/<svg\b([^>]*)>/i, `<svg width="800" height="600" viewBox="0 0 800 600" $1>`);
1428
+ }
1429
+ return clean;
1430
+ }
1431
+ async function svgToPng(svg, width = 800) {
1432
+ const normalized = normalizeSvg(svg);
1433
+ const { Resvg } = await import("@resvg/resvg-js");
1434
+ const resvg = new Resvg(normalized, {
1435
+ fitTo: { mode: "width", value: width }
1436
+ });
1437
+ const pngData = resvg.render();
1438
+ return Buffer.from(pngData.asPng());
1439
+ }
1440
+
1441
+ // src/diagrams/theme.ts
1442
+ function isDarkColor(hex) {
1443
+ const clean = hex.replace(/^#/, "");
1444
+ if (clean.length < 6) return false;
1445
+ const r = parseInt(clean.slice(0, 2), 16);
1446
+ const g = parseInt(clean.slice(2, 4), 16);
1447
+ const b = parseInt(clean.slice(4, 6), 16);
1448
+ if ([r, g, b].some((n) => Number.isNaN(n))) return false;
1449
+ return 0.299 * r + 0.587 * g + 0.114 * b < 128;
1450
+ }
1451
+ function isDarkTheme(theme) {
1452
+ return theme ? isDarkColor(theme.colors.background) : false;
1453
+ }
1454
+ function diagramFontFamily(theme) {
1455
+ if (!theme) return '"trebuchet ms", sans-serif';
1456
+ return `"${theme.fonts.cjk}", "${theme.fonts.body}", sans-serif`;
1457
+ }
1458
+
1459
+ // src/diagrams/mermaid-renderer.ts
1460
+ var initialized = false;
1461
+ var IDENTITY = [1, 0, 0, 1, 0, 0];
1462
+ var SKIP_TAGS = /* @__PURE__ */ new Set([
1463
+ "style",
1464
+ "script",
1465
+ "defs",
1466
+ "clippath",
1467
+ "marker",
1468
+ "filter",
1469
+ "mask",
1470
+ "pattern",
1471
+ "foreignobject"
1472
+ ]);
1473
+ function mul(m1, m2) {
1474
+ return [
1475
+ m1[0] * m2[0] + m1[2] * m2[1],
1476
+ m1[1] * m2[0] + m1[3] * m2[1],
1477
+ m1[0] * m2[2] + m1[2] * m2[3],
1478
+ m1[1] * m2[2] + m1[3] * m2[3],
1479
+ m1[0] * m2[4] + m1[2] * m2[5] + m1[4],
1480
+ m1[1] * m2[4] + m1[3] * m2[5] + m1[5]
1481
+ ];
1482
+ }
1483
+ function parseTransform(attr) {
1484
+ if (!attr) return IDENTITY;
1485
+ let m = IDENTITY;
1486
+ const re = /([a-zA-Z]+)\s*\(([^)]*)\)/g;
1487
+ let mt;
1488
+ while (mt = re.exec(attr)) {
1489
+ const name = mt[1];
1490
+ const args = mt[2].trim().split(/[\s,]+/).filter(Boolean).map(Number);
1491
+ if (args.some((n) => !Number.isFinite(n))) continue;
1492
+ let next = null;
1493
+ switch (name) {
1494
+ case "matrix":
1495
+ if (args.length >= 6) next = [args[0], args[1], args[2], args[3], args[4], args[5]];
1496
+ break;
1497
+ case "translate":
1498
+ next = [1, 0, 0, 1, args[0] || 0, args[1] || 0];
1499
+ break;
1500
+ case "scale": {
1501
+ const sx = args[0] || 1;
1502
+ const sy = args.length > 1 ? args[1] || 1 : sx;
1503
+ next = [sx, 0, 0, sy, 0, 0];
1504
+ break;
1505
+ }
1506
+ case "rotate": {
1507
+ const rad = (args[0] || 0) * Math.PI / 180;
1508
+ const cos = Math.cos(rad);
1509
+ const sin = Math.sin(rad);
1510
+ const rot = [cos, sin, -sin, cos, 0, 0];
1511
+ next = args.length >= 3 ? mul(mul([1, 0, 0, 1, args[1], args[2]], rot), [1, 0, 0, 1, -args[1], -args[2]]) : rot;
1512
+ break;
1513
+ }
1514
+ case "skewX":
1515
+ next = [1, 0, Math.tan((args[0] || 0) * Math.PI / 180), 1, 0, 0];
1516
+ break;
1517
+ case "skewY":
1518
+ next = [1, Math.tan((args[0] || 0) * Math.PI / 180), 0, 1, 0, 0];
1519
+ break;
1520
+ }
1521
+ if (next) m = mul(m, next);
1522
+ }
1523
+ return m;
1524
+ }
1525
+ function expandedBox(m, b) {
1526
+ let minX = Infinity;
1527
+ let minY = Infinity;
1528
+ let maxX = -Infinity;
1529
+ let maxY = -Infinity;
1530
+ const pts = [
1531
+ [b.x, b.y],
1532
+ [b.x + b.width, b.y],
1533
+ [b.x, b.y + b.height],
1534
+ [b.x + b.width, b.y + b.height]
1535
+ ];
1536
+ for (const [px, py] of pts) {
1537
+ const x = m[0] * px + m[2] * py + m[4];
1538
+ const y = m[1] * px + m[3] * py + m[5];
1539
+ if (x < minX) minX = x;
1540
+ if (y < minY) minY = y;
1541
+ if (x > maxX) maxX = x;
1542
+ if (y > maxY) maxY = y;
1543
+ }
1544
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
1545
+ }
1546
+ function textWidth(el) {
1547
+ let width = 0;
1548
+ const collect = (e) => {
1549
+ for (const child of Array.from(e.childNodes)) {
1550
+ if (child.nodeType === 3) {
1551
+ for (const ch of child.textContent ?? "") {
1552
+ width += (ch.codePointAt(0) ?? 0) > 11904 ? 16 : 9;
1553
+ }
1554
+ } else if (child.nodeType === 1) {
1555
+ collect(child);
1556
+ }
1557
+ }
1558
+ };
1559
+ collect(el);
1560
+ return width;
1561
+ }
1562
+ function pathBounds(d) {
1563
+ const re = /([a-zA-Z])|(-?\d*\.?\d+(?:e[-+]?\d+)?)/gi;
1564
+ const toks = [];
1565
+ let m;
1566
+ while (m = re.exec(d)) toks.push(m[1] ?? parseFloat(m[2]));
1567
+ let minX = Infinity;
1568
+ let minY = Infinity;
1569
+ let maxX = -Infinity;
1570
+ let maxY = -Infinity;
1571
+ const add = (x2, y2) => {
1572
+ if (x2 < minX) minX = x2;
1573
+ if (y2 < minY) minY = y2;
1574
+ if (x2 > maxX) maxX = x2;
1575
+ if (y2 > maxY) maxY = y2;
1576
+ };
1577
+ let i = 0;
1578
+ let x = 0;
1579
+ let y = 0;
1580
+ let sx = 0;
1581
+ let sy = 0;
1582
+ let lastCmd = "";
1583
+ let lastRel = false;
1584
+ const nums = (count) => {
1585
+ const out = [];
1586
+ while (count-- > 0) {
1587
+ const t = toks[i++];
1588
+ if (typeof t !== "number") return null;
1589
+ out.push(t);
1590
+ }
1591
+ return out;
1592
+ };
1593
+ while (i < toks.length) {
1594
+ let cmd = toks[i];
1595
+ let rel;
1596
+ if (typeof cmd === "string") {
1597
+ rel = cmd === cmd.toLowerCase();
1598
+ cmd = cmd.toUpperCase();
1599
+ lastCmd = cmd;
1600
+ lastRel = rel;
1601
+ i++;
1602
+ } else {
1603
+ cmd = lastCmd;
1604
+ rel = lastRel;
1605
+ }
1606
+ switch (cmd) {
1607
+ case "M": {
1608
+ const v = nums(2);
1609
+ if (!v) return null;
1610
+ x = rel ? x + v[0] : v[0];
1611
+ y = rel ? y + v[1] : v[1];
1612
+ sx = x;
1613
+ sy = y;
1614
+ add(x, y);
1615
+ lastCmd = "L";
1616
+ lastRel = rel;
1617
+ break;
1618
+ }
1619
+ case "L": {
1620
+ const v = nums(2);
1621
+ if (!v) return null;
1622
+ x = rel ? x + v[0] : v[0];
1623
+ y = rel ? y + v[1] : v[1];
1624
+ add(x, y);
1625
+ break;
1626
+ }
1627
+ case "H": {
1628
+ const v = nums(1);
1629
+ if (!v) return null;
1630
+ x = rel ? x + v[0] : v[0];
1631
+ add(x, y);
1632
+ break;
1633
+ }
1634
+ case "V": {
1635
+ const v = nums(1);
1636
+ if (!v) return null;
1637
+ y = rel ? y + v[0] : v[0];
1638
+ add(x, y);
1639
+ break;
1640
+ }
1641
+ case "C": {
1642
+ const v = nums(6);
1643
+ if (!v) return null;
1644
+ add(rel ? x + v[0] : v[0], rel ? y + v[1] : v[1]);
1645
+ add(rel ? x + v[2] : v[2], rel ? y + v[3] : v[3]);
1646
+ x = rel ? x + v[4] : v[4];
1647
+ y = rel ? y + v[5] : v[5];
1648
+ add(x, y);
1649
+ break;
1650
+ }
1651
+ case "S":
1652
+ case "Q": {
1653
+ const v = nums(4);
1654
+ if (!v) return null;
1655
+ add(rel ? x + v[0] : v[0], rel ? y + v[1] : v[1]);
1656
+ x = rel ? x + v[2] : v[2];
1657
+ y = rel ? y + v[3] : v[3];
1658
+ add(x, y);
1659
+ break;
1660
+ }
1661
+ case "T": {
1662
+ const v = nums(2);
1663
+ if (!v) return null;
1664
+ x = rel ? x + v[0] : v[0];
1665
+ y = rel ? y + v[1] : v[1];
1666
+ add(x, y);
1667
+ break;
1668
+ }
1669
+ case "A": {
1670
+ const v = nums(7);
1671
+ if (!v) return null;
1672
+ x = rel ? x + v[5] : v[5];
1673
+ y = rel ? y + v[6] : v[6];
1674
+ add(x, y);
1675
+ break;
1676
+ }
1677
+ case "Z":
1678
+ x = sx;
1679
+ y = sy;
1680
+ break;
1681
+ default:
1682
+ return null;
1683
+ }
1684
+ }
1685
+ if (!Number.isFinite(minX)) return null;
1686
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
1687
+ }
1688
+ function elementLocalBox(el) {
1689
+ const tag = el.tagName ? el.tagName.toLowerCase() : "";
1690
+ const num = (name) => {
1691
+ const v = parseFloat(el.getAttribute(name) ?? "");
1692
+ return Number.isFinite(v) ? v : 0;
1693
+ };
1694
+ switch (tag) {
1695
+ case "rect": {
1696
+ const w = num("width");
1697
+ const h = num("height");
1698
+ if (!(w > 0 && h > 0)) return null;
1699
+ return { x: num("x"), y: num("y"), width: w, height: h };
1700
+ }
1701
+ case "circle": {
1702
+ const r = num("r");
1703
+ if (!(r > 0)) return null;
1704
+ return { x: num("cx") - r, y: num("cy") - r, width: 2 * r, height: 2 * r };
1705
+ }
1706
+ case "ellipse": {
1707
+ const rx = num("rx");
1708
+ const ry = num("ry");
1709
+ if (!(rx > 0 && ry > 0)) return null;
1710
+ return { x: num("cx") - rx, y: num("cy") - ry, width: 2 * rx, height: 2 * ry };
1711
+ }
1712
+ case "line": {
1713
+ const x1 = num("x1");
1714
+ const y1 = num("y1");
1715
+ const x2 = num("x2");
1716
+ const y2 = num("y2");
1717
+ return { x: Math.min(x1, x2), y: Math.min(y1, y2), width: Math.abs(x2 - x1), height: Math.abs(y2 - y1) };
1718
+ }
1719
+ case "polygon":
1720
+ case "polyline": {
1721
+ const pts = (el.getAttribute("points") ?? "").split(/[\s,]+/).filter(Boolean).map(Number);
1722
+ if (pts.length < 4 || pts.some((n) => !Number.isFinite(n))) return null;
1723
+ let minX = Infinity;
1724
+ let minY = Infinity;
1725
+ let maxX = -Infinity;
1726
+ let maxY = -Infinity;
1727
+ for (let k = 0; k + 1 < pts.length; k += 2) {
1728
+ const px = pts[k];
1729
+ const py = pts[k + 1];
1730
+ if (px < minX) minX = px;
1731
+ if (py < minY) minY = py;
1732
+ if (px > maxX) maxX = px;
1733
+ if (py > maxY) maxY = py;
1734
+ }
1735
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
1736
+ }
1737
+ case "path":
1738
+ return pathBounds(el.getAttribute("d") ?? "");
1739
+ case "text":
1740
+ case "tspan": {
1741
+ const w = textWidth(el);
1742
+ if (w <= 4) return null;
1743
+ return { x: num("x") - w / 2, y: num("y") - 14, width: w, height: 24 };
1744
+ }
1745
+ default:
1746
+ return null;
1747
+ }
1748
+ }
1749
+ function contentBBox(root) {
1750
+ let minX = Infinity;
1751
+ let minY = Infinity;
1752
+ let maxX = -Infinity;
1753
+ let maxY = -Infinity;
1754
+ const accumulate = (b) => {
1755
+ if (b.x < minX) minX = b.x;
1756
+ if (b.y < minY) minY = b.y;
1757
+ if (b.x + b.width > maxX) maxX = b.x + b.width;
1758
+ if (b.y + b.height > maxY) maxY = b.y + b.height;
1759
+ };
1760
+ const walk = (el, m) => {
1761
+ for (const child of Array.from(el.children)) {
1762
+ const tag = child.tagName ? child.tagName.toLowerCase() : "";
1763
+ if (SKIP_TAGS.has(tag)) continue;
1764
+ const cm = mul(m, parseTransform(child.getAttribute("transform")));
1765
+ const local = elementLocalBox(child);
1766
+ if (local) {
1767
+ const b = expandedBox(cm, local);
1768
+ if (b.width > 0.5 || b.height > 0.5) accumulate(b);
1769
+ }
1770
+ walk(child, cm);
1771
+ }
1772
+ };
1773
+ walk(root, IDENTITY);
1774
+ if (!Number.isFinite(minX)) return null;
1775
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
1776
+ }
1777
+ var MermaidDiagramRenderer = class {
1778
+ type = "mermaid";
1779
+ renderCounter = 0;
1780
+ async initialize() {
1781
+ if (initialized) return;
1782
+ const { JSDOM } = await import("jsdom");
1783
+ const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>", {
1784
+ pretendToBeVisual: true
1785
+ });
1786
+ const assignGlobal = (key, value) => {
1787
+ try {
1788
+ Object.defineProperty(globalThis, key, {
1789
+ value,
1790
+ configurable: true,
1791
+ writable: true
1792
+ });
1793
+ } catch {
1794
+ globalThis[key] = value;
1795
+ }
1796
+ };
1797
+ assignGlobal("window", dom.window);
1798
+ assignGlobal("document", dom.window.document);
1799
+ assignGlobal("navigator", dom.window.navigator);
1800
+ assignGlobal("DOMParser", dom.window.DOMParser);
1801
+ assignGlobal("XMLSerializer", dom.window.XMLSerializer);
1802
+ assignGlobal("HTMLElement", dom.window.HTMLElement);
1803
+ assignGlobal("SVGElement", dom.window.SVGElement);
1804
+ assignGlobal("Element", dom.window.Element);
1805
+ assignGlobal("Node", dom.window.Node);
1806
+ assignGlobal("Text", dom.window.Text);
1807
+ assignGlobal("getComputedStyle", dom.window.getComputedStyle);
1808
+ assignGlobal("CustomEvent", dom.window.CustomEvent);
1809
+ assignGlobal("Event", dom.window.Event);
1810
+ assignGlobal("EventTarget", dom.window.EventTarget);
1811
+ assignGlobal("HTMLCollection", dom.window.HTMLCollection);
1812
+ assignGlobal("NodeList", dom.window.NodeList);
1813
+ assignGlobal("MutationObserver", dom.window.MutationObserver);
1814
+ if (dom.window.CSSStyleSheet) {
1815
+ assignGlobal("CSSStyleSheet", dom.window.CSSStyleSheet);
1816
+ } else {
1817
+ class MockCSSStyleSheet {
1818
+ cssRules = [];
1819
+ insertRule() {
1820
+ return 0;
1821
+ }
1822
+ deleteRule() {
1823
+ }
1824
+ replaceSync() {
1825
+ }
1826
+ replace() {
1827
+ return Promise.resolve(this);
1828
+ }
1829
+ }
1830
+ assignGlobal("CSSStyleSheet", MockCSSStyleSheet);
1831
+ }
1832
+ const getDirectText = (el) => {
1833
+ const tag = el.tagName ? el.tagName.toLowerCase() : "";
1834
+ if (tag === "style" || tag === "script" || tag === "defs" || tag === "marker") return "";
1835
+ let text = "";
1836
+ for (const child of Array.from(el.childNodes)) {
1837
+ if (child.nodeType === 3) {
1838
+ text += child.textContent || "";
1839
+ } else if (child.nodeType === 1) {
1840
+ const ctag = child.tagName.toLowerCase();
1841
+ if (ctag === "tspan" || ctag === "text" || ctag === "span" || ctag === "div" || ctag === "p") {
1842
+ text += getDirectText(child);
1843
+ }
1844
+ }
1845
+ }
1846
+ return text;
1847
+ };
1848
+ const toRect = (b) => ({
1849
+ x: b.x,
1850
+ y: b.y,
1851
+ width: b.width,
1852
+ height: b.height,
1853
+ top: b.y,
1854
+ right: b.x + b.width,
1855
+ bottom: b.y + b.height,
1856
+ left: b.x,
1857
+ toJSON: () => ({})
1858
+ });
1859
+ const polyfillBBox = function() {
1860
+ const tag = this.tagName ? this.tagName.toLowerCase() : "";
1861
+ if (tag === "style" || tag === "script" || tag === "defs" || tag === "clippath" || tag === "marker") {
1862
+ return toRect({ x: 0, y: 0, width: 0, height: 0 });
1863
+ }
1864
+ if (tag === "svg") {
1865
+ const box = contentBBox(this);
1866
+ if (box) return toRect(box);
1867
+ }
1868
+ const text = getDirectText(this);
1869
+ const width = text ? Math.max(30, text.length * 9 + 20) : 80;
1870
+ const height = text ? 28 : 40;
1871
+ return toRect({ x: 0, y: 0, width, height });
1872
+ };
1873
+ if (!dom.window.SVGElement.prototype.getBBox) {
1874
+ dom.window.SVGElement.prototype.getBBox = polyfillBBox;
1875
+ }
1876
+ if (!dom.window.Element.prototype.getBBox) {
1877
+ dom.window.Element.prototype.getBBox = polyfillBBox;
1878
+ }
1879
+ if (!dom.window.Element.prototype.getBoundingClientRect) {
1880
+ dom.window.Element.prototype.getBoundingClientRect = polyfillBBox;
1881
+ }
1882
+ const computeTextLength = function() {
1883
+ const text = getDirectText(this);
1884
+ return text.length * 9;
1885
+ };
1886
+ dom.window.Element.prototype.getComputedTextLength = computeTextLength;
1887
+ dom.window.SVGElement.prototype.getComputedTextLength = computeTextLength;
1888
+ dom.window.HTMLElement.prototype.getComputedTextLength = computeTextLength;
1889
+ if (dom.window.HTMLCanvasElement) {
1890
+ dom.window.HTMLCanvasElement.prototype.getContext = (() => ({
1891
+ measureText: (text) => ({ width: text.length * 8 }),
1892
+ fillRect: () => {
1893
+ },
1894
+ clearRect: () => {
1895
+ },
1896
+ getImageData: () => ({ data: new Array(4) }),
1897
+ putImageData: () => {
1898
+ },
1899
+ createImageData: () => [],
1900
+ setTransform: () => {
1901
+ },
1902
+ drawImage: () => {
1903
+ },
1904
+ save: () => {
1905
+ },
1906
+ fillText: () => {
1907
+ },
1908
+ restore: () => {
1909
+ },
1910
+ beginPath: () => {
1911
+ },
1912
+ moveTo: () => {
1913
+ },
1914
+ lineTo: () => {
1915
+ },
1916
+ closePath: () => {
1917
+ },
1918
+ stroke: () => {
1919
+ },
1920
+ translate: () => {
1921
+ },
1922
+ scale: () => {
1923
+ },
1924
+ rotate: () => {
1925
+ },
1926
+ arc: () => {
1927
+ },
1928
+ fill: () => {
1929
+ }
1930
+ }));
1931
+ }
1932
+ if (dom.window.SVGSVGElement) {
1933
+ if (!dom.window.SVGSVGElement.prototype.createSVGMatrix) {
1934
+ dom.window.SVGSVGElement.prototype.createSVGMatrix = () => ({
1935
+ a: 1,
1936
+ b: 0,
1937
+ c: 0,
1938
+ d: 1,
1939
+ e: 0,
1940
+ f: 0,
1941
+ multiply: function() {
1942
+ return this;
1943
+ },
1944
+ inverse: function() {
1945
+ return this;
1946
+ },
1947
+ translate: function() {
1948
+ return this;
1949
+ },
1950
+ scale: function() {
1951
+ return this;
1952
+ },
1953
+ rotate: function() {
1954
+ return this;
1955
+ }
1956
+ });
1957
+ }
1958
+ if (!dom.window.SVGSVGElement.prototype.createSVGPoint) {
1959
+ dom.window.SVGSVGElement.prototype.createSVGPoint = () => ({
1960
+ x: 0,
1961
+ y: 0,
1962
+ matrixTransform: function() {
1963
+ return this;
1964
+ }
1965
+ });
1966
+ }
1967
+ }
1968
+ if (!dom.window.matchMedia) {
1969
+ dom.window.matchMedia = () => ({
1970
+ matches: false,
1971
+ media: "",
1972
+ onchange: null,
1973
+ addListener: () => {
1974
+ },
1975
+ removeListener: () => {
1976
+ },
1977
+ addEventListener: () => {
1978
+ },
1979
+ removeEventListener: () => {
1980
+ },
1981
+ dispatchEvent: () => false
1982
+ });
1983
+ }
1984
+ const mermaid = (await import("mermaid")).default;
1985
+ mermaid.initialize(this.buildConfig());
1986
+ initialized = true;
1987
+ }
1988
+ buildConfig(theme) {
1989
+ return {
1990
+ startOnLoad: false,
1991
+ securityLevel: "loose",
1992
+ // Top-level htmlLabels is required: flowchart-v2 reads it (the
1993
+ // flowchart.htmlLabels key is ignored) and html labels rely on real DOM
1994
+ // layout (scrollWidth) that jsdom does not provide — with them on, nodes
1995
+ // collapse to 60x30 stubs and resvg drops the HTML content entirely.
1996
+ htmlLabels: false,
1997
+ theme: isDarkTheme(theme) ? "dark" : "default",
1998
+ themeVariables: {
1999
+ fontFamily: diagramFontFamily(theme),
2000
+ background: "transparent"
2001
+ },
2002
+ flowchart: { htmlLabels: false, curve: "basis", useMaxWidth: false },
2003
+ sequence: { useMaxWidth: false }
2004
+ };
2005
+ }
2006
+ async render(code, theme) {
2007
+ await this.initialize();
2008
+ const mermaid = (await import("mermaid")).default;
2009
+ const id = `mfly-mermaid-${Date.now()}-${this.renderCounter++}`;
2010
+ mermaid.initialize(this.buildConfig(theme));
2011
+ try {
2012
+ const { svg } = await mermaid.render(id, code.trim());
2013
+ return svgToPng(svg, 1200);
2014
+ } catch (err) {
2015
+ throw new Error(
2016
+ `Mermaid render failed: ${err instanceof Error ? err.message : String(err)}`
2017
+ );
2018
+ }
2019
+ }
2020
+ };
2021
+
2022
+ // src/diagrams/graphviz-renderer.ts
2023
+ var GraphvizDiagramRenderer = class {
2024
+ type = "dot";
2025
+ viz = null;
2026
+ async initialize() {
2027
+ if (this.viz) return;
2028
+ const vizModule = await import("@viz-js/viz");
2029
+ this.viz = await vizModule.instance();
2030
+ }
2031
+ /**
2032
+ * Dark theme support: prepend default node/edge/graph attributes so nodes,
2033
+ * edges, and labels become light-on-dark. User-specified attrs in the DOT
2034
+ * code still win because they appear after these defaults.
2035
+ */
2036
+ themeDefaults(code, dark) {
2037
+ if (!dark) return code;
2038
+ const prelude = ' graph [fontcolor="#c9d1d9" bgcolor="transparent"];\n node [color="#8b949e" fontcolor="#c9d1d9"];\n edge [color="#8b949e" fontcolor="#c9d1d9"];\n';
2039
+ return code.replace(/\{/, `{
2040
+ ${prelude}`);
2041
+ }
2042
+ async render(code, theme) {
2043
+ await this.initialize();
2044
+ try {
2045
+ const dark = isDarkTheme(theme);
2046
+ const svg = this.viz.renderString(this.themeDefaults(code.trim(), dark), {
2047
+ format: "svg",
2048
+ engine: "dot",
2049
+ graphAttributes: {
2050
+ bgcolor: "transparent"
2051
+ }
2052
+ });
2053
+ return svgToPng(svg, 1200);
2054
+ } catch (err) {
2055
+ throw new Error(
2056
+ `Graphviz render failed: ${err instanceof Error ? err.message : String(err)}`
2057
+ );
2058
+ }
2059
+ }
2060
+ };
2061
+
2062
+ // src/diagrams/echarts-renderer.ts
2063
+ var EChartsDiagramRenderer = class {
2064
+ type = "echarts";
2065
+ async initialize() {
2066
+ }
2067
+ async render(code, _theme) {
2068
+ const echarts = await import("echarts");
2069
+ let option;
2070
+ try {
2071
+ option = JSON.parse(code.trim());
2072
+ } catch {
2073
+ throw new Error("ECharts: invalid JSON option");
2074
+ }
2075
+ const width = typeof option.width === "number" ? option.width : 800;
2076
+ const height = typeof option.height === "number" ? option.height : 450;
2077
+ delete option.width;
2078
+ delete option.height;
2079
+ const chart = echarts.init(null, null, {
2080
+ renderer: "svg",
2081
+ ssr: true,
2082
+ width,
2083
+ height
2084
+ });
2085
+ try {
2086
+ chart.setOption({
2087
+ ...option,
2088
+ animation: false
2089
+ });
2090
+ const svg = chart.renderToSVGString();
2091
+ return svgToPng(svg, width * 2);
2092
+ } finally {
2093
+ chart.dispose();
2094
+ }
2095
+ }
2096
+ };
2097
+
2098
+ // src/diagrams/index.ts
2099
+ var DIAGRAM_LANGUAGES2 = {
2100
+ mermaid: () => new MermaidDiagramRenderer(),
2101
+ dot: () => new GraphvizDiagramRenderer(),
2102
+ graphviz: () => new GraphvizDiagramRenderer(),
2103
+ echarts: () => new EChartsDiagramRenderer()
2104
+ };
2105
+ var instances = /* @__PURE__ */ new Map();
2106
+ function getOrCreate(language) {
2107
+ const key = language === "graphviz" ? "dot" : language;
2108
+ if (instances.has(key)) return instances.get(key);
2109
+ const factory = DIAGRAM_LANGUAGES2[language];
2110
+ if (!factory) return void 0;
2111
+ const renderer = factory();
2112
+ instances.set(key, renderer);
2113
+ return renderer;
2114
+ }
2115
+ async function renderDiagram(language, code, theme) {
2116
+ const renderer = getOrCreate(language.toLowerCase());
2117
+ if (!renderer) {
2118
+ throw new Error(`No diagram renderer for language: ${language}`);
2119
+ }
2120
+ await renderer.initialize();
2121
+ return renderer.render(code, theme);
2122
+ }
2123
+
2124
+ // src/utils/gradient.ts
2125
+ import { deflateSync } from "zlib";
2126
+ var CRC_TABLE = (() => {
2127
+ const table = new Uint32Array(256);
2128
+ for (let n = 0; n < 256; n++) {
2129
+ let c = n;
2130
+ for (let k = 0; k < 8; k++) {
2131
+ c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
2132
+ }
2133
+ table[n] = c >>> 0;
2134
+ }
2135
+ return table;
2136
+ })();
2137
+ function crc32(bytes) {
2138
+ let crc = 4294967295;
2139
+ for (let i = 0; i < bytes.length; i++) {
2140
+ crc = CRC_TABLE[(crc ^ bytes[i]) & 255] ^ crc >>> 8;
2141
+ }
2142
+ return (crc ^ 4294967295) >>> 0;
2143
+ }
2144
+ function pngChunk(type, data) {
2145
+ const length = Buffer.alloc(4);
2146
+ length.writeUInt32BE(data.length);
2147
+ const payload = Buffer.concat([Buffer.from(type, "latin1"), Buffer.from(data)]);
2148
+ const crc = Buffer.alloc(4);
2149
+ crc.writeUInt32BE(crc32(payload));
2150
+ return Buffer.concat([length, payload, crc]);
2151
+ }
2152
+ function encodePngRgb(width, height, pixelAt) {
2153
+ const ihdr = Buffer.alloc(13);
2154
+ ihdr.writeUInt32BE(width, 0);
2155
+ ihdr.writeUInt32BE(height, 4);
2156
+ ihdr[8] = 8;
2157
+ ihdr[9] = 2;
2158
+ const raw = Buffer.alloc(height * (1 + width * 3));
2159
+ let offset = 0;
2160
+ for (let y = 0; y < height; y++) {
2161
+ raw[offset++] = 0;
2162
+ for (let x = 0; x < width; x++) {
2163
+ const [r, g, b] = pixelAt(x, y);
2164
+ raw[offset++] = r;
2165
+ raw[offset++] = g;
2166
+ raw[offset++] = b;
2167
+ }
2168
+ }
2169
+ const idat = deflateSync(raw, { level: 9 });
2170
+ return Buffer.concat([
2171
+ Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
2172
+ pngChunk("IHDR", ihdr),
2173
+ pngChunk("IDAT", new Uint8Array(idat)),
2174
+ pngChunk("IEND", new Uint8Array(0))
2175
+ ]);
2176
+ }
2177
+ function hexToRgb(hex) {
2178
+ let s = hex.replace(/^#/, "");
2179
+ if (s.length === 3) s = s.split("").map((ch) => ch + ch).join("");
2180
+ if (!/^[0-9a-fA-F]{6}$/.test(s)) throw new Error(`Invalid hex color: ${hex}`);
2181
+ return [
2182
+ parseInt(s.slice(0, 2), 16),
2183
+ parseInt(s.slice(2, 4), 16),
2184
+ parseInt(s.slice(4, 6), 16)
2185
+ ];
2186
+ }
2187
+ function createLinearGradientPng(from, to, angleDeg = 180, options = {}) {
2188
+ const width = options.width ?? 192;
2189
+ const height = options.height ?? 108;
2190
+ const angle = angleDeg * Math.PI / 180;
2191
+ const dx = Math.sin(angle);
2192
+ const dy = -Math.cos(angle);
2193
+ const extent = (Math.abs(dx) + Math.abs(dy)) / 2;
2194
+ const c1 = hexToRgb(from);
2195
+ const c2 = hexToRgb(to);
2196
+ return encodePngRgb(width, height, (x, y) => {
2197
+ const px = x / (width - 1) - 0.5;
2198
+ const py = y / (height - 1) - 0.5;
2199
+ let t = 0.5 + (px * dx + py * dy) / (2 * extent);
2200
+ t = t < 0 ? 0 : t > 1 ? 1 : t;
2201
+ return [
2202
+ Math.round(c1[0] + (c2[0] - c1[0]) * t),
2203
+ Math.round(c1[1] + (c2[1] - c1[1]) * t),
2204
+ Math.round(c1[2] + (c2[2] - c1[2]) * t)
2205
+ ];
2206
+ });
2207
+ }
2208
+
2209
+ // src/renderer/background.ts
2210
+ var cache = /* @__PURE__ */ new Map();
2211
+ function slideBackground(theme) {
2212
+ const gradient = theme.colors.backgroundGradient;
2213
+ if (!gradient) return { color: theme.colors.background };
2214
+ const key = `${gradient.from}|${gradient.to}|${gradient.angle ?? 180}`;
2215
+ let data = cache.get(key);
2216
+ if (!data) {
2217
+ const png = createLinearGradientPng(gradient.from, gradient.to, gradient.angle ?? 180, {
2218
+ width: 192,
2219
+ height: 108
2220
+ });
2221
+ data = `image/png;base64,${png.toString("base64")}`;
2222
+ cache.set(key, data);
2223
+ }
2224
+ return { color: theme.colors.background, data };
2225
+ }
2226
+
2227
+ // src/renderer/pptx-renderer.ts
2228
+ import { writeFileSync as writeFileSync2 } from "fs";
2229
+ import { dirname, resolve as resolve2 } from "path";
2230
+ async function renderPresentation(presentation, theme, outputPath, inputPath = "") {
2231
+ const pptx = new PptxGenJS();
2232
+ pptx.layout = "LAYOUT_WIDE";
2233
+ pptx.author = presentation.config.author ?? "MarkdownFly";
2234
+ pptx.title = presentation.slides[0]?.title ?? "Presentation";
2235
+ const { resourceDir } = presentation.config;
2236
+ const resourceBase = resourceDir ? resolve2(dirname(inputPath), resourceDir) : dirname(inputPath);
2237
+ const backgroundProps = slideBackground(theme);
2238
+ const ctx = {
2239
+ highlightCode: async (code, language, highlightLines = []) => {
2240
+ return highlightCode(code, language, theme, highlightLines);
2241
+ },
2242
+ resolveImage: async (src) => {
2243
+ return resolveImage(src, resourceBase);
2244
+ },
2245
+ renderDiagram: async (diagramType, code) => {
2246
+ return renderDiagram(diagramType, code, theme);
2247
+ },
2248
+ footerTemplate: presentation.config.footer,
2249
+ pageNumber: 0,
2250
+ totalSlides: presentation.slides.length,
2251
+ currentSection: ""
2252
+ };
2253
+ for (let i = 0; i < presentation.slides.length; i++) {
2254
+ const node = presentation.slides[i];
2255
+ const slide = pptx.addSlide();
2256
+ slide.background = backgroundProps;
2257
+ ctx.pageNumber = i + 1;
2258
+ if (node.layout === "section" && node.title) {
2259
+ ctx.currentSection = node.title;
2260
+ }
2261
+ await renderSlideLayout(slide, node, theme, ctx);
2262
+ }
2263
+ const buffer = await pptx.write({ outputType: "nodebuffer" });
2264
+ writeFileSync2(outputPath, Buffer.from(buffer));
2265
+ }
2266
+
2267
+ // src/themes/clean.ts
2268
+ var cleanTheme = {
2269
+ name: "clean",
2270
+ colors: {
2271
+ primary: "2563EB",
2272
+ secondary: "4B5563",
2273
+ background: "FFFFFF",
2274
+ text: "1F2937",
2275
+ accent: "10B981",
2276
+ codeBackground: "1E293B",
2277
+ codeText: "E2E8F0",
2278
+ titleBackground: "2563EB",
2279
+ titleText: "FFFFFF"
2280
+ },
2281
+ fonts: {
2282
+ heading: "Segoe UI",
2283
+ body: "Segoe UI",
2284
+ code: "Consolas",
2285
+ cjk: "\u5FAE\u8F6F\u96C5\u9ED1"
2286
+ },
2287
+ fontSize: {
2288
+ title: 36,
2289
+ heading: 28,
2290
+ body: 18,
2291
+ code: 14,
2292
+ small: 12
2293
+ },
2294
+ shikiTheme: "github-dark"
2295
+ };
2296
+
2297
+ // src/themes/academic.ts
2298
+ var academicTheme = {
2299
+ name: "academic",
2300
+ colors: {
2301
+ primary: "003366",
2302
+ secondary: "4A5568",
2303
+ background: "FFFFFF",
2304
+ text: "111827",
2305
+ accent: "991B1B",
2306
+ codeBackground: "1E293B",
2307
+ codeText: "E2E8F0",
2308
+ titleBackground: "003366",
2309
+ titleText: "FFFFFF"
2310
+ },
2311
+ fonts: {
2312
+ heading: "Times New Roman",
2313
+ body: "Segoe UI",
2314
+ code: "Consolas",
2315
+ cjk: "\u5B8B\u4F53"
2316
+ },
2317
+ fontSize: {
2318
+ title: 36,
2319
+ heading: 28,
2320
+ body: 18,
2321
+ code: 14,
2322
+ small: 12
2323
+ },
2324
+ shikiTheme: "github-dark"
2325
+ };
2326
+
2327
+ // src/themes/dark.ts
2328
+ var darkTheme = {
2329
+ name: "dark",
2330
+ colors: {
2331
+ primary: "38BDF8",
2332
+ secondary: "94A3B8",
2333
+ background: "0F172A",
2334
+ text: "F8FAFC",
2335
+ accent: "A855F7",
2336
+ codeBackground: "020617",
2337
+ codeText: "38BDF8",
2338
+ titleBackground: "0F172A",
2339
+ titleText: "38BDF8",
2340
+ highlightBackground: "334155",
2341
+ backgroundGradient: { from: "0F172A", to: "1E293B", angle: 135 }
2342
+ },
2343
+ fonts: {
2344
+ heading: "Segoe UI",
2345
+ body: "Segoe UI",
2346
+ code: "Consolas",
2347
+ cjk: "\u5FAE\u8F6F\u96C5\u9ED1"
2348
+ },
2349
+ fontSize: {
2350
+ title: 36,
2351
+ heading: 28,
2352
+ body: 18,
2353
+ code: 14,
2354
+ small: 12
2355
+ },
2356
+ shikiTheme: "dracula"
2357
+ };
2358
+
2359
+ // src/themes/business.ts
2360
+ var businessTheme = {
2361
+ name: "business",
2362
+ colors: {
2363
+ primary: "1E3A8A",
2364
+ secondary: "64748B",
2365
+ background: "F8FAFC",
2366
+ text: "0F172A",
2367
+ accent: "D97706",
2368
+ codeBackground: "0F172A",
2369
+ codeText: "E2E8F0",
2370
+ titleBackground: "1E3A8A",
2371
+ titleText: "FFFFFF"
2372
+ },
2373
+ fonts: {
2374
+ heading: "Segoe UI",
2375
+ body: "Segoe UI",
2376
+ code: "Consolas",
2377
+ cjk: "\u5FAE\u8F6F\u96C5\u9ED1"
2378
+ },
2379
+ fontSize: {
2380
+ title: 36,
2381
+ heading: 28,
2382
+ body: 18,
2383
+ code: 14,
2384
+ small: 12
2385
+ },
2386
+ shikiTheme: "github-dark"
2387
+ };
2388
+
2389
+ // src/themes/warm.ts
2390
+ var warmTheme = {
2391
+ name: "warm",
2392
+ colors: {
2393
+ primary: "065F46",
2394
+ secondary: "78716C",
2395
+ background: "FDFBF7",
2396
+ text: "292524",
2397
+ accent: "C2410C",
2398
+ codeBackground: "292524",
2399
+ codeText: "FDFBF7",
2400
+ titleBackground: "065F46",
2401
+ titleText: "292524",
2402
+ backgroundGradient: { from: "FDFBF7", to: "F5EEDF", angle: 180 }
2403
+ },
2404
+ fonts: {
2405
+ heading: "Georgia",
2406
+ body: "Segoe UI",
2407
+ code: "Consolas",
2408
+ cjk: "\u5FAE\u8F6F\u96C5\u9ED1"
2409
+ },
2410
+ fontSize: {
2411
+ title: 36,
2412
+ heading: 28,
2413
+ body: 18,
2414
+ code: 14,
2415
+ small: 12
2416
+ },
2417
+ shikiTheme: "github-dark"
2418
+ };
2419
+
2420
+ // src/themes/aurora.ts
2421
+ var auroraTheme = {
2422
+ name: "aurora",
2423
+ colors: {
2424
+ primary: "7AA2FF",
2425
+ secondary: "8FA3C9",
2426
+ background: "06091C",
2427
+ text: "E8F0FF",
2428
+ accent: "5EF2C6",
2429
+ codeBackground: "0B1128",
2430
+ codeText: "AECAF5",
2431
+ titleText: "F4F8FF",
2432
+ highlightBackground: "233152",
2433
+ backgroundGradient: { from: "101A3A", to: "05060F", angle: 135 }
2434
+ },
2435
+ fonts: {
2436
+ heading: "Segoe UI",
2437
+ body: "Segoe UI",
2438
+ code: "Consolas",
2439
+ cjk: "\u5FAE\u8F6F\u96C5\u9ED1"
2440
+ },
2441
+ fontSize: {
2442
+ title: 36,
2443
+ heading: 28,
2444
+ body: 18,
2445
+ code: 14,
2446
+ small: 12
2447
+ },
2448
+ shikiTheme: "dracula"
2449
+ };
2450
+
2451
+ // src/themes/neon.ts
2452
+ var neonTheme = {
2453
+ name: "neon",
2454
+ colors: {
2455
+ primary: "00E5FF",
2456
+ secondary: "9AA4B5",
2457
+ background: "121212",
2458
+ text: "FFFFFF",
2459
+ accent: "FF4081",
2460
+ codeBackground: "0C0C0C",
2461
+ codeText: "85E7F7",
2462
+ titleBackground: "121212",
2463
+ titleText: "00E5FF",
2464
+ highlightBackground: "2A2A32"
2465
+ },
2466
+ fonts: {
2467
+ heading: "Segoe UI",
2468
+ body: "Segoe UI",
2469
+ code: "Consolas",
2470
+ cjk: "\u5FAE\u8F6F\u96C5\u9ED1"
2471
+ },
2472
+ fontSize: {
2473
+ title: 36,
2474
+ heading: 28,
2475
+ body: 18,
2476
+ code: 14,
2477
+ small: 12
2478
+ },
2479
+ shikiTheme: "dracula"
2480
+ };
2481
+
2482
+ // src/themes/nord.ts
2483
+ var nordTheme = {
2484
+ name: "nord",
2485
+ colors: {
2486
+ primary: "88C0D0",
2487
+ secondary: "81A1C1",
2488
+ background: "2E3440",
2489
+ text: "D8DEE9",
2490
+ accent: "EBCB8B",
2491
+ codeBackground: "272C36",
2492
+ codeText: "A9BBD3",
2493
+ titleBackground: "2E3440",
2494
+ titleText: "88C0D0",
2495
+ highlightBackground: "3B4252"
2496
+ },
2497
+ fonts: {
2498
+ heading: "Segoe UI",
2499
+ body: "Segoe UI",
2500
+ code: "Consolas",
2501
+ cjk: "\u5FAE\u8F6F\u96C5\u9ED1"
2502
+ },
2503
+ fontSize: {
2504
+ title: 36,
2505
+ heading: 28,
2506
+ body: 18,
2507
+ code: 14,
2508
+ small: 12
2509
+ },
2510
+ shikiTheme: "nord"
2511
+ };
2512
+
2513
+ // src/themes/dracula.ts
2514
+ var draculaTheme = {
2515
+ name: "dracula",
2516
+ colors: {
2517
+ primary: "BD93F9",
2518
+ secondary: "6272A4",
2519
+ background: "282A36",
2520
+ text: "F8F8F2",
2521
+ accent: "FF79C6",
2522
+ codeBackground: "21222C",
2523
+ codeText: "F8F8F2",
2524
+ titleBackground: "282A36",
2525
+ titleText: "BD93F9",
2526
+ highlightBackground: "44475A"
2527
+ },
2528
+ fonts: {
2529
+ heading: "Segoe UI",
2530
+ body: "Segoe UI",
2531
+ code: "Consolas",
2532
+ cjk: "\u5FAE\u8F6F\u96C5\u9ED1"
2533
+ },
2534
+ fontSize: {
2535
+ title: 36,
2536
+ heading: 28,
2537
+ body: 18,
2538
+ code: 14,
2539
+ small: 12
2540
+ },
2541
+ shikiTheme: "dracula"
2542
+ };
2543
+
2544
+ // src/themes/beige.ts
2545
+ var beigeTheme = {
2546
+ name: "beige",
2547
+ colors: {
2548
+ primary: "8B6F3D",
2549
+ secondary: "6B6455",
2550
+ background: "F7F3DE",
2551
+ text: "2F2A1F",
2552
+ accent: "C0563C",
2553
+ codeBackground: "3A352B",
2554
+ codeText: "E8E0CC",
2555
+ titleBackground: "8B6F3D",
2556
+ titleText: "2F2A1F",
2557
+ highlightBackground: "F0E2B8",
2558
+ backgroundGradient: { from: "F7F3DE", to: "F1E8D2", angle: 180 }
2559
+ },
2560
+ fonts: {
2561
+ heading: "Georgia",
2562
+ body: "Segoe UI",
2563
+ code: "Consolas",
2564
+ cjk: "\u5FAE\u8F6F\u96C5\u9ED1"
2565
+ },
2566
+ fontSize: {
2567
+ title: 36,
2568
+ heading: 28,
2569
+ body: 18,
2570
+ code: 14,
2571
+ small: 12
2572
+ },
2573
+ shikiTheme: "github-dark"
2574
+ };
2575
+
2576
+ // src/themes/ink.ts
2577
+ var inkTheme = {
2578
+ name: "ink",
2579
+ colors: {
2580
+ primary: "2F3530",
2581
+ secondary: "6F6A5E",
2582
+ background: "F7F4EC",
2583
+ text: "262626",
2584
+ accent: "C0272D",
2585
+ codeBackground: "2B2924",
2586
+ codeText: "D8D2C0",
2587
+ titleBackground: "30352F",
2588
+ titleText: "262626",
2589
+ highlightBackground: "EBDDCD",
2590
+ backgroundGradient: { from: "F7F4EC", to: "EFE8DA", angle: 180 }
2591
+ },
2592
+ fonts: {
2593
+ heading: "KaiTi",
2594
+ body: "KaiTi",
2595
+ code: "Consolas",
2596
+ cjk: "KaiTi"
2597
+ },
2598
+ fontSize: {
2599
+ title: 36,
2600
+ heading: 28,
2601
+ body: 18,
2602
+ code: 14,
2603
+ small: 12
2604
+ },
2605
+ shikiTheme: "github-dark"
2606
+ };
2607
+
2608
+ // src/themes/default.ts
2609
+ var defaultTheme = {
2610
+ ...cleanTheme,
2611
+ name: "default"
2612
+ };
2613
+
2614
+ // src/themes/index.ts
2615
+ var themes = {
2616
+ clean: cleanTheme,
2617
+ academic: academicTheme,
2618
+ dark: darkTheme,
2619
+ business: businessTheme,
2620
+ warm: warmTheme,
2621
+ aurora: auroraTheme,
2622
+ neon: neonTheme,
2623
+ nord: nordTheme,
2624
+ dracula: draculaTheme,
2625
+ beige: beigeTheme,
2626
+ ink: inkTheme,
2627
+ default: defaultTheme
2628
+ };
2629
+ function getTheme(name) {
2630
+ if (!name) {
2631
+ return cleanTheme;
2632
+ }
2633
+ if (typeof name !== "string") {
2634
+ throw new Error(`Invalid theme value: expected a string, got ${typeof name}`);
2635
+ }
2636
+ const theme = themes[name.toLowerCase()];
2637
+ if (!theme) {
2638
+ console.warn(`Theme "${name}" not found, using "clean"`);
2639
+ return cleanTheme;
2640
+ }
2641
+ return theme;
2642
+ }
2643
+
2644
+ // src/utils/output-namer.ts
2645
+ import { existsSync as existsSync2 } from "fs";
2646
+ import { basename, extname, dirname as dirname2, join as join2 } from "path";
2647
+ import { randomUUID as randomUUID2 } from "crypto";
2648
+ function getOutputPath(inputPath, specifiedOutput) {
2649
+ if (specifiedOutput) return specifiedOutput;
2650
+ const dir = dirname2(inputPath);
2651
+ const name = basename(inputPath, extname(inputPath));
2652
+ const simple = join2(dir, `${name}.pptx`);
2653
+ if (!existsSync2(simple)) return simple;
2654
+ const now = /* @__PURE__ */ new Date();
2655
+ const ts = now.getFullYear().toString() + String(now.getMonth() + 1).padStart(2, "0") + String(now.getDate()).padStart(2, "0") + "-" + String(now.getHours()).padStart(2, "0") + String(now.getMinutes()).padStart(2, "0") + String(now.getSeconds()).padStart(2, "0");
2656
+ const withTimestamp = join2(dir, `${name}-${ts}.pptx`);
2657
+ if (!existsSync2(withTimestamp)) return withTimestamp;
2658
+ const uuid = randomUUID2().slice(0, 4);
2659
+ return join2(dir, `${name}-${ts}-${uuid}.pptx`);
2660
+ }
2661
+
2662
+ // src/index.ts
2663
+ async function convert(inputPath, options = {}) {
2664
+ const absInput = resolve3(inputPath);
2665
+ const markdown = readFileSync2(absInput, "utf-8");
2666
+ const presentation = parseMarkdown(markdown);
2667
+ if (options.theme) presentation.config.theme = options.theme;
2668
+ const theme = getTheme(presentation.config.theme);
2669
+ const outputPath = resolve3(getOutputPath(absInput, options.output));
2670
+ await renderPresentation(presentation, theme, outputPath, absInput);
2671
+ return outputPath;
2672
+ }
2673
+
2674
+ // src/utils/glob.ts
2675
+ import fg from "fast-glob";
2676
+ async function expandGlob(patterns) {
2677
+ const normalized = patterns.map((p) => p.replace(/\\/g, "/"));
2678
+ const files = await fg(normalized, {
2679
+ absolute: true,
2680
+ onlyFiles: true
2681
+ });
2682
+ return files.filter((f) => f.toLowerCase().endsWith(".md")).sort();
2683
+ }
2684
+
2685
+ // src/cli.ts
2686
+ var pkg = JSON.parse(
2687
+ readFileSync3(new URL("../package.json", import.meta.url), "utf-8")
2688
+ );
2689
+ var program = new Command();
2690
+ program.name("mfly").description("Markdown to PowerPoint (PPTX)").version(pkg.version ?? "0.0.0");
2691
+ var themeChoices = Object.keys(themes).filter((name) => name !== "default");
2692
+ program.argument("<files...>", "Markdown files to convert (supports glob)").option("-o, --output <path>", "Output file path (single file only)").option("-t, --theme <name>", `Theme name (${themeChoices.join(", ")})`).option("--quiet", "Suppress per-file progress output").option("--json", "Print machine-readable JSON result to stdout").action(async (filePatterns, options) => {
2693
+ const jsonMode = Boolean(options.json);
2694
+ if (jsonMode || options.quiet) setQuiet(true);
2695
+ const usageError = (msg) => {
2696
+ log.error(msg);
2697
+ if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
2698
+ process.exit(1);
2699
+ };
2700
+ try {
2701
+ const files = await expandGlob(filePatterns);
2702
+ if (files.length === 0) {
2703
+ usageError("No .md files found matching the given pattern(s)");
2704
+ }
2705
+ if (options.output && files.length > 1) {
2706
+ usageError("--output can only be used with a single input file");
2707
+ }
2708
+ if (options.theme && !themes[options.theme.toLowerCase()]) {
2709
+ usageError(`Unknown theme "${options.theme}". Available themes: ${themeChoices.join(", ")}`);
2710
+ }
2711
+ const startTime = Date.now();
2712
+ const results = [];
2713
+ for (const file of files) {
2714
+ const progress = new ProgressReporter();
2715
+ const fileName = file.split(/[\\/]/).pop() ?? file;
2716
+ progress.start(`Converting ${chalk2.cyan(fileName)}...`);
2717
+ try {
2718
+ const outputPath = await convert(file, {
2719
+ output: options.output,
2720
+ theme: options.theme
2721
+ });
2722
+ const outName = outputPath.split(/[\\/]/).pop() ?? outputPath;
2723
+ progress.succeed(`${chalk2.cyan(fileName)} \u2192 ${chalk2.green(outName)}`);
2724
+ results.push({ input: file, output: outputPath, ok: true });
2725
+ } catch (err) {
2726
+ const msg = err instanceof Error ? err.message : String(err);
2727
+ progress.fail(`${chalk2.cyan(fileName)}: ${chalk2.red(msg)}`);
2728
+ results.push({ input: file, ok: false, error: msg });
2729
+ }
2730
+ }
2731
+ const failed = results.filter((r) => !r.ok);
2732
+ if (jsonMode) {
2733
+ console.log(JSON.stringify({
2734
+ ok: failed.length === 0,
2735
+ durationMs: Date.now() - startTime,
2736
+ files: results
2737
+ }));
2738
+ } else {
2739
+ if (failed.length > 0) {
2740
+ log.error(`${failed.length} of ${files.length} file(s) failed`);
2741
+ }
2742
+ const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
2743
+ log.info(`Done in ${elapsed}s`);
2744
+ }
2745
+ if (failed.length > 0) {
2746
+ process.exit(1);
2747
+ }
2748
+ } catch (err) {
2749
+ const msg = err instanceof Error ? err.message : String(err);
2750
+ log.error(msg);
2751
+ if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
2752
+ process.exit(1);
2753
+ }
2754
+ });
2755
+ program.parse();
2756
+ //# sourceMappingURL=cli.js.map