@ncukondo/slide-generation 0.7.0 → 0.8.1

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.
Files changed (34) hide show
  1. package/dist/cli/index.js +647 -364
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/index.d.ts +38 -34
  4. package/dist/index.js +276 -16
  5. package/dist/index.js.map +1 -1
  6. package/icons/fetched/LICENSE +202 -0
  7. package/icons/fetched/NOTICE +14 -0
  8. package/icons/fetched/heroicons/arrow-right.svg +1 -0
  9. package/icons/fetched/material-icons/account_balance.svg +1 -0
  10. package/icons/fetched/material-icons/analytics.svg +1 -0
  11. package/icons/fetched/material-icons/business.svg +1 -0
  12. package/icons/fetched/material-icons/check_circle.svg +1 -0
  13. package/icons/fetched/material-icons/computer.svg +1 -0
  14. package/icons/fetched/material-icons/description.svg +1 -0
  15. package/icons/fetched/material-icons/done_all.svg +1 -0
  16. package/icons/fetched/material-icons/event_note.svg +1 -0
  17. package/icons/fetched/material-icons/lightbulb.svg +1 -0
  18. package/icons/fetched/material-icons/person.svg +1 -0
  19. package/icons/fetched/material-icons/play_arrow.svg +1 -0
  20. package/icons/fetched/material-icons/replay.svg +1 -0
  21. package/icons/fetched/material-icons/thumb_up.svg +1 -0
  22. package/icons/fetched/material-icons/trending_up.svg +1 -0
  23. package/icons/registry.yaml +1 -0
  24. package/package.json +1 -1
  25. package/templates/diagrams/flow-chart.yaml +5 -5
  26. package/templates/diagrams/hierarchy.yaml +4 -1
  27. package/templates/layouts/before-after.yaml +15 -15
  28. package/templates/layouts/gallery.yaml +12 -12
  29. package/templates/layouts/image-caption.yaml +6 -6
  30. package/templates/layouts/image-full.yaml +9 -9
  31. package/templates/layouts/image-text.yaml +26 -24
  32. package/templates/layouts/three-column.yaml +5 -5
  33. package/templates/layouts/two-column.yaml +3 -3
  34. package/icons/fetched/.gitkeep +0 -0
package/dist/cli/index.js CHANGED
@@ -338,6 +338,13 @@ ${slidesContent}`;
338
338
  lines.push(`${key}: ${this.formatFrontMatterValue(value)}`);
339
339
  }
340
340
  }
341
+ if (options?.templateCss && options.templateCss.trim()) {
342
+ lines.push("style: |");
343
+ const cssLines = options.templateCss.split("\n");
344
+ for (const line of cssLines) {
345
+ lines.push(` ${line}`);
346
+ }
347
+ }
341
348
  lines.push("---");
342
349
  return lines.join("\n");
343
350
  }
@@ -374,7 +381,7 @@ ${this.renderSpeakerNotes(note)}`;
374
381
  }
375
382
  parts.push(slideContent);
376
383
  }
377
- return parts.map((slide) => `---
384
+ return parts.map((slide, index) => index === 0 ? slide : `---
378
385
 
379
386
  ${slide}`).join("\n\n");
380
387
  }
@@ -615,6 +622,46 @@ var init_loader = __esm({
615
622
  }
616
623
  });
617
624
 
625
+ // src/templates/css-collector.ts
626
+ var CSSCollector;
627
+ var init_css_collector = __esm({
628
+ "src/templates/css-collector.ts"() {
629
+ "use strict";
630
+ CSSCollector = class {
631
+ constructor(templateLoader) {
632
+ this.templateLoader = templateLoader;
633
+ }
634
+ /**
635
+ * Collect CSS from the specified templates
636
+ * @param templateNames - Names of templates to collect CSS from
637
+ * @returns Combined CSS string from all templates
638
+ */
639
+ collect(templateNames) {
640
+ const uniqueNames = [...new Set(templateNames)];
641
+ const cssBlocks = [];
642
+ for (const name of uniqueNames) {
643
+ const template = this.templateLoader.get(name);
644
+ if (template?.css) {
645
+ cssBlocks.push(template.css);
646
+ }
647
+ }
648
+ return cssBlocks.join("\n\n");
649
+ }
650
+ };
651
+ }
652
+ });
653
+
654
+ // src/templates/index.ts
655
+ var init_templates = __esm({
656
+ "src/templates/index.ts"() {
657
+ "use strict";
658
+ init_engine();
659
+ init_loader();
660
+ init_validators();
661
+ init_css_collector();
662
+ }
663
+ });
664
+
618
665
  // src/icons/schema.ts
619
666
  import { z as z4 } from "zod";
620
667
  var iconSourceTypeSchema, iconSourceSchema, iconDefaultsSchema, iconRegistrySchema;
@@ -757,24 +804,217 @@ var init_registry = __esm({
757
804
  }
758
805
  });
759
806
 
760
- // src/icons/resolver.ts
807
+ // src/icons/fetcher.ts
761
808
  import * as fs3 from "fs/promises";
762
809
  import * as path2 from "path";
810
+ import { stringify as stringifyYaml, parse as parseYaml3 } from "yaml";
811
+ function isExternalSource(prefix) {
812
+ return ICON_SOURCES[prefix]?.external ?? false;
813
+ }
814
+ function isValidIconName(name, allowPath = false) {
815
+ if (!name || name.length > 100) {
816
+ return false;
817
+ }
818
+ if (name.includes("..") || name.startsWith("/") || name.endsWith("/")) {
819
+ return false;
820
+ }
821
+ const pattern = allowPath ? VALID_ICON_NAME_WITH_PATH : VALID_ICON_NAME;
822
+ return pattern.test(name);
823
+ }
824
+ var ICON_SOURCES, VALID_ICON_NAME, VALID_ICON_NAME_WITH_PATH, IconFetcher;
825
+ var init_fetcher = __esm({
826
+ "src/icons/fetcher.ts"() {
827
+ "use strict";
828
+ ICON_SOURCES = {
829
+ health: { set: "healthicons", license: "MIT", external: true },
830
+ ms: { set: "material-symbols", license: "Apache-2.0", external: true },
831
+ hero: { set: "heroicons", license: "MIT", external: true },
832
+ mi: { set: "material-icons", license: "Apache-2.0", external: false },
833
+ mdi: { set: "mdi", license: "Apache-2.0", external: true },
834
+ iconify: { set: "iconify", license: "Various", external: true }
835
+ };
836
+ VALID_ICON_NAME = /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/i;
837
+ VALID_ICON_NAME_WITH_PATH = /^[a-z0-9][a-z0-9-/]*[a-z0-9]$|^[a-z0-9]$/i;
838
+ IconFetcher = class {
839
+ fetchedDir;
840
+ saveLocally;
841
+ timeoutMs;
842
+ constructor(options = {}) {
843
+ this.fetchedDir = options.fetchedDir ?? "icons/fetched";
844
+ this.saveLocally = options.saveLocally ?? true;
845
+ this.timeoutMs = options.timeoutMs ?? 1e4;
846
+ }
847
+ /**
848
+ * Parse an icon reference string (e.g., "health:stethoscope")
849
+ * Returns null if the format is invalid or contains unsafe characters
850
+ */
851
+ parseReference(iconRef) {
852
+ const colonIndex = iconRef.indexOf(":");
853
+ if (colonIndex === -1) {
854
+ return null;
855
+ }
856
+ const prefix = iconRef.substring(0, colonIndex);
857
+ const name = iconRef.substring(colonIndex + 1);
858
+ if (!/^[a-z0-9]+$/i.test(prefix)) {
859
+ return null;
860
+ }
861
+ if (!isValidIconName(name)) {
862
+ return null;
863
+ }
864
+ return { prefix, name };
865
+ }
866
+ /**
867
+ * Get the Iconify set name for a prefix
868
+ */
869
+ getIconifySet(prefix) {
870
+ return ICON_SOURCES[prefix]?.set ?? prefix;
871
+ }
872
+ /**
873
+ * Get the local file path for an icon reference
874
+ */
875
+ getLocalPath(iconRef) {
876
+ const parsed = this.parseReference(iconRef);
877
+ if (!parsed) {
878
+ throw new Error(`Invalid icon reference: ${iconRef}`);
879
+ }
880
+ const setDir = this.getIconifySet(parsed.prefix);
881
+ return path2.join(this.fetchedDir, setDir, `${parsed.name}.svg`);
882
+ }
883
+ /**
884
+ * Check if an icon exists locally
885
+ */
886
+ async existsLocally(iconRef) {
887
+ const localPath = this.getLocalPath(iconRef);
888
+ try {
889
+ await fs3.access(localPath);
890
+ return true;
891
+ } catch {
892
+ return false;
893
+ }
894
+ }
895
+ /**
896
+ * Build the Iconify API URL for an icon
897
+ */
898
+ buildUrl(prefix, name) {
899
+ const set = this.getIconifySet(prefix);
900
+ return `https://api.iconify.design/${set}/${name}.svg`;
901
+ }
902
+ /**
903
+ * Resolve an icon reference (local first, then fetch)
904
+ */
905
+ async resolve(iconRef) {
906
+ if (await this.existsLocally(iconRef)) {
907
+ const localPath = this.getLocalPath(iconRef);
908
+ return await fs3.readFile(localPath, "utf-8");
909
+ }
910
+ return await this.fetchAndSave(iconRef);
911
+ }
912
+ /**
913
+ * Fetch an icon from external source and save locally
914
+ */
915
+ async fetchAndSave(iconRef) {
916
+ const parsed = this.parseReference(iconRef);
917
+ if (!parsed) {
918
+ throw new Error(`Invalid icon reference: ${iconRef}`);
919
+ }
920
+ const url = this.buildUrl(parsed.prefix, parsed.name);
921
+ const controller = new AbortController();
922
+ const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
923
+ let response;
924
+ try {
925
+ response = await fetch(url, { signal: controller.signal });
926
+ } catch (error) {
927
+ if (error instanceof Error && error.name === "AbortError") {
928
+ throw new Error(`Fetch timeout: ${iconRef} (exceeded ${this.timeoutMs}ms)`);
929
+ }
930
+ throw error;
931
+ } finally {
932
+ clearTimeout(timeoutId);
933
+ }
934
+ if (!response.ok) {
935
+ throw new Error(`Icon not found: ${iconRef} (HTTP ${response.status})`);
936
+ }
937
+ const svg = await response.text();
938
+ if (this.saveLocally) {
939
+ await this.saveLocallyInternal(parsed.prefix, parsed.name, svg, url);
940
+ }
941
+ return svg;
942
+ }
943
+ /**
944
+ * Save icon to local filesystem
945
+ */
946
+ async saveLocallyInternal(prefix, name, svg, sourceUrl) {
947
+ const setDir = this.getIconifySet(prefix);
948
+ const dirPath = path2.join(this.fetchedDir, setDir);
949
+ const filePath = path2.join(dirPath, `${name}.svg`);
950
+ await fs3.mkdir(dirPath, { recursive: true });
951
+ await fs3.writeFile(filePath, svg, "utf-8");
952
+ await this.updateSourcesYaml(setDir, name, sourceUrl);
953
+ }
954
+ /**
955
+ * Update _sources.yaml with source information
956
+ */
957
+ async updateSourcesYaml(setDir, name, sourceUrl) {
958
+ const sourcesPath = path2.join(this.fetchedDir, "_sources.yaml");
959
+ let sources = {};
960
+ try {
961
+ const content = await fs3.readFile(sourcesPath, "utf-8");
962
+ sources = parseYaml3(content) ?? {};
963
+ } catch {
964
+ }
965
+ const key = `${setDir}/${name}.svg`;
966
+ sources[key] = {
967
+ source: sourceUrl,
968
+ fetched_at: (/* @__PURE__ */ new Date()).toISOString(),
969
+ license: this.getLicense(setDir)
970
+ };
971
+ const header = `# Fetched icon sources
972
+ # This file tracks where icons were fetched from for traceability
973
+
974
+ `;
975
+ await fs3.writeFile(sourcesPath, header + stringifyYaml(sources), "utf-8");
976
+ }
977
+ /**
978
+ * Get the license for an icon set
979
+ */
980
+ getLicense(setDir) {
981
+ for (const config of Object.values(ICON_SOURCES)) {
982
+ if (config.set === setDir) {
983
+ return config.license;
984
+ }
985
+ }
986
+ return "Unknown";
987
+ }
988
+ };
989
+ }
990
+ });
991
+
992
+ // src/icons/resolver.ts
993
+ import * as fs4 from "fs/promises";
994
+ import * as path3 from "path";
763
995
  import nunjucks2 from "nunjucks";
