markdownfly 0.1.0 → 0.2.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 CHANGED
@@ -3,10 +3,10 @@
3
3
  // src/cli.ts
4
4
  import { Command } from "commander";
5
5
  import chalk2 from "chalk";
6
- import { readFileSync as readFileSync3 } from "fs";
6
+ import { readFileSync as readFileSync5 } from "fs";
7
7
 
8
8
  // src/index.ts
9
- import { readFileSync as readFileSync2 } from "fs";
9
+ import { readFileSync as readFileSync4 } from "fs";
10
10
  import { resolve as resolve3 } from "path";
11
11
 
12
12
  // src/parser/index.ts
@@ -18,9 +18,12 @@ import remarkGfm from "remark-gfm";
18
18
  // src/parser/frontmatter.ts
19
19
  import { parse as parseYaml } from "yaml";
20
20
 
21
+ // src/models/theme-preset.ts
22
+ var DEFAULT_THEME_NAME = "blue";
23
+
21
24
  // src/config/defaults.ts
22
25
  var DEFAULT_CONFIG = {
23
- theme: "clean"
26
+ theme: DEFAULT_THEME_NAME
24
27
  };
25
28
 
26
29
  // src/utils/progress.ts
@@ -71,7 +74,7 @@ function extractFrontmatter(tree) {
71
74
  }
