@miao-vision/cli 0.1.10 → 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.
Files changed (2) hide show
  1. package/dist/cli.cjs +1107 -75
  2. package/package.json +1 -1
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;
@@ -58754,29 +58754,203 @@ function hintForIssue(path, message) {
58754
58754
  return `Check ${path} in the DeckSpec.`;
58755
58755
  }
58756
58756
 
58757
- // src/article-infographic.ts
58757
+ // src/article-analyzer.ts
58758
58758
  var import_node_fs2 = require("node:fs");
58759
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");
58760
58903
  var ARTICLE_STYLES = ["editorial", "executive", "minimal"];
58761
- var ARTICLE_FORMATS = ["html", "json", "markdown"];
58904
+ var ARTICLE_FORMATS = ["html", "json", "markdown", "png", "pdf"];
58762
58905
  var infographicSectionItemSchema = external_exports.object({
58763
58906
  label: external_exports.string().optional(),
58764
58907
  value: external_exports.string().optional(),
58765
58908
  text: external_exports.string().min(1, "item.text must not be empty"),
58766
58909
  detail: external_exports.string().optional()
58767
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
+ });
58768
58929
  var infographicSectionSchema = external_exports.object({
58769
- 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
+ ]),
58770
58943
  title: external_exports.string().min(1, "section.title must not be empty"),
58771
58944
  items: external_exports.array(infographicSectionItemSchema).min(1, "section.items must have at least one item"),
58772
58945
  emphasis: external_exports.string().optional(),
58773
- notes: external_exports.string().optional()
58946
+ notes: external_exports.union([external_exports.string(), external_exports.array(external_exports.string())]).optional(),
58947
+ visual: infographicVisualSchema.optional()
58774
58948
  });
