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