72
75
  try {
73
76
  const parsed = parseYaml(yamlNode.value);
74
- const { resource_dir, ...parsedOnly } = parsed;
77
+ const { resource_dir, color_scheme, text_scheme, layout_scheme, ...parsedOnly } = parsed;
75
78
  const merged = {
76
79
  ...DEFAULT_CONFIG,
77
80
  ...parsedOnly
@@ -79,6 +82,15 @@ function extractFrontmatter(tree) {
79
82
  if (resource_dir && !merged.resourceDir) {
80
83
  merged.resourceDir = resource_dir;
81
84
  }
85
+ if (color_scheme && !merged.colorScheme) {
86
+ merged.colorScheme = color_scheme;
87
+ }
88
+ if (text_scheme && !merged.textScheme) {
89
+ merged.textScheme = text_scheme;
90
+ }
91
+ if (layout_scheme && !merged.layoutScheme) {
92
+ merged.layoutScheme = layout_scheme;
93
+ }
82
94
  return merged;
83
95
  } catch {
84
96
  log.warn("Failed to parse frontmatter YAML, using defaults");
@@ -86,8 +98,19 @@ function extractFrontmatter(tree) {
86
98
  }
87
99
  }
88
100
 
101
+ // src/diagrams/languages.ts
102
+ var ALIASES = {
103
+ graphviz: "dot",
104
+ puml: "plantuml"
105
+ };
106
+ var CANONICAL = /* @__PURE__ */ new Set(["mermaid", "dot", "echarts", "plantuml"]);
107
+ function normalizeDiagramLanguage(language) {
108
+ const lang = language.toLowerCase();
109
+ const canonical = ALIASES[lang] ?? lang;
110
+ return CANONICAL.has(canonical) ? canonical : void 0;
111
+ }
112
+
89
113
  // src/parser/splitter.ts
90
- var DIAGRAM_LANGUAGES = /* @__PURE__ */ new Set(["mermaid", "dot", "graphviz", "echarts"]);
91
114
  var CALLOUT_VARIANTS = /* @__PURE__ */ new Set([
92
115
  "note",
93
116
  "info",
@@ -271,10 +294,11 @@ function nodeToElement(node) {
271
294
  }
272
295
  case "code": {
273
296
  const lang = node.lang?.toLowerCase() ?? "";
274
- if (DIAGRAM_LANGUAGES.has(lang)) {
297
+ const diagramType = normalizeDiagramLanguage(lang);
298
+ if (diagramType) {
275
299
  return {
276
300
  type: "diagram",
277
- diagramType: lang === "graphviz" ? "dot" : lang,
301
+ diagramType,
278
302
  content: node.value
279
303
  };
280
304
  }
@@ -341,6 +365,19 @@ function detectLayout(title, elements, isFirstSlide, headingLevel) {
341
365
  if (nonEmptyElements.length > 0 && nonEmptyElements.every((e) => e.type === "blockquote")) {
342
366
  return "quote";
343
367
  }
368
+ if (nonEmptyElements.length > 0) {
369
+ const images = nonEmptyElements.filter((e) => e.type === "image");
370
+ const others = nonEmptyElements.filter(
371
+ (e) => e.type !== "image" && e.type !== "text"
372
+ );
373
+ const texts = nonEmptyElements.filter((e) => e.type === "text");
374
+ const substantialText = texts.some((e) => e.type === "text" && e.content.trim().length > 40);
375
+ if (others.length === 0 && !substantialText && images.length >= 1 && images.length <= 3) {
376
+ if (images.length === 1) return "image-single";
377
+ if (images.length === 2) return "image-double";
378
+ return "image-triple";
379
+ }
380
+ }
344
381
  return "content";
345
382
  }
346
383
  function splitIntoSlides(tree, defaultLayout) {
@@ -351,6 +388,8 @@ function splitIntoSlides(tree, defaultLayout) {
351
388
  let currentDirectives = {};
352
389
  let currentHeadingLevel = 0;
353
390
  let isFirstSlide = true;
391
+ let currentAuthor;
392
+ let currentDate;
354
393
  function flushSlide() {
355
394
  if (!currentTitle && currentElements.length === 0 && Object.keys(currentDirectives).length === 0) {
356
395
  return;
@@ -371,19 +410,23 @@ function splitIntoSlides(tree, defaultLayout) {
371
410
  currentHeadingLevel
372
411
  );
373
412
  const layout = currentDirectives.layout ?? (autoLayout === "content" ? defaultLayout : autoLayout) ?? "content";
413
+ const metadata = currentAuthor || currentDate ? { author: currentAuthor, date: currentDate } : void 0;
374
414
  slides.push({
375
415
  layout,
376
416
  title: currentTitle,
377
417
  subtitle: currentSubtitle,
378
418
  elements: currentElements,
379
419
  notes: currentDirectives.notes,
380
- directives: toDirectives(currentDirectives)
420
+ directives: toDirectives(currentDirectives),
421
+ ...metadata ? { metadata } : {}
381
422
  });
382
423
  currentTitle = void 0;
383
424
  currentSubtitle = void 0;
384
425
  currentElements = [];
385
426
  currentDirectives = {};
386
427
  currentHeadingLevel = 0;
428
+ currentAuthor = void 0;
429
+ currentDate = void 0;
387
430
  isFirstSlide = false;
388
431
  }
389
432
  for (const node of tree.children) {
@@ -409,6 +452,31 @@ function splitIntoSlides(tree, defaultLayout) {
409
452
  }
410
453
  continue;
411
454
  }
455
+ if (node.type === "paragraph" && isFirstSlide && slides.length === 0) {
456
+ const lines = node.children.map(extractText).join("").trim().split("\n");
457
+ let metaAuthor;
458
+ let metaDate;
459
+ const metaOnly = lines.every((raw) => {
460
+ const line = raw.trim();
461
+ if (!line) return true;
462
+ const authorMatch = line.match(/^作者\s*[::]\s*(.+)$/);
463
+ if (authorMatch) {
464
+ metaAuthor = authorMatch[1].trim();
465
+ return true;
466
+ }
467
+ const dateMatch = line.match(/^日期\s*[::]\s*(.+)$/);
468
+ if (dateMatch) {
469
+ metaDate = dateMatch[1].trim();
470
+ return true;
471
+ }
472
+ return false;
473
+ });
474
+ if (metaOnly && (metaAuthor || metaDate)) {
475
+ if (metaAuthor) currentAuthor = metaAuthor;
476
+ if (metaDate) currentDate = metaDate;
477
+ continue;
478
+ }
479
+ }
412
480
  const element = nodeToElement(node);
413
481
  if (element) {
414
482
  currentElements.push(element);
@@ -477,8 +545,69 @@ function parseMarkdown(markdown) {
477
545
  // src/renderer/pptx-renderer.ts
478
546
  import PptxGenJS from "pptxgenjs";
479
547
 
548
+ // src/renderer/layouts/layout-spec.ts
549
+ function layoutSpec(theme, key) {
550
+ return theme.layouts?.[key] ?? {};
551
+ }
552
+ function specNumber(value, fallback) {
553
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
554
+ }
555
+ function specAlign(value, fallback) {
556
+ return value ?? fallback;
557
+ }
558
+ function extraNumber(spec, key, fallback) {
559
+ const v = spec.extra?.[key];
560
+ return typeof v === "number" && Number.isFinite(v) ? v : fallback;
561
+ }
562
+ function extraBoolean(spec, key, fallback) {
563
+ const v = spec.extra?.[key];
564
+ return typeof v === "boolean" ? v : fallback;
565
+ }
566
+ function extraString(spec, key, fallback) {
567
+ const v = spec.extra?.[key];
568
+ return typeof v === "string" && v ? v : fallback;
569
+ }
570
+ function resolveSideMargins(spec, fallback) {
571
+ const base = specNumber(spec.margin, fallback);
572
+ return {
573
+ left: specNumber(spec.extra?.marginLeft, base),
574
+ right: specNumber(spec.extra?.marginRight, base)
575
+ };
576
+ }
577
+ function titleRuleHeight(spec, fallback) {
578
+ return extraNumber(spec, "titleRuleHeight", fallback);
579
+ }
580
+ function titleRuleRole(spec) {
581
+ const v = spec.extra?.titleRuleColor;
582
+ return v === "secondary" || v === "divider" ? v : "primary";
583
+ }
584
+ function ruleColor(theme, role) {
585
+ if (role === "secondary") return theme.colors.secondary;
586
+ if (role === "divider") return theme.colors.divider ?? theme.colors.secondary;
587
+ return theme.colors.primary;
588
+ }
589
+ function addHRuler(slide, opts) {
590
+ slide.addShape("rect", {
591
+ x: opts.x,
592
+ y: opts.y,
593
+ w: opts.w,
594
+ h: opts.h,
595
+ fill: { color: opts.color }
596
+ });
597
+ }
598
+
480
599
  // src/renderer/layouts/title.ts
481
600
  function renderTitleSlide(slide, node, theme) {
601
+ const spec = layoutSpec(theme, "title");
602
+ if (extraBoolean(spec, "minimalTitle", false)) {
603
+ renderMinimalTitle(slide, node, theme);
604
+ return;
605
+ }
606
+ const align = specAlign(spec.titleAlign, "center");
607
+ const { left, right } = resolveSideMargins(spec, 0.8);
608
+ const spineW = extraNumber(spec, "spineWidth", 0);
609
+ const boxX = left + spineW;
610
+ const contentW = Math.max(2, 13.33 - boxX - right);
482
611
  const textColor = theme.colors.titleText ?? "FFFFFF";
483
612
  const subtitleColor = textColor === "FFFFFF" ? "FFFFFFCC" : theme.colors.secondary;
484
613
  const metaColor = textColor === "FFFFFF" ? "FFFFFF99" : theme.colors.secondary;
@@ -486,65 +615,358 @@ function renderTitleSlide(slide, node, theme) {
486
615
  if (bgColor) {
487
616
  slide.background = { color: bgColor };
488
617
  }
618
+ if (spineW > 0) {
619
+ slide.addShape("rect", {
620
+ x: 0,
621
+ y: 0,
622
+ w: spineW,
623
+ h: 7.5,
624
+ fill: { color: theme.colors.primary }
625
+ });
626
+ }
627
+ const titleY = extraNumber(spec, "titleY", 2);
628
+ const subtitleY = extraNumber(spec, "subtitleY", 3.8);
629
+ const metaBottom = extraBoolean(spec, "metaBottom", false);
630
+ const metaY = extraNumber(spec, "metaY", metaBottom ? 6.55 : 5.5);
489
631
  if (node.title) {
632
+ const titleScale = extraNumber(spec, "titleFontSizeScale", 1);
490
633
  slide.addText(node.title, {
491
- x: 0.8,
492
- y: 2,
493
- w: 11.7,
634
+ x: boxX,
635
+ y: titleY,
636
+ w: contentW,
494
637
  h: 1.5,
495
- fontSize: theme.fontSize.title,
638
+ fontSize: Math.round(theme.fontSize.title * titleScale),
496
639
  fontFace: theme.fonts.heading,
497
640
  color: textColor,
498
641
  bold: true,
499
- align: "center",
642
+ align,
500
643
  valign: "middle"
501
644
  });
645
+ if (extraBoolean(spec, "titleRule", false)) {
646
+ const ruleH = extraNumber(spec, "titleRuleHeight", 0.03);
647
+ const ruleY = extraNumber(spec, "titleRuleY", titleY + 1.55);
648
+ const ruleW = extraNumber(spec, "titleRuleWidth", Math.min(contentW, 4.2));
649
+ let ruleX = boxX;
650
+ if (align === "center") ruleX = boxX + (contentW - ruleW) / 2;
651
+ else if (align === "right") ruleX = boxX + contentW - ruleW;
652
+ addHRuler(slide, { x: ruleX, y: ruleY, w: ruleW, h: ruleH, color: ruleColor(theme, titleRuleRole(spec)) });
653
+ }
502
654
  }
503
655
  if (node.subtitle) {
504
656
  slide.addText(node.subtitle, {
505
- x: 0.8,
506
- y: 3.8,
507
- w: 11.7,
657
+ x: boxX,
658
+ y: subtitleY,
659
+ w: contentW,
508
660
  h: 0.8,
509
661
  fontSize: theme.fontSize.body,
510
662
  fontFace: theme.fonts.body,
511
663
  color: subtitleColor,
512
- align: "center",
664
+ align,
513
665
  valign: "middle"
514
666
  });
515
667
  }
668
+ const meta = node.metadata ?? {};
669
+ const author = typeof meta.author === "string" ? meta.author : void 0;
670
+ const date = typeof meta.date === "string" ? meta.date : void 0;
516
671
  const textElements = node.elements.filter((e) => e.type === "text");
517
- if (textElements.length > 0) {
672
+ if (author || date) {
673
+ const lineH = 0.31;
674
+ if (author) {
675
+ slide.addText(author, {
676
+ x: boxX,
677
+ y: metaY,
678
+ w: contentW,
679
+ h: lineH,
680
+ fontSize: theme.fontSize.small,
681
+ fontFace: theme.fonts.body,
682
+ color: metaColor,
683
+ align,
684
+ valign: "middle"
685
+ });
686
+ }
687
+ if (date) {
688
+ slide.addText(date, {
689
+ x: boxX,
690
+ y: metaY + lineH + 0.03,
691
+ w: contentW,
692
+ h: lineH,
693
+ fontSize: theme.fontSize.small,
694
+ fontFace: theme.fonts.body,
695
+ color: metaColor,
696
+ align,
697
+ valign: "middle"
698
+ });
699
+ }
700
+ } else if (textElements.length > 0) {
518
701
  const metaText = textElements.map((e) => e.content).join(" \xB7 ");
519
702
  slide.addText(metaText, {
520
- x: 0.8,
521
- y: 5.5,
522
- w: 11.7,
703
+ x: boxX,
704
+ y: metaY,
705
+ w: contentW,
523
706
  h: 0.6,
524
707
  fontSize: theme.fontSize.small,
525
708
  fontFace: theme.fonts.body,
526
709
  color: metaColor,
527
- align: "center"
710
+ align
711
+ });
712
+ }
713
+ }
714
+ function renderMinimalTitle(slide, node, theme) {
715
+ const spec = layoutSpec(theme, "title");
716
+ const bgColor = theme.colors.backgroundGradient ? void 0 : theme.colors.titleBackground ?? theme.colors.primary;
717
+ if (bgColor) {
718
+ slide.background = { color: bgColor };
719
+ }
720
+ const textColor = theme.colors.titleText ?? "FFFFFF";
721
+ const softColor = textColor === "FFFFFF" ? "FFFFFFCC" : theme.colors.secondary;
722
+ const lineX = extraNumber(spec, "minimalLineX", 0.83);
723
+ const lineY1 = extraNumber(spec, "minimalLineY1", 2.64);
724
+ const lineH = extraNumber(spec, "minimalLineH", 1.94);
725
+ const lineW = extraNumber(spec, "minimalLineW", 0.04);
726
+ slide.addShape("rect", {
727
+ x: lineX,
728
+ y: lineY1,
729
+ w: lineW,
730
+ h: lineH,
731
+ fill: { color: textColor }
732
+ });
733
+ const boxX = extraNumber(spec, "minimalBoxX", 1.11);
734
+ const contentW = extraNumber(spec, "minimalBoxW", 8.89);
735
+ if (node.title) {
736
+ slide.addText(node.title, {
737
+ x: boxX,
738
+ y: extraNumber(spec, "minimalTitleY", 2.64),
739
+ w: contentW,
740
+ h: 0.89,
741
+ fontSize: theme.fontSize.title,
742
+ fontFace: theme.fonts.heading,
743
+ color: textColor,
744
+ bold: true,
745
+ align: "left",
746
+ valign: "middle"
747
+ });
748
+ }
749
+ if (node.subtitle) {
750
+ slide.addText(node.subtitle, {
751
+ x: boxX,
752
+ y: extraNumber(spec, "minimalSubtitleY", 3.64),
753
+ w: contentW,
754
+ h: 0.44,
755
+ fontSize: theme.fontSize.body,
756
+ fontFace: theme.fonts.body,
757
+ color: softColor,
758
+ align: "left",
759
+ valign: "middle"
760
+ });
761
+ }
762
+ const meta = node.metadata ?? {};
763
+ const author = typeof meta.author === "string" ? meta.author : void 0;
764
+ const date = typeof meta.date === "string" ? meta.date : void 0;
765
+ const metaX = boxX;
766
+ const metaW = extraNumber(spec, "minimalMetaW", 4.17);
767
+ const metaH = 0.31;
768
+ if (author) {
769
+ slide.addText(author, {
770
+ x: metaX,
771
+ y: extraNumber(spec, "minimalAuthorY", 5.28),
772
+ w: metaW,
773
+ h: metaH,
774
+ fontSize: theme.fontSize.small,
775
+ fontFace: theme.fonts.body,
776
+ color: softColor,
777
+ align: "left",
778
+ valign: "middle"
779
+ });
780
+ }
781
+ if (date) {
782
+ slide.addText(date, {
783
+ x: metaX,
784
+ y: extraNumber(spec, "minimalDateY", 5.64),
785
+ w: metaW,
786
+ h: metaH,
787
+ fontSize: theme.fontSize.small,
788
+ fontFace: theme.fonts.body,
789
+ color: softColor,
790
+ align: "left",
791
+ valign: "middle"
528
792
  });
529
793
  }
530
794
  }
531
795
 
532
796
  // src/renderer/layouts/section.ts
533
- function renderSectionSlide(slide, node, theme) {
797
+ function formatSectionNumber(n) {
798
+ return String(n).padStart(2, "0");
799
+ }
800
+ function renderSectionSlide(slide, node, theme, ctx) {
801
+ const spec = layoutSpec(theme, "section");
802
+ if (extraBoolean(spec, "minimalNumber", false)) {
803
+ renderMinimalSection(slide, node, theme, ctx);
804
+ return;
805
+ }
806
+ const barW = theme.styles?.sectionBarWidth ?? extraNumber(spec, "sectionBarWidth", 0.15);
807
+ const accentBar = spec.accentBar ?? "left";
808
+ const titleAlign = specAlign(spec.titleAlign, "left");
809
+ const { left, right } = resolveSideMargins(spec, accentBar === "left" ? 1 : 0.9);
810
+ if (accentBar !== "none") {
811
+ if (accentBar === "left") {
812
+ slide.addShape("rect", {
813
+ x: 0,
814
+ y: 0,
815
+ w: barW,
816
+ h: 7.5,
817
+ fill: { color: theme.colors.accent }
818
+ });
819
+ } else if (accentBar === "top") {
820
+ slide.addShape("rect", {
821
+ x: 0,
822
+ y: 0,
823
+ w: 13.33,
824
+ h: barW,
825
+ fill: { color: theme.colors.accent }
826
+ });
827
+ } else if (accentBar === "bottom") {
828
+ slide.addShape("rect", {
829
+ x: 0,
830
+ y: 7.5 - barW,
831
+ w: 13.33,
832
+ h: barW,
833
+ fill: { color: theme.colors.accent }
834
+ });
835
+ }
836
+ }
837
+ const leftPad = accentBar === "left" ? Math.max(left, barW + 0.85) : left;
838
+ const textW = Math.max(3, 13.33 - leftPad - right);
839
+ if (extraBoolean(spec, "doubleRules", false)) {
840
+ const weight = extraNumber(spec, "ruleWeight", 0.035);
841
+ const bandY = extraNumber(spec, "bandY", 2.35);
842
+ const bandH = extraNumber(spec, "bandH", 1.7);
843
+ const color = ruleColor(theme, titleRuleRole(spec) === "primary" ? "primary" : titleRuleRole(spec));
844
+ const ruleX = leftPad;
845
+ const ruleW = textW;
846
+ addHRuler(slide, { x: ruleX, y: bandY, w: ruleW, h: weight, color });
847
+ addHRuler(slide, { x: ruleX, y: bandY + bandH, w: ruleW, h: weight, color });
848
+ if (node.title) {
849
+ slide.addText(node.title, {
850
+ x: leftPad,
851
+ y: bandY + weight + 0.08,
852
+ w: textW,
853
+ h: bandH - weight * 2 - 0.16,
854
+ fontSize: theme.fontSize.heading + 6,
855
+ fontFace: theme.fonts.heading,
856
+ color: theme.colors.text,
857
+ bold: true,
858
+ align: titleAlign,
859
+ valign: "middle"
860
+ });
861
+ }
862
+ const sub = node.subtitle?.trim();
863
+ if (sub) {
864
+ slide.addText(sub, {
865
+ x: leftPad,
866
+ y: bandY + bandH + 0.35,
867
+ w: textW,
868
+ h: 0.45,
869
+ fontSize: theme.fontSize.small,
870
+ fontFace: theme.fonts.body,
871
+ color: theme.colors.secondary,
872
+ align: titleAlign
873
+ });
874
+ }
875
+ return;
876
+ }
877
+ if (extraBoolean(spec, "sectionRule", false)) {
878
+ const secRuleY = extraNumber(spec, "sectionRuleY", 4.64);
879
+ const secRuleW = extraNumber(spec, "sectionRuleWidth", 5);
880
+ const secRuleH = extraNumber(spec, "sectionRuleHeight", 0.04);
881
+ const secRuleX = extraNumber(
882
+ spec,
883
+ "sectionRuleX",
884
+ titleAlign === "right" ? 13.33 - right - secRuleW : leftPad
885
+ );
886
+ addHRuler(slide, {
887
+ x: secRuleX,
888
+ y: secRuleY,
889
+ w: secRuleW,
890
+ h: secRuleH,
891
+ color: ruleColor(
892
+ theme,
893
+ extraString(spec, "sectionRuleColor", "primary")
894
+ )
895
+ });
896
+ }
897
+ if (node.subtitle) {
898
+ const subY = extraNumber(spec, "subtitleY", 0.85);
899
+ const subH = extraNumber(spec, "subtitleH", 0.45);
900
+ slide.addText(node.subtitle, {
901
+ x: leftPad,
902
+ y: subY,
903
+ w: textW,
904
+ h: subH,
905
+ fontSize: theme.fontSize.body,
906
+ fontFace: theme.fonts.body,
907
+ color: theme.colors.secondary,
908
+ align: titleAlign,
909
+ valign: "middle"
910
+ });
911
+ }
912
+ if (node.title) {
913
+ const tY = extraNumber(spec, "titleY", 2.5);
914
+ const tH = extraNumber(spec, "titleH", 2);
915
+ slide.addText(node.title, {
916
+ x: leftPad,
917
+ y: tY,
918
+ w: textW,
919
+ h: tH,
920
+ fontSize: theme.fontSize.heading + 4,
921
+ fontFace: theme.fonts.heading,
922
+ color: theme.colors.text,
923
+ bold: true,
924
+ align: titleAlign,
925
+ valign: "middle"
926
+ });
927
+ }
928
+ }
929
+ function renderMinimalSection(slide, node, theme, ctx) {
930
+ const spec = layoutSpec(theme, "section");
931
+ const num = formatSectionNumber(ctx?.sectionNumber ?? 1);
932
+ const nX = extraNumber(spec, "minimalNumberX", 1.11);
933
+ const nY = extraNumber(spec, "minimalNumberY", 2.43);
934
+ const nW = extraNumber(spec, "minimalNumberW", 2.5);
935
+ const nH = extraNumber(spec, "minimalNumberH", 1.53);
936
+ slide.addText(num, {
937
+ x: nX,
938
+ y: nY,
939
+ w: nW,
940
+ h: nH,
941
+ fontSize: theme.fontSize.title,
942
+ fontFace: theme.fonts.heading,
943
+ color: theme.colors.primary,
944
+ bold: true,
945
+ align: "left",
946
+ valign: "middle"
947
+ });
948
+ const rX = extraNumber(spec, "minimalRuleX", 3.89);
949
+ const rY1 = extraNumber(spec, "minimalRuleY1", 2.71);
950
+ const rH = extraNumber(spec, "minimalRuleH", 1.11);
951
+ const rW = extraNumber(spec, "minimalRuleW", 0.03);
534
952
  slide.addShape("rect", {
535
- x: 0,
536
- y: 0,
537
- w: 0.15,
538
- h: 7.5,
539
- fill: { color: theme.colors.accent }
953
+ x: rX,
954
+ y: rY1,
955
+ w: rW,
956
+ h: rH,
957
+ fill: { color: theme.colors.divider ?? theme.colors.secondary }
540
958
  });
541
959
  if (node.title) {
960
+ const tX = extraNumber(spec, "minimalTitleX", 4.31);
961
+ const tY = extraNumber(spec, "minimalTitleY", rY1);
962
+ const tW = extraNumber(spec, "minimalTitleW", 6.94);
963
+ const tH = extraNumber(spec, "minimalTitleH", 0.69);
542
964
  slide.addText(node.title, {
543
- x: 1,
544
- y: 2.5,
545
- w: 11.3,
546
- h: 2,
547
- fontSize: theme.fontSize.heading + 4,
965
+ x: tX,
966
+ y: tY,
967
+ w: tW,
968
+ h: tH,
969
+ fontSize: theme.fontSize.heading,
548
970
  fontFace: theme.fonts.heading,
549
971
  color: theme.colors.text,
550
972
  bold: true,
@@ -554,15 +976,135 @@ function renderSectionSlide(slide, node, theme) {
554
976
  }
555
977
  }
556
978
 
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);
979
+ // src/utils/image-size.ts
980
+ function positive(width, height) {
563
981
  if (!(width > 0 && height > 0)) return null;
564
982
  return { width, height };
565
983
  }
984
+ function pngSize(data) {
985
+ if (data.length < 24) return null;
986
+ if (data.readUInt32BE(0) !== 2303741511) return null;
987
+ return positive(data.readUInt32BE(16), data.readUInt32BE(20));
988
+ }
989
+ function isFrameMarker(marker) {
990
+ return marker >= 192 && marker <= 207 && marker !== 196 && // DHT
991
+ marker !== 200 && // JPG
992
+ marker !== 204;
993
+ }
994
+ function jpegSize(data) {
995
+ if (data.length < 4 || data[0] !== 255 || data[1] !== 216) return null;
996
+ let offset = 2;
997
+ while (offset + 3 < data.length) {
998
+ if (data[offset] !== 255) {
999
+ offset++;
1000
+ continue;
1001
+ }
1002
+ let marker = data[offset + 1];
1003
+ while (marker === 255 && offset + 2 < data.length) {
1004
+ offset++;
1005
+ marker = data[offset + 1];
1006
+ }
1007
+ offset += 2;
1008
+ if (marker === 1 || marker >= 208 && marker <= 216) continue;
1009
+ if (marker === 218 || marker === 217) return null;
1010
+ if (offset + 2 > data.length) return null;
1011
+ const length = data.readUInt16BE(offset);
1012
+ if (length < 2) return null;
1013
+ if (isFrameMarker(marker)) {
1014
+ if (offset + 7 > data.length) return null;
1015
+ return positive(data.readUInt16BE(offset + 5), data.readUInt16BE(offset + 3));
1016
+ }
1017
+ offset += length;
1018
+ }
1019
+ return null;
1020
+ }
1021
+ function gifSize(data) {
1022
+ if (data.length < 10) return null;
1023
+ const signature = data.toString("latin1", 0, 6);
1024
+ if (signature !== "GIF87a" && signature !== "GIF89a") return null;
1025
+ return positive(data.readUInt16LE(6), data.readUInt16LE(8));
1026
+ }
1027
+ function webpSize(data) {
1028
+ if (data.length < 30) return null;
1029
+ if (data.toString("latin1", 0, 4) !== "RIFF") return null;
1030
+ if (data.toString("latin1", 8, 12) !== "WEBP") return null;
1031
+ const chunk = data.toString("latin1", 12, 16);
1032
+ if (chunk === "VP8X") {
1033
+ return positive(data.readUIntLE(24, 3) + 1, data.readUIntLE(27, 3) + 1);
1034
+ }
1035
+ if (chunk === "VP8 ") {
1036
+ if (data[23] !== 157 || data[24] !== 1 || data[25] !== 42) return null;
1037
+ return positive(data.readUInt16LE(26) & 16383, data.readUInt16LE(28) & 16383);
1038
+ }
1039
+ if (chunk === "VP8L") {
1040
+ if (data[20] !== 47) return null;
1041
+ const bits = data.readUInt32LE(21);
1042
+ return positive((bits & 16383) + 1, (bits >>> 14 & 16383) + 1);
1043
+ }
1044
+ return null;
1045
+ }
1046
+ function bmpSize(data) {
1047
+ if (data.length < 26) return null;
1048
+ if (data[0] !== 66 || data[1] !== 77) return null;
1049
+ const headerSize = data.readUInt32LE(14);
1050
+ if (headerSize === 12) {
1051
+ return positive(data.readUInt16LE(18), data.readUInt16LE(20));
1052
+ }
1053
+ if (headerSize >= 40) {
1054
+ return positive(Math.abs(data.readInt32LE(18)), Math.abs(data.readInt32LE(22)));
1055
+ }
1056
+ return null;
1057
+ }
1058
+ var SVG_UNITS = {
1059
+ px: 1,
1060
+ pt: 96 / 72,
1061
+ pc: 16,
1062
+ mm: 96 / 25.4,
1063
+ cm: 96 / 2.54,
1064
+ in: 96,
1065
+ q: 96 / 101.6
1066
+ };
1067
+ function parseSvgLength(value) {
1068
+ if (!value) return void 0;
1069
+ const match = value.match(/^([\d.]+)\s*([a-z%]*)$/i);
1070
+ if (!match) return void 0;
1071
+ const n = parseFloat(match[1]);
1072
+ if (!(n > 0)) return void 0;
1073
+ const unit = match[2].toLowerCase();
1074
+ if (unit === "%") return void 0;
1075
+ const scale = SVG_UNITS[unit || "px"];
1076
+ return scale === void 0 ? void 0 : n * scale;
1077
+ }
1078
+ function findSvgRoot(data) {
1079
+ const head = data.toString("utf8", 0, Math.min(data.length, 4096));
1080
+ return head.match(/<svg\b[^>]*>/i);
1081
+ }
1082
+ function isSvgImage(data) {
1083
+ return findSvgRoot(data) !== null;
1084
+ }
1085
+ function svgSize(data) {
1086
+ const root = findSvgRoot(data);
1087
+ if (!root) return null;
1088
+ const attribute = (name) => {
1089
+ const match = root[0].match(new RegExp(`\\b${name}\\s*=\\s*["']([^"']*)["']`, "i"));
1090
+ return match?.[1]?.trim();
1091
+ };
1092
+ const viewBox = attribute("viewBox");
1093
+ if (viewBox) {
1094
+ const parts = viewBox.split(/[\s,]+/).map(Number);
1095
+ if (parts.length === 4 && parts[2] > 0 && parts[3] > 0) {
1096
+ return { width: parts[2], height: parts[3] };
1097
+ }
1098
+ }
1099
+ const width = parseSvgLength(attribute("width"));
1100
+ const height = parseSvgLength(attribute("height"));
1101
+ if (width === void 0 || height === void 0) return null;
1102
+ return positive(width, height);
1103
+ }
1104
+ function getImageSize(data) {
1105
+ if (data.length < 10) return null;
1106
+ return pngSize(data) ?? jpegSize(data) ?? gifSize(data) ?? webpSize(data) ?? bmpSize(data) ?? svgSize(data);
1107
+ }
566
1108
 
567
1109
  // src/utils/image-fit.ts
568
1110
  function fitInBox(img, maxWidth, maxHeight) {
@@ -655,11 +1197,31 @@ function tableToChartOption(table, type) {
655
1197
  import { readFileSync } from "fs";
656
1198
  var SLIDE_W = 13.33;
657
1199
  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;
1200
+ var DEFAULT_MARGIN = 0.6;
1201
+ var DEFAULT_TITLE_H = 0.9;
1202
+ var DEFAULT_BOTTOM_MARGIN = 0.45;
1203
+ var DEFAULT_GRID_GAP = 0.25;
1204
+ function contentMetrics(theme) {
1205
+ const spec = layoutSpec(theme, "content");
1206
+ const { left, right } = resolveSideMargins(spec, DEFAULT_MARGIN);
1207
+ const titleH = specNumber(spec.titleHeight, DEFAULT_TITLE_H);
1208
+ const contentPadding = specNumber(spec.contentPadding, 0);
1209
+ const contentW = Math.max(2, SLIDE_W - left - right);
1210
+ return {
1211
+ marginLeft: left,
1212
+ marginRight: right,
1213
+ titleH,
1214
+ titleY: extraNumber(spec, "titleY", 0.3),
1215
+ contentW,
1216
+ bottom: DEFAULT_BOTTOM_MARGIN,
1217
+ gridGap: DEFAULT_GRID_GAP,
1218
+ contentPadding,
1219
+ titleRule: extraBoolean(spec, "titleRule", true),
1220
+ titleRuleH: titleRuleHeight(spec, 0.04),
1221
+ titleRuleWidth: extraNumber(spec, "titleRuleWidth", contentW),
1222
+ titleRuleColor: titleRuleRole(spec)
1223
+ };
1224
+ }
663
1225
  var IMAGE_MIME = {
664
1226
  png: "image/png",
665
1227
  jpg: "image/jpeg",
@@ -669,10 +1231,10 @@ var IMAGE_MIME = {
669
1231
  bmp: "image/bmp",
670
1232
  svg: "image/svg+xml"
671
1233
  };
672
- function contentAreaInsets(node) {
1234
+ function contentAreaInsets(node, metrics) {
673
1235
  return {
674
- top: node.title ? 0.3 + TITLE_H + 0.2 : 0.3,
675
- bottom: BOTTOM_MARGIN
1236
+ top: node.title ? metrics.titleY + metrics.titleH + 0.2 : metrics.titleY,
1237
+ bottom: metrics.bottom
676
1238
  };
677
1239
  }
678
1240
  function splitGrid(elements) {
@@ -703,20 +1265,32 @@ function splitGrid(elements) {
703
1265
  });
704
1266
  }
705
1267
  async function renderContentSlide(slide, node, theme, ctx) {
706
- const { top } = contentAreaInsets(node);
1268
+ const spec = layoutSpec(theme, "content");
1269
+ if (extraBoolean(spec, "minimalBox", false)) {
1270
+ await renderMinimalContent(slide, node, theme, ctx);
1271
+ return;
1272
+ }
1273
+ const metrics = contentMetrics(theme);
1274
+ const { marginLeft, titleH, titleY, contentW, gridGap, contentPadding, titleRuleWidth } = metrics;
1275
+ const { top } = contentAreaInsets(node, metrics);
1276
+ const pad = contentPadding > 0 ? contentPadding : 0;
1277
+ const drawX = marginLeft + pad;
1278
+ const drawW = contentW - pad * 2;
707
1279
  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
- });
1280
+ if (metrics.titleRule && metrics.titleRuleH > 0) {
1281
+ addHRuler(slide, {
1282
+ x: marginLeft,
1283
+ y: titleY + titleH - metrics.titleRuleH - 0.01,
1284
+ w: titleRuleWidth,
1285
+ h: metrics.titleRuleH,
1286
+ color: ruleColor(theme, metrics.titleRuleColor)
1287
+ });
1288
+ }
715
1289
  slide.addText(node.title, {
716
- x: MARGIN,
717
- y: 0.3,
718
- w: CONTENT_W,
719
- h: TITLE_H,
1290
+ x: marginLeft,
1291
+ y: titleY,
1292
+ w: contentW,
1293
+ h: titleH,
720
1294
  fontSize: theme.fontSize.heading,
721
1295
  fontFace: theme.fonts.heading,
722
1296
  color: theme.colors.primary,
@@ -726,11 +1300,11 @@ async function renderContentSlide(slide, node, theme, ctx) {
726
1300
  }
727
1301
  const grid = splitGrid(prepareElements(node));
728
1302
  const rows = grid.length;
729
- const availH = SLIDE_H - top - BOTTOM_MARGIN - GRID_GAP * (rows - 1);
1303
+ const availH = SLIDE_H - top - metrics.bottom - gridGap * (rows - 1);
730
1304
  const ests = grid.map(
731
1305
  (columns) => Math.max(
732
1306
  0.8,
733
- ...columns.map((c) => estimateColumnHeight(c, (CONTENT_W - GRID_GAP * (columns.length - 1)) / columns.length))
1307
+ ...columns.map((c) => estimateColumnHeight(c, (drawW - gridGap * (columns.length - 1)) / columns.length, theme))
734
1308
  )
735
1309
  );
736
1310
  const totalEst = ests.reduce((a, b) => a + b, 0);
@@ -739,19 +1313,19 @@ async function renderContentSlide(slide, node, theme, ctx) {
739
1313
  for (let r = 0; r < rows; r++) {
740
1314
  const columns = grid[r];
741
1315
  const cols = columns.length;
742
- const colW = (CONTENT_W - GRID_GAP * (cols - 1)) / cols;
1316
+ const colW = (drawW - gridGap * (cols - 1)) / cols;
743
1317
  const rowH = ests[r] * scale;
744
1318
  for (let c = 0; c < cols; c++) {
745
1319
  await renderColumn(
746
1320
  slide,
747
1321
  columns[c],
748
- { x: MARGIN + c * (colW + GRID_GAP), y, w: colW, h: rowH },
1322
+ { x: drawX + c * (colW + gridGap), y, w: colW, h: rowH },
749
1323
  node,
750
1324
  theme,
751
1325
  ctx
752
1326
  );
753
1327
  }
754
- y += rowH + GRID_GAP;
1328
+ y += rowH + gridGap;
755
1329
  }
756
1330
  }
757
1331
  function prepareElements(node) {
@@ -771,16 +1345,30 @@ function prepareElements(node) {
771
1345
  }
772
1346
  return elements;
773
1347
  }
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));
1348
+ var CODE_INSET_IN = 16 / 72;
1349
+ var CODE_LINE_FACTOR = 1.3;
1350
+ function codeBlockHeight(lines, fontSizePt) {
1351
+ return Math.max(1, lines * fontSizePt * CODE_LINE_FACTOR / 72 + CODE_INSET_IN);
777
1352
  }
778
- function estimateColumnHeight(elements, w) {
1353
+ function displayWidth(text) {
1354
+ let units = 0;
1355
+ for (const char of text) {
1356
+ units += (char.codePointAt(0) ?? 0) > 11904 ? 2 : 1;
1357
+ }
1358
+ return units;
1359
+ }
1360
+ function charsPerLine(widthIn, fontSizePt) {
1361
+ return Math.max(8, Math.floor(widthIn * 72 / (fontSizePt * 0.5)));
1362
+ }
1363
+ function estimateTextLines(content, w, fontSizePt) {
1364
+ return Math.max(1, Math.ceil(displayWidth(content) / charsPerLine(w, fontSizePt)));
1365
+ }
1366
+ function estimateColumnHeight(elements, w, theme) {
779
1367
  let h = 0;
780
1368
  for (const el of elements) {
781
1369
  switch (el.type) {
782
1370
  case "text":
783
- h += Math.max(0.5, Math.ceil(el.content.length / Math.max(20, Math.floor(w * 60))) * 0.35);
1371
+ h += Math.max(0.5, estimateTextLines(el.content, w, theme.fontSize.body) * 0.35);
784
1372
  break;
785
1373
  case "heading":
786
1374
  h += 0.7;
@@ -789,7 +1377,7 @@ function estimateColumnHeight(elements, w) {
789
1377
  h += el.items.length * 0.5;
790
1378
  break;
791
1379
  case "code":
792
- h += Math.max(1, el.content.split("\n").length * 0.25);
1380
+ h += codeBlockHeight(el.content.split("\n").length, theme.fontSize.code);
793
1381
  break;
794
1382
  case "diagram":
795
1383
  case "image":
@@ -805,7 +1393,7 @@ function estimateColumnHeight(elements, w) {
805
1393
  h += (el.rows.length + 1) * 0.4;
806
1394
  break;
807
1395
  case "callout":
808
- h += Math.min(4.5, 0.72 + estimateTextLines(el.content, w) * 0.3);
1396
+ h += Math.min(4.5, 0.72 + estimateTextLines(el.content, w, theme.fontSize.body) * 0.3);
809
1397
  break;
810
1398
  case "blockquote":
811
1399
  h += 1;
@@ -819,7 +1407,7 @@ function estimateColumnHeight(elements, w) {
819
1407
  return h;
820
1408
  }
821
1409
  async function renderColumn(slide, elements, box, node, theme, ctx) {
822
- const est = estimateColumnHeight(elements, box.w);
1410
+ const est = estimateColumnHeight(elements, box.w, theme);
823
1411
  const yPos = box.h > est ? box.y + (box.h - est) / 2 : box.y;
824
1412
  let cursor = yPos;
825
1413
  for (const element of elements) {
@@ -833,11 +1421,13 @@ async function renderElement(slide, element, yPos, box, node, theme, ctx) {
833
1421
  switch (element.type) {
834
1422
  case "text": {
835
1423
  if (!element.content.trim()) return 0;
1424
+ const lines = estimateTextLines(element.content, w, theme.fontSize.body);
1425
+ const height = Math.max(0.5, lines * 0.35);
836
1426
  slide.addText(element.content, {
837
1427
  x,
838
1428
  y: yPos,
839
1429
  w,
840
- h: 0.6,
1430
+ h: height,
841
1431
  fontSize: theme.fontSize.body,
842
1432
  fontFace: theme.fonts.body,
843
1433
  color: theme.colors.text,
@@ -846,9 +1436,7 @@ async function renderElement(slide, element, yPos, box, node, theme, ctx) {
846
1436
  valign: "top",
847
1437
  wrap: true
848
1438
  });
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);
1439
+ return height;
852
1440
  }
853
1441
  case "heading": {
854
1442
  slide.addText(element.content, {
@@ -902,7 +1490,7 @@ async function renderElement(slide, element, yPos, box, node, theme, ctx) {
902
1490
  element.highlightLines
903
1491
  );
904
1492
  const lines = element.content.split("\n").length;
905
- const height = Math.min(maxH, Math.max(1, lines * 0.25));
1493
+ const height = Math.min(maxH, codeBlockHeight(lines, theme.fontSize.code));
906
1494
  slide.addText(runs, {
907
1495
  x,
908
1496
  y: yPos,
@@ -920,12 +1508,16 @@ async function renderElement(slide, element, yPos, box, node, theme, ctx) {
920
1508
  case "diagram": {
921
1509
  try {
922
1510
  const pngBuffer = await ctx.renderDiagram(element.diagramType, element.content);
923
- const imgSize = getPngSize(pngBuffer) ?? { width: 8, height: 3.5 };
1511
+ const imgSize = getImageSize(pngBuffer) ?? { width: 8, height: 3.5 };
924
1512
  const boxH = Math.max(1.2, maxH);
925
1513
  const fitted = fitInBox(imgSize, w, boxH);
926
1514
  const base64 = pngBuffer.toString("base64");
927
1515
  slide.addImage({
928
1516
  data: `image/png;base64,${base64}`,
1517
+ // A diagram has no alt text of its own, so the slide title describes
1518
+ // it best; without one, name the notation at least. Passing nothing
1519
+ // would leave pptxgenjs' internal placeholder filename as the alt text.
1520
+ altText: node.title || `${element.diagramType} diagram`,
929
1521
  x: x + (w - fitted.width) / 2,
930
1522
  y: yPos,
931
1523
  w: fitted.width,
@@ -933,7 +1525,10 @@ async function renderElement(slide, element, yPos, box, node, theme, ctx) {
933
1525
  });
934
1526
  return fitted.height + 0.2;
935
1527
  } catch (err) {
936
- slide.addText(`[Diagram render error: ${err instanceof Error ? err.message : "unknown"}]`, {
1528
+ const reason = err instanceof Error ? err.message : "unknown";
1529
+ const message = `[Diagram render error: ${reason}]`;
1530
+ log.warn(`[${node.title ?? "slide"}] ${message}`);
1531
+ slide.addText(message, {
937
1532
  x,
938
1533
  y: yPos,
939
1534
  w,
@@ -952,7 +1547,8 @@ async function renderElement(slide, element, yPos, box, node, theme, ctx) {
952
1547
  log.warn(`[${node.title ?? "slide"}] ${resolved.error}`);
953
1548
  return 0;
954
1549
  }
955
- const imgSize = getPngSize(readFileSync(resolved.path)) ?? { width: 6, height: 3 };
1550
+ const fileData = readFileSync(resolved.path);
1551
+ const imgSize = getImageSize(fileData) ?? { width: 6, height: 3 };
956
1552
  const boxH = Math.max(1.2, maxH);
957
1553
  const placed = fitImageWithOptions(
958
1554
  imgSize,
@@ -961,10 +1557,19 @@ async function renderElement(slide, element, yPos, box, node, theme, ctx) {
961
1557
  );
962
1558
  const ext = resolved.path.split(".").pop()?.toLowerCase() ?? "png";
963
1559
  const mime = IMAGE_MIME[ext] ?? "image/png";
964
- const data = `${mime};base64,${readFileSync(resolved.path).toString("base64")}`;
1560
+ const data = `${mime};base64,${fileData.toString("base64")}`;
1561
+ if (ext === "webp") {
1562
+ log.warn(
1563
+ `[${node.title ?? "slide"}] WebP does not render in PowerPoint for the web or Office 2019 and earlier \u2014 some recipients may see a broken image (${element.src})`
1564
+ );
1565
+ }
965
1566
  slide.addImage({
966
1567
  data,
967
1568
  path: "preencoded.png",
1569
+ // The author's alt text when they wrote one; otherwise the source they
1570
+ // referenced, which at least beats pptxgenjs' internal placeholder name.
1571
+ // Falsy, not nullish: `![](...)` parses to an empty alt string.
1572
+ altText: element.alt || element.src,
968
1573
  x: x + placed.x,
969
1574
  y: yPos,
970
1575
  w: placed.width,
@@ -1049,7 +1654,7 @@ async function renderElement(slide, element, yPos, box, node, theme, ctx) {
1049
1654
  };
1050
1655
  const color = palette[element.variant] ?? theme.colors.primary;
1051
1656
  const label = element.title ?? element.variant.toUpperCase();
1052
- const textLines = estimateTextLines(element.content, w);
1657
+ const textLines = estimateTextLines(element.content, w, theme.fontSize.body);
1053
1658
  const cardH = Math.min(maxH, 0.72 + textLines * 0.3);
1054
1659
  const textH = cardH - 0.44;
1055
1660
  slide.addShape("roundRect", {
@@ -1095,22 +1700,120 @@ async function renderElement(slide, element, yPos, box, node, theme, ctx) {
1095
1700
  return 0;
1096
1701
  }
1097
1702
  }
1703
+ async function renderMinimalContent(slide, node, theme, ctx) {
1704
+ const spec = layoutSpec(theme, "content");
1705
+ const margin = specNumber(spec.margin, 0.83);
1706
+ const boxX = extraNumber(spec, "minimalBoxX", margin);
1707
+ const boxY = extraNumber(spec, "minimalBoxY", 1.33);
1708
+ const boxW = extraNumber(spec, "minimalBoxW", SLIDE_W - margin * 2);
1709
+ const boxH = extraNumber(spec, "minimalBoxH", 5.28);
1710
+ const lineColor = theme.colors.divider ?? theme.colors.secondary;
1711
+ if (node.title) {
1712
+ slide.addText(node.title, {
1713
+ x: boxX,
1714
+ y: extraNumber(spec, "minimalTitleY", 0.5),
1715
+ w: boxW,
1716
+ h: 0.44,
1717
+ fontSize: theme.fontSize.heading,
1718
+ fontFace: theme.fonts.heading,
1719
+ color: theme.colors.primary,
1720
+ bold: true,
1721
+ align: "left",
1722
+ valign: "middle"
1723
+ });
1724
+ addHRuler(slide, {
1725
+ x: boxX,
1726
+ y: extraNumber(spec, "minimalTitleRuleY", 1.03),
1727
+ w: extraNumber(spec, "minimalTitleRuleW", 0.56),
1728
+ h: extraNumber(spec, "minimalTitleRuleH", 0.03),
1729
+ color: lineColor
1730
+ });
1731
+ }
1732
+ slide.addShape("rect", {
1733
+ x: boxX,
1734
+ y: boxY,
1735
+ w: boxW,
1736
+ h: boxH,
1737
+ fill: { transparency: 100 },
1738
+ line: { color: lineColor, width: 0.01 }
1739
+ });
1740
+ const gap = 0.25;
1741
+ const pad = 0.3;
1742
+ const inner = { x: boxX + pad, y: boxY + pad, w: boxW - pad * 2, h: boxH - pad * 2 };
1743
+ const innerBox = {
1744
+ x: inner.x,
1745
+ y: inner.y,
1746
+ w: inner.w,
1747
+ h: inner.h
1748
+ };
1749
+ const grid = splitGrid(prepareElements(node));
1750
+ const rows = grid.length;
1751
+ const availH = innerBox.h - gap * (rows - 1);
1752
+ const ests = grid.map(
1753
+ (columns) => Math.max(
1754
+ 0.8,
1755
+ ...columns.map((c) => estimateColumnHeight(c, (innerBox.w - gap * (columns.length - 1)) / columns.length, theme))
1756
+ )
1757
+ );
1758
+ const totalEst = ests.reduce((a, b) => a + b, 0);
1759
+ const scale = totalEst > availH ? availH / totalEst : 1;
1760
+ let y = innerBox.y;
1761
+ for (let r = 0; r < rows; r++) {
1762
+ const columns = grid[r];
1763
+ const cols = columns.length;
1764
+ const colW = (innerBox.w - gap * (cols - 1)) / cols;
1765
+ const rowH = ests[r] * scale;
1766
+ for (let c = 0; c < cols; c++) {
1767
+ await renderColumn(
1768
+ slide,
1769
+ columns[c],
1770
+ { x: innerBox.x + c * (colW + gap), y, w: colW, h: rowH },
1771
+ node,
1772
+ theme,
1773
+ ctx
1774
+ );
1775
+ }
1776
+ y += rowH + gap;
1777
+ }
1778
+ }
1098
1779
 
1099
1780
  // src/renderer/layouts/code.ts
1100
1781
  async function renderCodeSlide(slide, node, theme, ctx) {
1101
- let yPos = 0.3;
1782
+ const spec = layoutSpec(theme, "code");
1783
+ if (extraBoolean(spec, "minimalCard", false)) {
1784
+ await renderMinimalCode(slide, node, theme, ctx);
1785
+ return;
1786
+ }
1787
+ const { left, right } = resolveSideMargins(spec, 0.5);
1788
+ const titleH = specNumber(spec.titleHeight, 0.7);
1789
+ const titleAlign = specAlign(spec.titleAlign, "left");
1790
+ const contentW = Math.max(3, 13.33 - left - right);
1791
+ const titleGap = 0.2;
1792
+ let yPos = extraNumber(spec, "titleY", 0.3);
1102
1793
  if (node.title) {
1103
1794
  slide.addText(node.title, {
1104
- x: 0.5,
1795
+ x: left,
1105
1796
  y: yPos,
1106
- w: 12.3,
1107
- h: 0.7,
1797
+ w: contentW,
1798
+ h: titleH,
1108
1799
  fontSize: theme.fontSize.heading,
1109
1800
  fontFace: theme.fonts.heading,
1110
1801
  color: theme.colors.primary,
1111
- bold: true
1802
+ bold: true,
1803
+ align: titleAlign === "center" ? "center" : "left"
1112
1804
  });
1113
- yPos += 0.9;
1805
+ if (extraBoolean(spec, "titleRule", false)) {
1806
+ const rh = titleRuleHeight(spec, 0.03);
1807
+ const ruleW = extraNumber(spec, "titleRuleWidth", contentW);
1808
+ addHRuler(slide, {
1809
+ x: left,
1810
+ y: yPos + titleH + 0.02,
1811
+ w: ruleW,
1812
+ h: rh,
1813
+ color: ruleColor(theme, titleRuleRole(spec))
1814
+ });
1815
+ }
1816
+ yPos += titleH + titleGap;
1114
1817
  }
1115
1818
  for (const element of node.elements) {
1116
1819
  if (element.type === "code") {
@@ -1120,10 +1823,10 @@ async function renderCodeSlide(slide, node, theme, ctx) {
1120
1823
  element.highlightLines
1121
1824
  );
1122
1825
  slide.addText(runs, {
1123
- x: 0.5,
1826
+ x: left,
1124
1827
  y: yPos,
1125
- w: 12.3,
1126
- h: 7.5 - yPos - 0.5,
1828
+ w: contentW,
1829
+ h: 7.5 - yPos - 0.45,
1127
1830
  fill: { color: theme.colors.codeBackground },
1128
1831
  color: theme.colors.codeText,
1129
1832
  fontFace: theme.fonts.code,
@@ -1136,54 +1839,521 @@ async function renderCodeSlide(slide, node, theme, ctx) {
1136
1839
  }
1137
1840
  }
1138
1841
  }
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,
1842
+ async function renderMinimalCode(slide, node, theme, ctx) {
1843
+ const spec = layoutSpec(theme, "code");
1844
+ const margin = specNumber(spec.margin, 0.83);
1845
+ const boxX = extraNumber(spec, "minimalCardX", margin);
1846
+ const boxY = extraNumber(spec, "minimalCardY", 1.33);
1847
+ const boxW = extraNumber(spec, "minimalCardW", 13.33 - margin * 2);
1848
+ const boxH = extraNumber(spec, "minimalCardH", 5.28);
1849
+ const lineColor = theme.colors.divider ?? theme.colors.secondary;
1850
+ const radius = extraNumber(spec, "minimalCardRadius", 0.08);
1851
+ if (node.title) {
1852
+ slide.addText(node.title, {
1853
+ x: boxX,
1854
+ y: extraNumber(spec, "minimalTitleY", 0.5),
1855
+ w: boxW,
1856
+ h: 0.44,
1166
1857
  fontSize: theme.fontSize.heading,
1167
- fontFace: theme.fonts.body,
1168
- color: theme.colors.text,
1169
- italic: true,
1170
- align: "center",
1858
+ fontFace: theme.fonts.heading,
1859
+ color: theme.colors.primary,
1860
+ bold: true,
1861
+ align: "left",
1171
1862
  valign: "middle"
1172
1863
  });
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
- }
1864
+ addHRuler(slide, {
1865
+ x: boxX,
1866
+ y: extraNumber(spec, "minimalTitleRuleY", 1.03),
1867
+ w: extraNumber(spec, "minimalTitleRuleW", 0.56),
1868
+ h: extraNumber(spec, "minimalTitleRuleH", 0.03),
1869
+ color: lineColor
1870
+ });
1185
1871
  }
1186
- }
1872
+ slide.addShape("roundRect", {
1873
+ x: boxX,
1874
+ y: boxY,
1875
+ w: boxW,
1876
+ h: boxH,
1877
+ rectRadius: radius,
1878
+ fill: { color: theme.colors.codeBackground },
1879
+ line: { color: lineColor, width: 0.01 }
1880
+ });
1881
+ const pad = 0.3;
1882
+ for (const element of node.elements) {
1883
+ if (element.type !== "code") continue;
1884
+ const runs = await ctx.highlightCode(
1885
+ element.content,
1886
+ element.language ?? "text",
1887
+ element.highlightLines
1888
+ );
1889
+ slide.addText(runs, {
1890
+ x: boxX + pad,
1891
+ y: boxY + pad,
1892
+ w: boxW - pad * 2,
1893
+ h: boxH - pad * 2,
1894
+ fill: { color: theme.colors.codeBackground },
1895
+ color: theme.colors.codeText,
1896
+ fontFace: theme.fonts.code,
1897
+ fontSize: theme.fontSize.code,
1898
+ valign: "top",
1899
+ paraSpaceAfter: 2,
1900
+ margin: [8, 12, 8, 12]
1901
+ });
1902
+ break;
1903
+ }
1904
+ }
1905
+
1906
+ // src/renderer/layouts/quote.ts
1907
+ function renderQuoteSlide(slide, node, theme) {
1908
+ const spec = layoutSpec(theme, "quote");
1909
+ if (extraBoolean(spec, "minimalQuote", false)) {
1910
+ renderMinimalQuote(slide, node, theme);
1911
+ return;
1912
+ }
1913
+ const showMark = extraBoolean(spec, "quoteMark", true);
1914
+ const markSize = extraNumber(spec, "quoteMarkSize", 72);
1915
+ const align = specAlign(spec.titleAlign, "center");
1916
+ const { left, right } = resolveSideMargins(spec, 1.5);
1917
+ const quoteBar = extraBoolean(spec, "quoteBar", false);
1918
+ const barW = extraNumber(spec, "quoteBarWidth", 0.09);
1919
+ const attrAlign = extraString(spec, "attributionAlign", "right");
1920
+ if (showMark && !quoteBar) {
1921
+ slide.addText("\u201C", {
1922
+ x: left,
1923
+ y: 1,
1924
+ w: 2,
1925
+ h: 1.5,
1926
+ fontSize: markSize,
1927
+ fontFace: "Georgia",
1928
+ color: theme.colors.accent,
1929
+ bold: true
1930
+ });
1931
+ }
1932
+ const quoteElement = node.elements.find((e) => e.type === "blockquote");
1933
+ if (!quoteElement || quoteElement.type !== "blockquote") return;
1934
+ let quoteText = quoteElement.content;
1935
+ let attribution = "";
1936
+ const attrMatch = quoteText.match(/\n?\s*[—–-]\s*(.+)$/);
1937
+ if (attrMatch) {
1938
+ attribution = attrMatch[1].trim();
1939
+ quoteText = quoteText.slice(0, attrMatch.index).trim();
1940
+ }
1941
+ const textX = quoteBar ? left + barW + 0.35 : Math.max(left, showMark && !quoteBar ? 1.8 : left);
1942
+ const textW = Math.max(4, 13.33 - textX - right);
1943
+ const textY = extraNumber(spec, "textY", quoteBar ? 2.2 : 2.5);
1944
+ const textH = extraNumber(spec, "textH", quoteBar ? 2.8 : 2.5);
1945
+ if (quoteBar) {
1946
+ addHRuler(slide, {
1947
+ // vertical bar via tall thin rect
1948
+ x: left,
1949
+ y: textY,
1950
+ w: barW,
1951
+ h: textH,
1952
+ color: theme.colors.accent
1953
+ });
1954
+ }
1955
+ const quoteRule = extraBoolean(spec, "quoteRule", false);
1956
+ if (quoteRule) {
1957
+ const qRuleY = extraNumber(spec, "quoteRuleY", 2.19);
1958
+ const qRuleW = extraNumber(spec, "quoteRuleWidth", 1.33);
1959
+ const qRuleH = extraNumber(spec, "quoteRuleHeight", 0.04);
1960
+ const qRuleColor = ruleColor(
1961
+ theme,
1962
+ extraString(spec, "quoteRuleColor", "primary")
1963
+ );
1964
+ const qRuleX = (13.33 - qRuleW) / 2;
1965
+ addHRuler(slide, {
1966
+ x: qRuleX,
1967
+ y: qRuleY,
1968
+ w: qRuleW,
1969
+ h: qRuleH,
1970
+ color: qRuleColor
1971
+ });
1972
+ }
1973
+ slide.addText(quoteText, {
1974
+ x: textX,
1975
+ y: textY,
1976
+ w: textW,
1977
+ h: textH,
1978
+ fontSize: theme.fontSize.heading,
1979
+ fontFace: theme.fonts.body,
1980
+ color: theme.colors.text,
1981
+ italic: true,
1982
+ align,
1983
+ valign: "middle"
1984
+ });
1985
+ if (attribution) {
1986
+ slide.addText(`\u2014 ${attribution}`, {
1987
+ x: textX,
1988
+ y: textY + textH + 0.25,
1989
+ w: textW,
1990
+ h: 0.55,
1991
+ fontSize: theme.fontSize.body,
1992
+ fontFace: theme.fonts.body,
1993
+ color: theme.colors.secondary,
1994
+ align: attrAlign === "left" ? "left" : attrAlign === "center" ? "center" : "right"
1995
+ });
1996
+ }
1997
+ }
1998
+ function renderMinimalQuote(slide, node, theme) {
1999
+ const spec = layoutSpec(theme, "quote");
2000
+ const lineX = extraNumber(spec, "minimalRuleX", 1.67);
2001
+ const lineY = extraNumber(spec, "minimalRuleY", 2.78);
2002
+ const lineH = extraNumber(spec, "minimalRuleH", 1.39);
2003
+ const lineW = extraNumber(spec, "minimalRuleW", 0.04);
2004
+ const color = theme.colors.divider ?? theme.colors.secondary;
2005
+ const textX = extraNumber(spec, "minimalTextX", 2.08);
2006
+ const textY = lineY;
2007
+ const textW = extraNumber(spec, "minimalTextW", 9.58);
2008
+ const textH = extraNumber(spec, "minimalTextH", 1.39);
2009
+ const quoteElement = node.elements.find((e) => e.type === "blockquote");
2010
+ if (!quoteElement || quoteElement.type !== "blockquote") return;
2011
+ let quoteText = quoteElement.content;
2012
+ let attribution = "";
2013
+ const attrMatch = quoteText.match(/\n?\s*[—–-]\s*(.+)$/);
2014
+ if (attrMatch) {
2015
+ attribution = attrMatch[1].trim();
2016
+ quoteText = quoteText.slice(0, attrMatch.index).trim();
2017
+ }
2018
+ slide.addShape("rect", {
2019
+ x: lineX,
2020
+ y: lineY,
2021
+ w: lineW,
2022
+ h: lineH,
2023
+ fill: { color }
2024
+ });
2025
+ slide.addText(quoteText, {
2026
+ x: textX,
2027
+ y: textY,
2028
+ w: textW,
2029
+ h: textH,
2030
+ fontSize: theme.fontSize.heading,
2031
+ fontFace: theme.fonts.body,
2032
+ color: theme.colors.text,
2033
+ italic: true,
2034
+ align: "left",
2035
+ valign: "middle",
2036
+ wrap: true
2037
+ });
2038
+ if (attribution) {
2039
+ slide.addText(`\u2014 ${attribution}`, {
2040
+ x: textX,
2041
+ y: textY + textH + 0.3,
2042
+ w: textW,
2043
+ h: 0.4,
2044
+ fontSize: theme.fontSize.body,
2045
+ fontFace: theme.fonts.body,
2046
+ color: theme.colors.secondary,
2047
+ align: "left"
2048
+ });
2049
+ }
2050
+ }
2051
+
2052
+ // src/renderer/layouts/closing.ts
2053
+ function renderClosingSlide(slide, node, theme) {
2054
+ const spec = layoutSpec(theme, "closing");
2055
+ if (extraBoolean(spec, "minimalClosing", false)) {
2056
+ renderMinimalClosing(slide, node, theme);
2057
+ return;
2058
+ }
2059
+ const align = specAlign(spec.titleAlign, "center");
2060
+ const { left, right } = resolveSideMargins(spec, 0.8);
2061
+ const contentW = Math.max(3, 13.33 - left - right);
2062
+ const thankText = extraString(spec, "thankText", "Thank you");
2063
+ const primary = node.title?.trim() || thankText;
2064
+ const textY = extraNumber(spec, "textY", 2.7);
2065
+ const showRules = extraBoolean(spec, "titleRule", false);
2066
+ const color = ruleColor(theme, titleRuleRole(spec) === "primary" ? "primary" : titleRuleRole(spec));
2067
+ const ruleW = extraNumber(spec, "ruleWidth", Math.min(contentW, 3.2));
2068
+ const closingRule = extraString(spec, "closingRule", "both");
2069
+ const ruleH = extraNumber(spec, "ruleHeight", 0.03);
2070
+ const ruleY = extraNumber(spec, "ruleY", textY + 1.5);
2071
+ const ruleX = align === "center" ? left + (contentW - ruleW) / 2 : align === "right" ? left + contentW - ruleW : left;
2072
+ const drawAbove = showRules && (closingRule === "both" || closingRule === "above");
2073
+ const drawBelow = showRules && (closingRule === "both" || closingRule === "below");
2074
+ if (drawAbove) {
2075
+ addHRuler(slide, { x: ruleX, y: textY - 0.35, w: ruleW, h: ruleH, color });
2076
+ }
2077
+ slide.addText(primary, {
2078
+ x: left,
2079
+ y: textY,
2080
+ w: contentW,
2081
+ h: 1.3,
2082
+ fontSize: theme.fontSize.title,
2083
+ fontFace: theme.fonts.heading,
2084
+ color: theme.colors.primary,
2085
+ bold: true,
2086
+ align,
2087
+ valign: "middle"
2088
+ });
2089
+ if (drawBelow) {
2090
+ addHRuler(slide, { x: ruleX, y: ruleY, w: ruleW, h: ruleH, color });
2091
+ }
2092
+ const secondary = node.subtitle?.trim();
2093
+ if (secondary) {
2094
+ slide.addText(secondary, {
2095
+ x: left,
2096
+ y: textY + 1.7,
2097
+ w: contentW,
2098
+ h: 0.6,
2099
+ fontSize: theme.fontSize.body,
2100
+ fontFace: theme.fonts.body,
2101
+ color: theme.colors.secondary,
2102
+ align,
2103
+ valign: "middle"
2104
+ });
2105
+ }
2106
+ }
2107
+ function renderMinimalClosing(slide, node, theme) {
2108
+ const spec = layoutSpec(theme, "closing");
2109
+ const color = theme.colors.divider ?? theme.colors.secondary;
2110
+ const thankText = extraString(spec, "thankText", "Thank you");
2111
+ const primary = node.title?.trim() || thankText;
2112
+ const ruleW = extraNumber(spec, "minimalRuleW", 1.11);
2113
+ const ruleH = extraNumber(spec, "minimalRuleH", 0.03);
2114
+ const ruleY = extraNumber(spec, "minimalRuleY", 2.78);
2115
+ addHRuler(slide, {
2116
+ x: (13.33 - ruleW) / 2,
2117
+ y: ruleY,
2118
+ w: ruleW,
2119
+ h: ruleH,
2120
+ color
2121
+ });
2122
+ slide.addText(primary, {
2123
+ x: extraNumber(spec, "minimalThankX", 2.5),
2124
+ y: extraNumber(spec, "minimalThankY", 3.06),
2125
+ w: extraNumber(spec, "minimalThankW", 8.33),
2126
+ h: 0.83,
2127
+ fontSize: theme.fontSize.title,
2128
+ fontFace: theme.fonts.heading,
2129
+ color: theme.colors.primary,
2130
+ bold: true,
2131
+ align: "center",
2132
+ valign: "middle"
2133
+ });
2134
+ const contact = node.subtitle?.trim() ?? "";
2135
+ if (contact) {
2136
+ slide.addText(contact, {
2137
+ x: extraNumber(spec, "minimalContactX", 3.89),
2138
+ y: extraNumber(spec, "minimalContactY", 4.31),
2139
+ w: extraNumber(spec, "minimalContactW", 5.56),
2140
+ h: 0.33,
2141
+ fontSize: theme.fontSize.body,
2142
+ fontFace: theme.fonts.body,
2143
+ color: theme.colors.secondary,
2144
+ align: "center",
2145
+ valign: "middle"
2146
+ });
2147
+ }
2148
+ }
2149
+
2150
+ // src/renderer/layouts/image-pages.ts
2151
+ import { readFileSync as readFileSync2 } from "fs";
2152
+ var SLIDE_W2 = 13.33;
2153
+ var SLIDE_H2 = 7.5;
2154
+ var IMAGE_MIME2 = {
2155
+ png: "image/png",
2156
+ jpg: "image/jpeg",
2157
+ jpeg: "image/jpeg",
2158
+ gif: "image/gif",
2159
+ webp: "image/webp",
2160
+ bmp: "image/bmp",
2161
+ svg: "image/svg+xml"
2162
+ };
2163
+ function specFor(theme, slots) {
2164
+ if (slots === 1) return layoutSpec(theme, "imageSingle");
2165
+ if (slots === 2) return layoutSpec(theme, "imageDouble");
2166
+ return layoutSpec(theme, "imageTriple");
2167
+ }
2168
+ function pickImages(node, slots) {
2169
+ const images = node.elements.filter((e) => e.type === "image").slice(0, slots);
2170
+ const captions = node.elements.filter((e) => e.type === "text").map((e) => e.type === "text" ? e.content.trim() : "").filter((t) => t.length > 0 && t.length <= 40);
2171
+ return { images, captions };
2172
+ }
2173
+ async function placeImage(slide, element, box, node, ctx) {
2174
+ try {
2175
+ const resolved = await ctx.resolveImage(element.src);
2176
+ if (!resolved.ok) {
2177
+ log.warn(`[${node.title ?? "slide"}] ${resolved.error}`);
2178
+ return;
2179
+ }
2180
+ const fileData = readFileSync2(resolved.path);
2181
+ const imgSize = getImageSize(fileData) ?? { width: 6, height: 3 };
2182
+ const fitted = fitInBox(imgSize, box.w, box.h);
2183
+ const ext = resolved.path.split(".").pop()?.toLowerCase() ?? "png";
2184
+ const mime = IMAGE_MIME2[ext] ?? "image/png";
2185
+ const data = `${mime};base64,${fileData.toString("base64")}`;
2186
+ if (ext === "webp") {
2187
+ log.warn(
2188
+ `[${node.title ?? "slide"}] WebP does not render in PowerPoint for the web or Office 2019 and earlier \u2014 some recipients may see a broken image (${element.src})`
2189
+ );
2190
+ }
2191
+ const x = box.x + (box.w - fitted.width) / 2;
2192
+ const y = box.y + (box.h - fitted.height) / 2;
2193
+ slide.addImage({
2194
+ data,
2195
+ altText: element.alt || element.src,
2196
+ x,
2197
+ y,
2198
+ w: fitted.width,
2199
+ h: fitted.height
2200
+ });
2201
+ } catch (err) {
2202
+ const reason = err instanceof Error ? err.message : "unknown";
2203
+ log.warn(`[${node.title ?? "slide"}] image error: ${reason}`);
2204
+ slide.addText(`[Image error: ${reason}]`, {
2205
+ x: box.x,
2206
+ y: box.y,
2207
+ w: box.w,
2208
+ h: 0.4,
2209
+ fontSize: 12,
2210
+ color: "FF0000"
2211
+ });
2212
+ }
2213
+ }
2214
+ async function renderImageSlide(slide, node, theme, ctx, slots) {
2215
+ const spec = specFor(theme, slots);
2216
+ if (extraBoolean(spec, "minimalImage", false)) {
2217
+ await renderMinimalImage(slide, node, theme, ctx, slots, spec);
2218
+ return;
2219
+ }
2220
+ const { left, right } = resolveSideMargins(spec, 0.6);
2221
+ const titleH = specNumber(spec.titleHeight, 0.9);
2222
+ const titleAlign = specAlign(spec.titleAlign, "left");
2223
+ const contentW = Math.max(2, SLIDE_W2 - left - right);
2224
+ let top = extraNumber(spec, "titleY", 0.3);
2225
+ if (node.title) {
2226
+ if (extraBoolean(spec, "titleRule", true)) {
2227
+ const rh = titleRuleHeight(spec, 0.04);
2228
+ const ruleW = extraNumber(spec, "titleRuleWidth", contentW);
2229
+ addHRuler(slide, {
2230
+ x: left,
2231
+ y: top + titleH - rh - 0.01,
2232
+ w: ruleW,
2233
+ h: rh,
2234
+ color: ruleColor(theme, titleRuleRole(spec))
2235
+ });
2236
+ }
2237
+ slide.addText(node.title, {
2238
+ x: left,
2239
+ y: top,
2240
+ w: contentW,
2241
+ h: titleH,
2242
+ fontSize: theme.fontSize.heading,
2243
+ fontFace: theme.fonts.heading,
2244
+ color: theme.colors.primary,
2245
+ bold: true,
2246
+ align: titleAlign === "center" ? "center" : "left",
2247
+ valign: "bottom"
2248
+ });
2249
+ top = top + titleH + 0.2;
2250
+ }
2251
+ const bottom = 0.45;
2252
+ const zoneH = SLIDE_H2 - top - bottom;
2253
+ const { images, captions } = pickImages(node, slots);
2254
+ if (images.length === 0) {
2255
+ const fallback = captions[0] ?? node.title ?? "No image";
2256
+ slide.addText(fallback, {
2257
+ x: left,
2258
+ y: top,
2259
+ w: contentW,
2260
+ h: zoneH,
2261
+ fontSize: theme.fontSize.body,
2262
+ fontFace: theme.fonts.body,
2263
+ color: theme.colors.secondary,
2264
+ align: "center",
2265
+ valign: "middle"
2266
+ });
2267
+ return;
2268
+ }
2269
+ const gap = 0.25;
2270
+ const captionH = 0.35;
2271
+ const cellW = (contentW - gap * (slots - 1)) / slots;
2272
+ const cellH = zoneH - captionH;
2273
+ for (let i = 0; i < images.length; i++) {
2274
+ const x = left + i * (cellW + gap);
2275
+ await placeImage(
2276
+ slide,
2277
+ images[i],
2278
+ { x, y: top, w: cellW, h: cellH },
2279
+ node,
2280
+ ctx
2281
+ );
2282
+ const caption = images[i].alt?.trim() || captions[i];
2283
+ if (caption) {
2284
+ slide.addText(caption, {
2285
+ x,
2286
+ y: top + cellH + 0.05,
2287
+ w: cellW,
2288
+ h: captionH,
2289
+ fontSize: theme.fontSize.small,
2290
+ fontFace: theme.fonts.body,
2291
+ color: theme.colors.secondary,
2292
+ align: "center",
2293
+ valign: "top"
2294
+ });
2295
+ }
2296
+ }
2297
+ }
2298
+ async function renderMinimalImage(slide, node, theme, ctx, slots, spec) {
2299
+ const margin = specNumber(spec.margin, 0.83);
2300
+ const boxX = margin;
2301
+ const boxY = extraNumber(spec, "minimalImageY", 1.33);
2302
+ const boxH = extraNumber(spec, "minimalImageH", 5.28);
2303
+ const contentW = 13.33 - margin * 2;
2304
+ const lineColor = theme.colors.divider ?? theme.colors.secondary;
2305
+ if (node.title) {
2306
+ slide.addText(node.title, {
2307
+ x: boxX,
2308
+ y: extraNumber(spec, "minimalTitleY", 0.5),
2309
+ w: contentW,
2310
+ h: 0.44,
2311
+ fontSize: theme.fontSize.heading,
2312
+ fontFace: theme.fonts.heading,
2313
+ color: theme.colors.primary,
2314
+ bold: true,
2315
+ align: "left",
2316
+ valign: "middle"
2317
+ });
2318
+ addHRuler(slide, {
2319
+ x: boxX,
2320
+ y: extraNumber(spec, "minimalTitleRuleY", 1.03),
2321
+ w: extraNumber(spec, "minimalTitleRuleW", 0.56),
2322
+ h: extraNumber(spec, "minimalTitleRuleH", 0.03),
2323
+ color: lineColor
2324
+ });
2325
+ }
2326
+ const gaps = { 1: 0, 2: 0.28, 3: 0.21 };
2327
+ const gap = gaps[slots];
2328
+ const cellW = (contentW - gap * (slots - 1)) / slots;
2329
+ const { images } = pickImages(node, slots);
2330
+ for (let i = 0; i < slots; i++) {
2331
+ const x = boxX + i * (cellW + gap);
2332
+ slide.addShape("rect", {
2333
+ x,
2334
+ y: boxY,
2335
+ w: cellW,
2336
+ h: boxH,
2337
+ fill: { transparency: 100 },
2338
+ line: { color: lineColor, width: 0.01, dashType: "dash" }
2339
+ });
2340
+ if (images[i]) {
2341
+ await placeImage(slide, images[i], { x, y: boxY, w: cellW, h: boxH }, node, ctx);
2342
+ } else {
2343
+ slide.addText("Image", {
2344
+ x: x + (cellW - 2.22) / 2,
2345
+ y: boxY + (boxH - 0.56) / 2,
2346
+ w: 2.22,
2347
+ h: 0.56,
2348
+ fontSize: theme.fontSize.small,
2349
+ fontFace: theme.fonts.body,
2350
+ color: lineColor,
2351
+ align: "center",
2352
+ valign: "middle"
2353
+ });
2354
+ }
2355
+ }
2356
+ }
1187
2357
 
1188
2358
  // src/renderer/layouts/index.ts
1189
2359
  async function renderSlideLayout(slide, node, theme, ctx) {
@@ -1202,7 +2372,7 @@ async function renderSlideLayout(slide, node, theme, ctx) {
1202
2372
  renderTitleSlide(slide, node, theme);
1203
2373
  break;
1204
2374
  case "section":
1205
- renderSectionSlide(slide, node, theme);
2375
+ renderSectionSlide(slide, node, theme, ctx);
1206
2376
  break;
1207
2377
  case "code":
1208
2378
  await renderCodeSlide(slide, node, theme, ctx);
@@ -1210,6 +2380,18 @@ async function renderSlideLayout(slide, node, theme, ctx) {
1210
2380
  case "quote":
1211
2381
  renderQuoteSlide(slide, node, theme);
1212
2382
  break;
2383
+ case "closing":
2384
+ renderClosingSlide(slide, node, theme);
2385
+ break;
2386
+ case "image-single":
2387
+ await renderImageSlide(slide, node, theme, ctx, 1);
2388
+ break;
2389
+ case "image-double":
2390
+ await renderImageSlide(slide, node, theme, ctx, 2);
2391
+ break;
2392
+ case "image-triple":
2393
+ await renderImageSlide(slide, node, theme, ctx, 3);
2394
+ break;
1213
2395
  case "content":
1214
2396
  default:
1215
2397
  await renderContentSlide(slide, node, theme, ctx);
@@ -1218,18 +2400,45 @@ async function renderSlideLayout(slide, node, theme, ctx) {
1218
2400
  if (node.notes) {
1219
2401
  slide.addNotes(node.notes);
1220
2402
  }
1221
- if (node.layout !== "title" && node.layout !== "closing" && node.layout !== "blank") {
2403
+ const sectionSpec = layoutSpec(theme, "section");
2404
+ const sectionNoFooter = extraString(sectionSpec, "footer", "on") === "off";
2405
+ if (node.layout !== "title" && node.layout !== "closing" && node.layout !== "blank" && !(node.layout === "section" && sectionNoFooter)) {
1222
2406
  const footerText = renderFooter(node, theme, ctx);
2407
+ const footerSpec = layoutSpec(theme, "footer");
2408
+ const sides = resolveSideMargins(
2409
+ footerSpec,
2410
+ specNumber(theme.layouts?.content?.margin, 0.6)
2411
+ );
2412
+ const marginLeft = footerSpec.extra?.marginLeft !== void 0 ? sides.left : specNumber(
2413
+ theme.layouts?.content?.extra?.marginLeft,
2414
+ specNumber(theme.layouts?.content?.margin, sides.left)
2415
+ );
2416
+ const marginRight = footerSpec.extra?.marginRight !== void 0 ? sides.right : specNumber(
2417
+ theme.layouts?.content?.extra?.marginRight,
2418
+ specNumber(theme.layouts?.content?.margin, sides.right)
2419
+ );
2420
+ const contentW = Math.max(2, 13.33 - marginLeft - marginRight);
2421
+ const showDivider = theme.styles?.footerDivider ?? footerSpec.footerDivider ?? false;
2422
+ const footerAlign = extraString(footerSpec, "footerAlign", "right");
2423
+ if (showDivider) {
2424
+ slide.addShape("rect", {
2425
+ x: marginLeft,
2426
+ y: extraNumber(footerSpec, "dividerY", 7.08),
2427
+ w: contentW,
2428
+ h: 0.015,
2429
+ fill: { color: theme.colors.divider ?? theme.colors.secondary }
2430
+ });
2431
+ }
1223
2432
  if (footerText) {
1224
2433
  slide.addText(footerText, {
1225
- x: 0.6,
1226
- y: 7.16,
1227
- w: 12.13,
2434
+ x: marginLeft,
2435
+ y: extraNumber(footerSpec, "footerY", 7.16),
2436
+ w: contentW,
1228
2437
  h: 0.28,
1229
2438
  fontSize: theme.fontSize.small - 1,
1230
2439
  fontFace: theme.fonts.body,
1231
2440
  color: theme.colors.secondary,
1232
- align: "right",
2441
+ align: footerAlign,
1233
2442
  valign: "middle"
1234
2443
  });
1235
2444
  }
@@ -1243,6 +2452,93 @@ function renderFooter(node, theme, ctx) {
1243
2452
 
1244
2453
  // src/renderer/code-highlighter.ts
1245
2454
  import { createHighlighter } from "shiki";
2455
+
2456
+ // src/utils/gradient.ts
2457
+ import { deflateSync } from "zlib";
2458
+ var CRC_TABLE = (() => {
2459
+ const table = new Uint32Array(256);
2460
+ for (let n = 0; n < 256; n++) {
2461
+ let c = n;
2462
+ for (let k = 0; k < 8; k++) {
2463
+ c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
2464
+ }
2465
+ table[n] = c >>> 0;
2466
+ }
2467
+ return table;
2468
+ })();
2469
+ function crc32(bytes) {
2470
+ let crc = 4294967295;
2471
+ for (let i = 0; i < bytes.length; i++) {
2472
+ crc = CRC_TABLE[(crc ^ bytes[i]) & 255] ^ crc >>> 8;
2473
+ }
2474
+ return (crc ^ 4294967295) >>> 0;
2475
+ }
2476
+ function pngChunk(type, data) {
2477
+ const length = Buffer.alloc(4);
2478
+ length.writeUInt32BE(data.length);
2479
+ const payload = Buffer.concat([Buffer.from(type, "latin1"), Buffer.from(data)]);
2480
+ const crc = Buffer.alloc(4);
2481
+ crc.writeUInt32BE(crc32(payload));
2482
+ return Buffer.concat([length, payload, crc]);
2483
+ }
2484
+ function encodePngRgb(width, height, pixelAt) {
2485
+ const ihdr = Buffer.alloc(13);
2486
+ ihdr.writeUInt32BE(width, 0);
2487
+ ihdr.writeUInt32BE(height, 4);
2488
+ ihdr[8] = 8;
2489
+ ihdr[9] = 2;
2490
+ const raw = Buffer.alloc(height * (1 + width * 3));
2491
+ let offset = 0;
2492
+ for (let y = 0; y < height; y++) {
2493
+ raw[offset++] = 0;
2494
+ for (let x = 0; x < width; x++) {
2495
+ const [r, g, b] = pixelAt(x, y);
2496
+ raw[offset++] = r;
2497
+ raw[offset++] = g;
2498
+ raw[offset++] = b;
2499
+ }
2500
+ }
2501
+ const idat = deflateSync(raw, { level: 9 });
2502
+ return Buffer.concat([
2503
+ Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
2504
+ pngChunk("IHDR", ihdr),
2505
+ pngChunk("IDAT", new Uint8Array(idat)),
2506
+ pngChunk("IEND", new Uint8Array(0))
2507
+ ]);
2508
+ }
2509
+ function hexToRgb(hex) {
2510
+ let s = hex.replace(/^#/, "");
2511
+ if (s.length === 3) s = s.split("").map((ch) => ch + ch).join("");
2512
+ if (!/^[0-9a-fA-F]{6}$/.test(s)) throw new Error(`Invalid hex color: ${hex}`);
2513
+ return [
2514
+ parseInt(s.slice(0, 2), 16),
2515
+ parseInt(s.slice(2, 4), 16),
2516
+ parseInt(s.slice(4, 6), 16)
2517
+ ];
2518
+ }
2519
+ function createLinearGradientPng(from, to, angleDeg = 180, options = {}) {
2520
+ const width = options.width ?? 192;
2521
+ const height = options.height ?? 108;
2522
+ const angle = angleDeg * Math.PI / 180;
2523
+ const dx = Math.sin(angle);
2524
+ const dy = -Math.cos(angle);
2525
+ const extent = (Math.abs(dx) + Math.abs(dy)) / 2;
2526
+ const c1 = hexToRgb(from);
2527
+ const c2 = hexToRgb(to);
2528
+ return encodePngRgb(width, height, (x, y) => {
2529
+ const px = x / (width - 1) - 0.5;
2530
+ const py = y / (height - 1) - 0.5;
2531
+ let t = 0.5 + (px * dx + py * dy) / (2 * extent);
2532
+ t = t < 0 ? 0 : t > 1 ? 1 : t;
2533
+ return [
2534
+ Math.round(c1[0] + (c2[0] - c1[0]) * t),
2535
+ Math.round(c1[1] + (c2[1] - c1[1]) * t),
2536
+ Math.round(c1[2] + (c2[2] - c1[2]) * t)
2537
+ ];
2538
+ });
2539
+ }
2540
+
2541
+ // src/renderer/code-highlighter.ts
1246
2542
  var highlighterInstance = null;
1247
2543
  async function getHighlighter() {
1248
2544
  if (!highlighterInstance) {
@@ -1281,11 +2577,21 @@ function cleanColor(color) {
1281
2577
  if (!color) return "E2E8F0";
1282
2578
  return color.replace(/^#/, "");
1283
2579
  }
2580
+ function tint(hex, amount) {
2581
+ const mix = (channel) => Math.round(channel + (255 - channel) * amount).toString(16).padStart(2, "0");
2582
+ const [r, g, b] = hexToRgb(hex);
2583
+ return `${mix(r)}${mix(g)}${mix(b)}`;
2584
+ }
2585
+ function highlightBackgroundFor(theme) {
2586
+ const explicit = theme.colors.highlightBackground;
2587
+ if (explicit) return explicit;
2588
+ return tint(theme.colors.codeBackground, 0.25);
2589
+ }
1284
2590
  async function highlightCode(code, language, theme, highlightLines = []) {
1285
2591
  const highlighter = await getHighlighter();
1286
2592
  const runs = [];
1287
2593
  const shikiThemeName = theme.shikiTheme ?? "github-dark";
1288
- const highlightColor = theme.colors.highlightBackground ?? "FFF3C4";
2594
+ const highlightColor = highlightBackgroundFor(theme);
1289
2595
  try {
1290
2596
  const loadedLangs = highlighter.getLoadedLanguages();
1291
2597
  if (!loadedLangs.includes(language)) {
@@ -1355,27 +2661,113 @@ async function highlightCode(code, language, theme, highlightLines = []) {
1355
2661
  }
1356
2662
 
1357
2663
  // src/renderer/image-handler.ts
1358
- import { existsSync, writeFileSync } from "fs";
2664
+ import { existsSync, readFileSync as readFileSync3, writeFileSync } from "fs";
1359
2665
  import { resolve } from "path";
1360
2666
  import { tmpdir } from "os";
1361
2667
  import { join } from "path";
1362
2668
  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)}` };
2669
+
2670
+ // src/diagrams/svg-to-png.ts
2671
+ function ensureSvgNamespace(svg) {
2672
+ if (svg.includes("xmlns=")) return svg;
2673
+ return svg.replace(/<svg\b([^>]*)>/i, '<svg xmlns="http://www.w3.org/2000/svg" $1>');
2674
+ }
2675
+ function normalizeSvg(svg) {
2676
+ let clean = ensureSvgNamespace(svg.trim());
2677
+ clean = clean.replace(/(<svg\b[^>]*)\s+width=["'][^"']*["']/gi, "$1");
2678
+ clean = clean.replace(/(<svg\b[^>]*)\s+height=["'][^"']*["']/gi, "$1");
2679
+ const viewBoxMatch = clean.match(/viewBox\s*=\s*["']([^"']+)["']/i);
2680
+ let frameUsable = true;
2681
+ if (viewBoxMatch) {
2682
+ const parts = viewBoxMatch[1].trim().split(/[\s,]+/).map(Number);
2683
+ if (parts.length === 4 && parts[2] > 0 && parts[3] > 0) {
2684
+ const w = parts[2];
2685
+ const h = parts[3];
2686
+ clean = clean.replace(/<svg\b([^>]*)>/i, `<svg width="${w}" height="${h}" $1>`);
2687
+ } else {
2688
+ frameUsable = false;
1376
2689
  }
1377
- if (src.startsWith("http://") || src.startsWith("https://")) {
1378
- let response;
2690
+ } else {
2691
+ clean = clean.replace(/<svg\b([^>]*)>/i, `<svg width="800" height="600" viewBox="0 0 800 600" $1>`);
2692
+ }
2693
+ return { svg: clean, frameUsable };
2694
+ }
2695
+ async function svgToPng(svg, width = 800) {
2696
+ const { svg: normalized, frameUsable } = normalizeSvg(svg);
2697
+ const { Resvg } = await import("@resvg/resvg-js");
2698
+ const resvg = new Resvg(normalized, {
2699
+ fitTo: { mode: "width", value: width }
2700
+ });
2701
+ if (frameUsable) reframeToContent(resvg);
2702
+ const pngData = resvg.render();
2703
+ return Buffer.from(pngData.asPng());
2704
+ }
2705
+ var SVG_EMBED_WIDTH = 1200;
2706
+ async function rasterizeSvg(svg, width) {
2707
+ const { Resvg } = await import("@resvg/resvg-js");
2708
+ const resvg = new Resvg(ensureSvgNamespace(svg.trim()), {
2709
+ fitTo: { mode: "width", value: width }
2710
+ });
2711
+ return Buffer.from(resvg.render().asPng());
2712
+ }
2713
+ var CONTENT_PADDING = 4;
2714
+ var MAX_ASPECT_SHIFT = 3;
2715
+ function reframeToContent(resvg, padding = CONTENT_PADDING) {
2716
+ const bbox = resvg.getBBox();
2717
+ if (!bbox || !(bbox.width > 0) || !(bbox.height > 0)) return;
2718
+ const frameAspect = resvg.width / resvg.height;
2719
+ if (!Number.isFinite(frameAspect) || frameAspect <= 0) return;
2720
+ const contentAspect = bbox.width / bbox.height;
2721
+ const shift = Math.max(contentAspect / frameAspect, frameAspect / contentAspect);
2722
+ if (shift > MAX_ASPECT_SHIFT) return;
2723
+ try {
2724
+ bbox.x -= padding;
2725
+ bbox.y -= padding;
2726
+ bbox.width += padding * 2;
2727
+ bbox.height += padding * 2;
2728
+ } catch {
2729
+ }
2730
+ resvg.cropByBBox(bbox);
2731
+ }
2732
+
2733
+ // src/renderer/image-handler.ts
2734
+ var REMOTE_FETCH_TIMEOUT_MS = 1e4;
2735
+ async function resolveImage(src, basePath) {
2736
+ const located = await locateImage(src, basePath);
2737
+ if (!located.ok) return located;
2738
+ return rasterizeIfSvg(located);
2739
+ }
2740
+ async function rasterizeIfSvg(resolved) {
2741
+ if (!resolved.ok) return resolved;
2742
+ try {
2743
+ const data = readFileSync3(resolved.path);
2744
+ if (!isSvgImage(data)) return resolved;
2745
+ const png = await rasterizeSvg(data.toString("utf8"), SVG_EMBED_WIDTH);
2746
+ const tmpPath = join(tmpdir(), `mfly-${randomUUID().slice(0, 8)}.png`);
2747
+ writeFileSync(tmpPath, png);
2748
+ return { ok: true, path: tmpPath };
2749
+ } catch (err) {
2750
+ log.warn(
2751
+ `Could not rasterize ${resolved.path}: ${err instanceof Error ? err.message : String(err)} \u2014 embedding the SVG as-is`
2752
+ );
2753
+ return resolved;
2754
+ }
2755
+ }
2756
+ async function locateImage(src, basePath) {
2757
+ try {
2758
+ if (src.startsWith("data:")) {
2759
+ const match = src.match(/^data:image\/([\w.+-]+);base64,(.+)/);
2760
+ if (match) {
2761
+ const ext = match[1].split("+")[0];
2762
+ const base64 = match[2];
2763
+ const tmpPath = join(tmpdir(), `mfly-${randomUUID().slice(0, 8)}.${ext}`);
2764
+ writeFileSync(tmpPath, Buffer.from(base64, "base64"));
2765
+ return { ok: true, path: tmpPath };
2766
+ }
2767
+ return { ok: false, error: `Unsupported data URI: ${src.slice(0, 48)}` };
2768
+ }
2769
+ if (src.startsWith("http://") || src.startsWith("https://")) {
2770
+ let response;
1379
2771
  try {
1380
2772
  response = await fetch(src, { signal: AbortSignal.timeout(REMOTE_FETCH_TIMEOUT_MS) });
1381
2773
  } catch (err) {
@@ -1405,39 +2797,6 @@ async function resolveImage(src, basePath) {
1405
2797
  }
1406
2798
  }
1407
2799
 
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
2800
  // src/diagrams/theme.ts
1442
2801
  function isDarkColor(hex) {
1443
2802
  const clean = hex.replace(/^#/, "");
@@ -1774,25 +3133,55 @@ function contentBBox(root) {
1774
3133
  if (!Number.isFinite(minX)) return null;
1775
3134
  return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
1776
3135
  }
3136
+ var browserGlobals = [];
3137
+ function installBrowserGlobals() {
3138
+ const saved = browserGlobals.map(
3139
+ ([key]) => [key, Object.getOwnPropertyDescriptor(globalThis, key)]
3140
+ );
3141
+ for (const [key, value] of browserGlobals) {
3142
+ try {
3143
+ Object.defineProperty(globalThis, key, {
3144
+ value,
3145
+ configurable: true,
3146
+ writable: true
3147
+ });
3148
+ } catch {
3149
+ globalThis[key] = value;
3150
+ }
3151
+ }
3152
+ return () => {
3153
+ for (const [key, descriptor] of saved) {
3154
+ if (descriptor) {
3155
+ Object.defineProperty(globalThis, key, descriptor);
3156
+ } else {
3157
+ delete globalThis[key];
3158
+ }
3159
+ }
3160
+ };
3161
+ }
1777
3162
  var MermaidDiagramRenderer = class {
1778
3163
  type = "mermaid";
1779
3164
  renderCounter = 0;
1780
3165
  async initialize() {
1781
3166
  if (initialized) return;
3167
+ browserGlobals = [];
1782
3168
  const { JSDOM } = await import("jsdom");
1783
3169
  const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>", {
1784
3170
  pretendToBeVisual: true
1785
3171
  });
3172
+ const jsdomGetComputedStyle = dom.window.getComputedStyle.bind(dom.window);
3173
+ dom.window.getComputedStyle = ((element, pseudo) => {
3174
+ const style = jsdomGetComputedStyle(element, pseudo);
3175
+ const read = style.getPropertyValue.bind(style);
3176
+ style.getPropertyValue = (property) => {
3177
+ const value = read(property);
3178
+ if (value !== "" && value != null) return value;
3179
+ return property.startsWith("padding") ? "0px" : value;
3180
+ };
3181
+ return style;
3182
+ });
1786
3183
  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
- }
3184
+ browserGlobals.push([key, value]);
1796
3185
  };
1797
3186
  assignGlobal("window", dom.window);
1798
3187
  assignGlobal("document", dom.window.document);
@@ -1850,25 +3239,36 @@ var MermaidDiagramRenderer = class {
1850
3239
  y: b.y,
1851
3240
  width: b.width,
1852
3241
  height: b.height,
3242
+ toJSON: () => ({})
3243
+ });
3244
+ const toDomRect = (b) => ({
3245
+ ...toRect(b),
1853
3246
  top: b.y,
1854
3247
  right: b.x + b.width,
1855
3248
  bottom: b.y + b.height,
1856
- left: b.x,
1857
- toJSON: () => ({})
3249
+ left: b.x
1858
3250
  });
3251
+ const isMeasuredNode = (el) => {
3252
+ const parent = el.parentElement;
3253
+ if (!parent) return false;
3254
+ return (parent.getAttribute("class") ?? "").split(/\s+/).includes("nodes");
3255
+ };
1859
3256
  const polyfillBBox = function() {
1860
3257
  const tag = this.tagName ? this.tagName.toLowerCase() : "";
1861
3258
  if (tag === "style" || tag === "script" || tag === "defs" || tag === "clippath" || tag === "marker") {
1862
3259
  return toRect({ x: 0, y: 0, width: 0, height: 0 });
1863
3260
  }
1864
- if (tag === "svg") {
3261
+ if (tag === "svg" || isMeasuredNode(this)) {
1865
3262
  const box = contentBBox(this);
1866
3263
  if (box) return toRect(box);
1867
3264
  }
1868
3265
  const text = getDirectText(this);
1869
3266
  const width = text ? Math.max(30, text.length * 9 + 20) : 80;
1870
3267
  const height = text ? 28 : 40;
1871
- return toRect({ x: 0, y: 0, width, height });
3268
+ const isText = tag === "text" || tag === "tspan";
3269
+ const anchor = parseFloat(this.getAttribute("x") ?? "");
3270
+ const x = isText ? (Number.isFinite(anchor) ? anchor : 0) - width / 2 : 0;
3271
+ return toRect({ x, y: 0, width, height });
1872
3272
  };
1873
3273
  if (!dom.window.SVGElement.prototype.getBBox) {
1874
3274
  dom.window.SVGElement.prototype.getBBox = polyfillBBox;
@@ -1877,7 +3277,10 @@ var MermaidDiagramRenderer = class {
1877
3277
  dom.window.Element.prototype.getBBox = polyfillBBox;
1878
3278
  }
1879
3279
  if (!dom.window.Element.prototype.getBoundingClientRect) {
1880
- dom.window.Element.prototype.getBoundingClientRect = polyfillBBox;
3280
+ dom.window.Element.prototype.getBoundingClientRect = (function() {
3281
+ const box = polyfillBBox.call(this);
3282
+ return toDomRect(box);
3283
+ });
1881
3284
  }
1882
3285
  const computeTextLength = function() {
1883
3286
  const text = getDirectText(this);
@@ -1981,8 +3384,13 @@ var MermaidDiagramRenderer = class {
1981
3384
  dispatchEvent: () => false
1982
3385
  });
1983
3386
  }
1984
- const mermaid = (await import("mermaid")).default;
1985
- mermaid.initialize(this.buildConfig());
3387
+ const restore = installBrowserGlobals();
3388
+ try {
3389
+ const mermaid = (await import("mermaid")).default;
3390
+ mermaid.initialize(this.buildConfig());
3391
+ } finally {
3392
+ restore();
3393
+ }
1986
3394
  initialized = true;
1987
3395
  }
1988
3396
  buildConfig(theme) {
@@ -2000,22 +3408,30 @@ var MermaidDiagramRenderer = class {
2000
3408
  background: "transparent"
2001
3409
  },
2002
3410
  flowchart: { htmlLabels: false, curve: "basis", useMaxWidth: false },
2003
- sequence: { useMaxWidth: false }
3411
+ sequence: { useMaxWidth: false },
3412
+ // jsdom has no layout engine: offsetWidth is 0 rather than undefined, so
3413
+ // mermaid's own "no parent width" fallback never fires and the gantt chart
3414
+ // ends up with a zero-width viewBox. Pin the viewport width instead.
3415
+ gantt: { useWidth: 1200 }
2004
3416
  };
2005
3417
  }
2006
3418
  async render(code, theme) {
2007
3419
  await this.initialize();
2008
3420
  const mermaid = (await import("mermaid")).default;
2009
3421
  const id = `mfly-mermaid-${Date.now()}-${this.renderCounter++}`;
2010
- mermaid.initialize(this.buildConfig(theme));
3422
+ let svg;
3423
+ const restore = installBrowserGlobals();
2011
3424
  try {
2012
- const { svg } = await mermaid.render(id, code.trim());
2013
- return svgToPng(svg, 1200);
3425
+ mermaid.initialize(this.buildConfig(theme));
3426
+ ({ svg } = await mermaid.render(id, code.trim()));
2014
3427
  } catch (err) {
2015
3428
  throw new Error(
2016
3429
  `Mermaid render failed: ${err instanceof Error ? err.message : String(err)}`
2017
3430
  );
3431
+ } finally {
3432
+ restore();
2018
3433
  }
3434
+ return svgToPng(svg, 1200);
2019
3435
  }
2020
3436
  };
2021
3437
 
@@ -2095,25 +3511,213 @@ var EChartsDiagramRenderer = class {
2095
3511
  }
2096
3512
  };
2097
3513
 
3514
+ // src/diagrams/plantuml-renderer.ts
3515
+ var RENDER_WIDTH = 1200;
3516
+ var RENDER_TIMEOUT_MS = 3e4;
3517
+ function originalLine(reported, prepared) {
3518
+ if (reported === void 0) return void 0;
3519
+ return reported > prepared.shiftAfter ? reported - prepared.lineShift : reported;
3520
+ }
3521
+ async function withEngineLogsMuted(fn) {
3522
+ const originals = { log: console.log, info: console.info, debug: console.debug };
3523
+ const sink = process.env.MFLY_DEBUG ? (...args) => console.error(...args) : () => {
3524
+ };
3525
+ console.log = sink;
3526
+ console.info = sink;
3527
+ console.debug = sink;
3528
+ try {
3529
+ return await fn();
3530
+ } finally {
3531
+ console.log = originals.log;
3532
+ console.info = originals.info;
3533
+ console.debug = originals.debug;
3534
+ }
3535
+ }
3536
+ function fontValue(theme) {
3537
+ return `"${theme.fonts.cjk.replace(/"/g, "")}"`;
3538
+ }
3539
+ var LINE_STROKE = "stroke:#181818;stroke-width:0.5;";
3540
+ var LIFELINE_STROKE = `${LINE_STROKE}stroke-dasharray:5,5;`;
3541
+ function restoreLineStrokes(svg, diagramType) {
3542
+ const dashed = /sequence/i.test(diagramType ?? "");
3543
+ return svg.replace(
3544
+ /<line\b([^>]*?)\/>/g,
3545
+ (match, attributes) => /\b(stroke|style)=/.test(attributes) ? match : `<line${attributes} style="${dashed ? LIFELINE_STROKE : LINE_STROKE}"/>`
3546
+ );
3547
+ }
3548
+ var PlantUmlDiagramRenderer = class {
3549
+ type = "plantuml";
3550
+ engine = null;
3551
+ async initialize() {
3552
+ if (this.engine) return;
3553
+ this.engine = await withEngineLogsMuted(async () => {
3554
+ const globalWithViz = globalThis;
3555
+ if (!globalWithViz.Viz) {
3556
+ const vizModule = await import("@viz-js/viz");
3557
+ let instancePromise = null;
3558
+ globalWithViz.Viz = {
3559
+ instance: () => instancePromise ??= vizModule.instance()
3560
+ };
3561
+ }
3562
+ return import("@plantuml/mcp-js/engine.js");
3563
+ });
3564
+ }
3565
+ /**
3566
+ * PlantUML has no dark-mode switch in this build, so the palette is injected
3567
+ * as skinparams — the same shape as the DOT renderer's attribute prelude.
3568
+ * Anything the author writes after the prelude still wins.
3569
+ *
3570
+ * `backgroundColor transparent` is stated explicitly rather than left to the
3571
+ * default: PlantUML otherwise writes `background:#FFFFFF` into the SVG style,
3572
+ * which only stays invisible because the rasterizer happens to ignore it.
3573
+ */
3574
+ skinparams(theme) {
3575
+ const lines = ["skinparam backgroundColor transparent"];
3576
+ if (theme) {
3577
+ lines.push(`skinparam defaultFontName ${fontValue(theme)}`);
3578
+ }
3579
+ if (!isDarkTheme(theme)) {
3580
+ lines.push("skinparam defaultFontColor #1F2328");
3581
+ return lines.join("\n");
3582
+ }
3583
+ lines.push(
3584
+ "skinparam defaultFontColor #C9D1D9",
3585
+ "skinparam shadowing false",
3586
+ // Boxes are light by default, so the fills have to move with the text or
3587
+ // labels end up dark-on-dark.
3588
+ "skinparam classBackgroundColor #1F2937",
3589
+ "skinparam classBorderColor #6B7280",
3590
+ "skinparam classFontColor #E5E7EB",
3591
+ "skinparam classAttributeFontColor #CBD5E1",
3592
+ "skinparam participantBackgroundColor #1F2937",
3593
+ "skinparam participantBorderColor #6B7280",
3594
+ "skinparam actorBackgroundColor #1F2937",
3595
+ "skinparam actorBorderColor #6B7280",
3596
+ "skinparam sequenceArrowColor #9CA3AF",
3597
+ "skinparam sequenceLifeLineBorderColor #6B7280",
3598
+ "skinparam noteBackgroundColor #374151",
3599
+ "skinparam noteBorderColor #6B7280",
3600
+ "skinparam activityBackgroundColor #1F2937",
3601
+ "skinparam activityBorderColor #6B7280",
3602
+ "skinparam activityDiamondBackgroundColor #1F2937",
3603
+ "skinparam activityDiamondBorderColor #6B7280",
3604
+ "skinparam stateBackgroundColor #1F2937",
3605
+ "skinparam stateBorderColor #6B7280",
3606
+ "skinparam componentBackgroundColor #1F2937",
3607
+ "skinparam componentBorderColor #6B7280",
3608
+ "skinparam componentFontColor #E5E7EB"
3609
+ );
3610
+ return lines.join("\n");
3611
+ }
3612
+ /**
3613
+ * Drop `!theme` directives and inject the theme prelude.
3614
+ *
3615
+ * `!theme` needs themes.js, which the headless build does not ship — the
3616
+ * directive is ignored with a warning anyway. Dropping it up front also avoids
3617
+ * the theme loader entirely, which is what can leave renderSvg without ever
3618
+ * calling back (a second *distinct* theme in one process is enough).
3619
+ */
3620
+ prepare(code, theme) {
3621
+ const warnings = [];
3622
+ const kept = [];
3623
+ for (const line of code.trim().split("\n")) {
3624
+ if (/^\s*!theme\b/i.test(line)) {
3625
+ warnings.push(
3626
+ `"${line.trim()}" ignored \u2014 the bundled PlantUML engine ships no themes; use skinparam instead`
3627
+ );
3628
+ continue;
3629
+ }
3630
+ kept.push(line);
3631
+ }
3632
+ const prelude = this.skinparams(theme);
3633
+ const preludeLines = prelude.split("\n").length;
3634
+ const startIndex = kept.findIndex((line) => /^\s*@start\w+/i.test(line));
3635
+ if (startIndex === -1) {
3636
+ return {
3637
+ source: `@startuml
3638
+ ${prelude}
3639
+ ${kept.join("\n")}
3640
+ @enduml`,
3641
+ warnings,
3642
+ lineShift: preludeLines + 1,
3643
+ shiftAfter: 1
3644
+ };
3645
+ }
3646
+ kept.splice(startIndex + 1, 0, prelude);
3647
+ if (!kept.some((line) => /^\s*@end\w+/i.test(line))) {
3648
+ const kind = kept[startIndex].match(/^\s*@start(\w+)/i)?.[1] ?? "uml";
3649
+ kept.push(`@end${kind}`);
3650
+ }
3651
+ return {
3652
+ source: kept.join("\n"),
3653
+ warnings,
3654
+ lineShift: preludeLines,
3655
+ shiftAfter: startIndex + 1
3656
+ };
3657
+ }
3658
+ async render(code, theme) {
3659
+ await this.initialize();
3660
+ const prepared = this.prepare(code, theme);
3661
+ for (const warning of prepared.warnings) log.warn(`[PlantUML] ${warning}`);
3662
+ try {
3663
+ const result = await this.renderToSvg(prepared.source);
3664
+ if (!result.valid || !result.svg) {
3665
+ const line = originalLine(result.errorLineNumber, prepared);
3666
+ const at = line ? ` (line ${line})` : "";
3667
+ throw new Error(`${result.errorMessage ?? "invalid diagram"}${at}`);
3668
+ }
3669
+ return svgToPng(restoreLineStrokes(result.svg, result.diagramType), RENDER_WIDTH);
3670
+ } catch (err) {
3671
+ throw new Error(
3672
+ `PlantUML render failed: ${err instanceof Error ? err.message : String(err)}`
3673
+ );
3674
+ }
3675
+ }
3676
+ async renderToSvg(source) {
3677
+ const engine = this.engine;
3678
+ const json = await withEngineLogsMuted(
3679
+ () => new Promise((resolve4, reject) => {
3680
+ let settled = false;
3681
+ const timer = setTimeout(() => {
3682
+ if (settled) return;
3683
+ settled = true;
3684
+ reject(new Error(`timed out after ${RENDER_TIMEOUT_MS / 1e3}s`));
3685
+ }, RENDER_TIMEOUT_MS);
3686
+ engine.renderSvg(source, (result) => {
3687
+ if (settled) return;
3688
+ settled = true;
3689
+ clearTimeout(timer);
3690
+ resolve4(result);
3691
+ });
3692
+ })
3693
+ );
3694
+ try {
3695
+ return JSON.parse(json);
3696
+ } catch {
3697
+ throw new Error("engine returned malformed JSON");
3698
+ }
3699
+ }
3700
+ };
3701
+
2098
3702
  // src/diagrams/index.ts
2099
- var DIAGRAM_LANGUAGES2 = {
3703
+ var RENDERERS = {
2100
3704
  mermaid: () => new MermaidDiagramRenderer(),
2101
3705
  dot: () => new GraphvizDiagramRenderer(),
2102
- graphviz: () => new GraphvizDiagramRenderer(),
2103
- echarts: () => new EChartsDiagramRenderer()
3706
+ echarts: () => new EChartsDiagramRenderer(),
3707
+ plantuml: () => new PlantUmlDiagramRenderer()
2104
3708
  };
2105
3709
  var instances = /* @__PURE__ */ new Map();
2106
3710
  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);
3711
+ const canonical = normalizeDiagramLanguage(language);
3712
+ if (!canonical) return void 0;
3713
+ const cached = instances.get(canonical);
3714
+ if (cached) return cached;
3715
+ const renderer = RENDERERS[canonical]();
3716
+ instances.set(canonical, renderer);
2113
3717
  return renderer;
2114
3718
  }
2115
3719
  async function renderDiagram(language, code, theme) {
2116
- const renderer = getOrCreate(language.toLowerCase());
3720
+ const renderer = getOrCreate(language);
2117
3721
  if (!renderer) {
2118
3722
  throw new Error(`No diagram renderer for language: ${language}`);
2119
3723
  }
@@ -2121,91 +3725,6 @@ async function renderDiagram(language, code, theme) {
2121
3725
  return renderer.render(code, theme);
2122
3726
  }
2123
3727
 
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
3728
  // src/renderer/background.ts
2210
3729
  var cache = /* @__PURE__ */ new Map();
2211
3730
  function slideBackground(theme) {
@@ -2248,15 +3767,17 @@ async function renderPresentation(presentation, theme, outputPath, inputPath = "
2248
3767
  footerTemplate: presentation.config.footer,
2249
3768
  pageNumber: 0,
2250
3769
  totalSlides: presentation.slides.length,
2251
- currentSection: ""
3770
+ currentSection: "",
3771
+ sectionNumber: 0
2252
3772
  };
2253
3773
  for (let i = 0; i < presentation.slides.length; i++) {
2254
3774
  const node = presentation.slides[i];
2255
3775
  const slide = pptx.addSlide();
2256
3776
  slide.background = backgroundProps;
2257
3777
  ctx.pageNumber = i + 1;
2258
- if (node.layout === "section" && node.title) {
2259
- ctx.currentSection = node.title;
3778
+ if (node.layout === "section") {
3779
+ if (node.title) ctx.currentSection = node.title;
3780
+ ctx.sectionNumber = (ctx.sectionNumber ?? 0) + 1;
2260
3781
  }
2261
3782
  await renderSlideLayout(slide, node, theme, ctx);
2262
3783
  }
@@ -2264,381 +3785,1023 @@ async function renderPresentation(presentation, theme, outputPath, inputPath = "
2264
3785
  writeFileSync2(outputPath, Buffer.from(buffer));
2265
3786
  }
2266
3787
 
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"
3788
+ // src/utils/color-mix.ts
3789
+ var HEX6 = /^([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/;
3790
+ function parseHex(hex) {
3791
+ const raw = hex.startsWith("#") ? hex.slice(1) : hex;
3792
+ const m = HEX6.exec(raw);
3793
+ if (!m) {
3794
+ throw new Error(`Invalid hex color (expected RRGGBB): ${hex}`);
3795
+ }
3796
+ return [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16)];
3797
+ }
3798
+ function toHex(r, g, b) {
3799
+ const c = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0").toUpperCase();
3800
+ return `${c(r)}${c(g)}${c(b)}`;
3801
+ }
3802
+ function mixHex(a, b, t) {
3803
+ const [ar, ag, ab] = parseHex(a);
3804
+ const [br, bg, bb] = parseHex(b);
3805
+ const k = Math.max(0, Math.min(1, t));
3806
+ return toHex(ar + (br - ar) * k, ag + (bg - ag) * k, ab + (bb - ab) * k);
3807
+ }
3808
+ function relativeLuminance(hex) {
3809
+ const [r, g, b] = parseHex(hex).map((v) => {
3810
+ const s = v / 255;
3811
+ return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
3812
+ });
3813
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
3814
+ }
3815
+ function isDarkColor2(hex) {
3816
+ return relativeLuminance(hex) < 0.4;
3817
+ }
3818
+
3819
+ // src/models/color-scheme.ts
3820
+ function resolveSchemeMode(scheme) {
3821
+ if (scheme.mode) return scheme.mode;
3822
+ return isDarkColor2(scheme.paper) ? "dark" : "light";
3823
+ }
3824
+
3825
+ // src/themes/color-schemes/ocean.ts
3826
+ var oceanScheme = {
3827
+ name: "ocean",
3828
+ mode: "light",
3829
+ ink: "1E4A6F",
3830
+ paper: "F0F8FF",
3831
+ primary: "4F9FD9",
3832
+ secondary: "2D6A9F"
2295
3833
  };
2296
3834
 
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"
3835
+ // src/themes/color-schemes/ocean-dark.ts
3836
+ var oceanDarkScheme = {
3837
+ name: "ocean-dark",
3838
+ mode: "dark",
3839
+ ink: "D6E7F5",
3840
+ paper: "0B1C2E",
3841
+ primary: "5BAAE8",
3842
+ secondary: "8BBCDD"
2325
3843
  };
2326
3844
 
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"
3845
+ // src/themes/color-schemes/forest.ts
3846
+ var forestScheme = {
3847
+ name: "forest",
3848
+ mode: "light",
3849
+ ink: "2A4A3F",
3850
+ paper: "F0FFF5",
3851
+ primary: "5F9A8A",
3852
+ secondary: "3F6A5A"
2357
3853
  };
2358
3854
 
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"
3855
+ // src/themes/color-schemes/champagne.ts
3856
+ var champagneScheme = {
3857
+ name: "champagne",
3858
+ mode: "light",
3859
+ ink: "CFB53B",
3860
+ paper: "FFFCE6",
3861
+ primary: "E5CD5F",
3862
+ secondary: "F5E08A"
2387
3863
  };
2388
3864
 
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"
3865
+ // src/themes/color-schemes/graphite.ts
3866
+ var graphiteScheme = {
3867
+ name: "graphite",
3868
+ mode: "light",
3869
+ ink: "4D4D4D",
3870
+ paper: "F8F8F8",
3871
+ primary: "D9D9D9",
3872
+ secondary: "A6A6A6"
2418
3873
  };
2419
3874
 
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"
3875
+ // src/themes/color-schemes/index.ts
3876
+ var builtInSchemes = [
3877
+ oceanScheme,
3878
+ oceanDarkScheme,
3879
+ forestScheme,
3880
+ champagneScheme,
3881
+ graphiteScheme
3882
+ ];
3883
+ var colorSchemes = Object.fromEntries(
3884
+ builtInSchemes.map((s) => [s.name.toLowerCase(), s])
3885
+ );
3886
+ function getColorScheme(name) {
3887
+ if (!name) return void 0;
3888
+ return colorSchemes[name.toLowerCase()];
3889
+ }
3890
+ function listColorSchemes() {
3891
+ return Object.values(colorSchemes);
3892
+ }
3893
+
3894
+ // src/models/text-set.ts
3895
+ var DEFAULT_TEXT_SCHEME_NAME = "system";
3896
+ var DEFAULT_UNIFORM_SIZES = {
3897
+ coverTitle: 36,
3898
+ slideTitle: 28,
3899
+ body: 18,
3900
+ quote: 20,
3901
+ code: 14,
3902
+ small: 12
2449
3903
  };
3904
+ function defineUniformTextScheme(options) {
3905
+ const sizes = { ...DEFAULT_UNIFORM_SIZES, ...options.sizes };
3906
+ const boldOn = {
3907
+ coverTitle: true,
3908
+ slideTitle: true,
3909
+ ...options.bold
3910
+ };
3911
+ const italicOn = {
3912
+ quote: true,
3913
+ ...options.italic
3914
+ };
3915
+ const make = (key, face) => {
3916
+ const entry = { face, size: sizes[key] };
3917
+ if (boldOn[key]) entry.bold = true;
3918
+ if (italicOn[key]) entry.italic = true;
3919
+ return entry;
3920
+ };
3921
+ return {
3922
+ name: options.name,
3923
+ note: options.note ?? `Uniform typeface: ${options.face}`,
3924
+ positions: {
3925
+ coverTitle: make("coverTitle", options.face),
3926
+ slideTitle: make("slideTitle", options.face),
3927
+ body: make("body", options.face),
3928
+ quote: make("quote", options.face),
3929
+ code: options.codeEntry ?? make("code", options.codeFace ?? "Consolas"),
3930
+ small: make("small", options.face)
3931
+ }
3932
+ };
3933
+ }
2450
3934
 
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"
3935
+ // src/themes/resolve-scheme.ts
3936
+ function resolveColorScheme(scheme) {
3937
+ const { primary, secondary, ink, paper } = scheme;
3938
+ const dark = resolveSchemeMode(scheme) === "dark";
3939
+ const background = paper;
3940
+ const text = ink;
3941
+ const accent = mixHex(primary, ink, 0.45);
3942
+ const codeBackground = dark ? mixHex(paper, ink, 0.35) : "FFFFFF";
3943
+ const codeText = dark ? mixHex(ink, paper, 0.15) : ink;
3944
+ const titleBackground = mixHex(paper, primary, 0.12);
3945
+ const titleText = ink;
3946
+ const highlightBackground = mixHex(codeBackground, primary, 0.28);
3947
+ const secondaryText = mixHex(secondary, ink, 0.25);
3948
+ return {
3949
+ primary,
3950
+ secondary: secondaryText,
3951
+ background,
3952
+ text,
3953
+ accent,
3954
+ codeBackground,
3955
+ codeText,
3956
+ titleBackground,
3957
+ titleText,
3958
+ highlightBackground,
3959
+ accents: [primary, secondary],
3960
+ subtitle: secondaryText,
3961
+ divider: mixHex(ink, paper, 0.45),
3962
+ muted: mixHex(ink, paper, 0.35),
3963
+ tableHeader: mixHex(paper, primary, 0.12),
3964
+ tableZebra: mixHex(paper, ink, 0.04)
3965
+ };
3966
+ }
3967
+
3968
+ // src/themes/layout-schemes/folio/title.ts
3969
+ var titleLayout = {
3970
+ titleAlign: "left",
3971
+ margin: 0.9,
3972
+ extra: {
3973
+ spineWidth: 0.2,
3974
+ marginLeft: 0.55,
3975
+ marginRight: 0.7,
3976
+ titleY: 2.35,
3977
+ subtitleY: 4.05,
3978
+ titleRule: true,
3979
+ titleRuleHeight: 0.035,
3980
+ titleRuleWidth: 3.6,
3981
+ titleRuleColor: "primary",
3982
+ metaBottom: true
3983
+ }
2480
3984
  };
2481
3985
 
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"
3986
+ // src/themes/layout-schemes/folio/section.ts
3987
+ var sectionLayout = {
3988
+ accentBar: "none",
3989
+ titleAlign: "left",
3990
+ margin: 1,
3991
+ extra: {
3992
+ doubleRules: true,
3993
+ ruleWeight: 0.04,
3994
+ bandY: 2.55,
3995
+ bandH: 1.55,
3996
+ titleRuleColor: "primary",
3997
+ marginLeft: 1,
3998
+ marginRight: 0.7
3999
+ }
2511
4000
  };
2512
4001
 
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"
4002
+ // src/themes/layout-schemes/folio/content.ts
4003
+ var contentLayout = {
4004
+ titleAlign: "left",
4005
+ margin: 0.55,
4006
+ titleHeight: 0.82,
4007
+ contentPadding: 0,
4008
+ extra: {
4009
+ marginLeft: 0.72,
4010
+ marginRight: 0.38,
4011
+ titleRule: true,
4012
+ titleRuleHeight: 0.07,
4013
+ titleRuleColor: "secondary"
4014
+ }
2542
4015
  };
2543
4016
 
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
4017
+ // src/themes/layout-schemes/folio/code.ts
4018
+ var codeLayout = {
4019
+ titleAlign: "left",
4020
+ margin: 0.35,
4021
+ titleHeight: 0.55,
4022
+ extra: {
4023
+ marginLeft: 0.4,
4024
+ marginRight: 0.4,
4025
+ titleRule: true,
4026
+ titleRuleHeight: 0.025,
4027
+ titleRuleColor: "secondary"
4028
+ }
4029
+ };
4030
+
4031
+ // src/themes/layout-schemes/folio/quote.ts
4032
+ var quoteLayout = {
4033
+ titleAlign: "left",
4034
+ accentBar: "none",
4035
+ margin: 0.9,
4036
+ extra: {
4037
+ quoteMark: false,
4038
+ quoteBar: true,
4039
+ quoteBarWidth: 0.1,
4040
+ marginLeft: 0.9,
4041
+ marginRight: 0.7,
4042
+ attributionAlign: "left"
4043
+ }
4044
+ };
4045
+
4046
+ // src/themes/layout-schemes/folio/closing.ts
4047
+ var closingLayout = {
4048
+ titleAlign: "center",
4049
+ margin: 0.7,
4050
+ extra: {
4051
+ thankText: "Thank you",
4052
+ titleRule: true,
4053
+ ruleWidth: 2.8,
4054
+ textY: 2.85
4055
+ }
4056
+ };
4057
+
4058
+ // src/themes/layout-schemes/folio/images.ts
4059
+ var imageBase = {
4060
+ titleAlign: "left",
4061
+ margin: 0.55,
4062
+ titleHeight: 0.75,
4063
+ extra: {
4064
+ marginLeft: 0.72,
4065
+ marginRight: 0.38,
4066
+ titleRule: true,
4067
+ titleRuleHeight: 0.055,
4068
+ titleRuleColor: "secondary"
4069
+ }
4070
+ };
4071
+ var imageSingleLayout = { ...imageBase };
4072
+ var imageDoubleLayout = { ...imageBase };
4073
+ var imageTripleLayout = { ...imageBase };
4074
+
4075
+ // src/themes/layout-schemes/folio/footer.ts
4076
+ var footerLayout = {
4077
+ margin: 0.55,
4078
+ footerDivider: true,
4079
+ extra: {
4080
+ footerAlign: "center"
4081
+ }
4082
+ };
4083
+
4084
+ // src/themes/layout-schemes/folio/styles.ts
4085
+ var folioLayoutStyles = {
4086
+ sectionBarWidth: 0.04,
4087
+ footerDivider: true,
4088
+ coverBadge: false
4089
+ };
4090
+
4091
+ // src/themes/layout-schemes/folio/index.ts
4092
+ var folioLayoutScheme = {
4093
+ name: "folio",
4094
+ layouts: {
4095
+ title: titleLayout,
4096
+ section: sectionLayout,
4097
+ content: contentLayout,
4098
+ code: codeLayout,
4099
+ quote: quoteLayout,
4100
+ closing: closingLayout,
4101
+ imageSingle: imageSingleLayout,
4102
+ imageDouble: imageDoubleLayout,
4103
+ imageTriple: imageTripleLayout,
4104
+ footer: footerLayout
2572
4105
  },
2573
- shikiTheme: "github-dark"
4106
+ styles: folioLayoutStyles,
4107
+ note: "\u7F16\u8F91\u518C\u9875\uFF1A\u4E66\u810A\u5C01\u9762\u3001\u53CC\u7EBF\u7AE0\u8282\u3001\u4E0D\u5BF9\u79F0\u6B63\u6587\u3001pull-quote\u3001\u9875\u811A\u5C45\u4E2D\u3002"
2574
4108
  };
2575
4109
 
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 }
4110
+ // src/themes/layout-schemes/legacy/title.ts
4111
+ var titleLayout2 = {
4112
+ titleAlign: "center",
4113
+ margin: 0.8
4114
+ };
4115
+
4116
+ // src/themes/layout-schemes/legacy/section.ts
4117
+ var sectionLayout2 = {
4118
+ accentBar: "left",
4119
+ titleAlign: "left",
4120
+ margin: 1
4121
+ };
4122
+
4123
+ // src/themes/layout-schemes/legacy/content.ts
4124
+ var contentLayout2 = {
4125
+ titleAlign: "left",
4126
+ margin: 0.6,
4127
+ titleHeight: 0.9,
4128
+ contentPadding: 0
4129
+ };
4130
+
4131
+ // src/themes/layout-schemes/legacy/code.ts
4132
+ var codeLayout2 = {
4133
+ titleAlign: "left",
4134
+ margin: 0.5,
4135
+ titleHeight: 0.7
4136
+ };
4137
+
4138
+ // src/themes/layout-schemes/legacy/quote.ts
4139
+ var quoteLayout2 = {
4140
+ titleAlign: "center",
4141
+ accentBar: "none",
4142
+ extra: {
4143
+ quoteMark: true,
4144
+ quoteMarkSize: 72
4145
+ }
4146
+ };
4147
+
4148
+ // src/themes/layout-schemes/legacy/footer.ts
4149
+ var footerLayout2 = {
4150
+ margin: 0.6,
4151
+ footerDivider: false
4152
+ };
4153
+
4154
+ // src/themes/layout-schemes/legacy/styles.ts
4155
+ var legacyLayoutStyles = {
4156
+ sectionBarWidth: 0.15,
4157
+ footerDivider: false,
4158
+ coverBadge: false
4159
+ };
4160
+
4161
+ // src/themes/layout-schemes/legacy/index.ts
4162
+ var legacyLayoutScheme = {
4163
+ name: "legacy",
4164
+ layouts: {
4165
+ title: titleLayout2,
4166
+ section: sectionLayout2,
4167
+ content: contentLayout2,
4168
+ code: codeLayout2,
4169
+ quote: quoteLayout2,
4170
+ footer: footerLayout2
2591
4171
  },
2592
- fonts: {
2593
- heading: "KaiTi",
2594
- body: "KaiTi",
2595
- code: "Consolas",
2596
- cjk: "KaiTi"
4172
+ styles: legacyLayoutStyles,
4173
+ note: "\u65E7\u7248\u7248\u5F0F\uFF1A\u73B0\u7F51\u4E94\u79CD\u57FA\u7840\u9875 + \u9875\u811A\uFF1B\u65E0\u7ED3\u5C3E/\u56FE\u6587\u6761\u76EE\u3002"
4174
+ };
4175
+
4176
+ // src/themes/layout-schemes/golden/title.ts
4177
+ var titleLayout3 = {
4178
+ titleAlign: "right",
4179
+ margin: 0.83,
4180
+ extra: {
4181
+ marginLeft: 6.94,
4182
+ marginRight: 0.83,
4183
+ titleY: 3,
4184
+ subtitleY: 4,
4185
+ titleRule: true,
4186
+ titleRuleHeight: 0.04,
4187
+ titleRuleWidth: 8.22,
4188
+ titleRuleY: 4.64,
4189
+ titleRuleColor: "primary",
4190
+ metaBottom: true,
4191
+ metaY: 5
4192
+ }
4193
+ };
4194
+
4195
+ // src/themes/layout-schemes/golden/section.ts
4196
+ var sectionLayout3 = {
4197
+ accentBar: "none",
4198
+ titleAlign: "right",
4199
+ margin: 0.83,
4200
+ extra: {
4201
+ marginLeft: 6.67,
4202
+ marginRight: 0.83,
4203
+ // 章节序号占位(node.subtitle)+ 标题
4204
+ subtitleY: 3.03,
4205
+ subtitleH: 0.36,
4206
+ titleY: 3.5,
4207
+ titleH: 0.89,
4208
+ // 黄金分割横线
4209
+ sectionRule: true,
4210
+ sectionRuleY: 4.64,
4211
+ sectionRuleWidth: 8.22,
4212
+ sectionRuleHeight: 0.04,
4213
+ sectionRuleColor: "primary"
4214
+ }
4215
+ };
4216
+
4217
+ // src/themes/layout-schemes/golden/content.ts
4218
+ var contentLayout3 = {
4219
+ titleAlign: "left",
4220
+ margin: 1.11,
4221
+ titleHeight: 1.3,
4222
+ contentPadding: 0,
4223
+ extra: {
4224
+ marginLeft: 1.11,
4225
+ marginRight: 1.11,
4226
+ titleY: 0.72,
4227
+ titleRule: true,
4228
+ titleRuleWidth: 0.89,
4229
+ titleRuleHeight: 0.04,
4230
+ titleRuleColor: "primary"
4231
+ }
4232
+ };
4233
+
4234
+ // src/themes/layout-schemes/golden/code.ts
4235
+ var codeLayout3 = {
4236
+ titleAlign: "left",
4237
+ margin: 1.11,
4238
+ titleHeight: 0.6,
4239
+ extra: {
4240
+ marginLeft: 1.11,
4241
+ marginRight: 1.11,
4242
+ titleY: 0.72,
4243
+ titleRule: true,
4244
+ titleRuleWidth: 0.89,
4245
+ titleRuleHeight: 0.04,
4246
+ titleRuleColor: "primary"
4247
+ }
4248
+ };
4249
+
4250
+ // src/themes/layout-schemes/golden/quote.ts
4251
+ var quoteLayout3 = {
4252
+ titleAlign: "center",
4253
+ accentBar: "none",
4254
+ margin: 2.22,
4255
+ extra: {
4256
+ quoteMark: false,
4257
+ quoteBar: false,
4258
+ marginLeft: 2.22,
4259
+ marginRight: 2.22,
4260
+ quoteRule: true,
4261
+ quoteRuleY: 2.19,
4262
+ quoteRuleWidth: 1.33,
4263
+ quoteRuleHeight: 0.04,
4264
+ quoteRuleColor: "primary",
4265
+ textY: 2.58,
4266
+ textH: 1.94,
4267
+ attributionAlign: "center"
4268
+ }
4269
+ };
4270
+
4271
+ // src/themes/layout-schemes/golden/closing.ts
4272
+ var closingLayout2 = {
4273
+ titleAlign: "right",
4274
+ margin: 0.83,
4275
+ extra: {
4276
+ thankText: "Thank you",
4277
+ marginLeft: 5.83,
4278
+ marginRight: 0.83,
4279
+ textY: 3.22,
4280
+ titleRule: true,
4281
+ closingRule: "below",
4282
+ ruleWidth: 8.22,
4283
+ ruleHeight: 0.04,
4284
+ ruleY: 4.64
4285
+ }
4286
+ };
4287
+
4288
+ // src/themes/layout-schemes/golden/images.ts
4289
+ var imageBase2 = {
4290
+ titleAlign: "left",
4291
+ margin: 1.11,
4292
+ titleHeight: 1.3,
4293
+ extra: {
4294
+ marginLeft: 1.11,
4295
+ marginRight: 1.11,
4296
+ titleY: 0.72,
4297
+ titleRule: true,
4298
+ titleRuleWidth: 0.89,
4299
+ titleRuleHeight: 0.04,
4300
+ titleRuleColor: "primary"
4301
+ }
4302
+ };
4303
+ var imageSingleLayout2 = { ...imageBase2 };
4304
+ var imageDoubleLayout2 = { ...imageBase2 };
4305
+ var imageTripleLayout2 = { ...imageBase2 };
4306
+
4307
+ // src/themes/layout-schemes/golden/footer.ts
4308
+ var footerLayout3 = {
4309
+ margin: 1.11,
4310
+ footerDivider: true,
4311
+ extra: {
4312
+ footerAlign: "left",
4313
+ marginLeft: 1.11,
4314
+ marginRight: 1.11
4315
+ }
4316
+ };
4317
+
4318
+ // src/themes/layout-schemes/golden/styles.ts
4319
+ var goldenLayoutStyles = {
4320
+ sectionBarWidth: 0.04,
4321
+ footerDivider: true,
4322
+ coverBadge: false,
4323
+ codeRadius: 16
4324
+ };
4325
+
4326
+ // src/themes/layout-schemes/golden/index.ts
4327
+ var goldenLayoutScheme = {
4328
+ name: "golden",
4329
+ layouts: {
4330
+ title: titleLayout3,
4331
+ section: sectionLayout3,
4332
+ content: contentLayout3,
4333
+ code: codeLayout3,
4334
+ quote: quoteLayout3,
4335
+ closing: closingLayout2,
4336
+ imageSingle: imageSingleLayout2,
4337
+ imageDouble: imageDoubleLayout2,
4338
+ imageTriple: imageTripleLayout2,
4339
+ footer: footerLayout3
2597
4340
  },
2598
- fontSize: {
2599
- title: 36,
2600
- heading: 28,
2601
- body: 18,
2602
- code: 14,
2603
- small: 12
4341
+ styles: goldenLayoutStyles,
4342
+ note: "\u9EC4\u91D1\u6BD4\u4F8B\uFF1A\u53F3\u5BF9\u9F50\u7559\u767D\u5C01\u9762/\u7AE0\u8282/\u7ED3\u5C3E + \u9EC4\u91D1\u5206\u5272\u6A2A\u7EBF\uFF0C\u6B63\u6587/\u4EE3\u7801/\u5F15\u7528/\u56FE\u6587\u5171\u7528\u9AA8\u67B6\u8BED\u8A00\u3002"
4343
+ };
4344
+
4345
+ // src/themes/layout-schemes/minimal/title.ts
4346
+ var titleLayout4 = {
4347
+ titleAlign: "left",
4348
+ margin: 0.83,
4349
+ extra: {
4350
+ minimalTitle: true,
4351
+ minimalLineX: 0.83,
4352
+ minimalLineY1: 2.64,
4353
+ minimalLineH: 1.94,
4354
+ minimalLineW: 0.04,
4355
+ minimalBoxX: 1.11,
4356
+ minimalBoxW: 8.89,
4357
+ minimalTitleY: 2.64,
4358
+ minimalSubtitleY: 3.64,
4359
+ minimalAuthorY: 5.28,
4360
+ minimalDateY: 5.64,
4361
+ minimalMetaW: 4.17
4362
+ }
4363
+ };
4364
+
4365
+ // src/themes/layout-schemes/minimal/section.ts
4366
+ var sectionLayout4 = {
4367
+ titleAlign: "left",
4368
+ margin: 0.83,
4369
+ extra: {
4370
+ minimalNumber: true,
4371
+ footer: "off",
4372
+ minimalNumberX: 1.11,
4373
+ minimalNumberY: 2.43,
4374
+ minimalNumberW: 2.5,
4375
+ minimalNumberH: 1.53,
4376
+ minimalRuleX: 3.89,
4377
+ minimalRuleY1: 2.71,
4378
+ minimalRuleW: 0.03,
4379
+ minimalRuleH: 1.11,
4380
+ minimalTitleX: 4.31,
4381
+ minimalTitleY: 2.71,
4382
+ minimalTitleW: 6.94,
4383
+ minimalTitleH: 0.69
4384
+ }
4385
+ };
4386
+
4387
+ // src/themes/layout-schemes/minimal/content.ts
4388
+ var contentLayout4 = {
4389
+ titleAlign: "left",
4390
+ margin: 0.83,
4391
+ titleHeight: 0.44,
4392
+ extra: {
4393
+ minimalBox: true,
4394
+ minimalBoxX: 0.83,
4395
+ minimalBoxY: 1.33,
4396
+ minimalBoxW: 11.67,
4397
+ minimalBoxH: 5.28,
4398
+ minimalTitleY: 0.5,
4399
+ minimalTitleRuleY: 1.03,
4400
+ minimalTitleRuleW: 0.56,
4401
+ minimalTitleRuleH: 0.03
4402
+ }
4403
+ };
4404
+
4405
+ // src/themes/layout-schemes/minimal/code.ts
4406
+ var codeLayout4 = {
4407
+ titleAlign: "left",
4408
+ margin: 0.83,
4409
+ extra: {
4410
+ minimalCard: true,
4411
+ minimalCardX: 0.83,
4412
+ minimalCardY: 1.33,
4413
+ minimalCardW: 11.67,
4414
+ minimalCardH: 5.28,
4415
+ minimalCardRadius: 0.08,
4416
+ minimalTitleY: 0.5,
4417
+ minimalTitleRuleY: 1.03,
4418
+ minimalTitleRuleW: 0.56,
4419
+ minimalTitleRuleH: 0.03
4420
+ }
4421
+ };
4422
+
4423
+ // src/themes/layout-schemes/minimal/quote.ts
4424
+ var quoteLayout4 = {
4425
+ titleAlign: "left",
4426
+ margin: 0.83,
4427
+ extra: {
4428
+ minimalQuote: true,
4429
+ minimalRuleX: 1.67,
4430
+ minimalRuleY: 2.78,
4431
+ minimalRuleH: 1.39,
4432
+ minimalRuleW: 0.04,
4433
+ minimalTextX: 2.08,
4434
+ minimalTextW: 9.58,
4435
+ minimalTextH: 1.39
4436
+ }
4437
+ };
4438
+
4439
+ // src/themes/layout-schemes/minimal/closing.ts
4440
+ var closingLayout3 = {
4441
+ titleAlign: "center",
4442
+ margin: 0.83,
4443
+ extra: {
4444
+ minimalClosing: true,
4445
+ thankText: "Thank you",
4446
+ minimalRuleW: 1.11,
4447
+ minimalRuleH: 0.03,
4448
+ minimalRuleY: 2.78,
4449
+ minimalThankX: 2.5,
4450
+ minimalThankY: 3.06,
4451
+ minimalThankW: 8.33,
4452
+ minimalContactX: 3.89,
4453
+ minimalContactY: 4.31,
4454
+ minimalContactW: 5.56
4455
+ }
4456
+ };
4457
+
4458
+ // src/themes/layout-schemes/minimal/images.ts
4459
+ var imageBase3 = {
4460
+ titleAlign: "left",
4461
+ margin: 0.83,
4462
+ extra: {
4463
+ minimalImage: true,
4464
+ minimalImageY: 1.33,
4465
+ minimalImageH: 5.28,
4466
+ minimalTitleY: 0.5,
4467
+ minimalTitleRuleY: 1.03,
4468
+ minimalTitleRuleW: 0.56,
4469
+ minimalTitleRuleH: 0.03
4470
+ }
4471
+ };
4472
+ var imageSingleLayout3 = { ...imageBase3 };
4473
+ var imageDoubleLayout3 = { ...imageBase3 };
4474
+ var imageTripleLayout3 = { ...imageBase3 };
4475
+
4476
+ // src/themes/layout-schemes/minimal/footer.ts
4477
+ var footerLayout4 = {
4478
+ margin: 0.83,
4479
+ footerDivider: true,
4480
+ extra: {
4481
+ footerAlign: "left",
4482
+ dividerY: 6.94,
4483
+ footerY: 7.03
4484
+ }
4485
+ };
4486
+
4487
+ // src/themes/layout-schemes/minimal/styles.ts
4488
+ var minimalLayoutStyles = {
4489
+ footerDivider: true,
4490
+ coverBadge: false
4491
+ };
4492
+
4493
+ // src/themes/layout-schemes/minimal/index.ts
4494
+ var minimalLayoutScheme = {
4495
+ name: "minimal",
4496
+ layouts: {
4497
+ title: titleLayout4,
4498
+ section: sectionLayout4,
4499
+ content: contentLayout4,
4500
+ code: codeLayout4,
4501
+ quote: quoteLayout4,
4502
+ closing: closingLayout3,
4503
+ imageSingle: imageSingleLayout3,
4504
+ imageDouble: imageDoubleLayout3,
4505
+ imageTriple: imageTripleLayout3,
4506
+ footer: footerLayout4
2604
4507
  },
2605
- shikiTheme: "github-dark"
4508
+ styles: minimalLayoutStyles,
4509
+ note: "\u6781\u7B80\u6863\u6848\uFF1A\u7EDF\u4E000.83\u8FB9\u8DDD\u3001\u6A2A/\u7AD6\u7EBF\u6027\u88C5\u9970\u3001\u8D85\u5927\u7AE0\u5E8F\u53F7\u3001\u5706\u89D2\u4EE3\u7801\u5361\u7247\u3001\u865A\u7EBF\u56FE\u7247\u5360\u4F4D\u3002"
2606
4510
  };
2607
4511
 
2608
- // src/themes/default.ts
2609
- var defaultTheme = {
2610
- ...cleanTheme,
2611
- name: "default"
4512
+ // src/themes/layout-schemes/index.ts
4513
+ var builtInLayoutSchemes = [
4514
+ folioLayoutScheme,
4515
+ legacyLayoutScheme,
4516
+ goldenLayoutScheme,
4517
+ minimalLayoutScheme
4518
+ ];
4519
+ var layoutSchemes = Object.fromEntries(
4520
+ builtInLayoutSchemes.map((s) => [s.name.toLowerCase(), s])
4521
+ );
4522
+ function getLayoutScheme(name) {
4523
+ if (!name) return void 0;
4524
+ return layoutSchemes[name.toLowerCase()];
4525
+ }
4526
+ function listLayoutSchemes() {
4527
+ return Object.values(layoutSchemes);
4528
+ }
4529
+
4530
+ // src/themes/presets/blue.ts
4531
+ var bluePreset = {
4532
+ name: "blue",
4533
+ colorScheme: "ocean",
4534
+ textScheme: "academic",
4535
+ layoutScheme: "legacy"
2612
4536
  };
2613
4537
 
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
4538
+ // src/themes/presets/emerald.ts
4539
+ var emeraldPreset = {
4540
+ name: "emerald",
4541
+ colorScheme: "forest",
4542
+ textScheme: "system",
4543
+ layoutScheme: "folio"
4544
+ };
4545
+
4546
+ // src/themes/presets/gold.ts
4547
+ var goldPreset = {
4548
+ name: "gold",
4549
+ colorScheme: "champagne",
4550
+ textScheme: "kai",
4551
+ layoutScheme: "golden"
4552
+ };
4553
+
4554
+ // src/themes/presets/slate.ts
4555
+ var slatePreset = {
4556
+ name: "slate",
4557
+ colorScheme: "graphite",
4558
+ textScheme: "source-han-serif",
4559
+ layoutScheme: "minimal"
4560
+ };
4561
+
4562
+ // src/themes/presets/index.ts
4563
+ var builtInThemePresets = [
4564
+ bluePreset,
4565
+ emeraldPreset,
4566
+ goldPreset,
4567
+ slatePreset
4568
+ ];
4569
+ var themePresets = Object.fromEntries(
4570
+ builtInThemePresets.map((p) => [p.name.toLowerCase(), p])
4571
+ );
4572
+ function getThemePreset(name) {
4573
+ if (!name) return void 0;
4574
+ return themePresets[name.toLowerCase()];
4575
+ }
4576
+ function listThemePresets() {
4577
+ return Object.values(themePresets);
4578
+ }
4579
+
4580
+ // src/themes/text-entries.ts
4581
+ var sharedCode = {
4582
+ face: "Consolas",
4583
+ cjkFace: "Consolas",
4584
+ size: 14
2628
4585
  };
2629
- function getTheme(name) {
2630
- if (!name) {
2631
- return cleanTheme;
4586
+
4587
+ // src/themes/text-schemes/academic.ts
4588
+ var academicTextScheme = defineUniformTextScheme({
4589
+ name: "academic",
4590
+ face: "\u5B8B\u4F53",
4591
+ codeEntry: sharedCode,
4592
+ sizes: {
4593
+ coverTitle: 42,
4594
+ slideTitle: 32
4595
+ },
4596
+ note: "\u5B66\u672F\uFF1A\u6B63\u6587/\u6807\u9898\u7EDF\u4E00\u5B8B\u4F53\uFF0C\u4EE3\u7801 Consolas\uFF1B\u6807\u9898\u52A0\u5927\uFF08\u5C01\u9762 42 / \u9875\u6807\u9898 32\uFF09\u3002"
4597
+ });
4598
+
4599
+ // src/themes/text-schemes/system.ts
4600
+ var systemTextScheme = defineUniformTextScheme({
4601
+ name: "system",
4602
+ face: "\u5FAE\u8F6F\u96C5\u9ED1",
4603
+ codeEntry: sharedCode,
4604
+ note: "\u7CFB\u7EDF\u9ED8\u8BA4\uFF1A\u6B63\u6587/\u6807\u9898\u7EDF\u4E00\u5FAE\u8F6F\u96C5\u9ED1\uFF0C\u4EE3\u7801 Consolas\u3002"
4605
+ });
4606
+
4607
+ // src/themes/text-schemes/kai.ts
4608
+ var kaiTextScheme = defineUniformTextScheme({
4609
+ name: "kai",
4610
+ face: "KaiTi",
4611
+ codeEntry: sharedCode,
4612
+ note: "\u6977\u4F53\uFF1A\u6B63\u6587/\u6807\u9898\u7EDF\u4E00 KaiTi\uFF0C\u4EE3\u7801 Consolas\u3002"
4613
+ });
4614
+
4615
+ // src/themes/text-schemes/source-han-serif.ts
4616
+ var sourceHanSerifTextScheme = defineUniformTextScheme({
4617
+ name: "source-han-serif",
4618
+ face: "\u601D\u6E90\u5B8B\u4F53",
4619
+ codeEntry: sharedCode,
4620
+ note: "\u601D\u6E90\u5B8B\u4F53\uFF1A\u6B63\u6587/\u6807\u9898\u7EDF\u4E00\u601D\u6E90\u5B8B\u4F53\uFF0C\u4EE3\u7801 Consolas\u3002"
4621
+ });
4622
+
4623
+ // src/themes/text-schemes/text-index.ts
4624
+ var builtInTextSchemes = [
4625
+ systemTextScheme,
4626
+ academicTextScheme,
4627
+ kaiTextScheme,
4628
+ sourceHanSerifTextScheme
4629
+ ];
4630
+ var textSchemes = Object.fromEntries(
4631
+ builtInTextSchemes.map((s) => [s.name.toLowerCase(), s])
4632
+ );
4633
+ function getTextScheme(name) {
4634
+ if (!name) return void 0;
4635
+ return textSchemes[name.toLowerCase()];
4636
+ }
4637
+ function listTextSchemes() {
4638
+ return Object.values(textSchemes);
4639
+ }
4640
+
4641
+ // src/themes/from-scheme.ts
4642
+ function resolveThemePresetOption(input) {
4643
+ if (input === void 0 || input === null) return void 0;
4644
+ if (typeof input === "object") return input;
4645
+ const name = input.trim();
4646
+ if (!name) return void 0;
4647
+ const preset = getThemePreset(name);
4648
+ if (!preset) {
4649
+ console.warn(`Theme preset "${name}" not found; using scheme defaults only`);
4650
+ return void 0;
4651
+ }
4652
+ return preset;
4653
+ }
4654
+ function resolveColorSchemeOption(input) {
4655
+ if (input && typeof input === "object") return input;
4656
+ if (typeof input !== "string" || !input.trim()) return void 0;
4657
+ const name = input.trim();
4658
+ const scheme = getColorScheme(name);
4659
+ if (!scheme) {
4660
+ console.warn(`Color scheme "${name}" not found`);
4661
+ return void 0;
4662
+ }
4663
+ return scheme;
4664
+ }
4665
+ function resolveTextSchemeOption(input) {
4666
+ if (input && typeof input === "object") return input;
4667
+ const name = typeof input === "string" && input.trim() ? input.trim() : DEFAULT_TEXT_SCHEME_NAME;
4668
+ const scheme = getTextScheme(name);
4669
+ if (!scheme) {
4670
+ if (typeof input === "string" && input.trim()) {
4671
+ console.warn(`Text scheme "${name}" not found, using "${DEFAULT_TEXT_SCHEME_NAME}"`);
4672
+ }
4673
+ const fallback = getTextScheme(DEFAULT_TEXT_SCHEME_NAME);
4674
+ if (!fallback) {
4675
+ throw new Error(`Default text scheme "${DEFAULT_TEXT_SCHEME_NAME}" is not registered`);
4676
+ }
4677
+ return fallback;
2632
4678
  }
2633
- if (typeof name !== "string") {
4679
+ return scheme;
4680
+ }
4681
+ function bridgeFontsFromTextScheme(scheme) {
4682
+ const p = scheme.positions;
4683
+ const body = p.body;
4684
+ const headingSource = p.slideTitle ?? p.coverTitle;
4685
+ return {
4686
+ body: body.face,
4687
+ cjk: body.cjkFace ?? body.face,
4688
+ heading: headingSource.face ?? body.face,
4689
+ code: p.code.face
4690
+ };
4691
+ }
4692
+ function bridgeFontSizesFromTextScheme(scheme) {
4693
+ const p = scheme.positions;
4694
+ return {
4695
+ title: p.coverTitle.size,
4696
+ heading: p.slideTitle.size,
4697
+ body: p.body.size,
4698
+ code: p.code.size,
4699
+ small: p.small.size
4700
+ };
4701
+ }
4702
+ function textStylesFromScheme(scheme) {
4703
+ return { ...scheme.positions };
4704
+ }
4705
+ function resolveLayoutSchemeOption(input) {
4706
+ if (input === void 0) return {};
4707
+ if (typeof input === "object" && input !== null) {
4708
+ return {
4709
+ layouts: input.layouts,
4710
+ styles: input.styles,
4711
+ layoutSet: input.name,
4712
+ scheme: input
4713
+ };
4714
+ }
4715
+ const name = input.trim();
4716
+ if (!name) return {};
4717
+ const scheme = getLayoutScheme(name);
4718
+ if (!scheme) {
4719
+ console.warn(`Layout scheme "${name}" not found, using built-in layout defaults`);
4720
+ return {};
4721
+ }
4722
+ return {
4723
+ layouts: scheme.layouts,
4724
+ styles: scheme.styles,
4725
+ layoutSet: scheme.name,
4726
+ scheme
4727
+ };
4728
+ }
4729
+ function createThemeFromScheme(scheme, options = {}) {
4730
+ const colorScheme = resolveColorSchemeOption(options.colorScheme) ?? scheme;
4731
+ const textScheme = resolveTextSchemeOption(options.textScheme);
4732
+ const fonts = options.fonts ?? bridgeFontsFromTextScheme(textScheme);
4733
+ const fontSize = options.fontSize ?? bridgeFontSizesFromTextScheme(textScheme);
4734
+ const fromLayout = resolveLayoutSchemeOption(options.layoutScheme);
4735
+ return {
4736
+ name: scheme.name,
4737
+ colors: resolveColorScheme(colorScheme),
4738
+ fonts,
4739
+ fontSize,
4740
+ shikiTheme: options.shikiTheme ?? (resolveSchemeMode(colorScheme) === "dark" ? "github-dark" : "github-light"),
4741
+ layouts: options.layouts ?? fromLayout.layouts,
4742
+ styles: options.styles ?? fromLayout.styles,
4743
+ typography: options.typography,
4744
+ avoid: options.avoid,
4745
+ textSet: textScheme.name,
4746
+ textScheme,
4747
+ textStyles: textStylesFromScheme(textScheme),
4748
+ layoutSet: fromLayout.layoutSet,
4749
+ presetSet: options.presetSet
4750
+ };
4751
+ }
4752
+
4753
+ // src/themes/index.ts
4754
+ function resolveThemeName(name) {
4755
+ const trimmed = typeof name === "string" ? name.trim() : "";
4756
+ const key = trimmed || DEFAULT_THEME_NAME;
4757
+ const preset = getThemePreset(key);
4758
+ if (preset) return { kind: "preset", preset };
4759
+ if (getColorScheme(key)) return { kind: "color", colorName: key };
4760
+ return { kind: "default" };
4761
+ }
4762
+ function getTheme(name, options = {}) {
4763
+ if (name !== void 0 && name !== null && typeof name !== "string") {
2634
4764
  throw new Error(`Invalid theme value: expected a string, got ${typeof name}`);
2635
4765
  }
2636
- const theme = themes[name.toLowerCase()];
2637
- if (!theme) {
2638
- console.warn(`Theme "${name}" not found, using "clean"`);
2639
- return cleanTheme;
4766
+ const explicitPreset = resolveThemePresetOption(options.preset);
4767
+ const resolved = resolveThemeName(name);
4768
+ if (resolved.kind === "preset" || resolved.kind === "default" && !explicitPreset) {
4769
+ const preset = resolved.kind === "preset" ? resolved.preset : getThemePreset(DEFAULT_THEME_NAME);
4770
+ if (resolved.kind === "default" && name?.trim()) {
4771
+ console.warn(`Theme "${name}" not found, using "${preset.name}"`);
4772
+ }
4773
+ return createThemeFromScheme(getColorScheme(preset.colorScheme), {
4774
+ colorScheme: options.colorScheme,
4775
+ textScheme: options.textScheme ?? preset.textScheme,
4776
+ layoutScheme: options.layoutScheme ?? preset.layoutScheme,
4777
+ presetSet: preset.name
4778
+ });
4779
+ }
4780
+ if (resolved.kind === "default" && explicitPreset) {
4781
+ console.warn(`Theme "${name}" not found, using preset "${explicitPreset.name}"`);
4782
+ return createThemeFromScheme(getColorScheme(explicitPreset.colorScheme), {
4783
+ colorScheme: options.colorScheme,
4784
+ textScheme: options.textScheme ?? explicitPreset.textScheme,
4785
+ layoutScheme: options.layoutScheme ?? explicitPreset.layoutScheme,
4786
+ presetSet: explicitPreset.name
4787
+ });
2640
4788
  }
2641
- return theme;
4789
+ const colorName = resolved.kind === "color" ? resolved.colorName : explicitPreset.colorScheme;
4790
+ return createThemeFromScheme(getColorScheme(colorName), {
4791
+ colorScheme: options.colorScheme,
4792
+ textScheme: options.textScheme ?? explicitPreset?.textScheme,
4793
+ layoutScheme: options.layoutScheme ?? explicitPreset?.layoutScheme,
4794
+ presetSet: explicitPreset?.name
4795
+ });
4796
+ }
4797
+ function themeNames() {
4798
+ const presets = listThemePresets().map((p) => p.name.toLowerCase());
4799
+ const colors = listColorSchemes().map((s) => s.name.toLowerCase());
4800
+ return [...presets, ...colors.filter((c) => !presets.includes(c))];
4801
+ }
4802
+ function hasTheme(name) {
4803
+ if (!name) return false;
4804
+ return Boolean(getThemePreset(name) || getColorScheme(name));
2642
4805
  }
2643
4806
 
2644
4807
  // src/utils/output-namer.ts
@@ -2662,10 +4825,13 @@ function getOutputPath(inputPath, specifiedOutput) {
2662
4825
  // src/index.ts
2663
4826
  async function convert(inputPath, options = {}) {
2664
4827
  const absInput = resolve3(inputPath);
2665
- const markdown = readFileSync2(absInput, "utf-8");
4828
+ const markdown = readFileSync4(absInput, "utf-8");
2666
4829
  const presentation = parseMarkdown(markdown);
2667
- if (options.theme) presentation.config.theme = options.theme;
2668
- const theme = getTheme(presentation.config.theme);
4830
+ const baseTheme = options.theme ?? presentation.config.theme;
4831
+ const colorScheme = options.colorScheme ?? presentation.config.colorScheme;
4832
+ const textScheme = options.textScheme ?? presentation.config.textScheme;
4833
+ const layoutScheme = options.layoutScheme ?? presentation.config.layoutScheme;
4834
+ const theme = getTheme(baseTheme, { colorScheme, textScheme, layoutScheme });
2669
4835
  const outputPath = resolve3(getOutputPath(absInput, options.output));
2670
4836
  await renderPresentation(presentation, theme, outputPath, absInput);
2671
4837
  return outputPath;
@@ -2684,12 +4850,15 @@ async function expandGlob(patterns) {
2684
4850
 
2685
4851
  // src/cli.ts
2686
4852
  var pkg = JSON.parse(
2687
- readFileSync3(new URL("../package.json", import.meta.url), "utf-8")
4853
+ readFileSync5(new URL("../package.json", import.meta.url), "utf-8")
2688
4854
  );
2689
4855
  var program = new Command();
2690
4856
  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) => {
4857
+ var themeChoices = themeNames();
4858
+ var colorChoices = listColorSchemes().map((s) => s.name).join(", ");
4859
+ var textChoices = listTextSchemes().map((s) => s.name).join(", ");
4860
+ var layoutChoices = listLayoutSchemes().map((s) => s.name).join(", ");
4861
+ 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("--color <name>", `Color scheme override (${colorChoices})`).option("--text <name>", `Text scheme override (${textChoices})`).option("--layout <name>", `Layout scheme override (${layoutChoices})`).option("--quiet", "Suppress per-file progress output").option("--json", "Print machine-readable JSON result to stdout").action(async (filePatterns, options) => {
2693
4862
  const jsonMode = Boolean(options.json);
2694
4863
  if (jsonMode || options.quiet) setQuiet(true);
2695
4864
  const usageError = (msg) => {
@@ -2705,9 +4874,18 @@ program.argument("<files...>", "Markdown files to convert (supports glob)").opti
2705
4874
  if (options.output && files.length > 1) {
2706
4875
  usageError("--output can only be used with a single input file");
2707
4876
  }
2708
- if (options.theme && !themes[options.theme.toLowerCase()]) {
4877
+ if (options.theme && !hasTheme(options.theme)) {
2709
4878
  usageError(`Unknown theme "${options.theme}". Available themes: ${themeChoices.join(", ")}`);
2710
4879
  }
4880
+ if (options.color && !getColorScheme(options.color)) {
4881
+ usageError(`Unknown color scheme "${options.color}". Available: ${colorChoices}`);
4882
+ }
4883
+ if (options.text && !getTextScheme(options.text)) {
4884
+ usageError(`Unknown text scheme "${options.text}". Available: ${textChoices}`);
4885
+ }
4886
+ if (options.layout && !getLayoutScheme(options.layout)) {
4887
+ usageError(`Unknown layout scheme "${options.layout}". Available: ${layoutChoices}`);
4888
+ }
2711
4889
  const startTime = Date.now();
2712
4890
  const results = [];
2713
4891
  for (const file of files) {
@@ -2717,7 +4895,10 @@ program.argument("<files...>", "Markdown files to convert (supports glob)").opti
2717
4895
  try {
2718
4896
  const outputPath = await convert(file, {
2719
4897
  output: options.output,
2720
- theme: options.theme
4898
+ theme: options.theme,
4899
+ colorScheme: options.color,
4900
+ textScheme: options.text,
4901
+ layoutScheme: options.layout
2721
4902
  });
2722
4903
  const outName = outputPath.split(/[\\/]/).pop() ?? outputPath;
2723
4904
  progress.succeed(`${chalk2.cyan(fileName)} \u2192 ${chalk2.green(outName)}`);