58775
58949
  var infographicSpecSchema = external_exports.object({
58776
58950
  title: external_exports.string().min(1, "title must not be empty"),
58777
58951
  subtitle: external_exports.string().optional(),
58778
58952
  source: external_exports.string().optional(),
58779
- 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"),
58780
58954
  summary: external_exports.string().min(1, "summary must not be empty"),
58781
58955
  sections: external_exports.array(infographicSectionSchema).min(1, "sections must have at least one entry"),
58782
58956
  metadata: external_exports.object({
@@ -58788,7 +58962,7 @@ var infographicSpecSchema = external_exports.object({
58788
58962
  function loadInfographicSpec(file2) {
58789
58963
  let raw;
58790
58964
  try {
58791
- raw = JSON.parse((0, import_node_fs2.readFileSync)(file2, "utf8"));
58965
+ raw = JSON.parse((0, import_node_fs3.readFileSync)(file2, "utf8"));
58792
58966
  } catch (error51) {
58793
58967
  return agentError("ARTICLE_INPUT_UNREADABLE", error51 instanceof Error ? error51.message : "Spec file could not be read.", { file: file2 });
58794
58968
  }
@@ -58813,7 +58987,7 @@ function parseArticleFormat(value) {
58813
58987
  return ARTICLE_FORMATS.includes(value) ? value : void 0;
58814
58988
  }
58815
58989
  function generateInfographicFromFile(file2, style) {
58816
- const extension = (0, import_node_path2.extname)(file2).toLowerCase();
58990
+ const extension = (0, import_node_path3.extname)(file2).toLowerCase();
58817
58991
  if (extension && ![".md", ".markdown", ".txt"].includes(extension)) {
58818
58992
  return agentError("UNSUPPORTED_ARTICLE_INPUT", "Article input must be a Markdown or plain-text file.", {
58819
58993
  supportedExtensions: [".md", ".markdown", ".txt"]
@@ -58821,7 +58995,7 @@ function generateInfographicFromFile(file2, style) {
58821
58995
  }
58822
58996
  let raw;
58823
58997
  try {
58824
- raw = (0, import_node_fs2.readFileSync)(file2, "utf8");
58998
+ raw = (0, import_node_fs3.readFileSync)(file2, "utf8");
58825
58999
  } catch (error51) {
58826
59000
  return agentError("ARTICLE_INPUT_UNREADABLE", error51 instanceof Error ? error51.message : "Article input could not be read.", {
58827
59001
  file: file2
@@ -58840,16 +59014,17 @@ function normalizeArticleText(raw) {
58840
59014
  }
58841
59015
  function parseArticle(text, file2) {
58842
59016
  const lines = text.split("\n");
58843
- const metadata = extractMetadata(lines);
58844
- const title = findTitle(lines) ?? titleFromFilename(file2);
58845
- 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) => {
58846
59021
  const trimmed = line.trim();
58847
- 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);
58848
59023
  });
58849
- const quotes = contentLines.filter((line) => line.trim().startsWith(">")).map((line) => cleanMarkdown(line.replace(/^>\s?/, ""))).filter(Boolean);
58850
- const listItems = contentLines.filter((line) => line.trim().match(/^[-*+]\s+/) || line.trim().match(/^\d+\.\s+/)).map((line) => cleanMarkdown(line.replace(/^\s*(?:[-*+]|\d+\.)\s+/, ""))).filter(Boolean);
58851
- const tableRows = extractTableRows(contentLines);
58852
- 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+/));
58853
59028
  return {
58854
59029
  title,
58855
59030
  subtitle: metadata.subtitle ?? firstUsefulParagraph(paragraphs),
@@ -58860,12 +59035,68 @@ function parseArticle(text, file2) {
58860
59035
  tableRows
58861
59036
  };
58862
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
+ }
58863
59093
  function buildInfographicSpec(parsed, style, file2) {
58864
59094
  const evidence = [...parsed.listItems, ...sentences(parsed.paragraphs.join(" "))];
58865
59095
  const facts = collectFacts(evidence);
58866
59096
  const timeline = collectTimeline(evidence);
58867
59097
  const comparison = collectComparison(evidence, parsed.tableRows);
58868
59098
  const takeaways = collectTakeaways(evidence, facts);
59099
+ const process3 = detectProcess(parsed.listItems, evidence);
58869
59100
  const summary = parsed.subtitle ?? takeaways[0]?.text ?? facts[0]?.text ?? "A concise visual summary of the source article.";
58870
59101
  const sections = [
58871
59102
  {
@@ -58875,9 +59106,28 @@ function buildInfographicSpec(parsed, style, file2) {
58875
59106
  items: [{ text: summary }]
58876
59107
  }
58877
59108
  ];
58878
- if (facts.length > 0) sections.push({ type: "facts", title: "Key Facts", items: facts.slice(0, 6) });
58879
- if (timeline.length > 1) sections.push({ type: "timeline", title: "Timeline", items: timeline.slice(0, 6) });
58880
- 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
+ }
58881
59131
  if (parsed.quotes.length > 0) {
58882
59132
  sections.push({
58883
59133
  type: "quote",
@@ -58916,25 +59166,35 @@ function renderInfographicMarkdown(spec) {
58916
59166
  }
58917
59167
  return lines.join("\n").trimEnd() + "\n";
58918
59168
  }
58919
- 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) {
58920
59177
  const sourceLine = lines.find((line) => line.match(/^(source|url):\s*/i));
58921
59178
  const subtitleLine = lines.find((line) => line.match(/^subtitle:\s*/i));
59179
+ const titleLine = lines.find((line) => line.match(/^title:\s*/i));
58922
59180
  return {
58923
- source: sourceLine?.replace(/^(source|url):\s*/i, "").trim(),
58924
- 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()
58925
59184
  };
58926
59185
  }
58927
- function findTitle(lines) {
59186
+ function findTitle2(lines, frontmatterTitle) {
59187
+ if (frontmatterTitle) return frontmatterTitle;
58928
59188
  const heading = lines.find((line) => line.trim().match(/^#\s+\S/));
58929
- if (heading) return cleanMarkdown(heading.replace(/^#\s+/, ""));
58930
- const first = lines.find((line) => line.trim() && !line.match(/^(source|url|author|date):\s*/i));
58931
- 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;
58932
59192
  }
58933
59193
  function titleFromFilename(file2) {
58934
- 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());
58935
59195
  }
58936
- function extractTableRows(lines) {
58937
- 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);
58938
59198
  }
58939
59199
  function collectFacts(candidates) {
58940
59200
  return uniqueItems(candidates.filter((text) => NUMBER_PATTERN.test(text)).map((text) => ({
@@ -58962,7 +59222,7 @@ function collectTakeaways(candidates, facts) {
58962
59222
  return facts.slice(0, 3).map((item) => ({ text: item.text }));
58963
59223
  }
58964
59224
  function sentences(text) {
58965
- return text.split(/(?<=[.!?。!?])\s+/).map(cleanMarkdown).filter((sentence) => sentence.length > 20);
59225
+ return text.split(/(?<=[.!?。!?])\s+/).map(cleanMarkdown2).filter((sentence) => sentence.length > 20);
58966
59226
  }
58967
59227
  function firstUsefulParagraph(paragraphs) {
58968
59228
  return paragraphs.find((paragraph) => paragraph.length > 40)?.slice(0, 220);
@@ -58977,15 +59237,493 @@ function uniqueItems(items) {
58977
59237
  });
58978
59238
  }
58979
59239
  function compactText(text, max) {
58980
- const clean = cleanMarkdown(text);
59240
+ const clean = cleanMarkdown2(text);
58981
59241
  return clean.length > max ? `${clean.slice(0, max - 1).trim()}...` : clean;
58982
59242
  }
58983
- function cleanMarkdown(value) {
59243
+ function cleanMarkdown2(value) {
58984
59244
  return value.replace(/`([^`]+)`/g, "$1").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/^#+\s*/, "").replace(/\s+/g, " ").trim();
58985
59245
  }
58986
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
+
58987
59713
  // src/article-html.ts
58988
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");
58989
59727
  return `<!doctype html>
58990
59728
  <html lang="en">
58991
59729
  <head>
@@ -58998,19 +59736,25 @@ function renderInfographicHtml(spec) {
58998
59736
  </head>
58999
59737
  <body>
59000
59738
  <main class="mv-infographic mv-infographic-${spec.style}">
59001
- ${spec.sections.map(renderSection).join("\n")}
59739
+ ${renderedSections}
59002
59740
  </main>
59003
59741
  <script type="application/json" id="miao-infographic-spec">${escapeHtml(JSON.stringify(spec, null, 2))}</script>
59004
59742
  </body>
59005
59743
  </html>`;
59006
59744
  }
59007
- function renderSection(section) {
59008
- if (section.type === "hero") return renderHero(section);
59009
- if (section.type === "facts") return renderFacts(section);
59010
- if (section.type === "timeline") return renderTimeline(section);
59011
- if (section.type === "comparison") return renderComparison(section);
59012
- if (section.type === "quote") return renderQuote(section);
59013
- 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);
59014
59758
  }
59015
59759
  function renderHero(section) {
59016
59760
  const lead = section.emphasis ?? section.items[0]?.text ?? "";
@@ -59020,9 +59764,9 @@ function renderHero(section) {
59020
59764
  <p class="mv-lead">${escapeHtml(lead)}</p>
59021
59765
  </section>`;
59022
59766
  }
59023
- function renderFacts(section) {
59767
+ function renderFacts(section, index) {
59024
59768
  return `<section class="mv-section mv-facts">
59025
- <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>
59026
59770
  <div class="mv-fact-grid">
59027
59771
  ${section.items.map((item) => `<article class="mv-fact">
59028
59772
  ${item.value ? `<strong>${escapeHtml(item.value)}</strong>` : ""}
@@ -59031,17 +59775,17 @@ function renderFacts(section) {
59031
59775
  </div>
59032
59776
  </section>`;
59033
59777
  }
59034
- function renderTimeline(section) {
59778
+ function renderTimeline(section, index) {
59035
59779
  return `<section class="mv-section mv-timeline">
59036
- <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>
59037
59781
  <ol>
59038
59782
  ${section.items.map((item) => `<li><time>${escapeHtml(item.label ?? "")}</time><p>${escapeHtml(item.text)}</p></li>`).join("\n")}
59039
59783
  </ol>
59040
59784
  </section>`;
59041
59785
  }
59042
- function renderComparison(section) {
59786
+ function renderComparison(section, index) {
59043
59787
  return `<section class="mv-section mv-comparison">
59044
- <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>
59045
59789
  <div class="mv-comparison-grid">
59046
59790
  ${section.items.map((item) => `<article>
59047
59791
  ${item.label ? `<h3>${escapeHtml(item.label)}</h3>` : ""}
@@ -59056,14 +59800,95 @@ function renderQuote(section) {
59056
59800
  <blockquote>${escapeHtml(quote)}</blockquote>
59057
59801
  </section>`;
59058
59802
  }
59059
- function renderTakeaways(section) {
59803
+ function renderTakeaways(section, index) {
59060
59804
  return `<section class="mv-section mv-takeaways">
59061
- <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>
59062
59806
  <ul>
59063
59807
  ${section.items.map((item) => `<li>${escapeHtml(item.text)}</li>`).join("\n")}
59064
59808
  </ul>
59065
59809
  </section>`;
59066
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
+ }
59067
59892
  function buildCss(style) {
59068
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" };
59069
59894
  return `
@@ -59081,24 +59906,183 @@ function buildCss(style) {
59081
59906
  h2 { margin: 0; font-size: 28px; line-height: 1.15; letter-spacing: 0; }
59082
59907
  .mv-fact-grid, .mv-comparison-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; }
59083
59908
  .mv-fact, .mv-comparison article { background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: 18px; }
59084
- .mv-fact strong { display: block; color: var(--accent); font-size: 30px; line-height: 1; margin-bottom: 12px; }
59085
- .mv-fact p, .mv-comparison p, .mv-timeline p { margin: 0; color: var(--muted); line-height: 1.5; }
59086
- .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; }
59087
59912
  .mv-timeline ol { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
59088
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; }
59089
59914
  .mv-timeline time { color: var(--accent); font-weight: 800; }
59090
59915
  .mv-quote blockquote { margin: 0; max-width: 900px; color: var(--ink); font-size: 34px; line-height: 1.25; font-weight: 750; }
59091
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; }
59092
59971
  @media (max-width: 720px) {
59093
59972
  .mv-infographic { width: min(100% - 24px, 1120px); padding-top: 28px; }
59094
- h1 { font-size: 38px; }
59973
+ .mv-hero { min-height: auto; padding: 24px 0 36px; }
59974
+ h1 { font-size: 38px; word-break: break-word; }
59095
59975
  .mv-lead { font-size: 18px; }
59096
59976
  .mv-timeline li { grid-template-columns: 1fr; gap: 6px; }
59097
- .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; }
59098
59988
  }
59099
59989
  `;
59100
59990
  }
59101
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
+
59102
60086
  // src/report-block-registry.ts
59103
60087
  function scoreCatalogCoverage(chartTypes, ctx) {
59104
60088
  const allAvailable = chartTypes.every((t) => ctx.catalog.charts.includes(t));
@@ -60205,15 +61189,25 @@ Options:
60205
61189
  article: `Usage:
60206
61190
  miao-viz article <file> --output <file> [options]
60207
61191
  miao-viz article --spec-input <spec.json> --output <file> [options]
61192
+ miao-viz article analyze <file> [--output <context.json>]
60208
61193
 
60209
- Convert a local article into a static infographic artifact, or render a
60210
- 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
60211
61201
 
60212
61202
  Options:
60213
- --output <file> Output file path (required)
60214
- --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)
60215
61205
  --style <name> Style: editorial, executive, minimal (default: editorial; ignored with --spec-input)
60216
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
60217
61211
  `,
60218
61212
  query: `Usage: miao-viz query <file> [options]
60219
61213
 
@@ -60259,8 +61253,8 @@ Run "miao-viz <command> --help" for command-specific options.
60259
61253
  var YAML3 = __toESM(require_dist(), 1);
60260
61254
 
60261
61255
  // src/cli-utils.ts
60262
- var import_node_fs3 = require("node:fs");
60263
- var import_node_path3 = require("node:path");
61256
+ var import_node_fs6 = require("node:fs");
61257
+ var import_node_path5 = require("node:path");
60264
61258
  var YAML2 = __toESM(require_dist(), 1);
60265
61259
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
60266
61260
  "h",
@@ -60322,8 +61316,8 @@ function formatOutputPath(output, ext, multiple) {
60322
61316
  return output;
60323
61317
  }
60324
61318
  function writeOutput(file2, content) {
60325
- (0, import_node_fs3.mkdirSync)((0, import_node_path3.dirname)(file2), { recursive: true });
60326
- (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");
60327
61321
  }
60328
61322
  function fail(error51) {
60329
61323
  process.exitCode = 1;
@@ -60334,12 +61328,12 @@ function printJson(value) {
60334
61328
  `);
60335
61329
  }
60336
61330
  function readSpec(file2) {
60337
- const text = (0, import_node_fs3.readFileSync)(file2, "utf8");
61331
+ const text = (0, import_node_fs6.readFileSync)(file2, "utf8");
60338
61332
  if (file2.endsWith(".json")) return JSON.parse(text);
60339
61333
  return YAML2.parse(text);
60340
61334
  }
60341
61335
  function readJson(file2) {
60342
- return JSON.parse((0, import_node_fs3.readFileSync)(file2, "utf8"));
61336
+ return JSON.parse((0, import_node_fs6.readFileSync)(file2, "utf8"));
60343
61337
  }
60344
61338
  function readProfile(file2) {
60345
61339
  const parsed = readJson(file2);
@@ -60629,7 +61623,7 @@ async function main() {
60629
61623
  return;
60630
61624
  }
60631
61625
  if (args.command === "article") {
60632
- printJson(runArticle(args));
61626
+ printJson(await runArticle(args));
60633
61627
  return;
60634
61628
  }
60635
61629
  if (args.command === "query") {
@@ -60862,34 +61856,61 @@ async function runAnalyze(args) {
60862
61856
  printJson(result);
60863
61857
  }
60864
61858
  }
60865
- 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
+ }
60866
61876
  const specInputPath = stringFlag(args, "spec-input");
61877
+ const strictVisuals = args.flags["strict-visuals"] === true;
60867
61878
  const output = requiredFlag(args, "output");
60868
61879
  if (isAgentError(output)) return fail(output);
60869
61880
  const formatFlag = stringFlag(args, "format");
60870
61881
  const format = parseArticleFormat(formatFlag);
60871
61882
  if (!format) {
60872
61883
  return fail(agentError("UNSUPPORTED_ARTICLE_FORMAT", `Unsupported article output format: ${formatFlag}`, {
60873
- supportedFormats: ["html", "json", "markdown"]
61884
+ supportedFormats: ["html", "json", "markdown", "png", "pdf"]
60874
61885
  }));
60875
61886
  }
60876
61887
  if (specInputPath) {
60877
61888
  const loaded = loadInfographicSpec(specInputPath);
60878
61889
  if (isAgentError(loaded)) return fail(loaded);
60879
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
+ }
60880
61897
  if (format === "json") {
60881
61898
  writeOutput(output, `${JSON.stringify(spec, null, 2)}
60882
61899
  `);
60883
61900
  } else if (format === "markdown") {
60884
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);
60885
61906
  } else {
60886
61907
  writeOutput(output, renderInfographicHtml(spec));
60887
61908
  }
60888
- 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 } };
60889
61910
  }
60890
61911
  const file2 = args.positional[0];
60891
61912
  if (!file2) {
60892
- 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]"));
60893
61914
  }
60894
61915
  const styleFlag = stringFlag(args, "style");
60895
61916
  const style = parseArticleStyle(styleFlag);
@@ -60905,16 +61926,27 @@ function runArticle(args) {
60905
61926
  `);
60906
61927
  } else if (format === "markdown") {
60907
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);
60908
61933
  } else {
60909
61934
  writeOutput(output, renderInfographicHtml(generated.value.spec));
60910
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
+ }
60911
61942
  return {
60912
61943
  ok: true,
60913
61944
  value: {
60914
61945
  output,
60915
61946
  format,
60916
61947
  style,
60917
- sections: generated.value.spec.sections.map((section) => section.type)
61948
+ sections: generated.value.spec.sections.map((section) => section.type),
61949
+ warnings: quality.warnings
60918
61950
  }
60919
61951
  };
60920
61952
  }