@miao-vision/cli 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -32384,7 +32384,7 @@ var require_xlsx = __commonJS({
32384
32384
  if (DBF_SUPPORTED_VERSIONS.indexOf(n[0]) > -1 && n[2] <= 12 && n[3] <= 31) return DBF.to_workbook(d, o);
32385
32385
  return read_prn(data, d, o, str);
32386
32386
  }
32387
- function readFileSync4(filename, opts) {
32387
+ function readFileSync5(filename, opts) {
32388
32388
  var o = opts || {};
32389
32389
  o.type = "file";
32390
32390
  return readSync(filename, o);
@@ -32619,7 +32619,7 @@ var require_xlsx = __commonJS({
32619
32619
  if (ext.match(/^\.[a-z]+$/)) o.bookType = ext.slice(1);
32620
32620
  o.bookType = _BT[o.bookType] || o.bookType;
32621
32621
  }
32622
- function writeFileSync2(wb, filename, opts) {
32622
+ function writeFileSync3(wb, filename, opts) {
32623
32623
  var o = opts || {};
32624
32624
  o.type = "file";
32625
32625
  o.file = filename;
@@ -33220,11 +33220,11 @@ var require_xlsx = __commonJS({
33220
33220
  if (typeof parse_xlscfb !== "undefined") XLSX3.parse_xlscfb = parse_xlscfb;
33221
33221
  XLSX3.parse_zip = parse_zip;
33222
33222
  XLSX3.read = readSync;
33223
- XLSX3.readFile = readFileSync4;
33224
- XLSX3.readFileSync = readFileSync4;
33223
+ XLSX3.readFile = readFileSync5;
33224
+ XLSX3.readFileSync = readFileSync5;
33225
33225
  XLSX3.write = writeSync;
33226
- XLSX3.writeFile = writeFileSync2;
33227
- XLSX3.writeFileSync = writeFileSync2;
33226
+ XLSX3.writeFile = writeFileSync3;
33227
+ XLSX3.writeFileSync = writeFileSync3;
33228
33228
  XLSX3.writeFileAsync = writeFileAsync;
33229
33229
  XLSX3.utils = utils2;
33230
33230
  XLSX3.writeXLSX = writeSyncXLSX;
@@ -41719,7 +41719,19 @@ var defaultTheme = {
41719
41719
  labelColor: "#475569"
41720
41720
  },
41721
41721
  css: `
41722
- :root { color-scheme: light; font-family: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif; }
41722
+ :root {
41723
+ color-scheme: light;
41724
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
41725
+ --mv-paper: #f8fafc;
41726
+ --mv-surface: #ffffff;
41727
+ --mv-border: #e2e8f0;
41728
+ --mv-ink: #0f172a;
41729
+ --mv-muted: #64748b;
41730
+ --mv-soft: #475569;
41731
+ --mv-brand: #2563eb;
41732
+ --mv-serif: Georgia, "Times New Roman", serif;
41733
+ --mv-mono: "SF Mono", "JetBrains Mono", Consolas, monospace;
41734
+ }
41723
41735
  body { margin: 0; background: #f8fafc; color: #0f172a; }
41724
41736
  .miao-viz-report { max-width: 1120px; margin: 0 auto; padding: 40px 24px 56px; }
41725
41737
  header { margin-bottom: 28px; }
@@ -41760,6 +41772,7 @@ var editorialTheme = {
41760
41772
  --mv-ink: #141413;
41761
41773
  --mv-muted: #6b6a64;
41762
41774
  --mv-soft-text: #504e49;
41775
+ --mv-soft: #504e49;
41763
41776
  --mv-brand: #1b365d;
41764
41777
  --mv-mono: "SF Mono", "JetBrains Mono", Consolas, monospace;
41765
41778
  --mv-serif: Charter, Georgia, "Times New Roman", serif;
@@ -41824,6 +41837,7 @@ var darkTheme = {
41824
41837
  --mv-ink: #e2e0d8;
41825
41838
  --mv-muted: #737069;
41826
41839
  --mv-soft-text: #a8a69f;
41840
+ --mv-soft: #a8a69f;
41827
41841
  --mv-brand: #7eb8f7;
41828
41842
  --mv-mono: "SF Mono", "JetBrains Mono", Consolas, monospace;
41829
41843
  --mv-serif: Charter, Georgia, "Times New Roman", serif;
@@ -41881,7 +41895,19 @@ var minimalTheme = {
41881
41895
  labelColor: "#6b7280"
41882
41896
  },
41883
41897
  css: `
41884
- :root { color-scheme: light; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; }
41898
+ :root {
41899
+ color-scheme: light;
41900
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
41901
+ --mv-paper: #ffffff;
41902
+ --mv-surface: #ffffff;
41903
+ --mv-border: #e5e7eb;
41904
+ --mv-ink: #111111;
41905
+ --mv-muted: #6b7280;
41906
+ --mv-soft: #374151;
41907
+ --mv-brand: #1d4ed8;
41908
+ --mv-serif: Georgia, "Times New Roman", serif;
41909
+ --mv-mono: "SF Mono", "JetBrains Mono", Consolas, monospace;
41910
+ }
41885
41911
  body { margin: 0; background: #fff; color: #111; }
41886
41912
  .miao-viz-report { max-width: 960px; margin: 0 auto; padding: 48px 24px 64px; }
41887
41913
  header { margin-bottom: 32px; }
@@ -58128,6 +58154,42 @@ function parseAnalyzeContext(value) {
58128
58154
  function withSize(chart, width, height) {
58129
58155
  return { ...chart, style: { ...chart.style, width, height } };
58130
58156
  }
58157
+ function formatMetricValue(num, fmt) {
58158
+ const hasCurrency = fmt.includes("$");
58159
+ const hasPercent = fmt.includes("%");
58160
+ const hasThousands = fmt.includes(",");
58161
+ const hasSiPrefix = fmt.includes("s");
58162
+ const precisionMatch = fmt.match(/\.(\d+)/);
58163
+ let precision = precisionMatch ? parseInt(precisionMatch[1], 10) : void 0;
58164
+ let value = num;
58165
+ let suffix = "";
58166
+ if (hasPercent) {
58167
+ value = num < 2 ? num * 100 : num;
58168
+ suffix = "%";
58169
+ if (precision === void 0) precision = 1;
58170
+ }
58171
+ if (hasSiPrefix) {
58172
+ const abs = Math.abs(value);
58173
+ if (abs >= 1e9) {
58174
+ value /= 1e9;
58175
+ suffix = "B" + suffix;
58176
+ } else if (abs >= 1e6) {
58177
+ value /= 1e6;
58178
+ suffix = "M" + suffix;
58179
+ } else if (abs >= 1e3) {
58180
+ value /= 1e3;
58181
+ suffix = "K" + suffix;
58182
+ }
58183
+ if (precision === void 0) precision = 0;
58184
+ }
58185
+ if (precision === void 0 && hasThousands) precision = 0;
58186
+ const fixed = precision !== void 0 ? value.toFixed(precision) : Math.round(value).toString();
58187
+ const [intPart, decPart] = fixed.split(".");
58188
+ const formattedInt = hasThousands ? intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : intPart;
58189
+ const formatted = decPart !== void 0 ? `${formattedInt}.${decPart}` : formattedInt;
58190
+ const prefix = hasCurrency ? "$" : "";
58191
+ return prefix + formatted + suffix;
58192
+ }
58131
58193
  function resolveMetricValue(metric, rows) {
58132
58194
  let raw = metric.value;
58133
58195
  if (raw === void 0 && metric.data?.transform?.length) {
@@ -58137,10 +58199,7 @@ function resolveMetricValue(metric, rows) {
58137
58199
  }
58138
58200
  const num = Number(raw);
58139
58201
  if (!Number.isFinite(num)) return String(raw ?? "\u2014");
58140
- const fmt = metric.format ?? "";
58141
- if (fmt.includes("$")) return "$" + Math.round(num).toLocaleString();
58142
- if (fmt.includes("%")) return (num < 2 ? (num * 100).toFixed(1) : num.toFixed(1)) + "%";
58143
- return Math.round(num).toLocaleString();
58202
+ return formatMetricValue(num, metric.format ?? "");
58144
58203
  }
58145
58204
  function renderEyebrow(text) {
58146
58205
  return `<div class="slide-eyebrow">${escapeHtml(text)}</div>`;
@@ -58166,21 +58225,22 @@ function pageFooter(index, _total, mark) {
58166
58225
  <div class="slide-page-num">${String(index + 1).padStart(2, "0")}</div>`;
58167
58226
  }
58168
58227
  var COVER_DECO = `<svg viewBox="0 0 320 320" xmlns="http://www.w3.org/2000/svg" style="width:260px;opacity:0.9">
58169
- <circle cx="160" cy="160" r="140" fill="none" stroke="#e5e3d8" stroke-width="1"/>
58170
- <circle cx="160" cy="160" r="100" fill="none" stroke="#e5e3d8" stroke-width="1"/>
58171
- <circle cx="160" cy="160" r="60" fill="#eef2f7" stroke="#1b365d" stroke-width="1.2"/>
58172
- <path d="M160 20 A140 140 0 0 1 300 160" fill="none" stroke="#1b365d" stroke-width="2.2" stroke-linecap="round"/>
58173
- <path d="M300 160 A140 140 0 0 1 160 300" fill="none" stroke="#6b6a64" stroke-width="1" stroke-linecap="round"/>
58174
- <path d="M160 300 A140 140 0 0 1 20 160" fill="none" stroke="#6b6a64" stroke-width="1" stroke-linecap="round"/>
58175
- <path d="M20 160 A140 140 0 0 1 160 20" fill="none" stroke="#6b6a64" stroke-width="1" stroke-linecap="round"/>
58228
+ <circle cx="160" cy="160" r="140" fill="none" stroke="var(--mv-border, #e5e3d8)" stroke-width="1"/>
58229
+ <circle cx="160" cy="160" r="100" fill="none" stroke="var(--mv-border, #e5e3d8)" stroke-width="1"/>
58230
+ <circle cx="160" cy="160" r="60" fill="var(--mv-surface, #eef2f7)" stroke="var(--mv-brand, #1b365d)" stroke-width="1.2"/>
58231
+ <path d="M160 20 A140 140 0 0 1 300 160" fill="none" stroke="var(--mv-brand, #1b365d)" stroke-width="2.2" stroke-linecap="round"/>
58232
+ <path d="M300 160 A140 140 0 0 1 160 300" fill="none" stroke="var(--mv-muted, #6b6a64)" stroke-width="1" stroke-linecap="round"/>
58233
+ <path d="M160 300 A140 140 0 0 1 20 160" fill="none" stroke="var(--mv-muted, #6b6a64)" stroke-width="1" stroke-linecap="round"/>
58234
+ <path d="M20 160 A140 140 0 0 1 160 20" fill="none" stroke="var(--mv-muted, #6b6a64)" stroke-width="1" stroke-linecap="round"/>
58176
58235
  </svg>`;
58177
- function renderCoverSlide(slide, _rows, _svg, index, total) {
58236
+ function renderCoverSlide(slide, _rows, _svg, index, total, deckDescription) {
58237
+ const subtitle = slide.claim ?? deckDescription;
58178
58238
  return `<div class="slide">
58179
58239
  <div class="slide-cover">
58180
58240
  <div class="slide-cover-left">
58181
58241
  <div class="slide-mark">miao-vision</div>
58182
58242
  <h1>${escapeHtml(slide.title ?? "Presentation")}</h1>
58183
- ${slide.claim ? `<div class="sub">${escapeHtml(slide.claim)}</div>` : ""}
58243
+ ${subtitle ? `<div class="sub">${escapeHtml(subtitle)}</div>` : ""}
58184
58244
  <div class="line"></div>
58185
58245
  <div class="meta">${escapeHtml(slide.eyebrow ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10))}</div>
58186
58246
  </div>
@@ -58266,18 +58326,6 @@ function renderEndingSlide(slide, _rows, _svg, index, total) {
58266
58326
 
58267
58327
  // src/deck-renderer.ts
58268
58328
  var SLIDE_CSS = `
58269
- :root {
58270
- --mv-paper: #f5f4ed;
58271
- --mv-surface: #faf9f5;
58272
- --mv-brand: #1b365d;
58273
- --mv-ink: #141413;
58274
- --mv-muted: #6b6a64;
58275
- --mv-soft: #504e49;
58276
- --mv-border: #e5e3d8;
58277
- --mv-serif: Charter, Georgia, "Times New Roman", serif;
58278
- --mv-mono: "SF Mono", "JetBrains Mono", Consolas, monospace;
58279
- --slide-scale: 1;
58280
- }
58281
58329
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
58282
58330
  body { font-family: var(--mv-serif); -webkit-font-smoothing: antialiased; }
58283
58331
 
@@ -58369,7 +58417,7 @@ var SLIDE_CSS = `
58369
58417
  .slide:last-child { break-after: auto; page-break-after: auto; }
58370
58418
  .slide-nav { display: none !important; }
58371
58419
  }
58372
- @page { size: A4 landscape; margin: 0; background: #f5f4ed; }
58420
+ @page { size: A4 landscape; margin: 0; background: var(--mv-paper, #f5f4ed); }
58373
58421
  `;
58374
58422
  var SLIDE_JS = `
58375
58423
  (function() {
@@ -58421,10 +58469,14 @@ var SLIDE_JS = `
58421
58469
  goTo(0);
58422
58470
  })();
58423
58471
  `;
58424
- function renderSlide(slide, rows, svgTheme, index, total) {
58472
+ function extractRootVars(css) {
58473
+ const match = css.match(/:root\s*\{([\s\S]*?)\}/);
58474
+ return match ? `:root {${match[1]}}` : "";
58475
+ }
58476
+ function renderSlide(slide, rows, svgTheme, index, total, deckDescription) {
58425
58477
  switch (slide.layout) {
58426
58478
  case "cover":
58427
- return renderCoverSlide(slide, rows, svgTheme, index, total);
58479
+ return renderCoverSlide(slide, rows, svgTheme, index, total, deckDescription);
58428
58480
  case "title-only":
58429
58481
  return renderTitleOnlySlide(slide, rows, svgTheme, index, total);
58430
58482
  case "text-points":
@@ -58443,16 +58495,19 @@ function renderSlide(slide, rows, svgTheme, index, total) {
58443
58495
  }
58444
58496
  function renderDeckHtml(spec, rows, themeOverride) {
58445
58497
  const theme = getTheme(themeOverride ?? spec.theme ?? "editorial");
58498
+ const themeRootVars = extractRootVars(theme.css);
58446
58499
  const title = spec.title ?? "Presentation";
58447
58500
  const total = spec.slides.length;
58448
- const slidesHtml = spec.slides.map((slide, i) => renderSlide(slide, rows, theme.svg, i, total)).join("\n");
58501
+ const slidesHtml = spec.slides.map((slide, i) => renderSlide(slide, rows, theme.svg, i, total, spec.description)).join("\n");
58449
58502
  return `<!doctype html>
58450
58503
  <html lang="en">
58451
58504
  <head>
58452
58505
  <meta charset="utf-8" />
58453
58506
  <meta name="viewport" content="width=device-width, initial-scale=1" />
58507
+ ${spec.description ? `<meta name="description" content="${escapeHtml(spec.description)}" />` : ""}
58454
58508
  <title>${escapeHtml(title)}</title>
58455
58509
  <style>${SLIDE_CSS}</style>
58510
+ ${themeRootVars ? `<style id="miao-deck-theme">${themeRootVars}</style>` : ""}
58456
58511
  </head>
58457
58512
  <body class="present-mode">
58458
58513
  <div class="slide-viewport">
@@ -58574,6 +58629,14 @@ function validateDeckSpecSemantics(spec) {
58574
58629
  hint: hintForIssue(path, "at most 4 metrics")
58575
58630
  });
58576
58631
  }
58632
+ if (slide.charts && slide.charts.length > 1) {
58633
+ const path = `slides[${index}].charts`;
58634
+ errors.push({
58635
+ path,
58636
+ message: `${path}: A slide can include at most 1 chart.`,
58637
+ hint: hintForIssue(path, "at most 1 chart")
58638
+ });
58639
+ }
58577
58640
  if (slide.layout === "table-full" && slide.charts?.[0] && slide.charts[0].type !== "table") {
58578
58641
  const path = `slides[${index}].charts[0].type`;
58579
58642
  errors.push({
@@ -58686,33 +58749,208 @@ function hintForIssue(path, message) {
58686
58749
  if (message.includes("requires at least one chart")) return `Add a chart under ${path}.`;
58687
58750
  if (message.includes("requires at least one metric")) return `Add one to four metrics under ${path}.`;
58688
58751
  if (message.includes("at most 4 metrics")) return `Reduce ${path} to four metrics or split them across multiple slides.`;
58752
+ if (message.includes("at most 1 chart")) return `Reduce ${path} to a single chart or spread multiple charts across separate slides.`;
58689
58753
  if (message.includes("only accepts a table chart")) return `Change ${path} to 'table' or use a chart-focused layout.`;
58690
58754
  return `Check ${path} in the DeckSpec.`;
58691
58755
  }
58692
58756
 
58693
- // src/article-infographic.ts
58757
+ // src/article-analyzer.ts
58694
58758
  var import_node_fs2 = require("node:fs");
58695
58759
  var import_node_path2 = require("node:path");
58760
+ var ACRONYM_PATTERN = /\b[A-Z]{2,}(?:s)?\b/g;
58761
+ var TECHNICAL_PATTERN = /\b(?:[A-Z][a-z]+[A-Z]\w*|[a-z]+[A-Z]\w*)\b/g;
58762
+ var NUMBERED_TERM_PATTERN = /\b[A-Za-z]+(?:\s+\d+(?:\.\d+)?){1,2}\b/g;
58763
+ function extractTermCandidates(text) {
58764
+ const candidates = /* @__PURE__ */ new Set();
58765
+ const lines = text.split("\n");
58766
+ for (const line of lines) {
58767
+ const trimmed = line.trim();
58768
+ if (!trimmed || trimmed.startsWith(">") || trimmed.startsWith("|") || trimmed.startsWith("#")) continue;
58769
+ const matches = trimmed.matchAll(ACRONYM_PATTERN);
58770
+ for (const m of matches) {
58771
+ if (m[0].length >= 2 && !["I", "II", "III", "IV", "VI"].includes(m[0])) {
58772
+ candidates.add(m[0]);
58773
+ }
58774
+ }
58775
+ const techMatches = trimmed.matchAll(TECHNICAL_PATTERN);
58776
+ for (const m of techMatches) {
58777
+ if (m[0].length >= 6) candidates.add(m[0]);
58778
+ }
58779
+ const numberedMatches = trimmed.matchAll(NUMBERED_TERM_PATTERN);
58780
+ for (const m of numberedMatches) {
58781
+ if (m[0].length >= 5) candidates.add(m[0]);
58782
+ }
58783
+ }
58784
+ return [...candidates].sort();
58785
+ }
58786
+ function extractHeadings(lines) {
58787
+ return lines.map((line) => line.trim().match(/^(#{1,6})\s+(.+)/)).filter((m) => m !== null).map((m) => ({ level: m[1].length, text: m[2].replace(/[`*_]/g, "").trim() }));
58788
+ }
58789
+ function extractSections(lines) {
58790
+ const sections = [];
58791
+ let currentHeading = null;
58792
+ let currentContent = [];
58793
+ function flush() {
58794
+ const text = currentContent.join(" ").replace(/\s+/g, " ").trim();
58795
+ if (text) {
58796
+ sections.push({
58797
+ heading: currentHeading,
58798
+ content: text,
58799
+ wordCount: text.split(/\s+/).filter(Boolean).length
58800
+ });
58801
+ }
58802
+ currentContent = [];
58803
+ }
58804
+ for (const line of lines) {
58805
+ const headingMatch = line.trim().match(/^#{1,6}\s+/);
58806
+ if (headingMatch) {
58807
+ flush();
58808
+ currentHeading = line.trim().replace(/^#+\s*/, "").replace(/[`*_]/g, "").trim();
58809
+ } else {
58810
+ const stripped = line.trim();
58811
+ if (stripped) currentContent.push(stripped);
58812
+ }
58813
+ }
58814
+ flush();
58815
+ return sections;
58816
+ }
58817
+ function stripFrontmatter(lines) {
58818
+ if (lines.length > 0 && lines[0].trim() === "---") {
58819
+ const end = lines.findIndex((l, i) => i > 0 && l.trim() === "---");
58820
+ if (end > 0) return lines.slice(end + 1);
58821
+ }
58822
+ return lines;
58823
+ }
58824
+ function extractMetadata(lines) {
58825
+ const sourceLine = lines.find((line) => line.match(/^(source|url):\s*/i));
58826
+ const titleLine = lines.find((line) => line.match(/^title:\s*/i));
58827
+ return {
58828
+ source: sourceLine?.replace(/^(source|url):\s*/i, "").replace(/^["\s]+|["\s]+$/g, "").trim(),
58829
+ title: titleLine?.replace(/^title:\s*/i, "").replace(/^["\s]+|["\s]+$/g, "").trim()
58830
+ };
58831
+ }
58832
+ function findTitle(lines, frontmatterTitle, file2) {
58833
+ if (frontmatterTitle) return frontmatterTitle;
58834
+ const heading = lines.find((line) => line.trim().match(/^#\s+\S/));
58835
+ if (heading) return heading.replace(/^#\s+/, "").replace(/[`*_]/g, "").trim();
58836
+ if (file2) {
58837
+ return (0, import_node_path2.basename)(file2, (0, import_node_path2.extname)(file2)).replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
58838
+ }
58839
+ return "Untitled";
58840
+ }
58841
+ function cleanMarkdown(value) {
58842
+ return value.replace(/`([^`]+)`/g, "$1").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/^#+\s*/, "").replace(/\s+/g, " ").trim();
58843
+ }
58844
+ function extractTableRows(lines) {
58845
+ return lines.filter((line) => line.includes("|") && !line.match(/^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$/)).map((line) => line.split("|").map((cell) => cleanMarkdown(cell)).filter(Boolean)).filter((row) => row.length > 1);
58846
+ }
58847
+ function analyzeArticle(file2) {
58848
+ const extension = (0, import_node_path2.extname)(file2).toLowerCase();
58849
+ if (extension && ![".md", ".markdown", ".txt"].includes(extension)) {
58850
+ return agentError("UNSUPPORTED_ARTICLE_INPUT", "Article input must be a Markdown or plain-text file.", {
58851
+ supportedExtensions: [".md", ".markdown", ".txt"]
58852
+ });
58853
+ }
58854
+ let raw;
58855
+ try {
58856
+ raw = (0, import_node_fs2.readFileSync)(file2, "utf8");
58857
+ } catch (error51) {
58858
+ return agentError("ARTICLE_INPUT_UNREADABLE", error51 instanceof Error ? error51.message : "Article input could not be read.", { file: file2 });
58859
+ }
58860
+ const normalized = raw.replace(/\r\n/g, "\n").replace(/\t/g, " ").split("\n").map((line) => line.replace(/[ \u00a0]+$/g, "")).join("\n").trim();
58861
+ if (!normalized) {
58862
+ return agentError("EMPTY_ARTICLE_INPUT", "Article input is empty after normalization.", { file: file2 });
58863
+ }
58864
+ const lines = normalized.split("\n");
58865
+ const bodyLines = stripFrontmatter(lines);
58866
+ const metadata = extractMetadata(lines);
58867
+ const title = findTitle(bodyLines, metadata.title, file2);
58868
+ const contentLines = bodyLines.filter((line) => {
58869
+ const trimmed = line.trim();
58870
+ return !trimmed.match(/^#\s+/) && !trimmed.match(/^(source|url|author|date|title|published|created|tags?|description):\s*/i);
58871
+ });
58872
+ const quotes = contentLines.filter((line) => line.trim().startsWith(">")).map((line) => cleanMarkdown(line.replace(/^>\s?/, ""))).filter(Boolean);
58873
+ const listItems = contentLines.filter((line) => line.trim().match(/^[-*+]\s+/) || line.trim().match(/^\d+\.\s+/)).map((line) => cleanMarkdown(line.replace(/^\s*(?:[-*+]|\d+\.)\s+/, ""))).filter(Boolean);
58874
+ const tables = extractTableRows(contentLines);
58875
+ const paragraphs = contentLines.join("\n").split(/\n{2,}/).map((block) => cleanMarkdown(block.replace(/\n/g, " "))).filter((block) => block.length > 0 && !block.startsWith("|") && !block.match(/^[-*+]\s+/));
58876
+ const headings = extractHeadings(bodyLines);
58877
+ const sections = extractSections(bodyLines);
58878
+ const allText = paragraphs.join(" ");
58879
+ const wordCount = allText.split(/\s+/).filter(Boolean).length;
58880
+ const termCandidates = extractTermCandidates(normalized);
58881
+ return ok({
58882
+ title,
58883
+ source: metadata.source,
58884
+ headings,
58885
+ sections,
58886
+ paragraphs,
58887
+ listItems,
58888
+ quotes,
58889
+ tables,
58890
+ metadata: {
58891
+ inputFile: file2,
58892
+ wordCount,
58893
+ estimatedReadingMinutes: Math.max(1, Math.round(wordCount / 200)),
58894
+ lineCount: lines.length
58895
+ },
58896
+ termCandidates
58897
+ });
58898
+ }
58899
+
58900
+ // src/article-infographic.ts
58901
+ var import_node_fs3 = require("node:fs");
58902
+ var import_node_path3 = require("node:path");
58696
58903
  var ARTICLE_STYLES = ["editorial", "executive", "minimal"];
58697
- var ARTICLE_FORMATS = ["html", "json", "markdown"];
58904
+ var ARTICLE_FORMATS = ["html", "json", "markdown", "png", "pdf"];
58698
58905
  var infographicSectionItemSchema = external_exports.object({
58699
58906
  label: external_exports.string().optional(),
58700
58907
  value: external_exports.string().optional(),
58701
58908
  text: external_exports.string().min(1, "item.text must not be empty"),
58702
58909
  detail: external_exports.string().optional()
58703
58910
  });
58911
+ var infographicVisualSchema = external_exports.object({
58912
+ type: external_exports.union([
58913
+ external_exports.literal("kpi-strip"),
58914
+ external_exports.literal("metric-bars"),
58915
+ external_exports.literal("process-flow"),
58916
+ external_exports.literal("concept-contrast"),
58917
+ external_exports.literal("timeline-path"),
58918
+ external_exports.literal("part-to-whole"),
58919
+ external_exports.literal("before-after"),
58920
+ external_exports.literal("tradeoff-matrix"),
58921
+ external_exports.literal("ranked-list-chart"),
58922
+ external_exports.literal("system-diagram"),
58923
+ external_exports.literal("callout-diagram"),
58924
+ external_exports.literal("icon-cluster")
58925
+ ]),
58926
+ data: external_exports.record(external_exports.string(), external_exports.unknown()),
58927
+ caption: external_exports.string().optional()
58928
+ });
58704
58929
  var infographicSectionSchema = external_exports.object({
58705
- type: external_exports.enum(["hero", "facts", "timeline", "comparison", "quote", "takeaways"]),
58930
+ type: external_exports.union([
58931
+ external_exports.literal("hero"),
58932
+ external_exports.literal("facts"),
58933
+ external_exports.literal("timeline"),
58934
+ external_exports.literal("comparison"),
58935
+ external_exports.literal("quote"),
58936
+ external_exports.literal("takeaways"),
58937
+ external_exports.literal("process"),
58938
+ external_exports.literal("pros-cons"),
58939
+ external_exports.literal("stat-grid"),
58940
+ external_exports.literal("risk-matrix"),
58941
+ external_exports.literal("checklist")
58942
+ ]),
58706
58943
  title: external_exports.string().min(1, "section.title must not be empty"),
58707
58944
  items: external_exports.array(infographicSectionItemSchema).min(1, "section.items must have at least one item"),
58708
58945
  emphasis: external_exports.string().optional(),
58709
- notes: external_exports.string().optional()
58946
+ notes: external_exports.union([external_exports.string(), external_exports.array(external_exports.string())]).optional(),
58947
+ visual: infographicVisualSchema.optional()
58710
58948
  });
58711
58949
  var infographicSpecSchema = external_exports.object({
58712
58950
  title: external_exports.string().min(1, "title must not be empty"),
58713
58951
  subtitle: external_exports.string().optional(),
58714
58952
  source: external_exports.string().optional(),
58715
- style: external_exports.enum(["editorial", "executive", "minimal"]).default("editorial"),
58953
+ style: external_exports.union([external_exports.literal("editorial"), external_exports.literal("executive"), external_exports.literal("minimal")]).default("editorial"),
58716
58954
  summary: external_exports.string().min(1, "summary must not be empty"),
58717
58955
  sections: external_exports.array(infographicSectionSchema).min(1, "sections must have at least one entry"),
58718
58956
  metadata: external_exports.object({
@@ -58724,7 +58962,7 @@ var infographicSpecSchema = external_exports.object({
58724
58962
  function loadInfographicSpec(file2) {
58725
58963
  let raw;
58726
58964
  try {
58727
- raw = JSON.parse((0, import_node_fs2.readFileSync)(file2, "utf8"));
58965
+ raw = JSON.parse((0, import_node_fs3.readFileSync)(file2, "utf8"));
58728
58966
  } catch (error51) {
58729
58967
  return agentError("ARTICLE_INPUT_UNREADABLE", error51 instanceof Error ? error51.message : "Spec file could not be read.", { file: file2 });
58730
58968
  }
@@ -58749,7 +58987,7 @@ function parseArticleFormat(value) {
58749
58987
  return ARTICLE_FORMATS.includes(value) ? value : void 0;
58750
58988
  }
58751
58989
  function generateInfographicFromFile(file2, style) {
58752
- const extension = (0, import_node_path2.extname)(file2).toLowerCase();
58990
+ const extension = (0, import_node_path3.extname)(file2).toLowerCase();
58753
58991
  if (extension && ![".md", ".markdown", ".txt"].includes(extension)) {
58754
58992
  return agentError("UNSUPPORTED_ARTICLE_INPUT", "Article input must be a Markdown or plain-text file.", {
58755
58993
  supportedExtensions: [".md", ".markdown", ".txt"]
@@ -58757,7 +58995,7 @@ function generateInfographicFromFile(file2, style) {
58757
58995
  }
58758
58996
  let raw;
58759
58997
  try {
58760
- raw = (0, import_node_fs2.readFileSync)(file2, "utf8");
58998
+ raw = (0, import_node_fs3.readFileSync)(file2, "utf8");
58761
58999
  } catch (error51) {
58762
59000
  return agentError("ARTICLE_INPUT_UNREADABLE", error51 instanceof Error ? error51.message : "Article input could not be read.", {
58763
59001
  file: file2
@@ -58776,16 +59014,17 @@ function normalizeArticleText(raw) {
58776
59014
  }
58777
59015
  function parseArticle(text, file2) {
58778
59016
  const lines = text.split("\n");
58779
- const metadata = extractMetadata(lines);
58780
- const title = findTitle(lines) ?? titleFromFilename(file2);
58781
- const contentLines = lines.filter((line) => {
59017
+ const bodyLines = stripFrontmatter2(lines);
59018
+ const metadata = extractMetadata2(lines);
59019
+ const title = findTitle2(bodyLines, metadata.title) ?? titleFromFilename(file2);
59020
+ const contentLines = bodyLines.filter((line) => {
58782
59021
  const trimmed = line.trim();
58783
- return trimmed !== title && !trimmed.match(/^#\s+/) && !trimmed.match(/^(source|url|author|date):\s*/i);
59022
+ return trimmed !== title && !trimmed.match(/^#\s+/) && !trimmed.match(/^(source|url|author|date|title|published|created|tags?|description):\s*/i);
58784
59023
  });
58785
- const quotes = contentLines.filter((line) => line.trim().startsWith(">")).map((line) => cleanMarkdown(line.replace(/^>\s?/, ""))).filter(Boolean);
58786
- const listItems = contentLines.filter((line) => line.trim().match(/^[-*+]\s+/) || line.trim().match(/^\d+\.\s+/)).map((line) => cleanMarkdown(line.replace(/^\s*(?:[-*+]|\d+\.)\s+/, ""))).filter(Boolean);
58787
- const tableRows = extractTableRows(contentLines);
58788
- const paragraphs = contentLines.join("\n").split(/\n{2,}/).map((block) => cleanMarkdown(block.replace(/\n/g, " "))).filter((block) => block.length > 0 && !block.startsWith("|") && !block.match(/^[-*+]\s+/));
59024
+ const quotes = contentLines.filter((line) => line.trim().startsWith(">")).map((line) => cleanMarkdown2(line.replace(/^>\s?/, ""))).filter(Boolean);
59025
+ const listItems = contentLines.filter((line) => line.trim().match(/^[-*+]\s+/) || line.trim().match(/^\d+\.\s+/)).map((line) => cleanMarkdown2(line.replace(/^\s*(?:[-*+]|\d+\.)\s+/, ""))).filter(Boolean);
59026
+ const tableRows = extractTableRows2(contentLines);
59027
+ const paragraphs = contentLines.join("\n").split(/\n{2,}/).map((block) => cleanMarkdown2(block.replace(/\n/g, " "))).filter((block) => block.length > 0 && !block.startsWith("|") && !block.match(/^[-*+]\s+/));
58789
59028
  return {
58790
59029
  title,
58791
59030
  subtitle: metadata.subtitle ?? firstUsefulParagraph(paragraphs),
@@ -58796,12 +59035,68 @@ function parseArticle(text, file2) {
58796
59035
  tableRows
58797
59036
  };
58798
59037
  }
59038
+ function extractUnit(value) {
59039
+ return value.replace(/[0-9,.\-]/g, "").replace(/[^a-zA-Z%\/\u4e00-\u9fff]/g, "").trim();
59040
+ }
59041
+ function detectSameUnit(items) {
59042
+ const units = items.filter((f) => f.value).map((f) => extractUnit(f.value)).filter((u) => u.length > 0);
59043
+ if (units.length < 2) return false;
59044
+ const first = units[0];
59045
+ return units.every((u) => u === first);
59046
+ }
59047
+ function buildFactsVisual(facts) {
59048
+ const numeric = facts.filter((f) => f.value && /[\d]/.test(f.value));
59049
+ if (numeric.length < 2) return void 0;
59050
+ const rankingPattern = /\b(top|rank|#1|#2|leading|biggest|largest|highest|most|best|领先|最大|最高|排名)\b/i;
59051
+ const pctItems = numeric.filter((f) => f.value?.includes("%"));
59052
+ if (pctItems.length >= 2) {
59053
+ return {
59054
+ type: "part-to-whole",
59055
+ data: { items: pctItems.slice(0, 6).map((f) => ({ label: f.text, value: Number.parseFloat(f.value.replace(/[^0-9.\-]/g, "")) || 0, text: f.text })) },
59056
+ caption: "Proportional breakdown of key metrics."
59057
+ };
59058
+ }
59059
+ if (numeric.some((f) => rankingPattern.test(f.text)) && numeric.length >= 3) {
59060
+ return {
59061
+ type: "ranked-list-chart",
59062
+ data: { items: numeric.slice(0, 8).map((f) => ({ label: f.text, value: Number.parseFloat(f.value.replace(/[^0-9.\-]/g, "")) || 0, text: f.text })) },
59063
+ caption: "Ranked metrics from the article."
59064
+ };
59065
+ }
59066
+ const sameUnit = detectSameUnit(numeric);
59067
+ if (sameUnit && numeric.length >= 2 && numeric.length <= 8) {
59068
+ return {
59069
+ type: "metric-bars",
59070
+ data: { items: numeric.slice(0, 6).map((f) => ({ label: f.text, value: Number.parseFloat(f.value.replace(/[^0-9.\-]/g, "")) || 0, unit: extractUnit(f.value) })) },
59071
+ caption: "Key metrics compared side by side."
59072
+ };
59073
+ }
59074
+ return {
59075
+ type: "kpi-strip",
59076
+ data: { items: numeric.slice(0, 6).map((f) => ({ label: f.text, value: Number.parseFloat(f.value.replace(/[^0-9.\-]/g, "")) || 0, unit: extractUnit(f.value) || void 0 })) }
59077
+ };
59078
+ }
59079
+ function buildTimelineVisual(timeline) {
59080
+ if (timeline.length >= 2) {
59081
+ return {
59082
+ type: "timeline-path",
59083
+ data: { items: timeline.slice(0, 6).map((f) => ({ label: f.label || "", text: f.text })) }
59084
+ };
59085
+ }
59086
+ return void 0;
59087
+ }
59088
+ function detectProcess(listItems, evidence) {
59089
+ const stepPattern = /\b(step|stage|phase|first|then|next|finally|步骤|阶段|首先|然后|最后)\b/i;
59090
+ const candidates = [...listItems, ...evidence].filter((text) => stepPattern.test(text)).map((text) => ({ text: compactText(text, 150) }));
59091
+ return uniqueItems(candidates);
59092
+ }
58799
59093
  function buildInfographicSpec(parsed, style, file2) {
58800
59094
  const evidence = [...parsed.listItems, ...sentences(parsed.paragraphs.join(" "))];
58801
59095
  const facts = collectFacts(evidence);
58802
59096
  const timeline = collectTimeline(evidence);
58803
59097
  const comparison = collectComparison(evidence, parsed.tableRows);
58804
59098
  const takeaways = collectTakeaways(evidence, facts);
59099
+ const process3 = detectProcess(parsed.listItems, evidence);
58805
59100
  const summary = parsed.subtitle ?? takeaways[0]?.text ?? facts[0]?.text ?? "A concise visual summary of the source article.";
58806
59101
  const sections = [
58807
59102
  {
@@ -58811,9 +59106,28 @@ function buildInfographicSpec(parsed, style, file2) {
58811
59106
  items: [{ text: summary }]
58812
59107
  }
58813
59108
  ];
58814
- if (facts.length > 0) sections.push({ type: "facts", title: "Key Facts", items: facts.slice(0, 6) });
58815
- if (timeline.length > 1) sections.push({ type: "timeline", title: "Timeline", items: timeline.slice(0, 6) });
58816
- if (comparison.length > 1) sections.push({ type: "comparison", title: "Comparison", items: comparison.slice(0, 6) });
59109
+ if (process3.length >= 3) {
59110
+ sections.push({
59111
+ type: "process",
59112
+ title: "Process",
59113
+ items: process3.slice(0, 6),
59114
+ visual: {
59115
+ type: "process-flow",
59116
+ data: { items: process3.slice(0, 6).map((item, i) => ({ label: `Step ${i + 1}`, text: item.text })) }
59117
+ }
59118
+ });
59119
+ }
59120
+ if (facts.length > 0) {
59121
+ const v = buildFactsVisual(facts);
59122
+ sections.push({ type: "facts", title: "Key Facts", items: facts.slice(0, 6), ...v ? { visual: v } : {} });
59123
+ }
59124
+ if (timeline.length > 1) {
59125
+ const v = buildTimelineVisual(timeline);
59126
+ sections.push({ type: "timeline", title: "Timeline", items: timeline.slice(0, 6), ...v ? { visual: v } : {} });
59127
+ }
59128
+ if (comparison.length > 1) {
59129
+ sections.push({ type: "comparison", title: "Comparison", items: comparison.slice(0, 6) });
59130
+ }
58817
59131
  if (parsed.quotes.length > 0) {
58818
59132
  sections.push({
58819
59133
  type: "quote",
@@ -58852,25 +59166,35 @@ function renderInfographicMarkdown(spec) {
58852
59166
  }
58853
59167
  return lines.join("\n").trimEnd() + "\n";
58854
59168
  }
58855
- function extractMetadata(lines) {
59169
+ function stripFrontmatter2(lines) {
59170
+ if (lines.length > 0 && lines[0].trim() === "---") {
59171
+ const end = lines.findIndex((l, i) => i > 0 && l.trim() === "---");
59172
+ if (end > 0) return lines.slice(end + 1);
59173
+ }
59174
+ return lines;
59175
+ }
59176
+ function extractMetadata2(lines) {
58856
59177
  const sourceLine = lines.find((line) => line.match(/^(source|url):\s*/i));
58857
59178
  const subtitleLine = lines.find((line) => line.match(/^subtitle:\s*/i));
59179
+ const titleLine = lines.find((line) => line.match(/^title:\s*/i));
58858
59180
  return {
58859
- source: sourceLine?.replace(/^(source|url):\s*/i, "").trim(),
58860
- subtitle: subtitleLine?.replace(/^subtitle:\s*/i, "").trim()
59181
+ source: sourceLine?.replace(/^(source|url):\s*/i, "").replace(/^["\s]+|["\s]+$/g, "").trim(),
59182
+ subtitle: subtitleLine?.replace(/^subtitle:\s*/i, "").replace(/^["\s]+|["\s]+$/g, "").trim(),
59183
+ title: titleLine?.replace(/^title:\s*/i, "").replace(/^["\s]+|["\s]+$/g, "").trim()
58861
59184
  };
58862
59185
  }
58863
- function findTitle(lines) {
59186
+ function findTitle2(lines, frontmatterTitle) {
59187
+ if (frontmatterTitle) return frontmatterTitle;
58864
59188
  const heading = lines.find((line) => line.trim().match(/^#\s+\S/));
58865
- if (heading) return cleanMarkdown(heading.replace(/^#\s+/, ""));
58866
- const first = lines.find((line) => line.trim() && !line.match(/^(source|url|author|date):\s*/i));
58867
- return first ? cleanMarkdown(first).slice(0, 120) : void 0;
59189
+ if (heading) return cleanMarkdown2(heading.replace(/^#\s+/, ""));
59190
+ const first = lines.find((line) => line.trim() && !line.match(/^(source|url|author|date|title|published|created|tags?|description):\s*/i));
59191
+ return first ? cleanMarkdown2(first).slice(0, 120) : void 0;
58868
59192
  }
58869
59193
  function titleFromFilename(file2) {
58870
- return (0, import_node_path2.basename)(file2, (0, import_node_path2.extname)(file2)).replace(/[-_]+/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
59194
+ return (0, import_node_path3.basename)(file2, (0, import_node_path3.extname)(file2)).replace(/[-_]+/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
58871
59195
  }
58872
- function extractTableRows(lines) {
58873
- return lines.filter((line) => line.includes("|") && !line.match(/^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$/)).map((line) => line.split("|").map((cell) => cleanMarkdown(cell)).filter(Boolean)).filter((row) => row.length > 1);
59196
+ function extractTableRows2(lines) {
59197
+ return lines.filter((line) => line.includes("|") && !line.match(/^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$/)).map((line) => line.split("|").map((cell) => cleanMarkdown2(cell)).filter(Boolean)).filter((row) => row.length > 1);
58874
59198
  }
58875
59199
  function collectFacts(candidates) {
58876
59200
  return uniqueItems(candidates.filter((text) => NUMBER_PATTERN.test(text)).map((text) => ({
@@ -58898,7 +59222,7 @@ function collectTakeaways(candidates, facts) {
58898
59222
  return facts.slice(0, 3).map((item) => ({ text: item.text }));
58899
59223
  }
58900
59224
  function sentences(text) {
58901
- return text.split(/(?<=[.!?。!?])\s+/).map(cleanMarkdown).filter((sentence) => sentence.length > 20);
59225
+ return text.split(/(?<=[.!?。!?])\s+/).map(cleanMarkdown2).filter((sentence) => sentence.length > 20);
58902
59226
  }
58903
59227
  function firstUsefulParagraph(paragraphs) {
58904
59228
  return paragraphs.find((paragraph) => paragraph.length > 40)?.slice(0, 220);
@@ -58913,15 +59237,493 @@ function uniqueItems(items) {
58913
59237
  });
58914
59238
  }
58915
59239
  function compactText(text, max) {
58916
- const clean = cleanMarkdown(text);
59240
+ const clean = cleanMarkdown2(text);
58917
59241
  return clean.length > max ? `${clean.slice(0, max - 1).trim()}...` : clean;
58918
59242
  }
58919
- function cleanMarkdown(value) {
59243
+ function cleanMarkdown2(value) {
58920
59244
  return value.replace(/`([^`]+)`/g, "$1").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/^#+\s*/, "").replace(/\s+/g, " ").trim();
58921
59245
  }
58922
59246
 
59247
+ // src/infographic-visual-primitives.ts
59248
+ function escapeHtml2(value) {
59249
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
59250
+ }
59251
+ function svgFrame2(width, height, bgColor, body) {
59252
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="100%" style="background:${bgColor};display:block;max-width:100%;height:auto">
59253
+ ${body}
59254
+ </svg>`;
59255
+ }
59256
+ function svgTextBlock(options) {
59257
+ const {
59258
+ x,
59259
+ y,
59260
+ width,
59261
+ text,
59262
+ fontSize,
59263
+ lineHeight = Math.round(fontSize * 1.35),
59264
+ fill,
59265
+ anchor = "start",
59266
+ fontWeight,
59267
+ maxLines = 2,
59268
+ opacity,
59269
+ fontStyle
59270
+ } = options;
59271
+ const lines = wrapSvgText(text, width, fontSize, maxLines);
59272
+ const attrs = [
59273
+ `x="${x}"`,
59274
+ `y="${y}"`,
59275
+ `font-size="${fontSize}"`,
59276
+ `fill="${fill}"`,
59277
+ anchor !== "start" ? `text-anchor="${anchor}"` : "",
59278
+ fontWeight ? `font-weight="${fontWeight}"` : "",
59279
+ opacity !== void 0 ? `opacity="${opacity}"` : "",
59280
+ fontStyle ? `font-style="${fontStyle}"` : ""
59281
+ ].filter(Boolean).join(" ");
59282
+ const tspans = lines.map(
59283
+ (line, i) => `<tspan x="${x}" dy="${i === 0 ? 0 : lineHeight}">${escapeHtml2(line)}</tspan>`
59284
+ ).join("");
59285
+ return `<text ${attrs}>${tspans}</text>`;
59286
+ }
59287
+ function wrapSvgText(text, width, fontSize, maxLines) {
59288
+ const clean = text.replace(/\s+/g, " ").trim();
59289
+ if (!clean) return [""];
59290
+ const maxChars = Math.max(4, Math.floor(width / (fontSize * 0.56)));
59291
+ const words = clean.split(" ");
59292
+ const lines = [];
59293
+ let current = "";
59294
+ for (const word of words) {
59295
+ const next = current ? `${current} ${word}` : word;
59296
+ if (next.length <= maxChars) {
59297
+ current = next;
59298
+ continue;
59299
+ }
59300
+ if (current) lines.push(current);
59301
+ current = word.length > maxChars ? `${word.slice(0, Math.max(1, maxChars - 3))}...` : word;
59302
+ if (lines.length === maxLines) break;
59303
+ }
59304
+ if (lines.length < maxLines && current) lines.push(current);
59305
+ if (lines.length > maxLines) lines.length = maxLines;
59306
+ const consumed = lines.join(" ").replace(/\.\.\.$/, "");
59307
+ if (clean.length > consumed.length && lines.length > 0) {
59308
+ const last = lines[lines.length - 1];
59309
+ lines[lines.length - 1] = last.length > maxChars - 3 ? `${last.slice(0, Math.max(1, maxChars - 3))}...` : `${last}...`;
59310
+ }
59311
+ return lines;
59312
+ }
59313
+ function bar(x, y, w, h, fill, label = "") {
59314
+ const title = label ? `<title>${escapeHtml2(label)}</title>` : "";
59315
+ return `<rect x="${x}" y="${y}" width="${Math.max(w, 1)}" height="${h}" fill="${fill}" rx="2">${title}</rect>`;
59316
+ }
59317
+ function visualCard(title, svgContent, caption) {
59318
+ const cap = caption ? `<p class="mv-visual-caption">${escapeHtml2(caption)}</p>` : "";
59319
+ return `<div class="mv-visual-card">
59320
+ <h3 class="mv-visual-label">${escapeHtml2(title)}</h3>
59321
+ <div class="mv-visual-svg">${svgContent}</div>
59322
+ ${cap}
59323
+ </div>`;
59324
+ }
59325
+ function getPalette(theme) {
59326
+ return theme.palette;
59327
+ }
59328
+
59329
+ // src/infographic-visuals.ts
59330
+ function articleStyleToTheme(style) {
59331
+ const map2 = { editorial: "editorial", executive: "minimal", minimal: "minimal" };
59332
+ return getTheme(map2[style] ?? "default").svg;
59333
+ }
59334
+ function renderSectionVisual(visual, style) {
59335
+ const theme = articleStyleToTheme(style);
59336
+ const palette = getPalette(theme);
59337
+ switch (visual.type) {
59338
+ case "kpi-strip":
59339
+ return renderKpiStrip(visual, theme, palette);
59340
+ case "metric-bars":
59341
+ return renderMetricBars(visual, theme, palette);
59342
+ case "process-flow":
59343
+ return renderProcessFlow(visual, theme, palette);
59344
+ case "concept-contrast":
59345
+ return renderConceptContrast(visual, theme, palette);
59346
+ case "timeline-path":
59347
+ return renderTimelinePath(visual, theme, palette);
59348
+ case "part-to-whole":
59349
+ return renderPartToWhole(visual, theme, palette);
59350
+ case "before-after":
59351
+ return renderBeforeAfter(visual, theme, palette);
59352
+ case "tradeoff-matrix":
59353
+ return renderTradeoffMatrix(visual, theme, palette);
59354
+ case "ranked-list-chart":
59355
+ return renderRankedListChart(visual, theme, palette);
59356
+ case "system-diagram":
59357
+ return renderSystemDiagram(visual, theme, palette);
59358
+ case "callout-diagram":
59359
+ return renderCalloutDiagram(visual, theme, palette);
59360
+ case "icon-cluster":
59361
+ return renderIconCluster(visual, theme, palette);
59362
+ }
59363
+ }
59364
+ function itemLabel(item, fallback) {
59365
+ return typeof item.label === "string" ? item.label : fallback;
59366
+ }
59367
+ function itemValue(item) {
59368
+ const v = item.value;
59369
+ return typeof v === "number" ? v : typeof v === "string" ? Number.parseFloat(v) || 0 : 0;
59370
+ }
59371
+ function itemText(item, fallback = "") {
59372
+ return typeof item.text === "string" ? item.text : fallback;
59373
+ }
59374
+ function renderKpiStrip(visual, _theme, palette) {
59375
+ const items = visual.data.items ?? [];
59376
+ const parts = items.map((item, i) => {
59377
+ const val = escapeHtml2(itemValue(item).toLocaleString());
59378
+ const label = escapeHtml2(itemLabel(item, ""));
59379
+ const unit = typeof item.unit === "string" ? ` <span class="mv-visual-unit">${escapeHtml2(item.unit)}</span>` : "";
59380
+ const delta = typeof item.delta === "string" ? ` <span class="mv-visual-delta">${escapeHtml2(item.delta)}</span>` : "";
59381
+ const color = palette[i % palette.length];
59382
+ return `<div class="mv-visual-kpi" style="border-top-color:${color}">
59383
+ <strong>${val}${unit}${delta}</strong>
59384
+ <span>${label}</span>
59385
+ </div>`;
59386
+ }).join("\n");
59387
+ return visualCard("", `<div class="mv-visual-kpi-strip">${parts}</div>`, visual.caption);
59388
+ }
59389
+ function renderMetricBars(visual, theme, palette) {
59390
+ const items = visual.data.items ?? [];
59391
+ const values = items.map(itemValue);
59392
+ const max = Math.max(...values, 1);
59393
+ const barH = 22;
59394
+ const rowH = 50;
59395
+ const labelW = 190;
59396
+ const valueW = 78;
59397
+ const barW = 300;
59398
+ const w = labelW + barW + valueW + 24;
59399
+ const h = items.length * rowH + 14;
59400
+ const defs = palette.map((c, i) => `<linearGradient id="mg-${i}" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="${c}"/><stop offset="100%" stop-color="${c}" stop-opacity="0.6"/></linearGradient>`).join("");
59401
+ const bars = items.map((item, i) => {
59402
+ const y = 10 + i * rowH;
59403
+ const v = values[i];
59404
+ const bw = v / max * barW;
59405
+ const label = itemLabel(item, "");
59406
+ const val = escapeHtml2(v.toLocaleString());
59407
+ const unit = typeof item.unit === "string" ? ` ${escapeHtml2(item.unit)}` : "";
59408
+ return `${svgTextBlock({ x: 0, y: y + 13, width: labelW - 12, text: label, fontSize: 11, fill: theme.labelColor, maxLines: 2 })}
59409
+ ${bar(labelW, y, bw, barH, `url(#mg-${i % palette.length})`, `${label}: ${val}${unit}`)}
59410
+ <text x="${labelW + barW + valueW}" y="${y + 15}" font-size="11" fill="${theme.labelColor}" text-anchor="end">${val}${unit}</text>`;
59411
+ }).join("\n");
59412
+ const svg = svgFrame2(w, h, theme.background, `<defs>${defs}</defs>${bars}`);
59413
+ return visualCard("", svg, visual.caption);
59414
+ }
59415
+ function renderProcessFlow(visual, _theme, palette) {
59416
+ const items = visual.data.items ?? [];
59417
+ const nodes = items.map((item, i) => {
59418
+ const label = itemLabel(item, `Step ${i + 1}`);
59419
+ const text = itemText(item, "");
59420
+ const color = palette[i % palette.length];
59421
+ return `<article class="mv-visual-process-node" style="--node-color:${color}">
59422
+ <div class="mv-visual-process-head">
59423
+ <span>${i + 1}</span>
59424
+ <strong>${escapeHtml2(label)}</strong>
59425
+ </div>
59426
+ <p>${escapeHtml2(text)}</p>
59427
+ </article>`;
59428
+ }).join("\n");
59429
+ return visualCard("", `<div class="mv-visual-process-grid">${nodes}</div>`, visual.caption);
59430
+ }
59431
+ function requireVisualCriteria(visual) {
59432
+ const items = visual.data.items ?? [];
59433
+ const criteria = extractCriteria(items);
59434
+ if (criteria.length === 0) {
59435
+ const example = { label: "Option A", text: "Description", dimension1: "value1", dimension2: "value2" };
59436
+ throw new Error(
59437
+ `concept-contrast visual requires at least one comparison dimension per item beyond 'label' and 'text'. Add shared keys (e.g., ${JSON.stringify(example)}) or use a different visual type.`
59438
+ );
59439
+ }
59440
+ return criteria;
59441
+ }
59442
+ function renderConceptContrast(visual, theme, palette) {
59443
+ const items = visual.data.items ?? [];
59444
+ const criteria = requireVisualCriteria(visual);
59445
+ const cols = items.length;
59446
+ const colW = 240;
59447
+ const rowH = 24;
59448
+ const headerH = 30;
59449
+ const rowCount = criteria.length;
59450
+ const h = headerH + rowCount * rowH + 16;
59451
+ const w = cols * colW + 60;
59452
+ const headers = items.map(
59453
+ (item, i) => `<text x="${60 + i * colW + colW / 2}" y="20" text-anchor="middle" font-size="11" font-weight="600" fill="${palette[i % palette.length]}">${escapeHtml2(itemLabel(item, `Item ${i + 1}`))}</text>`
59454
+ ).join("\n");
59455
+ const rows = criteria.map((criterion, ri) => {
59456
+ const y = headerH + ri * rowH + 15;
59457
+ const label = `<text x="0" y="${y}" font-size="10" fill="${theme.labelColor}">${escapeHtml2(criterion)}</text>`;
59458
+ const cells = items.map((item, ci) => {
59459
+ const val = extractValue(item, criterion);
59460
+ const x = 60 + ci * colW + colW / 2;
59461
+ return `<text x="${x}" y="${y}" text-anchor="middle" font-size="10" fill="${theme.axisColor}">${escapeHtml2(val)}</text>`;
59462
+ }).join("\n");
59463
+ return `${label}${cells}`;
59464
+ }).join("\n");
59465
+ const svg = svgFrame2(w, h, theme.background, headers + "\n" + rows);
59466
+ return visualCard("", svg, visual.caption);
59467
+ }
59468
+ function extractCriteria(items) {
59469
+ const keys = /* @__PURE__ */ new Set();
59470
+ for (const item of items) {
59471
+ for (const key of Object.keys(item)) {
59472
+ if (key !== "label" && key !== "text") keys.add(key);
59473
+ }
59474
+ }
59475
+ return Array.from(keys).slice(0, 6);
59476
+ }
59477
+ function extractValue(item, key) {
59478
+ const v = item[key];
59479
+ if (typeof v === "string") return v;
59480
+ if (typeof v === "number") return String(v);
59481
+ if (v === true) return "\u2713";
59482
+ if (v === false) return "\u2717";
59483
+ return "";
59484
+ }
59485
+ function renderTimelinePath(visual, theme, palette) {
59486
+ const items = visual.data.items ?? [];
59487
+ const dotR = 6;
59488
+ const rowH = 56;
59489
+ const h = items.length * rowH + 30;
59490
+ const lineX = 30;
59491
+ let defs = `<marker id="tl-arr" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" fill="${theme.axisColor}"/></marker>`;
59492
+ const nodes = items.map((item, i) => {
59493
+ const y = 15 + i * rowH;
59494
+ const label = escapeHtml2(itemLabel(item, ""));
59495
+ const text = escapeHtml2(itemText(item, ""));
59496
+ const color = palette[i % palette.length];
59497
+ const line = i < items.length - 1 ? `<line x1="${lineX}" y1="${y + dotR * 2 + 4}" x2="${lineX}" y2="${y + rowH - 4}" stroke="${theme.axisColor}" stroke-width="1.5"/>` : "";
59498
+ return `<g>
59499
+ <circle cx="${lineX}" cy="${y + dotR}" r="${dotR}" fill="${color}"/>
59500
+ ${line}
59501
+ ${svgTextBlock({ x: lineX + 18, y: y + 4, width: 320, text: label, fontSize: 12, fontWeight: 600, fill: color, maxLines: 1 })}
59502
+ ${svgTextBlock({ x: lineX + 18, y: y + 20, width: 340, text, fontSize: 11, fill: theme.labelColor, maxLines: 2 })}
59503
+ </g>`;
59504
+ }).join("\n");
59505
+ const svg = svgFrame2(400, h, theme.background, `<defs>${defs}</defs>${nodes}`);
59506
+ return visualCard("", svg, visual.caption);
59507
+ }
59508
+ function renderPartToWhole(visual, theme, palette) {
59509
+ const items = visual.data.items ?? [];
59510
+ const values = items.map(itemValue);
59511
+ const total = values.reduce((a, b) => a + b, 0) || 1;
59512
+ const barH = 28;
59513
+ const gap = 8;
59514
+ const w = 400;
59515
+ const h = items.length * (barH + gap) + 40;
59516
+ const bars = items.map((item, i) => {
59517
+ const y = 20 + i * (barH + gap);
59518
+ const pct = values[i] / total * 100;
59519
+ const bw = pct / 100 * (w - 120);
59520
+ const color = palette[i % palette.length];
59521
+ const label = escapeHtml2(itemLabel(item, ""));
59522
+ const val = escapeHtml2(pct.toFixed(1));
59523
+ return `<g>
59524
+ <text x="0" y="${y + 18}" font-size="11" fill="${theme.labelColor}">${label}</text>
59525
+ ${bar(110, y, bw, barH, color, `${label}: ${val}%`)}
59526
+ <text x="${w - 8}" y="${y + 18}" font-size="11" fill="${theme.labelColor}" text-anchor="end" font-weight="600">${val}%</text>
59527
+ </g>`;
59528
+ }).join("\n");
59529
+ const svg = svgFrame2(w, h, theme.background, bars);
59530
+ return visualCard("", svg, visual.caption);
59531
+ }
59532
+ function renderBeforeAfter(visual, theme, palette) {
59533
+ const before = visual.data.before ?? [];
59534
+ const after = visual.data.after ?? [];
59535
+ const items = visual.data.items ?? (before.length > 0 ? before : []);
59536
+ const afterItems = after.length > 0 ? after : items;
59537
+ const rows = Math.max(items.length, afterItems.length);
59538
+ const colW = 180;
59539
+ const rowH = 28;
59540
+ const h = 50 + rows * rowH;
59541
+ const w = colW * 2 + 80;
59542
+ const midX = colW + 40;
59543
+ const beforeLabel = escapeHtml2(visual.data.beforeLabel ?? "Before");
59544
+ const afterLabel = escapeHtml2(visual.data.afterLabel ?? "After");
59545
+ const header = `<text x="${midX / 2}" y="22" text-anchor="middle" font-size="13" font-weight="700" fill="${palette[1]}">${beforeLabel}</text>
59546
+ <text x="${midX + colW / 2}" y="22" text-anchor="middle" font-size="13" font-weight="700" fill="${palette[0]}">${afterLabel}</text>
59547
+ <line x1="${midX - 40}" y1="30" x2="${midX + 40}" y2="30" stroke="${theme.axisColor}" stroke-width="1"/>
59548
+ <text x="${midX}" y="42" text-anchor="middle" font-size="10" fill="${theme.axisColor}">\u2192</text>`;
59549
+ const rowsSvg = Array.from({ length: rows }, (_, i) => {
59550
+ const y = 48 + i * rowH;
59551
+ const bItem = items[i];
59552
+ const aItem = afterItems[i];
59553
+ const bVal = bItem ? escapeHtml2(String(bItem.value ?? bItem.text ?? "")) : "";
59554
+ const aVal = aItem ? escapeHtml2(String(aItem.value ?? aItem.text ?? "")) : "";
59555
+ const bLabel = bItem ? escapeHtml2(itemLabel(bItem, "")) : "";
59556
+ const aLabel = aItem ? escapeHtml2(itemLabel(aItem, "")) : "";
59557
+ return `<text x="${midX / 2}" y="${y + 14}" text-anchor="middle" font-size="11" fill="${theme.labelColor}">${bLabel}${bVal ? ` ${bVal}` : ""}</text>
59558
+ <text x="${midX + colW / 2}" y="${y + 14}" text-anchor="middle" font-size="11" fill="${theme.labelColor}">${aLabel}${aVal ? ` ${aVal}` : ""}</text>`;
59559
+ }).join("\n");
59560
+ const svg = svgFrame2(w, h, theme.background, header + "\n" + rowsSvg);
59561
+ return visualCard("", svg, visual.caption);
59562
+ }
59563
+ function renderTradeoffMatrix(visual, theme, palette) {
59564
+ const items = visual.data.items ?? [];
59565
+ const cellW = 170;
59566
+ const cellH = 70;
59567
+ const gap = 8;
59568
+ const w = cellW * 2 + gap + 80;
59569
+ const h = cellH * 2 + gap + 80;
59570
+ const xLabel = escapeHtml2(visual.data.xLabel ?? "Low \u2192 High");
59571
+ const yLabel = escapeHtml2(visual.data.yLabel ?? "Low \u2192 High");
59572
+ const quads = [
59573
+ { col: 0, row: 0, color: palette[3], label: "Avoid", opacity: "0.08" },
59574
+ { col: 1, row: 0, color: palette[1], label: "Investigate", opacity: "0.08" },
59575
+ { col: 0, row: 1, color: palette[1], label: "Monitor", opacity: "0.08" },
59576
+ { col: 1, row: 1, color: palette[0], label: "Priority", opacity: "0.12" }
59577
+ ];
59578
+ const quadrants = quads.map((q, i) => {
59579
+ const px = 50 + q.col * (cellW + gap);
59580
+ const py = 50 + q.row * (cellH + gap);
59581
+ const item = items[i];
59582
+ const text = item ? escapeHtml2(itemText(item, "")) : "";
59583
+ const detail = item && typeof item.detail === "string" ? escapeHtml2(item.detail) : "";
59584
+ const itemTitle = item ? escapeHtml2(itemLabel(item, q.label)) : q.label;
59585
+ return `<g>
59586
+ <rect x="${px}" y="${py}" width="${cellW}" height="${cellH}" rx="4" fill="${q.color}" opacity="${q.opacity}" stroke="${q.color}" stroke-width="1"/>
59587
+ <text x="${px + cellW / 2}" y="${py + cellH / 2 - (detail ? 8 : 0)}" text-anchor="middle" font-size="12" font-weight="600" fill="${q.color}">${itemTitle}</text>
59588
+ ${text ? `<text x="${px + cellW / 2}" y="${py + cellH / 2 + 10}" text-anchor="middle" font-size="10" fill="${theme.labelColor}">${text}</text>` : ""}
59589
+ ${detail ? `<text x="${px + cellW / 2}" y="${py + cellH / 2 + 24}" text-anchor="middle" font-size="9" fill="${theme.labelColor}" opacity="0.7">${detail}</text>` : ""}
59590
+ </g>`;
59591
+ }).join("\n");
59592
+ const axes = `<text x="50" y="38" font-size="10" fill="${theme.axisColor}">${yLabel}</text>
59593
+ <text x="${50 + cellW + gap - 5}" y="${h - 8}" text-anchor="end" font-size="10" fill="${theme.axisColor}">${xLabel}</text>`;
59594
+ const svg = svgFrame2(w, h, theme.background, axes + "\n" + quadrants);
59595
+ return visualCard("", svg, visual.caption);
59596
+ }
59597
+ function renderRankedListChart(visual, _theme, palette) {
59598
+ const items = visual.data.items ?? [];
59599
+ const values = items.map(itemValue);
59600
+ const max = Math.max(...values, 1);
59601
+ const rows = items.map((item, i) => {
59602
+ const v = values[i];
59603
+ const pct = Math.max(v / max * 100, 1);
59604
+ const color = palette[i % palette.length];
59605
+ const label = itemLabel(item, "");
59606
+ const val = escapeHtml2(v.toLocaleString());
59607
+ const rank = i + 1;
59608
+ return `<div class="mv-visual-ranked-row">
59609
+ <span class="mv-visual-ranked-rank">${rank}</span>
59610
+ <p>${escapeHtml2(label)}</p>
59611
+ <div class="mv-visual-ranked-track"><span style="width:${pct.toFixed(2)}%;background:${color}"></span></div>
59612
+ <strong>${val}</strong>
59613
+ </div>`;
59614
+ }).join("\n");
59615
+ return visualCard("", `<div class="mv-visual-ranked">${rows}</div>`, visual.caption);
59616
+ }
59617
+ function renderSystemDiagram(visual, theme, palette) {
59618
+ const nodes = visual.data.nodes ?? [];
59619
+ const edges = visual.data.edges ?? [];
59620
+ const nodeW = 120;
59621
+ const nodeH = 40;
59622
+ const gap = 30;
59623
+ const pad = 30;
59624
+ const cols = 3;
59625
+ const rows = Math.ceil(nodes.length / cols);
59626
+ const w = cols * (nodeW + gap) + pad;
59627
+ const h = rows * (nodeH + gap) + pad + 30;
59628
+ const defEdges = edges.map((edge, _i) => {
59629
+ const from = Number(edge.from ?? 0);
59630
+ const to = Number(edge.to ?? 1);
59631
+ const fx = pad + from % cols * (nodeW + gap) + nodeW / 2;
59632
+ const fy = pad + Math.floor(from / cols) * (nodeH + gap) + nodeH / 2;
59633
+ const tx = pad + to % cols * (nodeW + gap) + nodeW / 2;
59634
+ const ty = pad + Math.floor(to / cols) * (nodeH + gap) + nodeH / 2;
59635
+ return `<line x1="${fx}" y1="${fy}" x2="${tx}" y2="${ty}" stroke="${theme.axisColor}" stroke-width="1.5" marker-end="url(#sd-arr)"/>`;
59636
+ }).join("\n");
59637
+ let defs = `<marker id="sd-arr" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" fill="${theme.axisColor}"/></marker>`;
59638
+ const defNodes = nodes.map((node, i) => {
59639
+ const col = i % cols;
59640
+ const row = Math.floor(i / cols);
59641
+ const x = pad + col * (nodeW + gap);
59642
+ const y = pad + row * (nodeH + gap);
59643
+ const color = typeof node.color === "string" ? node.color : palette[i % palette.length];
59644
+ const label = escapeHtml2(itemLabel(node, `Node ${i + 1}`));
59645
+ const zone = typeof node.zone === "string" ? node.zone : "";
59646
+ const zoneLabel = zone ? `<text x="${x + nodeW / 2}" y="${y - 6}" text-anchor="middle" font-size="9" fill="${theme.axisColor}" font-style="italic">${escapeHtml2(zone)}</text>` : "";
59647
+ return `<g>
59648
+ ${zoneLabel}
59649
+ <rect x="${x}" y="${y}" width="${nodeW}" height="${nodeH}" rx="4" fill="${color}" opacity="0.12" stroke="${color}" stroke-width="1.5"/>
59650
+ <text x="${x + nodeW / 2}" y="${y + nodeH / 2 + 4}" text-anchor="middle" font-size="11" font-weight="600" fill="${color}">${label}</text>
59651
+ </g>`;
59652
+ }).join("\n");
59653
+ const svg = svgFrame2(w, h, theme.background, `<defs>${defs}</defs>${defEdges}
59654
+ ${defNodes}`);
59655
+ return visualCard("", svg, visual.caption);
59656
+ }
59657
+ function renderCalloutDiagram(visual, theme, palette) {
59658
+ const items = visual.data.items ?? [];
59659
+ const w = 400;
59660
+ const rowH = 36;
59661
+ const h = items.length * rowH + 30;
59662
+ const calloutX = 20;
59663
+ const lineX = 50;
59664
+ const textX = 60;
59665
+ const dotR = 4;
59666
+ const defs = `<marker id="cd-arr" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" fill="${theme.axisColor}"/></marker>`;
59667
+ const callouts = items.map((item, i) => {
59668
+ const y = 15 + i * rowH;
59669
+ const color = palette[i % palette.length];
59670
+ const label = escapeHtml2(itemLabel(item, ""));
59671
+ const text = escapeHtml2(itemText(item, ""));
59672
+ const detail = item.detail ? escapeHtml2(String(item.detail)) : "";
59673
+ return `<g>
59674
+ <circle cx="${calloutX}" cy="${y + 6}" r="${dotR}" fill="${color}"/>
59675
+ <line x1="${calloutX + dotR + 2}" y1="${y + 6}" x2="${lineX - 4}" y2="${y + 6}" stroke="${theme.axisColor}" stroke-width="1" marker-end="url(#cd-arr)"/>
59676
+ ${svgTextBlock({ x: textX, y: y + 4, width: 320, text: label, fontSize: 12, fontWeight: 600, fill: color, maxLines: 1 })}
59677
+ ${svgTextBlock({ x: textX, y: y + 18, width: 320, text: `${text}${detail ? ` - ${detail}` : ""}`, fontSize: 11, fill: theme.labelColor, maxLines: 2 })}
59678
+ </g>`;
59679
+ }).join("\n");
59680
+ const svg = svgFrame2(w, h, theme.background, `<defs>${defs}</defs>${callouts}`);
59681
+ return visualCard("", svg, visual.caption);
59682
+ }
59683
+ function renderIconCluster(visual, theme, palette) {
59684
+ const items = visual.data.items ?? [];
59685
+ const cols = 3;
59686
+ const cellW = 110;
59687
+ const cellH = 80;
59688
+ const gap = 12;
59689
+ const rows = Math.ceil(items.length / cols);
59690
+ const w = cols * (cellW + gap) + 20;
59691
+ const h = rows * (cellH + gap) + 20;
59692
+ const iconR = 18;
59693
+ const grid = items.map((item, i) => {
59694
+ const col = i % cols;
59695
+ const row = Math.floor(i / cols);
59696
+ const cx = 10 + col * (cellW + gap) + cellW / 2;
59697
+ const cy = 10 + row * (cellH + gap) + 28;
59698
+ const color = palette[i % palette.length];
59699
+ const label = escapeHtml2(itemLabel(item, ""));
59700
+ const text = escapeHtml2(itemText(item, ""));
59701
+ const initial = label.charAt(0).toUpperCase();
59702
+ return `<g>
59703
+ <circle cx="${cx}" cy="${cy}" r="${iconR}" fill="${color}" opacity="0.15" stroke="${color}" stroke-width="1.5"/>
59704
+ <text x="${cx}" y="${cy + 5}" text-anchor="middle" font-size="16" font-weight="700" fill="${color}">${initial}</text>
59705
+ ${svgTextBlock({ x: cx, y: cy + iconR + 14, width: cellW - 10, text: label, fontSize: 11, fontWeight: 600, fill: color, anchor: "middle", maxLines: 1 })}
59706
+ ${svgTextBlock({ x: cx, y: cy + iconR + 30, width: cellW - 10, text, fontSize: 10, fill: theme.labelColor, anchor: "middle", maxLines: 2 })}
59707
+ </g>`;
59708
+ }).join("\n");
59709
+ const svg = svgFrame2(w, h, theme.background, grid);
59710
+ return visualCard("", svg, visual.caption);
59711
+ }
59712
+
58923
59713
  // src/article-html.ts
58924
59714
  function renderInfographicHtml(spec) {
59715
+ let sectionIndex = 0;
59716
+ const renderedSections = spec.sections.map((section) => {
59717
+ if (section.visual) {
59718
+ sectionIndex++;
59719
+ return renderVisualSection(section, sectionIndex, spec.style);
59720
+ }
59721
+ if (section.type === "hero" || section.type === "quote") {
59722
+ return section.type === "hero" ? renderHero(section) : renderQuote(section);
59723
+ }
59724
+ sectionIndex++;
59725
+ return renderSection(section, sectionIndex);
59726
+ }).join("\n");
58925
59727
  return `<!doctype html>
58926
59728
  <html lang="en">
58927
59729
  <head>
@@ -58934,19 +59736,25 @@ function renderInfographicHtml(spec) {
58934
59736
  </head>
58935
59737
  <body>
58936
59738
  <main class="mv-infographic mv-infographic-${spec.style}">
58937
- ${spec.sections.map(renderSection).join("\n")}
59739
+ ${renderedSections}
58938
59740
  </main>
58939
59741
  <script type="application/json" id="miao-infographic-spec">${escapeHtml(JSON.stringify(spec, null, 2))}</script>
58940
59742
  </body>
58941
59743
  </html>`;
58942
59744
  }
58943
- function renderSection(section) {
58944
- if (section.type === "hero") return renderHero(section);
58945
- if (section.type === "facts") return renderFacts(section);
58946
- if (section.type === "timeline") return renderTimeline(section);
58947
- if (section.type === "comparison") return renderComparison(section);
58948
- if (section.type === "quote") return renderQuote(section);
58949
- return renderTakeaways(section);
59745
+ function sectionNumber(index) {
59746
+ return String(index).padStart(2, "0");
59747
+ }
59748
+ function renderSection(section, index) {
59749
+ if (section.type === "facts") return renderFacts(section, index);
59750
+ if (section.type === "timeline") return renderTimeline(section, index);
59751
+ if (section.type === "comparison") return renderComparison(section, index);
59752
+ if (section.type === "process") return renderProcess(section, index);
59753
+ if (section.type === "pros-cons") return renderProsCons(section, index);
59754
+ if (section.type === "stat-grid") return renderStatGrid(section, index);
59755
+ if (section.type === "risk-matrix") return renderRiskMatrix(section, index);
59756
+ if (section.type === "checklist") return renderChecklist(section, index);
59757
+ return renderTakeaways(section, index);
58950
59758
  }
58951
59759
  function renderHero(section) {
58952
59760
  const lead = section.emphasis ?? section.items[0]?.text ?? "";
@@ -58956,9 +59764,9 @@ function renderHero(section) {
58956
59764
  <p class="mv-lead">${escapeHtml(lead)}</p>
58957
59765
  </section>`;
58958
59766
  }
58959
- function renderFacts(section) {
59767
+ function renderFacts(section, index) {
58960
59768
  return `<section class="mv-section mv-facts">
58961
- <div class="mv-section-head"><span>01</span><h2>${escapeHtml(section.title)}</h2></div>
59769
+ <div class="mv-section-head"><span>${sectionNumber(index)}</span><h2>${escapeHtml(section.title)}</h2></div>
58962
59770
  <div class="mv-fact-grid">
58963
59771
  ${section.items.map((item) => `<article class="mv-fact">
58964
59772
  ${item.value ? `<strong>${escapeHtml(item.value)}</strong>` : ""}
@@ -58967,17 +59775,17 @@ function renderFacts(section) {
58967
59775
  </div>
58968
59776
  </section>`;
58969
59777
  }
58970
- function renderTimeline(section) {
59778
+ function renderTimeline(section, index) {
58971
59779
  return `<section class="mv-section mv-timeline">
58972
- <div class="mv-section-head"><span>02</span><h2>${escapeHtml(section.title)}</h2></div>
59780
+ <div class="mv-section-head"><span>${sectionNumber(index)}</span><h2>${escapeHtml(section.title)}</h2></div>
58973
59781
  <ol>
58974
59782
  ${section.items.map((item) => `<li><time>${escapeHtml(item.label ?? "")}</time><p>${escapeHtml(item.text)}</p></li>`).join("\n")}
58975
59783
  </ol>
58976
59784
  </section>`;
58977
59785
  }
58978
- function renderComparison(section) {
59786
+ function renderComparison(section, index) {
58979
59787
  return `<section class="mv-section mv-comparison">
58980
- <div class="mv-section-head"><span>03</span><h2>${escapeHtml(section.title)}</h2></div>
59788
+ <div class="mv-section-head"><span>${sectionNumber(index)}</span><h2>${escapeHtml(section.title)}</h2></div>
58981
59789
  <div class="mv-comparison-grid">
58982
59790
  ${section.items.map((item) => `<article>
58983
59791
  ${item.label ? `<h3>${escapeHtml(item.label)}</h3>` : ""}
@@ -58992,14 +59800,95 @@ function renderQuote(section) {
58992
59800
  <blockquote>${escapeHtml(quote)}</blockquote>
58993
59801
  </section>`;
58994
59802
  }
58995
- function renderTakeaways(section) {
59803
+ function renderTakeaways(section, index) {
58996
59804
  return `<section class="mv-section mv-takeaways">
58997
- <div class="mv-section-head"><span>04</span><h2>${escapeHtml(section.title)}</h2></div>
59805
+ <div class="mv-section-head"><span>${sectionNumber(index)}</span><h2>${escapeHtml(section.title)}</h2></div>
58998
59806
  <ul>
58999
59807
  ${section.items.map((item) => `<li>${escapeHtml(item.text)}</li>`).join("\n")}
59000
59808
  </ul>
59001
59809
  </section>`;
59002
59810
  }
59811
+ function renderProcess(section, index) {
59812
+ return `<section class="mv-section mv-process">
59813
+ <div class="mv-section-head"><span>${sectionNumber(index)}</span><h2>${escapeHtml(section.title)}</h2></div>
59814
+ <ol class="mv-process-steps">
59815
+ ${section.items.map((item, i) => `<li>
59816
+ <span class="mv-step-num">${i + 1}</span>
59817
+ <div>
59818
+ ${item.label ? `<strong>${escapeHtml(item.label)}</strong>` : ""}
59819
+ <p>${escapeHtml(item.text)}</p>
59820
+ </div>
59821
+ </li>`).join("\n")}
59822
+ </ol>
59823
+ </section>`;
59824
+ }
59825
+ function renderProsCons(section, index) {
59826
+ const pros = section.items.filter((item) => item.label?.toLowerCase() === "pro" || item.label?.toLowerCase() === "pros");
59827
+ const cons = section.items.filter((item) => item.label?.toLowerCase() === "con" || item.label?.toLowerCase() === "cons");
59828
+ const unlabeled = section.items.filter((item) => !item.label);
59829
+ return `<section class="mv-section mv-pros-cons">
59830
+ <div class="mv-section-head"><span>${sectionNumber(index)}</span><h2>${escapeHtml(section.title)}</h2></div>
59831
+ <div class="mv-pros-cons-grid">
59832
+ <div class="mv-pros-col">
59833
+ <h3>Pros</h3>
59834
+ <ul>${[...pros, ...unlabeled].map((item) => `<li>${escapeHtml(item.text)}</li>`).join("\n")}</ul>
59835
+ </div>
59836
+ <div class="mv-cons-col">
59837
+ <h3>Cons</h3>
59838
+ <ul>${cons.map((item) => `<li>${escapeHtml(item.text)}</li>`).join("\n")}</ul>
59839
+ ${cons.length === 0 ? '<p class="mv-muted">No cons listed</p>' : ""}
59840
+ </div>
59841
+ </div>
59842
+ </section>`;
59843
+ }
59844
+ function renderStatGrid(section, index) {
59845
+ return `<section class="mv-section mv-stat-grid">
59846
+ <div class="mv-section-head"><span>${sectionNumber(index)}</span><h2>${escapeHtml(section.title)}</h2></div>
59847
+ <div class="mv-stat-grid-items">
59848
+ ${section.items.map((item) => `<article class="mv-stat-card">
59849
+ ${item.value ? `<strong>${escapeHtml(item.value)}</strong>` : ""}
59850
+ <p>${escapeHtml(item.text)}</p>
59851
+ </article>`).join("\n")}
59852
+ </div>
59853
+ </section>`;
59854
+ }
59855
+ function renderRiskMatrix(section, index) {
59856
+ const quadrants = section.items.slice(0, 4);
59857
+ return `<section class="mv-section mv-risk-matrix">
59858
+ <div class="mv-section-head"><span>${sectionNumber(index)}</span><h2>${escapeHtml(section.title)}</h2></div>
59859
+ <div class="mv-risk-matrix-grid">
59860
+ <div class="mv-risk-header mv-risk-hl">Low Likelihood / High Impact</div>
59861
+ <div class="mv-risk-header mv-risk-hh">High Likelihood / High Impact</div>
59862
+ <div class="mv-risk-header mv-risk-ll">Low Likelihood / Low Impact</div>
59863
+ <div class="mv-risk-header mv-risk-lh">High Likelihood / Low Impact</div>
59864
+ ${quadrants.map((item) => `<article class="mv-risk-cell">
59865
+ ${item.label ? `<h3>${escapeHtml(item.label)}</h3>` : ""}
59866
+ <p>${escapeHtml(item.text)}</p>
59867
+ ${item.detail ? `<p class="mv-risk-detail">${escapeHtml(item.detail)}</p>` : ""}
59868
+ </article>`).join("\n")}
59869
+ </div>
59870
+ </section>`;
59871
+ }
59872
+ function renderChecklist(section, index) {
59873
+ return `<section class="mv-section mv-checklist">
59874
+ <div class="mv-section-head"><span>${sectionNumber(index)}</span><h2>${escapeHtml(section.title)}</h2></div>
59875
+ <ul class="mv-checklist-items">
59876
+ ${section.items.map((item) => `<li>
59877
+ <span class="mv-check-icon">${item.label === "done" ? "&#10003;" : "&#9744;"}</span>
59878
+ <span>${escapeHtml(item.text)}</span>
59879
+ </li>`).join("\n")}
59880
+ </ul>
59881
+ </section>`;
59882
+ }
59883
+ function renderVisualSection(section, index, style) {
59884
+ const visualHtml = renderSectionVisual(section.visual, style);
59885
+ const notes = section.notes ? Array.isArray(section.notes) ? section.notes.map((n) => `<li>${escapeHtml(n)}</li>`).join("") : `<li>${escapeHtml(section.notes)}</li>` : "";
59886
+ const notesBlock = notes ? `<ul class="mv-visual-notes">${notes}</ul>` : "";
59887
+ const safeItems = section.items ?? [];
59888
+ const items = safeItems.length > 0 ? `<div class="mv-section-head" style="margin-top:12px"><span>${sectionNumber(index)}</span><h2>${escapeHtml(section.title)}</h2></div>
59889
+ <ul class="mv-visual-support-items">${safeItems.map((item) => `<li>${escapeHtml(item.text)}</li>`).join("\n")}</ul>` : "";
59890
+ return `<section class="mv-section mv-visual-section">${visualHtml}${notesBlock}${items}</section>`;
59891
+ }
59003
59892
  function buildCss(style) {
59004
59893
  const palette = style === "minimal" ? { bg: "#ffffff", ink: "#161616", muted: "#666666", card: "#ffffff", accent: "#111111", line: "#d8d8d8" } : style === "executive" ? { bg: "#f4f0e8", ink: "#18212f", muted: "#667085", card: "#ffffff", accent: "#1f5d8c", line: "#d7c9b8" } : { bg: "#f7efe2", ink: "#241b16", muted: "#75695d", card: "#fffaf2", accent: "#b64f2a", line: "#dfcdb7" };
59005
59894
  return `
@@ -59017,24 +59906,183 @@ function buildCss(style) {
59017
59906
  h2 { margin: 0; font-size: 28px; line-height: 1.15; letter-spacing: 0; }
59018
59907
  .mv-fact-grid, .mv-comparison-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; }
59019
59908
  .mv-fact, .mv-comparison article { background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: 18px; }
59020
- .mv-fact strong { display: block; color: var(--accent); font-size: 30px; line-height: 1; margin-bottom: 12px; }
59021
- .mv-fact p, .mv-comparison p, .mv-timeline p { margin: 0; color: var(--muted); line-height: 1.5; }
59022
- .mv-comparison h3 { margin: 0 0 8px; font-size: 16px; }
59909
+ .mv-fact strong { display: block; color: var(--accent); font-size: 30px; line-height: 1; margin-bottom: 12px; overflow-wrap: break-word; word-break: break-word; }
59910
+ .mv-fact p, .mv-comparison p, .mv-timeline p { margin: 0; color: var(--muted); line-height: 1.5; overflow-wrap: break-word; word-break: break-word; }
59911
+ .mv-comparison h3 { margin: 0 0 8px; font-size: 16px; overflow-wrap: break-word; word-break: break-word; }
59023
59912
  .mv-timeline ol { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
59024
59913
  .mv-timeline li { display: grid; grid-template-columns: 150px 1fr; gap: 18px; align-items: start; background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: 16px; }
59025
59914
  .mv-timeline time { color: var(--accent); font-weight: 800; }
59026
59915
  .mv-quote blockquote { margin: 0; max-width: 900px; color: var(--ink); font-size: 34px; line-height: 1.25; font-weight: 750; }
59027
59916
  .mv-takeaways ul { margin: 0; padding-left: 22px; display: grid; gap: 10px; color: var(--muted); line-height: 1.55; }
59917
+ .mv-visual-section { padding: 34px 0; border-bottom: 1px solid var(--line); }
59918
+ .mv-visual-card { margin-bottom: 8px; }
59919
+ .mv-visual-label { margin: 0 0 8px; font-size: 14px; color: var(--muted); font-weight: 600; }
59920
+ .mv-visual-svg { margin: 0 0 4px; max-width: 100%; }
59921
+ .mv-visual-caption { margin: 4px 0 0; font-size: 12px; color: var(--muted); font-style: italic; }
59922
+ .mv-visual-notes { margin: 8px 0 0; padding-left: 18px; font-size: 13px; color: var(--muted); line-height: 1.5; }
59923
+ .mv-visual-support-items { margin: 8px 0 0; padding-left: 18px; font-size: 14px; color: var(--muted); line-height: 1.55; }
59924
+ .mv-visual-kpi-strip { display: flex; gap: 14px; flex-wrap: wrap; }
59925
+ .mv-visual-kpi { flex: 1; min-width: 140px; background: var(--card); border: 1px solid var(--line); border-top: 3px solid; border-radius: 6px; padding: 14px 16px; }
59926
+ .mv-visual-kpi strong { display: block; font-size: 26px; line-height: 1.1; color: var(--ink); }
59927
+ .mv-visual-kpi span { display: block; margin-top: 4px; font-size: 12px; color: var(--muted); }
59928
+ .mv-visual-unit { font-size: 14px; font-weight: 400; color: var(--muted); }
59929
+ .mv-visual-delta { font-size: 13px; font-weight: 600; }
59930
+ .mv-visual-process-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; background: rgba(255,255,255,0.62); padding: 16px; }
59931
+ .mv-visual-process-node { min-width: 0; border: 1px solid color-mix(in srgb, var(--node-color) 45%, var(--line)); background: color-mix(in srgb, var(--node-color) 10%, var(--card)); border-radius: 6px; padding: 14px; }
59932
+ .mv-visual-process-head { display: flex; align-items: center; gap: 10px; min-width: 0; }
59933
+ .mv-visual-process-head span { flex: 0 0 auto; width: 24px; height: 24px; display: grid; place-items: center; border-radius: 50%; background: color-mix(in srgb, var(--node-color) 16%, transparent); color: var(--node-color); font-size: 12px; font-weight: 800; }
59934
+ .mv-visual-process-head strong { min-width: 0; color: var(--node-color); font-size: 15px; line-height: 1.2; overflow-wrap: break-word; }
59935
+ .mv-visual-process-node p { margin: 12px 0 0; color: var(--muted); font-size: 14px; line-height: 1.45; overflow-wrap: break-word; }
59936
+ .mv-visual-ranked { display: grid; gap: 10px; background: rgba(255,255,255,0.62); padding: 16px; }
59937
+ .mv-visual-ranked-row { display: grid; grid-template-columns: 34px minmax(180px, 1.2fr) minmax(140px, 0.8fr) 64px; gap: 14px; align-items: center; min-width: 0; }
59938
+ .mv-visual-ranked-rank { color: color-mix(in srgb, var(--muted) 35%, transparent); font-size: 24px; font-weight: 800; line-height: 1; }
59939
+ .mv-visual-ranked-row p { margin: 0; color: var(--muted); font-size: 15px; line-height: 1.35; overflow-wrap: break-word; }
59940
+ .mv-visual-ranked-track { height: 18px; background: color-mix(in srgb, var(--line) 55%, transparent); border-radius: 3px; overflow: hidden; }
59941
+ .mv-visual-ranked-track span { display: block; height: 100%; min-width: 2px; border-radius: inherit; }
59942
+ .mv-visual-ranked-row strong { color: var(--muted); font-size: 15px; text-align: right; }
59943
+ .mv-process-steps { list-style: none; margin: 0; padding: 0; display: grid; gap: 16px; }
59944
+ .mv-process-steps li { display: flex; gap: 14px; align-items: flex-start; background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: 16px; }
59945
+ .mv-step-num { flex-shrink: 0; width: 28px; height: 28px; display: grid; place-items: center; background: var(--accent); color: #fff; border-radius: 50%; font-size: 13px; font-weight: 800; }
59946
+ .mv-process-steps li strong { display: block; margin-bottom: 4px; font-size: 16px; }
59947
+ .mv-process-steps li p { margin: 0; color: var(--muted); line-height: 1.5; }
59948
+ .mv-pros-cons-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
59949
+ .mv-pros-cons-grid h3 { margin: 0 0 10px; font-size: 18px; }
59950
+ .mv-pros-cons-grid ul { margin: 0; padding-left: 18px; display: grid; gap: 8px; color: var(--muted); line-height: 1.55; }
59951
+ .mv-pros-col h3 { color: #1a7d3a; }
59952
+ .mv-cons-col h3 { color: #c42e2e; }
59953
+ .mv-muted { color: var(--muted); font-style: italic; }
59954
+ .mv-stat-grid-items { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; }
59955
+ .mv-stat-card { background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: 20px; text-align: center; }
59956
+ .mv-stat-card strong { display: block; color: var(--accent); font-size: 32px; line-height: 1; margin-bottom: 10px; overflow-wrap: break-word; word-break: break-word; }
59957
+ .mv-stat-card p { margin: 0; color: var(--muted); font-size: 14px; line-height: 1.4; }
59958
+ .mv-risk-matrix-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
59959
+ .mv-risk-header { font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.08em; padding: 8px 10px; border-radius: 4px; }
59960
+ .mv-risk-hh { background: #fde8e8; color: #9b1c1c; }
59961
+ .mv-risk-hl { background: #fef3cd; color: #856404; }
59962
+ .mv-risk-lh { background: #e2f0d9; color: #2d6a2d; }
59963
+ .mv-risk-ll { background: #e8f0fe; color: #1a56db; }
59964
+ .mv-risk-cell { background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: 14px; }
59965
+ .mv-risk-cell h3 { margin: 0 0 6px; font-size: 15px; }
59966
+ .mv-risk-cell p { margin: 0; color: var(--muted); font-size: 14px; line-height: 1.5; }
59967
+ .mv-risk-detail { margin-top: 8px !important; font-size: 13px !important; opacity: 0.75; }
59968
+ .mv-checklist-items { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
59969
+ .mv-checklist-items li { display: flex; gap: 10px; align-items: flex-start; color: var(--muted); line-height: 1.55; }
59970
+ .mv-check-icon { flex-shrink: 0; width: 20px; text-align: center; color: var(--accent); font-size: 16px; }
59028
59971
  @media (max-width: 720px) {
59029
59972
  .mv-infographic { width: min(100% - 24px, 1120px); padding-top: 28px; }
59030
- h1 { font-size: 38px; }
59973
+ .mv-hero { min-height: auto; padding: 24px 0 36px; }
59974
+ h1 { font-size: 38px; word-break: break-word; }
59031
59975
  .mv-lead { font-size: 18px; }
59032
59976
  .mv-timeline li { grid-template-columns: 1fr; gap: 6px; }
59033
- .mv-quote blockquote { font-size: 25px; }
59977
+ .mv-pros-cons-grid, .mv-risk-matrix-grid { grid-template-columns: 1fr; }
59978
+ .mv-quote blockquote { font-size: 25px; word-break: break-word; }
59979
+ .mv-section-head { align-items: flex-start; }
59980
+ .mv-visual-svg { overflow-x: auto; -webkit-overflow-scrolling: touch; }
59981
+ .mv-visual-svg svg { min-width: 640px; }
59982
+ .mv-visual-process-grid, .mv-visual-ranked { padding: 10px; }
59983
+ .mv-visual-ranked-row { grid-template-columns: 26px 1fr 44px; gap: 10px; align-items: start; }
59984
+ .mv-visual-ranked-track { grid-column: 2 / -1; height: 12px; }
59985
+ .mv-visual-ranked-row p { font-size: 13px; }
59986
+ .mv-visual-ranked-rank { font-size: 18px; }
59987
+ .mv-visual-ranked-row strong { grid-column: 3; grid-row: 1; font-size: 13px; }
59034
59988
  }
59035
59989
  `;
59036
59990
  }
59037
59991
 
59992
+ // src/article-export.ts
59993
+ var import_node_fs4 = require("node:fs");
59994
+ var import_node_fs5 = require("node:fs");
59995
+ var import_node_path4 = require("node:path");
59996
+ var import_node_os = require("node:os");
59997
+ var playwrightModule = null;
59998
+ async function loadPlaywright() {
59999
+ if (playwrightModule) return { ok: true, value: playwrightModule };
60000
+ for (const mod of ["playwright", "playwright-core", "@playwright/test"]) {
60001
+ try {
60002
+ const pw = await import(mod);
60003
+ playwrightModule = pw;
60004
+ return { ok: true, value: pw };
60005
+ } catch {
60006
+ }
60007
+ }
60008
+ return agentError(
60009
+ "MISSING_PLAYWRIGHT",
60010
+ "Playwright is required for PNG/PDF export. Install it with: npm install --save-dev @playwright/test && npx playwright install chromium",
60011
+ { installHint: "npm install --save-dev @playwright/test && npx playwright install chromium" }
60012
+ );
60013
+ }
60014
+ async function exportInfographicToFile(html, format, outputPath) {
60015
+ const loaded = await loadPlaywright();
60016
+ if (!loaded.ok) return loaded;
60017
+ const pw = loaded.value;
60018
+ const dir = (0, import_node_fs5.mkdtempSync)((0, import_node_path4.join)((0, import_node_os.tmpdir)(), "miao-viz-export-"));
60019
+ const tempHtml = (0, import_node_path4.join)(dir, "source.html");
60020
+ (0, import_node_fs4.writeFileSync)(tempHtml, html, "utf8");
60021
+ try {
60022
+ const browser = await pw.chromium.launch();
60023
+ const page = await browser.newPage({ viewport: { width: 1120, height: 800 } });
60024
+ await page.goto(`file://${tempHtml}`, { waitUntil: "networkidle" });
60025
+ if (format === "png") {
60026
+ await page.screenshot({ path: outputPath, fullPage: true });
60027
+ } else {
60028
+ await page.pdf({ path: outputPath, format: "A4", printBackground: true });
60029
+ }
60030
+ await browser.close();
60031
+ return { ok: true, value: outputPath };
60032
+ } catch (error51) {
60033
+ return agentError(
60034
+ "EXPORT_FAILED",
60035
+ error51 instanceof Error ? error51.message : "Export failed for an unknown reason.",
60036
+ { format, outputPath }
60037
+ );
60038
+ }
60039
+ }
60040
+
60041
+ // src/infographic-quality.ts
60042
+ var SVG_VISUAL_TYPES = ["metric-bars", "process-flow", "concept-contrast", "timeline-path", "part-to-whole", "before-after", "tradeoff-matrix", "ranked-list-chart", "system-diagram", "callout-diagram", "icon-cluster"];
60043
+ var QUANTIFIED_VISUAL_TYPES = ["kpi-strip", "metric-bars", "part-to-whole", "ranked-list-chart"];
60044
+ function assessInfographicQuality(spec) {
60045
+ const sections = spec.sections;
60046
+ const visualSections = sections.filter((s) => s.visual);
60047
+ const textOnlySections = sections.filter((s) => !s.visual && s.type !== "hero" && s.type !== "quote");
60048
+ const visualComponentCount = visualSections.length;
60049
+ const svgVisualCount = visualSections.filter((s) => SVG_VISUAL_TYPES.includes(s.visual.type)).length;
60050
+ const quantifiedVisualCount = visualSections.filter((s) => QUANTIFIED_VISUAL_TYPES.includes(s.visual.type)).length;
60051
+ const totalWords = sections.reduce((sum, s) => {
60052
+ const itemWords = (s.items ?? []).reduce((w, i) => w + (i.text?.split(/\s+/).filter(Boolean).length ?? 0), 0);
60053
+ return sum + itemWords;
60054
+ }, 0);
60055
+ const averageWordsPerSection = sections.length > 0 ? Math.round(totalWords / sections.length) : 0;
60056
+ const warnings = [];
60057
+ if (visualComponentCount < 4) {
60058
+ warnings.push({ code: "low_visual_density", message: `Only ${visualComponentCount} visual components (recommended \u2265 4). Consider adding more visual sections.` });
60059
+ }
60060
+ if (svgVisualCount < 2) {
60061
+ warnings.push({ code: "text_heavy_infographic", message: `Only ${svgVisualCount} SVG visuals (recommended \u2265 2). Consider converting text sections to graphic components.` });
60062
+ }
60063
+ if (textOnlySections.length > 3) {
60064
+ warnings.push({ code: "text_heavy_infographic", message: `${textOnlySections.length} text-only sections exceed the recommended limit of 3.` });
60065
+ }
60066
+ const hasNumericClaims = sections.some(
60067
+ (s) => (s.items ?? []).some((i) => /\d/.test(i.text ?? ""))
60068
+ );
60069
+ if (hasNumericClaims && quantifiedVisualCount < 1) {
60070
+ warnings.push({ code: "numeric_claims_not_visualized", message: "Article has numeric claims but no quantified visual (kpi-strip or metric-bars)." });
60071
+ }
60072
+ const hasTimelineText = sections.some((s) => s.type === "timeline" && !s.visual);
60073
+ if (hasTimelineText) {
60074
+ warnings.push({ code: "timeline_rendered_as_text", message: "Timeline section is rendered as text list. Consider adding a timeline-path visual." });
60075
+ }
60076
+ const hasComparisonText = sections.some((s) => s.type === "comparison" && !s.visual);
60077
+ if (hasComparisonText) {
60078
+ warnings.push({ code: "comparison_rendered_as_text", message: "Comparison section is rendered as text cards. Consider adding a concept-contrast visual." });
60079
+ }
60080
+ if (averageWordsPerSection > 90) {
60081
+ warnings.push({ code: "text_heavy_infographic", message: `Average ${averageWordsPerSection} words per section (recommended \u2264 90). Sections are too text-heavy.` });
60082
+ }
60083
+ return { visualComponentCount, svgVisualCount, textOnlySectionCount: textOnlySections.length, quantifiedVisualCount, averageWordsPerSection, warnings };
60084
+ }
60085
+
59038
60086
  // src/report-block-registry.ts
59039
60087
  function scoreCatalogCoverage(chartTypes, ctx) {
59040
60088
  const allAvailable = chartTypes.every((t) => ctx.catalog.charts.includes(t));
@@ -60141,15 +61189,25 @@ Options:
60141
61189
  article: `Usage:
60142
61190
  miao-viz article <file> --output <file> [options]
60143
61191
  miao-viz article --spec-input <spec.json> --output <file> [options]
61192
+ miao-viz article analyze <file> [--output <context.json>]
60144
61193
 
60145
- Convert a local article into a static infographic artifact, or render a
60146
- pre-built InfographicSpec JSON directly.
61194
+ Convert a local article into a static infographic artifact, render a
61195
+ pre-built InfographicSpec JSON directly, or analyze an article for
61196
+ LLM-driven spec generation.
61197
+
61198
+ Subcommands:
61199
+ analyze Extract article structure (headings, sections,
61200
+ paragraphs, terms) for LLM consumption
60147
61201
 
60148
61202
  Options:
60149
- --output <file> Output file path (required)
60150
- --format <fmt> Output format: html, json, markdown (default: html)
61203
+ --output <file> Output file path (required for render, optional for analyze)
61204
+ --format <fmt> Output format: html, json, markdown, png, pdf (default: html)
60151
61205
  --style <name> Style: editorial, executive, minimal (default: editorial; ignored with --spec-input)
60152
61206
  --spec-input <file> Path to a pre-built InfographicSpec JSON file
61207
+ --strict-visuals Fail if visual density is below recommended thresholds
61208
+
61209
+ Note: png and pdf export requires Playwright. Install with:
61210
+ npm install --save-dev @playwright/test && npx playwright install chromium
60153
61211
  `,
60154
61212
  query: `Usage: miao-viz query <file> [options]
60155
61213
 
@@ -60195,8 +61253,8 @@ Run "miao-viz <command> --help" for command-specific options.
60195
61253
  var YAML3 = __toESM(require_dist(), 1);
60196
61254
 
60197
61255
  // src/cli-utils.ts
60198
- var import_node_fs3 = require("node:fs");
60199
- var import_node_path3 = require("node:path");
61256
+ var import_node_fs6 = require("node:fs");
61257
+ var import_node_path5 = require("node:path");
60200
61258
  var YAML2 = __toESM(require_dist(), 1);
60201
61259
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
60202
61260
  "h",
@@ -60258,8 +61316,8 @@ function formatOutputPath(output, ext, multiple) {
60258
61316
  return output;
60259
61317
  }
60260
61318
  function writeOutput(file2, content) {
60261
- (0, import_node_fs3.mkdirSync)((0, import_node_path3.dirname)(file2), { recursive: true });
60262
- (0, import_node_fs3.writeFileSync)(file2, content, "utf8");
61319
+ (0, import_node_fs6.mkdirSync)((0, import_node_path5.dirname)(file2), { recursive: true });
61320
+ (0, import_node_fs6.writeFileSync)(file2, content, "utf8");
60263
61321
  }
60264
61322
  function fail(error51) {
60265
61323
  process.exitCode = 1;
@@ -60270,12 +61328,12 @@ function printJson(value) {
60270
61328
  `);
60271
61329
  }
60272
61330
  function readSpec(file2) {
60273
- const text = (0, import_node_fs3.readFileSync)(file2, "utf8");
61331
+ const text = (0, import_node_fs6.readFileSync)(file2, "utf8");
60274
61332
  if (file2.endsWith(".json")) return JSON.parse(text);
60275
61333
  return YAML2.parse(text);
60276
61334
  }
60277
61335
  function readJson(file2) {
60278
- return JSON.parse((0, import_node_fs3.readFileSync)(file2, "utf8"));
61336
+ return JSON.parse((0, import_node_fs6.readFileSync)(file2, "utf8"));
60279
61337
  }
60280
61338
  function readProfile(file2) {
60281
61339
  const parsed = readJson(file2);
@@ -60565,7 +61623,7 @@ async function main() {
60565
61623
  return;
60566
61624
  }
60567
61625
  if (args.command === "article") {
60568
- printJson(runArticle(args));
61626
+ printJson(await runArticle(args));
60569
61627
  return;
60570
61628
  }
60571
61629
  if (args.command === "query") {
@@ -60798,34 +61856,61 @@ async function runAnalyze(args) {
60798
61856
  printJson(result);
60799
61857
  }
60800
61858
  }
60801
- function runArticle(args) {
61859
+ async function runArticle(args) {
61860
+ const firstPos = args.positional[0];
61861
+ if (firstPos === "analyze") {
61862
+ const file3 = args.positional[1];
61863
+ if (!file3) {
61864
+ return fail(agentError("MISSING_INPUT", "Usage: miao-viz article analyze <file> [--output <context.json>]"));
61865
+ }
61866
+ const result = analyzeArticle(file3);
61867
+ if (isAgentError(result)) return fail(result);
61868
+ const outputPath = stringFlag(args, "output");
61869
+ if (outputPath) {
61870
+ writeOutput(outputPath, `${JSON.stringify({ ok: true, value: result.value }, null, 2)}
61871
+ `);
61872
+ return { ok: true, value: { output: outputPath } };
61873
+ }
61874
+ return { ok: true, value: result.value };
61875
+ }
60802
61876
  const specInputPath = stringFlag(args, "spec-input");
61877
+ const strictVisuals = args.flags["strict-visuals"] === true;
60803
61878
  const output = requiredFlag(args, "output");
60804
61879
  if (isAgentError(output)) return fail(output);
60805
61880
  const formatFlag = stringFlag(args, "format");
60806
61881
  const format = parseArticleFormat(formatFlag);
60807
61882
  if (!format) {
60808
61883
  return fail(agentError("UNSUPPORTED_ARTICLE_FORMAT", `Unsupported article output format: ${formatFlag}`, {
60809
- supportedFormats: ["html", "json", "markdown"]
61884
+ supportedFormats: ["html", "json", "markdown", "png", "pdf"]
60810
61885
  }));
60811
61886
  }
60812
61887
  if (specInputPath) {
60813
61888
  const loaded = loadInfographicSpec(specInputPath);
60814
61889
  if (isAgentError(loaded)) return fail(loaded);
60815
61890
  const spec = loaded.value;
61891
+ const quality2 = assessInfographicQuality(spec);
61892
+ if (strictVisuals && quality2.warnings.length > 0) {
61893
+ return fail(agentError("STRICT_VISUALS_FAILED", "Visual density check failed. Fix warnings or remove --strict-visuals.", {
61894
+ warnings: quality2.warnings
61895
+ }));
61896
+ }
60816
61897
  if (format === "json") {
60817
61898
  writeOutput(output, `${JSON.stringify(spec, null, 2)}
60818
61899
  `);
60819
61900
  } else if (format === "markdown") {
60820
61901
  writeOutput(output, renderInfographicMarkdown(spec));
61902
+ } else if (format === "png" || format === "pdf") {
61903
+ const html = renderInfographicHtml(spec);
61904
+ const exported = await exportInfographicToFile(html, format, output);
61905
+ if (isAgentError(exported)) return fail(exported);
60821
61906
  } else {
60822
61907
  writeOutput(output, renderInfographicHtml(spec));
60823
61908
  }
60824
- return { ok: true, value: { output, format, style: spec.style, sections: spec.sections.map((s) => s.type) } };
61909
+ return { ok: true, value: { output, format, style: spec.style, sections: spec.sections.map((s) => s.type), warnings: quality2.warnings } };
60825
61910
  }
60826
61911
  const file2 = args.positional[0];
60827
61912
  if (!file2) {
60828
- return fail(agentError("MISSING_INPUT", "Usage: miao-viz article <file> --output <file> [--style editorial|executive|minimal] [--format html|json|markdown]\n miao-viz article --spec-input <spec.json> --output <file> [--format html|json|markdown]"));
61913
+ return fail(agentError("MISSING_INPUT", "Usage: miao-viz article <file> --output <file> [--style editorial|executive|minimal] [--format html|json|markdown|png|pdf]\n miao-viz article --spec-input <spec.json> --output <file> [--format html|json|markdown|png|pdf]"));
60829
61914
  }
60830
61915
  const styleFlag = stringFlag(args, "style");
60831
61916
  const style = parseArticleStyle(styleFlag);
@@ -60841,16 +61926,27 @@ function runArticle(args) {
60841
61926
  `);
60842
61927
  } else if (format === "markdown") {
60843
61928
  writeOutput(output, generated.value.markdown);
61929
+ } else if (format === "png" || format === "pdf") {
61930
+ const html = renderInfographicHtml(generated.value.spec);
61931
+ const exported = await exportInfographicToFile(html, format, output);
61932
+ if (isAgentError(exported)) return fail(exported);
60844
61933
  } else {
60845
61934
  writeOutput(output, renderInfographicHtml(generated.value.spec));
60846
61935
  }
61936
+ const quality = assessInfographicQuality(generated.value.spec);
61937
+ if (strictVisuals && quality.warnings.length > 0) {
61938
+ return fail(agentError("STRICT_VISUALS_FAILED", "Visual density check failed. Fix warnings or remove --strict-visuals.", {
61939
+ warnings: quality.warnings
61940
+ }));
61941
+ }
60847
61942
  return {
60848
61943
  ok: true,
60849
61944
  value: {
60850
61945
  output,
60851
61946
  format,
60852
61947
  style,
60853
- sections: generated.value.spec.sections.map((section) => section.type)
61948
+ sections: generated.value.spec.sections.map((section) => section.type),
61949
+ warnings: quality.warnings
60854
61950
  }
60855
61951
  };
60856
61952
  }