@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 +1202 -106
- package/dist/examples/campaign-data.csv +33 -0
- package/dist/examples/campaign-report-deck.yaml +115 -0
- package/dist/examples/executive-overview-deck.yaml +137 -0
- package/dist/examples/executive-overview.csv +37 -0
- package/examples/campaign-data.csv +33 -0
- package/examples/campaign-report-deck.yaml +115 -0
- package/examples/executive-overview-deck.yaml +137 -0
- package/examples/executive-overview.csv +37 -0
- package/package.json +1 -1
- package/dist/examples/sales-dashboard.yaml +0 -54
- package/examples/sales-dashboard.yaml +0 -54
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
|
|
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
|
|
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 =
|
|
33224
|
-
XLSX3.readFileSync =
|
|
33223
|
+
XLSX3.readFile = readFileSync5;
|
|
33224
|
+
XLSX3.readFileSync = readFileSync5;
|
|
33225
33225
|
XLSX3.write = writeSync;
|
|
33226
|
-
XLSX3.writeFile =
|
|
33227
|
-
XLSX3.writeFileSync =
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
${
|
|
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
|
|
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-
|
|
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.
|
|
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.
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
58780
|
-
const
|
|
58781
|
-
const
|
|
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) =>
|
|
58786
|
-
const listItems = contentLines.filter((line) => line.trim().match(/^[-*+]\s+/) || line.trim().match(/^\d+\.\s+/)).map((line) =>
|
|
58787
|
-
const tableRows =
|
|
58788
|
-
const paragraphs = contentLines.join("\n").split(/\n{2,}/).map((block) =>
|
|
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 (
|
|
58815
|
-
|
|
58816
|
-
|
|
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
|
|
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
|
|
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
|
|
58866
|
-
const first = lines.find((line) => line.trim() && !line.match(/^(source|url|author|date):\s*/i));
|
|
58867
|
-
return first ?
|
|
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,
|
|
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
|
|
58873
|
-
return lines.filter((line) => line.includes("|") && !line.match(/^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$/)).map((line) => line.split("|").map((cell) =>
|
|
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(
|
|
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 =
|
|
59240
|
+
const clean = cleanMarkdown2(text);
|
|
58917
59241
|
return clean.length > max ? `${clean.slice(0, max - 1).trim()}...` : clean;
|
|
58918
59242
|
}
|
|
58919
|
-
function
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
-
${
|
|
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
|
|
58944
|
-
|
|
58945
|
-
|
|
58946
|
-
|
|
58947
|
-
if (section.type === "
|
|
58948
|
-
if (section.type === "
|
|
58949
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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" ? "✓" : "☐"}</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
|
-
|
|
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-
|
|
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,
|
|
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
|
|
60199
|
-
var
|
|
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,
|
|
60262
|
-
(0,
|
|
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,
|
|
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,
|
|
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
|
}
|