764
996
  var IconResolver;
765
997
  var init_resolver = __esm({
766
998
  "src/icons/resolver.ts"() {
767
999
  "use strict";
1000
+ init_fetcher();
768
1001
  IconResolver = class {
769
1002
  constructor(registry, options = {}) {
770
1003
  this.registry = registry;
771
1004
  this.nunjucksEnv = new nunjucks2.Environment(null, {
772
1005
  autoescape: false
773
1006
  });
774
- this.options = options;
1007
+ this.options = {
1008
+ autoFetch: true,
1009
+ ...options
1010
+ };
1011
+ this.fetcher = new IconFetcher({
1012
+ fetchedDir: options.fetchedDir ?? "icons/fetched"
1013
+ });
775
1014
  }
776
1015
  nunjucksEnv;
777
1016
  options;
1017
+ fetcher;
778
1018
  /**
779
1019
  * Render an icon by name or alias
780
1020
  */
@@ -796,6 +1036,17 @@ var init_resolver = __esm({
796
1036
  color: this.resolveColor(options?.color) ?? defaults.color,
797
1037
  ...options?.class !== void 0 ? { class: options.class } : {}
798
1038
  };
1039
+ const cachedSvg = await this.tryGetCachedSvg(parsed.prefix, parsed.name);
1040
+ if (cachedSvg) {
1041
+ return this.processSvg(cachedSvg, parsed.name, mergedOptions);
1042
+ }
1043
+ if (this.options.autoFetch && this.isFetchableSource(parsed.prefix)) {
1044
+ try {
1045
+ const fetchedSvg = await this.fetcher.fetchAndSave(`${parsed.prefix}:${parsed.name}`);
1046
+ return this.processSvg(fetchedSvg, parsed.name, mergedOptions);
1047
+ } catch {
1048
+ }
1049
+ }
799
1050
  switch (source.type) {
800
1051
  case "web-font":
801
1052
  return this.renderWebFont(source, parsed.name, mergedOptions);
@@ -809,6 +1060,29 @@ var init_resolver = __esm({
809
1060
  throw new Error(`Unsupported icon source type: "${source.type}"`);
810
1061
  }
811
1062
  }
1063
+ /**
1064
+ * Try to get a cached SVG from the fetched directory
1065
+ */
1066
+ async tryGetCachedSvg(prefix, name) {
1067
+ const iconSourceConfig = ICON_SOURCES[prefix];
1068
+ if (!iconSourceConfig) {
1069
+ return null;
1070
+ }
1071
+ const setDir = iconSourceConfig.set;
1072
+ const fetchedDir = this.options.fetchedDir ?? "icons/fetched";
1073
+ const svgPath = path3.join(fetchedDir, setDir, `${name}.svg`);
1074
+ try {
1075
+ return await fs4.readFile(svgPath, "utf-8");
1076
+ } catch {
1077
+ return null;
1078
+ }
1079
+ }
1080
+ /**
1081
+ * Check if a source prefix is fetchable from external API
1082
+ */
1083
+ isFetchableSource(prefix) {
1084
+ return prefix in ICON_SOURCES;
1085
+ }
812
1086
  /**
813
1087
  * Render a web-font icon
814
1088
  */
@@ -833,9 +1107,9 @@ var init_resolver = __esm({
833
1107
  if (!source.path) {
834
1108
  throw new Error(`Local SVG source "${source.name}" has no path defined`);
835
1109
  }
836
- const svgPath = path2.join(source.path, `${name}.svg`);
1110
+ const svgPath = path3.join(source.path, `${name}.svg`);
837
1111
  try {
838
- const svgContent = await fs3.readFile(svgPath, "utf-8");
1112
+ const svgContent = await fs4.readFile(svgPath, "utf-8");
839
1113
  return this.processSvg(svgContent, name, options);
840
1114
  } catch (error) {
841
1115
  if (error.code === "ENOENT") {
@@ -982,6 +1256,8 @@ var init_manager = __esm({
982
1256
  }
983
1257
  /**
984
1258
  * Get multiple references by IDs using ref export for better performance
1259
+ * Note: ref export returns exit code 1 when references are not found,
1260
+ * but still outputs valid JSON (empty array or partial results) to stdout
985
1261
  */
986
1262
  async getByIds(ids) {
987
1263
  if (ids.length === 0) {
@@ -989,7 +1265,8 @@ var init_manager = __esm({
989
1265
  }
990
1266
  const idsArg = ids.map((id) => `"${id}"`).join(" ");
991
1267
  const result = await this.execCommand(
992
- `${this.command} export ${idsArg}`
1268
+ `${this.command} export ${idsArg}`,
1269
+ true
993
1270
  );
994
1271
  const items = this.parseJSON(result);
995
1272
  const map = /* @__PURE__ */ new Map();
@@ -998,16 +1275,16 @@ var init_manager = __esm({
998
1275
  }
999
1276
  return map;
1000
1277
  }
1001
- execCommand(cmd) {
1002
- return new Promise((resolve8, reject) => {
1278
+ execCommand(cmd, ignoreExitCode = false) {
1279
+ return new Promise((resolve9, reject) => {
1003
1280
  exec(cmd, (error, stdout) => {
1004
- if (error) {
1281
+ if (error && !ignoreExitCode) {
1005
1282
  reject(
1006
1283
  new ReferenceManagerError(`Failed to execute: ${cmd}`, error)
1007
1284
  );
1008
1285
  return;
1009
1286
  }
1010
- resolve8(stdout.toString());
1287
+ resolve9(stdout.toString());
1011
1288
  });
1012
1289
  });
1013
1290
  }
@@ -1462,7 +1739,7 @@ var init_bibliography = __esm({
1462
1739
  });
1463
1740
 
1464
1741
  // src/core/pipeline.ts
1465
- import { writeFile } from "fs/promises";
1742
+ import { writeFile as writeFile2 } from "fs/promises";
1466
1743
  var PipelineError, Pipeline;
1467
1744
  var init_pipeline = __esm({
1468
1745
  "src/core/pipeline.ts"() {
@@ -1471,7 +1748,7 @@ var init_pipeline = __esm({
1471
1748
  init_transformer();
1472
1749
  init_renderer();
1473
1750
  init_engine();
1474
- init_loader();
1751
+ init_templates();
1475
1752
  init_registry();
1476
1753
  init_resolver();
1477
1754
  init_manager();
@@ -1493,7 +1770,9 @@ var init_pipeline = __esm({
1493
1770
  this.templateEngine = new TemplateEngine();
1494
1771
  this.templateLoader = new TemplateLoader();
1495
1772
  this.iconRegistry = new IconRegistryLoader();
1496
- this.iconResolver = new IconResolver(this.iconRegistry);
1773
+ this.iconResolver = new IconResolver(this.iconRegistry, {
1774
+ fetchedDir: config.icons.fetched
1775
+ });
1497
1776
  this.referenceManager = new ReferenceManager(
1498
1777
  config.references.connection.command
1499
1778
  );
@@ -1520,6 +1799,7 @@ var init_pipeline = __esm({
1520
1799
  this.citationFormatter
1521
1800
  );
1522
1801
  this.renderer = new Renderer();
1802
+ this.cssCollector = new CSSCollector(this.templateLoader);
1523
1803
  }
1524
1804
  parser;
1525
1805
  templateEngine;
@@ -1532,6 +1812,7 @@ var init_pipeline = __esm({
1532
1812
  bibliographyGenerator;
1533
1813
  transformer;
1534
1814
  renderer;
1815
+ cssCollector;
1535
1816
  warnings = [];
1536
1817
  /**
1537
1818
  * Run the full conversion pipeline
@@ -1546,7 +1827,7 @@ var init_pipeline = __esm({
1546
1827
  const transformedSlides = await this.transformSlides(presentation);
1547
1828
  const output = this.render(transformedSlides, presentation);
1548
1829
  if (options?.outputPath) {
1549
- await writeFile(options.outputPath, output, "utf-8");
1830
+ await writeFile2(options.outputPath, output, "utf-8");
1550
1831
  }
1551
1832
  return output;
1552
1833
  } catch (error) {
@@ -1573,7 +1854,7 @@ var init_pipeline = __esm({
1573
1854
  const transformedSlides = await this.transformSlides(presentation);
1574
1855
  const output = this.render(transformedSlides, presentation);
1575
1856
  if (options?.outputPath) {
1576
- await writeFile(options.outputPath, output, "utf-8");
1857
+ await writeFile2(options.outputPath, output, "utf-8");
1577
1858
  }
1578
1859
  return {
1579
1860
  output,
@@ -1687,9 +1968,12 @@ var init_pipeline = __esm({
1687
1968
  render(slides, presentation) {
1688
1969
  try {
1689
1970
  const notes = presentation.slides.map((slide) => slide.notes);
1971
+ const templateNames = presentation.slides.map((slide) => slide.template);
1972
+ const templateCss = this.cssCollector.collect(templateNames);
1690
1973
  const renderOptions = {
1691
1974
  includeTheme: true,
1692
- notes
1975
+ notes,
1976
+ ...templateCss ? { templateCss } : {}
1693
1977
  };
1694
1978
  return this.renderer.render(slides, presentation.meta, renderOptions);
1695
1979
  } catch (error) {
@@ -1804,11 +2088,7 @@ var init_schema2 = __esm({
1804
2088
  }).default({}),
1805
2089
  icons: z5.object({
1806
2090
  registry: z5.string().default("./icons/registry.yaml"),
1807
- cache: z5.object({
1808
- enabled: z5.boolean().default(true),
1809
- directory: z5.string().default(".cache/icons"),
1810
- ttl: z5.number().default(86400)
1811
- }).default({})
2091
+ fetched: z5.string().default("./icons/fetched")
1812
2092
  }).default({}),
1813
2093
  references: z5.object({
1814
2094
  enabled: z5.boolean().default(true),
@@ -1834,9 +2114,9 @@ var init_schema2 = __esm({
1834
2114
  });
1835
2115
 
1836
2116
  // src/config/loader.ts
1837
- import { access, readFile as readFile5 } from "fs/promises";
1838
- import { join as join3 } from "path";
1839
- import { parse as parseYaml3 } from "yaml";
2117
+ import { access as access2, readFile as readFile6 } from "fs/promises";
2118
+ import { join as join4 } from "path";
2119
+ import { parse as parseYaml4 } from "yaml";
1840
2120
  var CONFIG_NAMES, ConfigLoader;
1841
2121
  var init_loader2 = __esm({
1842
2122
  "src/config/loader.ts"() {
@@ -1850,9 +2130,9 @@ var init_loader2 = __esm({
1850
2130
  }
1851
2131
  async findConfig(directory) {
1852
2132
  for (const name of CONFIG_NAMES) {
1853
- const path12 = join3(directory, name);
2133
+ const path12 = join4(directory, name);
1854
2134
  try {
1855
- await access(path12);
2135
+ await access2(path12);
1856
2136
  return path12;
1857
2137
  } catch {
1858
2138
  }
@@ -1862,8 +2142,8 @@ var init_loader2 = __esm({
1862
2142
  async loadFile(configPath) {
1863
2143
  if (!configPath) return {};
1864
2144
  try {
1865
- const content = await readFile5(configPath, "utf-8");
1866
- return parseYaml3(content) ?? {};
2145
+ const content = await readFile6(configPath, "utf-8");
2146
+ return parseYaml4(content) ?? {};
1867
2147
  } catch (error) {
1868
2148
  if (error.code === "ENOENT") {
1869
2149
  return {};
@@ -1995,9 +2275,9 @@ var init_schema3 = __esm({
1995
2275
  });
1996
2276
 
1997
2277
  // src/images/metadata-loader.ts
1998
- import * as fs4 from "fs/promises";
1999
- import * as path3 from "path";
2000
- import { parse as parseYaml4 } from "yaml";
2278
+ import * as fs5 from "fs/promises";
2279
+ import * as path4 from "path";
2280
+ import { parse as parseYaml5 } from "yaml";
2001
2281
  var metadataWarnings, ImageMetadataLoader;
2002
2282
  var init_metadata_loader = __esm({
2003
2283
  "src/images/metadata-loader.ts"() {
@@ -2026,16 +2306,16 @@ var init_metadata_loader = __esm({
2026
2306
  */
2027
2307
  async loadDirectory(dirPath) {
2028
2308
  const result = /* @__PURE__ */ new Map();
2029
- const fullDirPath = path3.join(this.baseDir, dirPath);
2309
+ const fullDirPath = path4.join(this.baseDir, dirPath);
2030
2310
  try {
2031
- await fs4.access(fullDirPath);
2311
+ await fs5.access(fullDirPath);
2032
2312
  } catch {
2033
2313
  return result;
2034
2314
  }
2035
- const files = await fs4.readdir(fullDirPath);
2315
+ const files = await fs5.readdir(fullDirPath);
2036
2316
  const imageFiles = files.filter((f) => isImageFile(f));
2037
2317
  for (const file of imageFiles) {
2038
- const imagePath = path3.join(dirPath, file);
2318
+ const imagePath = path4.join(dirPath, file);
2039
2319
  const metadata = await this.load(imagePath);
2040
2320
  result.set(file, metadata);
2041
2321
  }
@@ -2047,7 +2327,7 @@ var init_metadata_loader = __esm({
2047
2327
  async hasMetadata(imagePath) {
2048
2328
  const individualPath = this.getIndividualMetadataPath(imagePath);
2049
2329
  try {
2050
- await fs4.access(individualPath);
2330
+ await fs5.access(individualPath);
2051
2331
  return true;
2052
2332
  } catch {
2053
2333
  }
@@ -2055,7 +2335,7 @@ var init_metadata_loader = __esm({
2055
2335
  if (dirMetadata === null) {
2056
2336
  return false;
2057
2337
  }
2058
- const filename = path3.basename(imagePath);
2338
+ const filename = path4.basename(imagePath);
2059
2339
  return filename in dirMetadata && filename !== "_defaults";
2060
2340
  }
2061
2341
  /**
@@ -2069,14 +2349,14 @@ var init_metadata_loader = __esm({
2069
2349
  * Get the path to individual metadata file
2070
2350
  */
2071
2351
  getIndividualMetadataPath(imagePath) {
2072
- return path3.join(this.baseDir, `${imagePath}.meta.yaml`);
2352
+ return path4.join(this.baseDir, `${imagePath}.meta.yaml`);
2073
2353
  }
2074
2354
  /**
2075
2355
  * Get the path to directory metadata file
2076
2356
  */
2077
2357
  getDirectoryMetadataPath(imagePath) {
2078
- const dir = path3.dirname(imagePath);
2079
- return path3.join(this.baseDir, dir, "images.yaml");
2358
+ const dir = path4.dirname(imagePath);
2359
+ return path4.join(this.baseDir, dir, "images.yaml");
2080
2360
  }
2081
2361
  /**
2082
2362
  * Load individual metadata file (.meta.yaml)
@@ -2084,13 +2364,13 @@ var init_metadata_loader = __esm({
2084
2364
  async loadIndividualMetadata(imagePath) {
2085
2365
  const metadataPath = this.getIndividualMetadataPath(imagePath);
2086
2366
  try {
2087
- await fs4.access(metadataPath);
2367
+ await fs5.access(metadataPath);
2088
2368
  } catch {
2089
2369
  return null;
2090
2370
  }
2091
2371
  try {
2092
- const content = await fs4.readFile(metadataPath, "utf-8");
2093
- const parsed = parseYaml4(content);
2372
+ const content = await fs5.readFile(metadataPath, "utf-8");
2373
+ const parsed = parseYaml5(content);
2094
2374
  const validated = individualMetadataSchema.safeParse(parsed);
2095
2375
  if (validated.success) {
2096
2376
  return validated.data;
@@ -2115,13 +2395,13 @@ var init_metadata_loader = __esm({
2115
2395
  async loadDirectoryMetadataFile(imagePath) {
2116
2396
  const metadataPath = this.getDirectoryMetadataPath(imagePath);
2117
2397
  try {
2118
- await fs4.access(metadataPath);
2398
+ await fs5.access(metadataPath);
2119
2399
  } catch {
2120
2400
  return null;
2121
2401
  }
2122
2402
  try {
2123
- const content = await fs4.readFile(metadataPath, "utf-8");
2124
- const parsed = parseYaml4(content);
2403
+ const content = await fs5.readFile(metadataPath, "utf-8");
2404
+ const parsed = parseYaml5(content);
2125
2405
  const validated = directoryMetadataSchema.safeParse(parsed);
2126
2406
  if (validated.success) {
2127
2407
  return validated.data;
@@ -2148,7 +2428,7 @@ var init_metadata_loader = __esm({
2148
2428
  if (dirMetadata === null) {
2149
2429
  return {};
2150
2430
  }
2151
- const filename = path3.basename(imagePath);
2431
+ const filename = path4.basename(imagePath);
2152
2432
  const defaults = dirMetadata["_defaults"] ?? {};
2153
2433
  const fileMetadata = dirMetadata[filename] ?? {};
2154
2434
  return this.mergeMetadata(defaults, fileMetadata);
@@ -2183,8 +2463,8 @@ var init_metadata_loader = __esm({
2183
2463
  });
2184
2464
 
2185
2465
  // src/images/validator.ts
2186
- import * as fs5 from "fs/promises";
2187
- import * as path4 from "path";
2466
+ import * as fs6 from "fs/promises";
2467
+ import * as path5 from "path";
2188
2468
  import imageSize from "image-size";
2189
2469
  var ImageValidator;
2190
2470
  var init_validator = __esm({
@@ -2204,14 +2484,14 @@ var init_validator = __esm({
2204
2484
  async validateImageExists(imagePath) {
2205
2485
  const errors = [];
2206
2486
  const warnings = [];
2207
- const fullPath = path4.join(this.baseDir, imagePath);
2208
- const ext = path4.extname(imagePath).toLowerCase();
2487
+ const fullPath = path5.join(this.baseDir, imagePath);
2488
+ const ext = path5.extname(imagePath).toLowerCase();
2209
2489
  if (!IMAGE_EXTENSIONS.has(ext)) {
2210
2490
  errors.push(`Unsupported image format: ${imagePath}`);
2211
2491
  return { valid: false, errors, warnings };
2212
2492
  }
2213
2493
  try {
2214
- await fs5.access(fullPath);
2494
+ await fs6.access(fullPath);
2215
2495
  } catch {
2216
2496
  errors.push(`Image not found: ${imagePath}`);
2217
2497
  return { valid: false, errors, warnings };
@@ -2222,9 +2502,9 @@ var init_validator = __esm({
2222
2502
  * Get image dimensions
2223
2503
  */
2224
2504
  async getImageDimensions(imagePath) {
2225
- const fullPath = path4.join(this.baseDir, imagePath);
2505
+ const fullPath = path5.join(this.baseDir, imagePath);
2226
2506
  try {
2227
- const buffer = await fs5.readFile(fullPath);
2507
+ const buffer = await fs6.readFile(fullPath);
2228
2508
  const dimensions = imageSize(buffer);
2229
2509
  if (dimensions.width && dimensions.height) {
2230
2510
  return {
@@ -2559,8 +2839,8 @@ var init_processor = __esm({
2559
2839
  });
2560
2840
 
2561
2841
  // src/images/processing-pipeline.ts
2562
- import * as fs6 from "fs/promises";
2563
- import * as path5 from "path";
2842
+ import * as fs7 from "fs/promises";
2843
+ import * as path6 from "path";
2564
2844
  var ImageProcessingPipeline;
2565
2845
  var init_processing_pipeline = __esm({
2566
2846
  "src/images/processing-pipeline.ts"() {
@@ -2573,7 +2853,7 @@ var init_processing_pipeline = __esm({
2573
2853
  this.baseDir = baseDir;
2574
2854
  this.metadataLoader = new ImageMetadataLoader(baseDir);
2575
2855
  this.processor = new ImageProcessor();
2576
- this.outputDir = options?.outputDir ?? path5.join(baseDir, ".processed");
2856
+ this.outputDir = options?.outputDir ?? path6.join(baseDir, ".processed");
2577
2857
  }
2578
2858
  metadataLoader;
2579
2859
  processor;
@@ -2582,7 +2862,7 @@ var init_processing_pipeline = __esm({
2582
2862
  * Process a single image based on its metadata instructions
2583
2863
  */
2584
2864
  async processImage(imagePath) {
2585
- const fullPath = path5.join(this.baseDir, imagePath);
2865
+ const fullPath = path6.join(this.baseDir, imagePath);
2586
2866
  const result = {
2587
2867
  success: true,
2588
2868
  originalPath: fullPath
@@ -2593,7 +2873,7 @@ var init_processing_pipeline = __esm({
2593
2873
  result.skipped = true;
2594
2874
  return result;
2595
2875
  }
2596
- await fs6.mkdir(this.outputDir, { recursive: true });
2876
+ await fs7.mkdir(this.outputDir, { recursive: true });
2597
2877
  const processedPath = await this.applyInstructions(
2598
2878
  fullPath,
2599
2879
  imagePath,
@@ -2622,7 +2902,7 @@ var init_processing_pipeline = __esm({
2622
2902
  imageMap: /* @__PURE__ */ new Map()
2623
2903
  };
2624
2904
  try {
2625
- const files = await fs6.readdir(this.baseDir);
2905
+ const files = await fs7.readdir(this.baseDir);
2626
2906
  const imageFiles = files.filter((f) => isImageFile(f));
2627
2907
  result.totalImages = imageFiles.length;
2628
2908
  for (const imageFile of imageFiles) {
@@ -2646,11 +2926,11 @@ var init_processing_pipeline = __esm({
2646
2926
  * Apply processing instructions to an image
2647
2927
  */
2648
2928
  async applyInstructions(inputPath, relativePath, instructions) {
2649
- const outputFilename = path5.basename(relativePath);
2650
- const finalOutputPath = path5.join(this.outputDir, outputFilename);
2929
+ const outputFilename = path6.basename(relativePath);
2930
+ const finalOutputPath = path6.join(this.outputDir, outputFilename);
2651
2931
  const tempPaths = [
2652
- path5.join(this.outputDir, `temp_${outputFilename}`),
2653
- path5.join(this.outputDir, `temp2_${outputFilename}`)
2932
+ path6.join(this.outputDir, `temp_${outputFilename}`),
2933
+ path6.join(this.outputDir, `temp2_${outputFilename}`)
2654
2934
  ];
2655
2935
  let currentInputPath = inputPath;
2656
2936
  for (let i = 0; i < instructions.length; i++) {
@@ -2663,11 +2943,11 @@ var init_processing_pipeline = __esm({
2663
2943
  }
2664
2944
  }
2665
2945
  try {
2666
- await fs6.unlink(path5.join(this.outputDir, `temp_${outputFilename}`));
2946
+ await fs7.unlink(path6.join(this.outputDir, `temp_${outputFilename}`));
2667
2947
  } catch {
2668
2948
  }
2669
2949
  try {
2670
- await fs6.unlink(path5.join(this.outputDir, `temp2_${outputFilename}`));
2950
+ await fs7.unlink(path6.join(this.outputDir, `temp2_${outputFilename}`));
2671
2951
  } catch {
2672
2952
  }
2673
2953
  return finalOutputPath;
@@ -2753,19 +3033,19 @@ var init_images = __esm({
2753
3033
 
2754
3034
  // src/cli/commands/convert.ts
2755
3035
  import { Command } from "commander";
2756
- import { access as access4, readFile as readFile8 } from "fs/promises";
2757
- import { basename as basename3, dirname as dirname2, join as join7 } from "path";
3036
+ import { access as access5, readFile as readFile9 } from "fs/promises";
3037
+ import { basename as basename3, dirname as dirname2, join as join8, resolve, isAbsolute } from "path";
2758
3038
  import chalk from "chalk";
2759
3039
  import ora from "ora";
2760
- import { parse as parseYaml5 } from "yaml";
3040
+ import { parse as parseYaml6 } from "yaml";
2761
3041
  function getDefaultOutputPath(inputPath) {
2762
3042
  const dir = dirname2(inputPath);
2763
3043
  const base = basename3(inputPath, ".yaml");
2764
- return join7(dir, `${base}.md`);
3044
+ return join8(dir, `${base}.md`);
2765
3045
  }
2766
3046
  async function extractImageDirectories(inputPath, baseDir) {
2767
- const content = await readFile8(inputPath, "utf-8");
2768
- const parsed = parseYaml5(content);
3047
+ const content = await readFile9(inputPath, "utf-8");
3048
+ const parsed = parseYaml6(content);
2769
3049
  const imageDirs = /* @__PURE__ */ new Set();
2770
3050
  if (!parsed.slides) return [];
2771
3051
  for (const slide of parsed.slides) {
@@ -2788,7 +3068,7 @@ async function extractImageDirectories(inputPath, baseDir) {
2788
3068
  for (const imagePath of imagePaths) {
2789
3069
  const dir = dirname2(imagePath);
2790
3070
  if (dir && dir !== ".") {
2791
- imageDirs.add(join7(baseDir, dir));
3071
+ imageDirs.add(join8(baseDir, dir));
2792
3072
  }
2793
3073
  }
2794
3074
  }
@@ -2799,7 +3079,7 @@ async function processImages(inputPath, baseDir) {
2799
3079
  let totalProcessed = 0;
2800
3080
  for (const imageDir of imageDirs) {
2801
3081
  try {
2802
- await access4(imageDir);
3082
+ await access5(imageDir);
2803
3083
  const pipeline = new ImageProcessingPipeline(imageDir);
2804
3084
  const result = await pipeline.processDirectory();
2805
3085
  totalProcessed += result.processedImages;
@@ -2824,7 +3104,7 @@ async function executeConvert(inputPath, options) {
2824
3104
  try {
2825
3105
  spinner?.start(`Reading ${inputPath}...`);
2826
3106
  try {
2827
- await access4(inputPath);
3107
+ await access5(inputPath);
2828
3108
  } catch {
2829
3109
  spinner?.fail(`File not found: ${inputPath}`);
2830
3110
  console.error(chalk.red(`Error: Input file not found: ${inputPath}`));
@@ -2839,6 +3119,19 @@ async function executeConvert(inputPath, options) {
2839
3119
  configPath = await configLoader.findConfig(dirname2(inputPath));
2840
3120
  }
2841
3121
  const config = await configLoader.load(configPath);
3122
+ const configDir = configPath ? dirname2(configPath) : dirname2(inputPath);
3123
+ if (!isAbsolute(config.icons.fetched)) {
3124
+ config.icons.fetched = resolve(configDir, config.icons.fetched);
3125
+ }
3126
+ if (!isAbsolute(config.icons.registry)) {
3127
+ config.icons.registry = resolve(configDir, config.icons.registry);
3128
+ }
3129
+ if (!isAbsolute(config.templates.builtin)) {
3130
+ config.templates.builtin = resolve(configDir, config.templates.builtin);
3131
+ }
3132
+ if (config.templates.custom && !isAbsolute(config.templates.custom)) {
3133
+ config.templates.custom = resolve(configDir, config.templates.custom);
3134
+ }
2842
3135
  if (options.references === false) {
2843
3136
  config.references.enabled = false;
2844
3137
  }
@@ -2942,16 +3235,16 @@ var init_convert = __esm({
2942
3235
 
2943
3236
  // src/cli/utils/marp-runner.ts
2944
3237
  import { existsSync } from "fs";
2945
- import { join as join8, resolve, dirname as dirname4 } from "path";
3238
+ import { join as join9, resolve as resolve2, dirname as dirname4 } from "path";
2946
3239
  import {
2947
3240
  execFileSync,
2948
3241
  spawn
2949
3242
  } from "child_process";
2950
3243
  function getMarpCommand(projectDir) {
2951
- const startDir = resolve(projectDir ?? process.cwd());
3244
+ const startDir = resolve2(projectDir ?? process.cwd());
2952
3245
  let currentDir = startDir;
2953
3246
  while (true) {
2954
- const localMarp = join8(currentDir, "node_modules", ".bin", "marp");
3247
+ const localMarp = join9(currentDir, "node_modules", ".bin", "marp");
2955
3248
  if (existsSync(localMarp)) {
2956
3249
  return localMarp;
2957
3250
  }
@@ -3013,13 +3306,20 @@ function parseMarpBrowserError(errorOutput) {
3013
3306
  }
3014
3307
  return null;
3015
3308
  }
3309
+ function addHtmlOption(args) {
3310
+ if (args.includes("--html") || args.includes("--no-html")) {
3311
+ return args;
3312
+ }
3313
+ return ["--html", ...args];
3314
+ }
3016
3315
  function runMarp(args, options = {}) {
3017
3316
  const { projectDir, ...execOptions } = options;
3018
3317
  const marpCmd = getMarpCommand(projectDir);
3019
3318
  if (!marpCmd) {
3020
3319
  throw new Error("Marp CLI not found. Install it with: npm install -D @marp-team/marp-cli");
3021
3320
  }
3022
- execFileSync(marpCmd, args, execOptions);
3321
+ const finalArgs = addHtmlOption(args);
3322
+ execFileSync(marpCmd, finalArgs, execOptions);
3023
3323
  }
3024
3324
  function spawnMarp(args, options = {}) {
3025
3325
  const { projectDir, ...spawnOptions } = options;
@@ -3027,7 +3327,8 @@ function spawnMarp(args, options = {}) {
3027
3327
  if (!marpCmd) {
3028
3328
  throw new Error("Marp CLI not found. Install it with: npm install -D @marp-team/marp-cli");
3029
3329
  }
3030
- return spawn(marpCmd, args, spawnOptions);
3330
+ const finalArgs = addHtmlOption(args);
3331
+ return spawn(marpCmd, finalArgs, spawnOptions);
3031
3332
  }
3032
3333
  var init_marp_runner = __esm({
3033
3334
  "src/cli/utils/marp-runner.ts"() {
@@ -3051,17 +3352,17 @@ __export(screenshot_exports, {
3051
3352
  generateContactSheet: () => generateContactSheet
3052
3353
  });
3053
3354
  import { Command as Command4 } from "commander";
3054
- import { access as access7, mkdir as mkdir3, readdir as readdir5, rename, unlink as unlink2 } from "fs/promises";
3055
- import { basename as basename5, dirname as dirname6, extname as extname3, join as join10 } from "path";
3355
+ import { access as access8, mkdir as mkdir4, readdir as readdir5, rename, unlink as unlink2 } from "fs/promises";
3356
+ import { basename as basename5, dirname as dirname6, extname as extname3, join as join11 } from "path";
3056
3357
  import chalk4 from "chalk";
3057
3358
  import ora3 from "ora";
3058
3359
  import sharp2 from "sharp";
3059
3360
  async function filterToSpecificSlide(outputDir, baseName, slideNumber, format) {
3060
3361
  const slideStr = slideNumber.toString().padStart(3, "0");
3061
3362
  const targetFileName = `${baseName}.${slideStr}.${format}`;
3062
- const targetPath = join10(outputDir, targetFileName);
3363
+ const targetPath = join11(outputDir, targetFileName);
3063
3364
  try {
3064
- await access7(targetPath);
3365
+ await access8(targetPath);
3065
3366
  } catch {
3066
3367
  return {
3067
3368
  success: false,
@@ -3074,7 +3375,7 @@ async function filterToSpecificSlide(outputDir, baseName, slideNumber, format) {
3074
3375
  );
3075
3376
  for (const file of slideFiles) {
3076
3377
  if (file !== targetFileName) {
3077
- await unlink2(join10(outputDir, file));
3378
+ await unlink2(join11(outputDir, file));
3078
3379
  }
3079
3380
  }
3080
3381
  return {
@@ -3091,8 +3392,8 @@ async function ensureFileExtensions(outputDir, baseName, format) {
3091
3392
  const renamedFiles = [];
3092
3393
  for (const file of files) {
3093
3394
  if (file.startsWith(baseName) && slidePattern.test(file)) {
3094
- const oldPath = join10(outputDir, file);
3095
- const newPath = join10(outputDir, `${file}.${format}`);
3395
+ const oldPath = join11(outputDir, file);
3396
+ const newPath = join11(outputDir, `${file}.${format}`);
3096
3397
  await rename(oldPath, newPath);
3097
3398
  renamedFiles.push(`${file}.${format}`);
3098
3399
  }
@@ -3186,14 +3487,14 @@ function formatAiOutput(options) {
3186
3487
  ""
3187
3488
  ];
3188
3489
  for (const file of files) {
3189
- lines.push(` ${join10(outputDir, file)}`);
3490
+ lines.push(` ${join11(outputDir, file)}`);
3190
3491
  }
3191
3492
  lines.push("");
3192
3493
  lines.push(`Estimated tokens: ~${totalTokens} (${files.length} ${imageLabel})`);
3193
3494
  if (files.length > 0) {
3194
3495
  lines.push("");
3195
3496
  lines.push("To review in Claude Code:");
3196
- lines.push(` Read ${join10(outputDir, files[0])}`);
3497
+ lines.push(` Read ${join11(outputDir, files[0])}`);
3197
3498
  }
3198
3499
  return lines.join("\n");
3199
3500
  }
@@ -3211,7 +3512,7 @@ function buildMarpCommandArgs(markdownPath, outputDir, options) {
3211
3512
  args.push("--jpeg-quality", String(quality));
3212
3513
  }
3213
3514
  const mdBaseName = basename5(markdownPath, extname3(markdownPath));
3214
- const outputPath = join10(outputDir, mdBaseName);
3515
+ const outputPath = join11(outputDir, mdBaseName);
3215
3516
  args.push("-o", outputPath);
3216
3517
  args.push(markdownPath);
3217
3518
  return args;
@@ -3226,7 +3527,7 @@ async function executeScreenshot(inputPath, options) {
3226
3527
  const spinner = options.verbose ? null : ora3();
3227
3528
  const outputDir = options.output || "./screenshots";
3228
3529
  try {
3229
- await access7(inputPath);
3530
+ await access8(inputPath);
3230
3531
  } catch {
3231
3532
  const message = `File not found: ${inputPath}`;
3232
3533
  console.error(chalk4.red(`Error: ${message}`));
@@ -3252,7 +3553,7 @@ async function executeScreenshot(inputPath, options) {
3252
3553
  }
3253
3554
  spinner?.succeed("Marp CLI found");
3254
3555
  try {
3255
- await mkdir3(outputDir, { recursive: true });
3556
+ await mkdir4(outputDir, { recursive: true });
3256
3557
  } catch {
3257
3558
  const message = `Failed to create output directory: ${outputDir}`;
3258
3559
  console.error(chalk4.red(`Error: ${message}`));
@@ -3359,7 +3660,7 @@ Error: ${browserError.message}
3359
3660
  const generatedFiles = allFiles.filter((f) => f.startsWith(mdBaseName) && f.endsWith(`.${actualFormat}`)).sort();
3360
3661
  if (generatedFiles.length > 0) {
3361
3662
  try {
3362
- const metadata = await sharp2(join10(outputDir, generatedFiles[0])).metadata();
3663
+ const metadata = await sharp2(join11(outputDir, generatedFiles[0])).metadata();
3363
3664
  if (metadata.width && metadata.height) {
3364
3665
  actualWidth = metadata.width;
3365
3666
  actualHeight = metadata.height;
@@ -3370,10 +3671,10 @@ Error: ${browserError.message}
3370
3671
  if (options.contactSheet && generatedFiles.length > 0) {
3371
3672
  spinner?.start("Generating contact sheet...");
3372
3673
  const slides = generatedFiles.map((file, index) => ({
3373
- path: join10(outputDir, file),
3674
+ path: join11(outputDir, file),
3374
3675
  index: index + 1
3375
3676
  }));
3376
- const contactSheetPath = join10(outputDir, `${mdBaseName}-contact.png`);
3677
+ const contactSheetPath = join11(outputDir, `${mdBaseName}-contact.png`);
3377
3678
  const contactResult = await generateContactSheet(slides, {
3378
3679
  outputPath: contactSheetPath,
3379
3680
  columns: options.columns || 2,
@@ -3433,13 +3734,9 @@ init_transformer();
3433
3734
  init_renderer();
3434
3735
  init_pipeline();
3435
3736
 
3436
- // src/templates/index.ts
3437
- init_engine();
3438
- init_loader();
3439
- init_validators();
3440
-
3441
3737
  // src/index.ts
3442
- var VERSION = "0.7.0";
3738
+ init_templates();
3739
+ var VERSION = "0.8.1";
3443
3740
 
3444
3741
  // src/cli/index.ts
3445
3742
  init_convert();
@@ -3451,11 +3748,11 @@ init_registry();
3451
3748
  init_loader2();
3452
3749
  init_convert();
3453
3750
  import { Command as Command2 } from "commander";
3454
- import { access as access5, readFile as readFile9 } from "fs/promises";
3751
+ import { access as access6, readFile as readFile10 } from "fs/promises";
3455
3752
  import { dirname as dirname3 } from "path";
3456
3753
  import chalk2 from "chalk";
3457
3754
  import ora2 from "ora";
3458
- import { parse as parseYaml6, stringify as yamlStringify } from "yaml";
3755
+ import { parse as parseYaml7, stringify as yamlStringify } from "yaml";
3459
3756
 
3460
3757
  // src/references/index.ts
3461
3758
  init_extractor();
@@ -3696,7 +3993,7 @@ async function executeValidate(inputPath, options) {
3696
3993
  try {
3697
3994
  spinner?.start(`Validating ${inputPath}...`);
3698
3995
  try {
3699
- await access5(inputPath);
3996
+ await access6(inputPath);
3700
3997
  } catch {
3701
3998
  spinner?.fail(`File not found: ${inputPath}`);
3702
3999
  result.errors.push(`File not found: ${inputPath}`);
@@ -3705,9 +4002,9 @@ async function executeValidate(inputPath, options) {
3705
4002
  process.exitCode = ExitCode.FileReadError;
3706
4003
  return;
3707
4004
  }
3708
- const content = await readFile9(inputPath, "utf-8");
4005
+ const content = await readFile10(inputPath, "utf-8");
3709
4006
  try {
3710
- parseYaml6(content);
4007
+ parseYaml7(content);
3711
4008
  result.stats.yamlSyntax = true;
3712
4009
  } catch (error) {
3713
4010
  const message = error instanceof Error ? error.message : "Unknown error";
@@ -4028,15 +4325,16 @@ function outputResult(result, options) {
4028
4325
  }
4029
4326
 
4030
4327
  // src/cli/commands/templates.ts
4328
+ init_templates();
4329
+ init_loader2();
4330
+ init_pipeline();
4331
+ init_convert();
4031
4332
  import { Command as Command5 } from "commander";
4032
4333
  import chalk5 from "chalk";
4033
4334
  import * as yaml2 from "yaml";
4034
- import { mkdir as mkdir4, writeFile as writeFile3, rm as rm2, readdir as readdir6 } from "fs/promises";
4035
- import { join as join11, basename as basename6 } from "path";
4335
+ import { mkdir as mkdir5, writeFile as writeFile4, rm as rm2, readdir as readdir6 } from "fs/promises";
4336
+ import { join as join12, basename as basename6 } from "path";
4036
4337
  import { tmpdir as tmpdir2 } from "os";
4037
- init_loader2();
4038
- init_pipeline();
4039
- init_convert();
4040
4338
 
4041
4339
  // src/cli/commands/preview.ts
4042
4340
  init_pipeline();
@@ -4044,9 +4342,9 @@ init_loader2();
4044
4342
  init_convert();
4045
4343
  init_marp_runner();
4046
4344
  import { Command as Command3 } from "commander";
4047
- import { access as access6, readdir as readdir4, mkdir as mkdir2, writeFile as writeFile2, readFile as readFile10, rm } from "fs/promises";
4048
- import { basename as basename4, dirname as dirname5, join as join9, extname as extname2 } from "path";
4049
- import * as path6 from "path";
4345
+ import { access as access7, readdir as readdir4, mkdir as mkdir3, writeFile as writeFile3, readFile as readFile11, rm } from "fs/promises";
4346
+ import { basename as basename4, dirname as dirname5, join as join10, extname as extname2 } from "path";
4347
+ import * as path7 from "path";
4050
4348
  import { tmpdir } from "os";
4051
4349
  import { createServer } from "http";
4052
4350
  import chalk3 from "chalk";
@@ -4164,7 +4462,7 @@ async function collectSlideInfo(dir, baseName, format) {
4164
4462
  if (match) {
4165
4463
  const index = parseInt(match[1], 10);
4166
4464
  slides.push({
4167
- path: join9(dir, file),
4465
+ path: join10(dir, file),
4168
4466
  title: `Slide ${index}`,
4169
4467
  index
4170
4468
  });
@@ -4180,7 +4478,7 @@ async function checkMarpCliAvailable(projectDir) {
4180
4478
  }
4181
4479
  function getTempPreviewDir(inputPath) {
4182
4480
  const base = basename4(inputPath, ".yaml");
4183
- return join9(tmpdir(), `slide-gen-preview-${base}-${Date.now()}`);
4481
+ return join10(tmpdir(), `slide-gen-preview-${base}-${Date.now()}`);
4184
4482
  }
4185
4483
  function buildMarpCommand(markdownDir, options) {
4186
4484
  const parts = ["marp", "--server", "-I", markdownDir];
@@ -4190,7 +4488,7 @@ function buildMarpCommand(markdownDir, options) {
4190
4488
  return parts.join(" ");
4191
4489
  }
4192
4490
  function startStaticServer(baseDir, port, options = {}) {
4193
- return new Promise((resolve8, reject) => {
4491
+ return new Promise((resolve9, reject) => {
4194
4492
  const mimeTypes = {
4195
4493
  ".html": "text/html",
4196
4494
  ".png": "image/png",
@@ -4203,8 +4501,8 @@ function startStaticServer(baseDir, port, options = {}) {
4203
4501
  try {
4204
4502
  const urlPath = new URL(req.url || "/", `http://localhost`).pathname;
4205
4503
  const requestedPath = urlPath === "/" ? "/index.html" : urlPath;
4206
- const resolvedBaseDir = path6.resolve(baseDir);
4207
- const filePath = path6.resolve(baseDir, "." + requestedPath);
4504
+ const resolvedBaseDir = path7.resolve(baseDir);
4505
+ const filePath = path7.resolve(baseDir, "." + requestedPath);
4208
4506
  if (!filePath.startsWith(resolvedBaseDir + "/") && filePath !== resolvedBaseDir) {
4209
4507
  res.writeHead(403);
4210
4508
  res.end("Forbidden");
@@ -4212,7 +4510,7 @@ function startStaticServer(baseDir, port, options = {}) {
4212
4510
  }
4213
4511
  const ext = extname2(filePath);
4214
4512
  const contentType = mimeTypes[ext] || "application/octet-stream";
4215
- const data = await readFile10(filePath);
4513
+ const data = await readFile11(filePath);
4216
4514
  res.writeHead(200, { "Content-Type": contentType });
4217
4515
  res.end(data);
4218
4516
  } catch {
@@ -4227,16 +4525,16 @@ function startStaticServer(baseDir, port, options = {}) {
4227
4525
  const prefix = options.messagePrefix ?? "Server";
4228
4526
  console.log(`${prefix} running at ${chalk3.cyan(url)}`);
4229
4527
  }
4230
- resolve8(server);
4528
+ resolve9(server);
4231
4529
  });
4232
4530
  });
4233
4531
  }
4234
4532
  async function executeGalleryPreview(inputPath, options) {
4235
4533
  const errors = [];
4236
4534
  const port = Number(options.port) || 8080;
4237
- const galleryDir = join9(tmpdir(), `slide-gen-gallery-${Date.now()}`);
4535
+ const galleryDir = join10(tmpdir(), `slide-gen-gallery-${Date.now()}`);
4238
4536
  try {
4239
- await access6(inputPath);
4537
+ await access7(inputPath);
4240
4538
  } catch {
4241
4539
  console.error(chalk3.red(`Error: File not found: ${inputPath}`));
4242
4540
  errors.push(`File not found: ${inputPath}`);
@@ -4257,7 +4555,7 @@ async function executeGalleryPreview(inputPath, options) {
4257
4555
  return { success: false, errors };
4258
4556
  }
4259
4557
  console.log(chalk3.green("\u2713") + " Marp CLI found");
4260
- await mkdir2(galleryDir, { recursive: true });
4558
+ await mkdir3(galleryDir, { recursive: true });
4261
4559
  const configLoader = new ConfigLoader();
4262
4560
  let configPath = options.config;
4263
4561
  if (!configPath) {
@@ -4276,7 +4574,7 @@ async function executeGalleryPreview(inputPath, options) {
4276
4574
  await rm(galleryDir, { recursive: true, force: true });
4277
4575
  return { success: false, errors };
4278
4576
  }
4279
- const tempMdPath = join9(galleryDir, "slides.md");
4577
+ const tempMdPath = join10(galleryDir, "slides.md");
4280
4578
  console.log(`Converting ${chalk3.cyan(inputPath)}...`);
4281
4579
  try {
4282
4580
  await pipeline.runWithResult(inputPath, { outputPath: tempMdPath });
@@ -4317,7 +4615,7 @@ async function executeGalleryPreview(inputPath, options) {
4317
4615
  path: basename4(s.path)
4318
4616
  }));
4319
4617
  const galleryHtml = generateGalleryHtml(relativeSlides);
4320
- await writeFile2(join9(galleryDir, "index.html"), galleryHtml);
4618
+ await writeFile3(join10(galleryDir, "index.html"), galleryHtml);
4321
4619
  console.log(`
4322
4620
  Starting gallery server on port ${chalk3.cyan(port)}...`);
4323
4621
  let server;
@@ -4359,11 +4657,11 @@ Showing ${slides.length} slides in gallery mode`);
4359
4657
  };
4360
4658
  process.on("SIGINT", signalHandler);
4361
4659
  process.on("SIGTERM", signalHandler);
4362
- await new Promise((resolve8) => {
4660
+ await new Promise((resolve9) => {
4363
4661
  if (options.signal) {
4364
- options.signal.addEventListener("abort", () => resolve8());
4662
+ options.signal.addEventListener("abort", () => resolve9());
4365
4663
  }
4366
- server.on("close", () => resolve8());
4664
+ server.on("close", () => resolve9());
4367
4665
  });
4368
4666
  return { success: true, errors };
4369
4667
  }
@@ -4381,7 +4679,7 @@ async function executePreview(inputPath, options) {
4381
4679
  const port = Number(options.port) || 8080;
4382
4680
  if (options.signal?.aborted) {
4383
4681
  try {
4384
- await access6(inputPath);
4682
+ await access7(inputPath);
4385
4683
  } catch {
4386
4684
  errors.push(`File not found: ${inputPath}`);
4387
4685
  return { success: false, errors };
@@ -4389,7 +4687,7 @@ async function executePreview(inputPath, options) {
4389
4687
  return { success: true, errors };
4390
4688
  }
4391
4689
  try {
4392
- await access6(inputPath);
4690
+ await access7(inputPath);
4393
4691
  } catch {
4394
4692
  console.error(chalk3.red(`Error: File not found: ${inputPath}`));
4395
4693
  errors.push(`File not found: ${inputPath}`);
@@ -4427,8 +4725,8 @@ async function executePreview(inputPath, options) {
4427
4725
  return { success: false, errors };
4428
4726
  }
4429
4727
  const tempDir = getTempPreviewDir(inputPath);
4430
- await mkdir2(tempDir, { recursive: true });
4431
- const tempMarkdownPath = join9(tempDir, "slides.md");
4728
+ await mkdir3(tempDir, { recursive: true });
4729
+ const tempMarkdownPath = join10(tempDir, "slides.md");
4432
4730
  console.log(`Converting ${chalk3.cyan(inputPath)}...`);
4433
4731
  try {
4434
4732
  await pipeline.runWithResult(inputPath, { outputPath: tempMarkdownPath });
@@ -4514,13 +4812,13 @@ Watching ${chalk3.cyan(inputPath)} for changes...`);
4514
4812
  };
4515
4813
  process.on("SIGINT", signalHandler);
4516
4814
  process.on("SIGTERM", signalHandler);
4517
- await new Promise((resolve8) => {
4815
+ await new Promise((resolve9) => {
4518
4816
  marpProcess.on("exit", () => {
4519
4817
  cleanup();
4520
- resolve8();
4818
+ resolve9();
4521
4819
  });
4522
4820
  if (options.signal) {
4523
- options.signal.addEventListener("abort", () => resolve8());
4821
+ options.signal.addEventListener("abort", () => resolve9());
4524
4822
  }
4525
4823
  });
4526
4824
  return {
@@ -4944,7 +5242,7 @@ async function executeTemplatePreview(name, options) {
4944
5242
  return;
4945
5243
  }
4946
5244
  const port = Number(options.port) || 8080;
4947
- const previewDir = join11(tmpdir2(), `slide-gen-template-preview-${Date.now()}`);
5245
+ const previewDir = join12(tmpdir2(), `slide-gen-template-preview-${Date.now()}`);
4948
5246
  console.log("Checking for Marp CLI...");
4949
5247
  const marpAvailable = await checkMarpCliAvailable();
4950
5248
  if (!marpAvailable) {
@@ -4981,7 +5279,7 @@ async function executeTemplatePreview(name, options) {
4981
5279
  return;
4982
5280
  }
4983
5281
  console.log(`Found ${templates.length} template(s) to preview`);
4984
- await mkdir4(previewDir, { recursive: true });
5282
+ await mkdir5(previewDir, { recursive: true });
4985
5283
  console.log("Initializing pipeline...");
4986
5284
  const pipeline = new Pipeline(config);
4987
5285
  try {
@@ -4997,9 +5295,9 @@ async function executeTemplatePreview(name, options) {
4997
5295
  for (const template of templates) {
4998
5296
  console.log(`Processing template: ${chalk5.cyan(template.name)}...`);
4999
5297
  const sampleYaml = generateSampleYaml(template);
5000
- const yamlPath = join11(previewDir, `${template.name}.yaml`);
5001
- await writeFile3(yamlPath, sampleYaml);
5002
- const mdPath = join11(previewDir, `${template.name}.md`);
5298
+ const yamlPath = join12(previewDir, `${template.name}.yaml`);
5299
+ await writeFile4(yamlPath, sampleYaml);
5300
+ const mdPath = join12(previewDir, `${template.name}.md`);
5003
5301
  try {
5004
5302
  await pipeline.runWithResult(yamlPath, { outputPath: mdPath });
5005
5303
  } catch (error) {
@@ -5032,7 +5330,7 @@ async function executeTemplatePreview(name, options) {
5032
5330
  return;
5033
5331
  }
5034
5332
  const previewHtml = generateTemplatePreviewHtml(templatePreviews);
5035
- await writeFile3(join11(previewDir, "index.html"), previewHtml);
5333
+ await writeFile4(join12(previewDir, "index.html"), previewHtml);
5036
5334
  console.log(`
5037
5335
  Starting preview server on port ${chalk5.cyan(port)}...`);
5038
5336
  let server;
@@ -5069,8 +5367,8 @@ Showing ${templatePreviews.length} template preview(s)`);
5069
5367
  };
5070
5368
  process.on("SIGINT", signalHandler);
5071
5369
  process.on("SIGTERM", signalHandler);
5072
- await new Promise((resolve8) => {
5073
- server.on("close", () => resolve8());
5370
+ await new Promise((resolve9) => {
5371
+ server.on("close", () => resolve9());
5074
5372
  });
5075
5373
  }
5076
5374
  function createScreenshotSubcommand() {
@@ -5114,7 +5412,7 @@ async function executeTemplateScreenshot(name, options) {
5114
5412
  return { success: true, errors: [], files: [] };
5115
5413
  }
5116
5414
  const outputDir = options.output || "./template-screenshots";
5117
- await mkdir4(outputDir, { recursive: true });
5415
+ await mkdir5(outputDir, { recursive: true });
5118
5416
  const configLoader = new ConfigLoader();
5119
5417
  const configPath = options.config || await configLoader.findConfig(process.cwd());
5120
5418
  const config = await configLoader.load(configPath);
@@ -5127,8 +5425,8 @@ async function executeTemplateScreenshot(name, options) {
5127
5425
  process.exitCode = ExitCode.GeneralError;
5128
5426
  return { success: false, errors: [message] };
5129
5427
  }
5130
- const tempDir = join11(tmpdir2(), `slide-gen-template-screenshot-${Date.now()}`);
5131
- await mkdir4(tempDir, { recursive: true });
5428
+ const tempDir = join12(tmpdir2(), `slide-gen-template-screenshot-${Date.now()}`);
5429
+ await mkdir5(tempDir, { recursive: true });
5132
5430
  const isAiFormat = options.format === "ai";
5133
5431
  const imageFormat = isAiFormat ? "jpeg" : options.format || "png";
5134
5432
  const imageWidth = isAiFormat ? 640 : options.width || 1280;
@@ -5139,9 +5437,9 @@ async function executeTemplateScreenshot(name, options) {
5139
5437
  console.log(`Processing: ${template.name}`);
5140
5438
  }
5141
5439
  const sampleYaml = generateSampleYaml(template);
5142
- const yamlPath = join11(tempDir, `${template.name}.yaml`);
5143
- await writeFile3(yamlPath, sampleYaml);
5144
- const mdPath = join11(tempDir, `${template.name}.md`);
5440
+ const yamlPath = join12(tempDir, `${template.name}.yaml`);
5441
+ await writeFile4(yamlPath, sampleYaml);
5442
+ const mdPath = join12(tempDir, `${template.name}.md`);
5145
5443
  try {
5146
5444
  await pipeline.runWithResult(yamlPath, { outputPath: mdPath });
5147
5445
  } catch (error) {
@@ -5158,7 +5456,7 @@ async function executeTemplateScreenshot(name, options) {
5158
5456
  if (imageFormat === "jpeg") {
5159
5457
  marpArgs.push("--jpeg-quality", String(options.quality || 80));
5160
5458
  }
5161
- const outputBasePath = join11(tempDir, template.name);
5459
+ const outputBasePath = join12(tempDir, template.name);
5162
5460
  marpArgs.push("-o", outputBasePath);
5163
5461
  marpArgs.push(mdPath);
5164
5462
  try {
@@ -5178,7 +5476,7 @@ async function executeTemplateScreenshot(name, options) {
5178
5476
  (f) => f.startsWith(template.name) && f.endsWith(`.${imageFormat}`)
5179
5477
  );
5180
5478
  for (const slideFile of slideFiles) {
5181
- const sourceFile = join11(tempDir, slideFile);
5479
+ const sourceFile = join12(tempDir, slideFile);
5182
5480
  const match = slideFile.match(/\.(\d{3})\.\w+$/);
5183
5481
  let targetName;
5184
5482
  if (slideFiles.length === 1 || !match) {
@@ -5186,7 +5484,7 @@ async function executeTemplateScreenshot(name, options) {
5186
5484
  } else {
5187
5485
  targetName = slideFile;
5188
5486
  }
5189
- const targetFile = join11(outputDir, targetName);
5487
+ const targetFile = join12(outputDir, targetName);
5190
5488
  const { copyFile: fsCopyFile } = await import("fs/promises");
5191
5489
  await fsCopyFile(sourceFile, targetFile);
5192
5490
  generatedFiles.push(targetName);
@@ -5198,10 +5496,10 @@ async function executeTemplateScreenshot(name, options) {
5198
5496
  console.log("Generating contact sheet...");
5199
5497
  const { generateContactSheet: genContactSheet } = await Promise.resolve().then(() => (init_screenshot(), screenshot_exports));
5200
5498
  const slides = generatedFiles.map((file, index) => ({
5201
- path: join11(outputDir, file),
5499
+ path: join12(outputDir, file),
5202
5500
  index: index + 1
5203
5501
  }));
5204
- const contactSheetPath = join11(outputDir, `templates-contact.${imageFormat === "jpeg" ? "jpeg" : "png"}`);
5502
+ const contactSheetPath = join12(outputDir, `templates-contact.${imageFormat === "jpeg" ? "jpeg" : "png"}`);
5205
5503
  const contactResult = await genContactSheet(slides, {
5206
5504
  outputPath: contactSheetPath,
5207
5505
  columns: options.columns || 3,
@@ -5224,7 +5522,7 @@ async function executeTemplateScreenshot(name, options) {
5224
5522
  console.log("Screenshots saved (AI-optimized):");
5225
5523
  console.log("");
5226
5524
  for (const file of generatedFiles) {
5227
- console.log(` ${join11(outputDir, file)}`);
5525
+ console.log(` ${join12(outputDir, file)}`);
5228
5526
  }
5229
5527
  console.log("");
5230
5528
  console.log(`Estimated tokens: ~${totalTokens} (${generatedFiles.length} images)`);
@@ -5252,194 +5550,14 @@ function createTemplatesCommand() {
5252
5550
  // src/cli/commands/icons.ts
5253
5551
  init_registry();
5254
5552
  init_resolver();
5553
+ init_fetcher();
5554
+ init_loader2();
5555
+ init_convert();
5255
5556
  import { Command as Command6 } from "commander";
5256
5557
  import chalk6 from "chalk";
5257
5558
  import * as fs8 from "fs/promises";
5258
5559
  import * as path8 from "path";
5259
5560
  import { parse as parseYaml8, stringify as stringifyYaml2 } from "yaml";
5260
-
5261
- // src/icons/fetcher.ts
5262
- import * as fs7 from "fs/promises";
5263
- import * as path7 from "path";
5264
- import { stringify as stringifyYaml, parse as parseYaml7 } from "yaml";
5265
- var ICON_SOURCES = {
5266
- health: { set: "healthicons", license: "MIT", external: true },
5267
- ms: { set: "material-symbols", license: "Apache-2.0", external: true },
5268
- hero: { set: "heroicons", license: "MIT", external: true },
5269
- mi: { set: "material-icons", license: "Apache-2.0", external: false },
5270
- mdi: { set: "mdi", license: "Apache-2.0", external: true },
5271
- iconify: { set: "iconify", license: "Various", external: true }
5272
- };
5273
- function isExternalSource(prefix) {
5274
- return ICON_SOURCES[prefix]?.external ?? false;
5275
- }
5276
- var VALID_ICON_NAME = /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/i;
5277
- var VALID_ICON_NAME_WITH_PATH = /^[a-z0-9][a-z0-9-/]*[a-z0-9]$|^[a-z0-9]$/i;
5278
- function isValidIconName(name, allowPath = false) {
5279
- if (!name || name.length > 100) {
5280
- return false;
5281
- }
5282
- if (name.includes("..") || name.startsWith("/") || name.endsWith("/")) {
5283
- return false;
5284
- }
5285
- const pattern = allowPath ? VALID_ICON_NAME_WITH_PATH : VALID_ICON_NAME;
5286
- return pattern.test(name);
5287
- }
5288
- var IconFetcher = class {
5289
- fetchedDir;
5290
- saveLocally;
5291
- timeoutMs;
5292
- constructor(options = {}) {
5293
- this.fetchedDir = options.fetchedDir ?? "icons/fetched";
5294
- this.saveLocally = options.saveLocally ?? true;
5295
- this.timeoutMs = options.timeoutMs ?? 1e4;
5296
- }
5297
- /**
5298
- * Parse an icon reference string (e.g., "health:stethoscope")
5299
- * Returns null if the format is invalid or contains unsafe characters
5300
- */
5301
- parseReference(iconRef) {
5302
- const colonIndex = iconRef.indexOf(":");
5303
- if (colonIndex === -1) {
5304
- return null;
5305
- }
5306
- const prefix = iconRef.substring(0, colonIndex);
5307
- const name = iconRef.substring(colonIndex + 1);
5308
- if (!/^[a-z0-9]+$/i.test(prefix)) {
5309
- return null;
5310
- }
5311
- if (!isValidIconName(name)) {
5312
- return null;
5313
- }
5314
- return { prefix, name };
5315
- }
5316
- /**
5317
- * Get the Iconify set name for a prefix
5318
- */
5319
- getIconifySet(prefix) {
5320
- return ICON_SOURCES[prefix]?.set ?? prefix;
5321
- }
5322
- /**
5323
- * Get the local file path for an icon reference
5324
- */
5325
- getLocalPath(iconRef) {
5326
- const parsed = this.parseReference(iconRef);
5327
- if (!parsed) {
5328
- throw new Error(`Invalid icon reference: ${iconRef}`);
5329
- }
5330
- const setDir = this.getIconifySet(parsed.prefix);
5331
- return path7.join(this.fetchedDir, setDir, `${parsed.name}.svg`);
5332
- }
5333
- /**
5334
- * Check if an icon exists locally
5335
- */
5336
- async existsLocally(iconRef) {
5337
- const localPath = this.getLocalPath(iconRef);
5338
- try {
5339
- await fs7.access(localPath);
5340
- return true;
5341
- } catch {
5342
- return false;
5343
- }
5344
- }
5345
- /**
5346
- * Build the Iconify API URL for an icon
5347
- */
5348
- buildUrl(prefix, name) {
5349
- const set = this.getIconifySet(prefix);
5350
- return `https://api.iconify.design/${set}/${name}.svg`;
5351
- }
5352
- /**
5353
- * Resolve an icon reference (local first, then fetch)
5354
- */
5355
- async resolve(iconRef) {
5356
- if (await this.existsLocally(iconRef)) {
5357
- const localPath = this.getLocalPath(iconRef);
5358
- return await fs7.readFile(localPath, "utf-8");
5359
- }
5360
- return await this.fetchAndSave(iconRef);
5361
- }
5362
- /**
5363
- * Fetch an icon from external source and save locally
5364
- */
5365
- async fetchAndSave(iconRef) {
5366
- const parsed = this.parseReference(iconRef);
5367
- if (!parsed) {
5368
- throw new Error(`Invalid icon reference: ${iconRef}`);
5369
- }
5370
- const url = this.buildUrl(parsed.prefix, parsed.name);
5371
- const controller = new AbortController();
5372
- const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
5373
- let response;
5374
- try {
5375
- response = await fetch(url, { signal: controller.signal });
5376
- } catch (error) {
5377
- if (error instanceof Error && error.name === "AbortError") {
5378
- throw new Error(`Fetch timeout: ${iconRef} (exceeded ${this.timeoutMs}ms)`);
5379
- }
5380
- throw error;
5381
- } finally {
5382
- clearTimeout(timeoutId);
5383
- }
5384
- if (!response.ok) {
5385
- throw new Error(`Icon not found: ${iconRef} (HTTP ${response.status})`);
5386
- }
5387
- const svg = await response.text();
5388
- if (this.saveLocally) {
5389
- await this.saveLocallyInternal(parsed.prefix, parsed.name, svg, url);
5390
- }
5391
- return svg;
5392
- }
5393
- /**
5394
- * Save icon to local filesystem
5395
- */
5396
- async saveLocallyInternal(prefix, name, svg, sourceUrl) {
5397
- const setDir = this.getIconifySet(prefix);
5398
- const dirPath = path7.join(this.fetchedDir, setDir);
5399
- const filePath = path7.join(dirPath, `${name}.svg`);
5400
- await fs7.mkdir(dirPath, { recursive: true });
5401
- await fs7.writeFile(filePath, svg, "utf-8");
5402
- await this.updateSourcesYaml(setDir, name, sourceUrl);
5403
- }
5404
- /**
5405
- * Update _sources.yaml with source information
5406
- */
5407
- async updateSourcesYaml(setDir, name, sourceUrl) {
5408
- const sourcesPath = path7.join(this.fetchedDir, "_sources.yaml");
5409
- let sources = {};
5410
- try {
5411
- const content = await fs7.readFile(sourcesPath, "utf-8");
5412
- sources = parseYaml7(content) ?? {};
5413
- } catch {
5414
- }
5415
- const key = `${setDir}/${name}.svg`;
5416
- sources[key] = {
5417
- source: sourceUrl,
5418
- fetched_at: (/* @__PURE__ */ new Date()).toISOString(),
5419
- license: this.getLicense(setDir)
5420
- };
5421
- const header = `# Fetched icon sources
5422
- # This file tracks where icons were fetched from for traceability
5423
-
5424
- `;
5425
- await fs7.writeFile(sourcesPath, header + stringifyYaml(sources), "utf-8");
5426
- }
5427
- /**
5428
- * Get the license for an icon set
5429
- */
5430
- getLicense(setDir) {
5431
- for (const config of Object.values(ICON_SOURCES)) {
5432
- if (config.set === setDir) {
5433
- return config.license;
5434
- }
5435
- }
5436
- return "Unknown";
5437
- }
5438
- };
5439
-
5440
- // src/cli/commands/icons.ts
5441
- init_loader2();
5442
- init_convert();
5443
5561
  function extractIconReferences(presentation) {
5444
5562
  const icons = /* @__PURE__ */ new Set();
5445
5563
  function extractFromValue(value) {
@@ -6040,7 +6158,7 @@ import { mkdir as mkdir8, writeFile as writeFile7, access as access11, readdir a
6040
6158
  import { existsSync as existsSync2 } from "fs";
6041
6159
  import { execSync, spawnSync } from "child_process";
6042
6160
  import { createInterface } from "readline";
6043
- import { basename as basename8, dirname as dirname8, join as join17, resolve as resolve6, sep } from "path";
6161
+ import { basename as basename8, dirname as dirname8, join as join17, resolve as resolve7, sep } from "path";
6044
6162
  import { fileURLToPath } from "url";
6045
6163
  import chalk7 from "chalk";
6046
6164
  import ora4 from "ora";
@@ -7014,6 +7132,73 @@ Take new screenshots and verify improvements.
7014
7132
  7. \`Read ./screenshots/presentation.003.jpeg\`
7015
7133
  8. Verify fix, move to next slide
7016
7134
 
7135
+ ## Custom Template Creation
7136
+
7137
+ When creating or modifying templates, follow these critical rules.
7138
+
7139
+ ### Critical Rules
7140
+
7141
+ #### 1. CSS Selectors (Marp Scoping)
7142
+
7143
+ When using \`<!-- _class: foo -->\`:
7144
+ - The class is added to \`<section>\` element itself
7145
+ - Use \`section.foo\` when targeting the \`<section>\` element itself
7146
+
7147
+ \`\`\`yaml
7148
+ # Correct
7149
+ output: |
7150
+ <!-- _class: my-slide -->
7151
+ <div class="container">...</div>
7152
+
7153
+ css: |
7154
+ section.my-slide .container { display: flex; }
7155
+ \`\`\`
7156
+
7157
+ \`\`\`yaml
7158
+ # WRONG - This does not target the section element itself
7159
+ css: |
7160
+ .my-slide .container { display: flex; }
7161
+ \`\`\`
7162
+
7163
+ Note: \`.my-slide .container\` is valid for descendants inside the section, but it does not target the \`<section>\` element itself.
7164
+
7165
+ #### 2. HTML + Markdown (CommonMark)
7166
+
7167
+ Markdown inside HTML is only parsed with blank lines:
7168
+
7169
+ \`\`\`yaml
7170
+ # Correct - blank line after <div>
7171
+ output: |
7172
+ <div class="container">
7173
+
7174
+ ## Heading works
7175
+ - List works
7176
+
7177
+ </div>
7178
+ \`\`\`
7179
+
7180
+ \`\`\`yaml
7181
+ # WRONG - no blank line, Markdown won't parse
7182
+ output: |
7183
+ <div class="container">
7184
+ ## This is plain text
7185
+ - Not a list
7186
+ </div>
7187
+ \`\`\`
7188
+
7189
+ ### Visual Regression Test (Required)
7190
+
7191
+ After creating/modifying templates, ALWAYS verify with screenshots:
7192
+
7193
+ \`\`\`bash
7194
+ slide-gen templates screenshot <template-name> --format png -o /tmp/test
7195
+ \`\`\`
7196
+
7197
+ **Check that:**
7198
+ - Layout is correct (columns, grids, flexbox)
7199
+ - Markdown is parsed (headings, lists rendered properly)
7200
+ - CSS is not silently failing
7201
+
7017
7202
  ## Reference Management
7018
7203
 
7019
7204
  For academic presentations, manage citations and references:
@@ -7312,6 +7497,92 @@ Image with text side by side.
7312
7497
  - \`items\` or \`text\`: Content (required)
7313
7498
 
7314
7499
  ## Run \`slide-gen templates list --format llm\` for full list.
7500
+
7501
+ ---
7502
+
7503
+ ## Creating Custom Templates
7504
+
7505
+ ### Template File Structure
7506
+
7507
+ \`\`\`yaml
7508
+ name: my-template
7509
+ description: "Description for LLM"
7510
+ category: custom
7511
+
7512
+ schema:
7513
+ type: object
7514
+ required: [title]
7515
+ properties:
7516
+ title: { type: string }
7517
+ content: { type: string }
7518
+
7519
+ example:
7520
+ title: "Sample"
7521
+ content: "Sample content"
7522
+
7523
+ output: |
7524
+ ---
7525
+ <!-- _class: my-template -->
7526
+
7527
+ # {{ title }}
7528
+
7529
+ <div class="content">
7530
+
7531
+ {{ content }}
7532
+
7533
+ </div>
7534
+
7535
+ css: |
7536
+ section.my-template .content {
7537
+ padding: 1em;
7538
+ }
7539
+ \`\`\`
7540
+
7541
+ ### Critical CSS Rule: Marp Scoping
7542
+
7543
+ When using \`<!-- _class: foo -->\`, Marp adds class to the \`<section>\` element.
7544
+
7545
+ **Correct (targeting the section element):**
7546
+ \`\`\`css
7547
+ section.foo .child { ... }
7548
+ \`\`\`
7549
+
7550
+ **Wrong (for targeting the section element):**
7551
+ \`\`\`css
7552
+ .foo .child { ... }
7553
+ \`\`\`
7554
+
7555
+ Note: \`.foo .child\` is valid for descendants inside the section, but it does not target the \`<section>\` element itself.
7556
+
7557
+ ### Critical HTML Rule: CommonMark
7558
+
7559
+ Markdown inside HTML only parses with blank lines after tags:
7560
+
7561
+ **Correct:**
7562
+ \`\`\`html
7563
+ <div>
7564
+
7565
+ ## Heading works
7566
+
7567
+ </div>
7568
+ \`\`\`
7569
+
7570
+ **Wrong:**
7571
+ \`\`\`html
7572
+ <div>
7573
+ ## Plain text, not heading
7574
+ </div>
7575
+ \`\`\`
7576
+
7577
+ ### Visual Verification
7578
+
7579
+ Always test templates with screenshots:
7580
+
7581
+ \`\`\`bash
7582
+ slide-gen templates screenshot <template-name> --format png -o /tmp/test
7583
+ \`\`\`
7584
+
7585
+ Check: Layout, Markdown parsing, CSS application.
7315
7586
  `;
7316
7587
  }
7317
7588
 
@@ -7989,7 +8260,8 @@ slide-gen validate presentation.yaml
7989
8260
  // src/cli/commands/init.ts
7990
8261
  function getPackageRoot() {
7991
8262
  const __dirname = dirname8(fileURLToPath(import.meta.url));
7992
- if (__dirname.includes(`${sep}src${sep}`) || __dirname.includes("/src/")) {
8263
+ const isInSourceDir = __dirname.endsWith(`${sep}src${sep}cli${sep}commands`) || __dirname.endsWith("/src/cli/commands");
8264
+ if (isInSourceDir) {
7993
8265
  return join17(__dirname, "..", "..", "..");
7994
8266
  }
7995
8267
  return join17(__dirname, "..", "..");
@@ -8001,7 +8273,7 @@ function createInitCommand() {
8001
8273
  }
8002
8274
  async function executeInit(directory, options) {
8003
8275
  const spinner = ora4();
8004
- const targetDir = resolve6(directory);
8276
+ const targetDir = resolve7(directory);
8005
8277
  const includeExamples = options.examples !== false;
8006
8278
  const includeAiConfig = options.aiConfig !== false;
8007
8279
  const includeSources = options.sources !== false;
@@ -8029,6 +8301,13 @@ async function executeInit(directory, options) {
8029
8301
  const sourceIconsRegistry = join17(packageRoot, "icons", "registry.yaml");
8030
8302
  const targetIconsRegistry = join17(targetDir, "icons", "registry.yaml");
8031
8303
  await copyFileIfNotExists(sourceIconsRegistry, targetIconsRegistry);
8304
+ const sourceIconsFetched = join17(packageRoot, "icons", "fetched");
8305
+ const targetIconsFetched = join17(targetDir, "icons", "fetched");
8306
+ try {
8307
+ await access11(targetIconsFetched);
8308
+ } catch {
8309
+ await cp(sourceIconsFetched, targetIconsFetched, { recursive: true });
8310
+ }
8032
8311
  const sourceDefaultTheme = join17(packageRoot, "themes", "default.css");
8033
8312
  const targetDefaultTheme = join17(targetDir, "themes", "default.css");
8034
8313
  await copyFileIfNotExists(sourceDefaultTheme, targetDefaultTheme);
@@ -8050,11 +8329,11 @@ async function executeInit(directory, options) {
8050
8329
  await sourcesManager.init({
8051
8330
  name: "Untitled Project",
8052
8331
  setup_pattern: options.fromDirectory ? "A" : void 0,
8053
- original_source: options.fromDirectory ? resolve6(options.fromDirectory) : void 0
8332
+ original_source: options.fromDirectory ? resolve7(options.fromDirectory) : void 0
8054
8333
  });
8055
8334
  if (options.fromDirectory) {
8056
8335
  const importer = new SourceImporter(targetDir, sourcesManager);
8057
- const result = await importer.importDirectory(resolve6(options.fromDirectory), {
8336
+ const result = await importer.importDirectory(resolve7(options.fromDirectory), {
8058
8337
  recursive: true
8059
8338
  });
8060
8339
  sourcesImported = result.imported;
@@ -8160,16 +8439,16 @@ async function promptMarpInstallChoice() {
8160
8439
  console.log(` ${chalk7.cyan("2)")} Local install ${chalk7.dim("(creates package.json)")}`);
8161
8440
  console.log(` ${chalk7.cyan("3)")} Skip ${chalk7.dim("(I'll install it later)")}`);
8162
8441
  console.log("");
8163
- return new Promise((resolve8) => {
8442
+ return new Promise((resolve9) => {
8164
8443
  rl.question("Choice [1]: ", (answer) => {
8165
8444
  rl.close();
8166
8445
  const normalized = answer.trim();
8167
8446
  if (normalized === "" || normalized === "1") {
8168
- resolve8("global");
8447
+ resolve9("global");
8169
8448
  } else if (normalized === "2") {
8170
- resolve8("local");
8449
+ resolve9("local");
8171
8450
  } else {
8172
- resolve8("skip");
8451
+ resolve9("skip");
8173
8452
  }
8174
8453
  });
8175
8454
  });
@@ -8341,10 +8620,8 @@ templates:
8341
8620
  icons:
8342
8621
  # Path to icon registry file
8343
8622
  registry: ./icons/registry.yaml
8344
- cache:
8345
- enabled: true
8346
- directory: .cache/icons
8347
- ttl: 86400
8623
+ # Path to fetched/pre-bundled icon SVGs
8624
+ fetched: ./icons/fetched
8348
8625
 
8349
8626
  references:
8350
8627
  enabled: true
@@ -8565,14 +8842,20 @@ async function executeWatch(inputPath, options) {
8565
8842
  }, debounceMs);
8566
8843
  };
8567
8844
  state.start();
8845
+ const isWSL = !!process.env["WSL_DISTRO_NAME"];
8568
8846
  watcher = chokidarWatch2(inputPath, {
8569
8847
  persistent: true,
8570
8848
  ignoreInitial: true,
8849
+ usePolling: isWSL,
8850
+ ...isWSL && { interval: 100 },
8571
8851
  awaitWriteFinish: {
8572
8852
  stabilityThreshold: 100,
8573
8853
  pollInterval: 50
8574
8854
  }
8575
8855
  });
8856
+ await new Promise((resolve9) => {
8857
+ watcher.on("ready", resolve9);
8858
+ });
8576
8859
  watcher.on("change", handleChange);
8577
8860
  const cleanup = () => {
8578
8861
  state.stop();
@@ -8591,9 +8874,9 @@ async function executeWatch(inputPath, options) {
8591
8874
  };
8592
8875
  process.on("SIGINT", signalHandler);
8593
8876
  process.on("SIGTERM", signalHandler);
8594
- await new Promise((resolve8) => {
8877
+ await new Promise((resolve9) => {
8595
8878
  if (options.signal) {
8596
- options.signal.addEventListener("abort", () => resolve8());
8879
+ options.signal.addEventListener("abort", () => resolve9());
8597
8880
  }
8598
8881
  });
8599
8882
  return {
@@ -9035,7 +9318,7 @@ init_screenshot();
9035
9318
  init_convert();
9036
9319
  import { Command as Command10 } from "commander";
9037
9320
  import { access as access13, stat as stat3 } from "fs/promises";
9038
- import { resolve as resolve7 } from "path";
9321
+ import { resolve as resolve8 } from "path";
9039
9322
  import chalk10 from "chalk";
9040
9323
  import ora5 from "ora";
9041
9324
  function createSourcesCommand() {
@@ -9084,7 +9367,7 @@ function createSourcesSyncCommand() {
9084
9367
  });
9085
9368
  }
9086
9369
  async function executeSourcesInit(projectDir, options) {
9087
- const resolvedDir = resolve7(projectDir);
9370
+ const resolvedDir = resolve8(projectDir);
9088
9371
  const spinner = ora5("Initializing sources...").start();
9089
9372
  try {
9090
9373
  const manager = new SourcesManager(resolvedDir);
@@ -9100,10 +9383,10 @@ async function executeSourcesInit(projectDir, options) {
9100
9383
  let originalSource;
9101
9384
  if (options.fromDirectory) {
9102
9385
  setupPattern = "A";
9103
- originalSource = resolve7(options.fromDirectory);
9386
+ originalSource = resolve8(options.fromDirectory);
9104
9387
  } else if (options.fromFile) {
9105
9388
  setupPattern = "B";
9106
- originalSource = resolve7(options.fromFile);
9389
+ originalSource = resolve8(options.fromFile);
9107
9390
  }
9108
9391
  await manager.init({
9109
9392
  name: projectName,
@@ -9114,14 +9397,14 @@ async function executeSourcesInit(projectDir, options) {
9114
9397
  if (options.fromDirectory) {
9115
9398
  const importer = new SourceImporter(resolvedDir, manager);
9116
9399
  const result = await importer.importDirectory(
9117
- resolve7(options.fromDirectory),
9400
+ resolve8(options.fromDirectory),
9118
9401
  { recursive: true }
9119
9402
  );
9120
9403
  filesImported = result.imported;
9121
9404
  }
9122
9405
  if (options.fromFile) {
9123
9406
  const importer = new SourceImporter(resolvedDir, manager);
9124
- await importer.importFile(resolve7(options.fromFile), {
9407
+ await importer.importFile(resolve8(options.fromFile), {
9125
9408
  type: "scenario"
9126
9409
  });
9127
9410
  filesImported = 1;
@@ -9146,8 +9429,8 @@ async function executeSourcesInit(projectDir, options) {
9146
9429
  }
9147
9430
  }
9148
9431
  async function executeSourcesImport(projectDir, sourcePath, options) {
9149
- const resolvedDir = resolve7(projectDir);
9150
- const resolvedSource = resolve7(sourcePath);
9432
+ const resolvedDir = resolve8(projectDir);
9433
+ const resolvedSource = resolve8(sourcePath);
9151
9434
  const spinner = ora5("Importing files...").start();
9152
9435
  try {
9153
9436
  const manager = new SourcesManager(resolvedDir);
@@ -9206,7 +9489,7 @@ async function executeSourcesImport(projectDir, sourcePath, options) {
9206
9489
  }
9207
9490
  }
9208
9491
  async function executeSourcesStatus(projectDir, _options) {
9209
- const resolvedDir = resolve7(projectDir);
9492
+ const resolvedDir = resolve8(projectDir);
9210
9493
  try {
9211
9494
  const manager = new SourcesManager(resolvedDir);
9212
9495
  if (!await manager.exists()) {
@@ -9293,7 +9576,7 @@ async function executeSourcesStatus(projectDir, _options) {
9293
9576
  }
9294
9577
  }
9295
9578
  async function executeSourcesSync(projectDir, options) {
9296
- const resolvedDir = resolve7(projectDir);
9579
+ const resolvedDir = resolve8(projectDir);
9297
9580
  try {
9298
9581
  const manager = new SourcesManager(resolvedDir);
9299
9582
  if (!await manager.exists()) {