ghost-paper 0.3.2 → 0.3.4
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.
|
@@ -42,10 +42,11 @@ function columnIsNumeric(rows, colIndex) {
|
|
|
42
42
|
const numericCount = values.filter(isNumericValue).length;
|
|
43
43
|
return numericCount / values.length >= 0.7;
|
|
44
44
|
}
|
|
45
|
+
var SUMMARY_TERM = /^(total|full\s*year|average|median|overall|summary|net|grand)\b/i;
|
|
45
46
|
function columnIsTime(rows, colIndex) {
|
|
46
47
|
const values = rows.map((r) => r[colIndex]).filter(Boolean);
|
|
47
48
|
if (values.length === 0) return false;
|
|
48
|
-
const timeCount = values.filter(isTimeValue).length;
|
|
49
|
+
const timeCount = values.filter((v) => !SUMMARY_TERM.test(v.trim()) && isTimeValue(v)).length;
|
|
49
50
|
return timeCount / values.length >= 0.6;
|
|
50
51
|
}
|
|
51
52
|
function countNumericColumns(table) {
|
|
@@ -68,11 +69,8 @@ function classifyTable(table) {
|
|
|
68
69
|
else textCols.push(i);
|
|
69
70
|
}
|
|
70
71
|
if (textCols.length >= 1 && numericCols.length >= 1) {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
const hasValueHeader = /value|amount|number|result|total|figure/i.test(headerStr);
|
|
74
|
-
const hasChangeHeader = /change|delta|trend|vs|growth|yoy|qoq|mom|diff/i.test(headerStr);
|
|
75
|
-
if (hasMetricHeader || hasValueHeader && colCount <= 3 || hasChangeHeader && hasValueHeader) {
|
|
72
|
+
const firstHeaderIsMetric = /metric|name|label|kpi|measure|indicator/i.test(headers[0]);
|
|
73
|
+
if (firstHeaderIsMetric) {
|
|
76
74
|
return "kpi";
|
|
77
75
|
}
|
|
78
76
|
}
|
|
@@ -88,7 +86,7 @@ function classifyTable(table) {
|
|
|
88
86
|
}
|
|
89
87
|
}
|
|
90
88
|
const sparseRatio = emptyCells / totalDataCells;
|
|
91
|
-
if (numericAfterFirst >= 1 && sparseRatio < 0.25) return "line";
|
|
89
|
+
if (numericAfterFirst >= 1 && sparseRatio < 0.25 && rowCount > 3 && numericAfterFirst <= rowCount) return "line";
|
|
92
90
|
}
|
|
93
91
|
if (colCount >= 2 && rowCount >= 3) {
|
|
94
92
|
const firstIsText = !columnIsNumeric(rows, 0) && !columnIsTime(rows, 0);
|
|
@@ -266,12 +264,23 @@ function parse(markdown) {
|
|
|
266
264
|
}
|
|
267
265
|
}
|
|
268
266
|
}
|
|
267
|
+
let hintType = null;
|
|
268
|
+
let hintWide = false;
|
|
269
|
+
const prevNode = i > 0 ? nodes[i - 1] : null;
|
|
270
|
+
if (prevNode && prevNode.type === "html") {
|
|
271
|
+
const hintMatch = prevNode.value?.match(/<!--\s*paper:\s*(table|line|bar|pie|kpi)(\s+wide)?\s*-->/i);
|
|
272
|
+
if (hintMatch) {
|
|
273
|
+
hintType = hintMatch[1].toLowerCase();
|
|
274
|
+
hintWide = !!hintMatch[2];
|
|
275
|
+
}
|
|
276
|
+
}
|
|
269
277
|
const tableData = extractTable(node);
|
|
270
|
-
const chartType = classifyTable(tableData);
|
|
278
|
+
const chartType = hintType ?? classifyTable(tableData);
|
|
279
|
+
const wide = hintWide || void 0;
|
|
271
280
|
if (chartType === "kpi") {
|
|
272
281
|
currentTab.elements.push({ kind: "kpi", table: tableData });
|
|
273
282
|
} else if (chartType === "table") {
|
|
274
|
-
currentTab.elements.push({ kind: "table", table: tableData });
|
|
283
|
+
currentTab.elements.push({ kind: "table", table: tableData, wide });
|
|
275
284
|
} else {
|
|
276
285
|
const chartId = `chart-${chartCounter++}`;
|
|
277
286
|
currentTab.elements.push({
|
|
@@ -279,7 +288,8 @@ function parse(markdown) {
|
|
|
279
288
|
chartType,
|
|
280
289
|
table: tableData,
|
|
281
290
|
chartId,
|
|
282
|
-
caption
|
|
291
|
+
caption,
|
|
292
|
+
wide
|
|
283
293
|
});
|
|
284
294
|
}
|
|
285
295
|
continue;
|
|
@@ -961,6 +971,20 @@ var CSS = `
|
|
|
961
971
|
color: var(--text-primary);
|
|
962
972
|
}
|
|
963
973
|
|
|
974
|
+
/* Row-level tinting: charcoal = confirmed, chartreuse = forward-looking */
|
|
975
|
+
.tab-content tbody tr.row-actual td {
|
|
976
|
+
background: #E8E7E3;
|
|
977
|
+
}
|
|
978
|
+
.tab-content tbody tr.row-baseline td {
|
|
979
|
+
background: #F1F3E4;
|
|
980
|
+
}
|
|
981
|
+
.tab-content tbody tr.row-forecast td {
|
|
982
|
+
background: #ECF0DA;
|
|
983
|
+
}
|
|
984
|
+
.tab-content tbody tr.row-stretch td {
|
|
985
|
+
background: #E8EDCF;
|
|
986
|
+
}
|
|
987
|
+
|
|
964
988
|
hr.pagebreak {
|
|
965
989
|
border: none;
|
|
966
990
|
border-top: 1px solid var(--border);
|
|
@@ -1238,6 +1262,10 @@ var PRINT_CSS = `
|
|
|
1238
1262
|
border-radius: 4px;
|
|
1239
1263
|
}
|
|
1240
1264
|
.table-wrap.table-wide { column-span: all; }
|
|
1265
|
+
.table-wrap.table-wide table { font-size: 12px; }
|
|
1266
|
+
.table-wrap.table-wide thead th { font-size: 9px; }
|
|
1267
|
+
.table-wrap.table-wide tbody td { padding: 8px 12px; }
|
|
1268
|
+
.chart-wide { column-span: all; }
|
|
1241
1269
|
.tab-content table {
|
|
1242
1270
|
font-size: 10px;
|
|
1243
1271
|
margin: 0;
|
|
@@ -1254,6 +1282,19 @@ var PRINT_CSS = `
|
|
|
1254
1282
|
.tab-content tbody td { padding: 8px 10px; font-weight: 400; border-bottom: 1px solid #E8E8E4; }
|
|
1255
1283
|
.tab-content tbody tr:last-child td { border-bottom: none; }
|
|
1256
1284
|
|
|
1285
|
+
.tab-content tbody tr.row-actual td {
|
|
1286
|
+
background: #E5E4E0;
|
|
1287
|
+
}
|
|
1288
|
+
.tab-content tbody tr.row-baseline td {
|
|
1289
|
+
background: #EFF1E1;
|
|
1290
|
+
}
|
|
1291
|
+
.tab-content tbody tr.row-forecast td {
|
|
1292
|
+
background: #E9EDD7;
|
|
1293
|
+
}
|
|
1294
|
+
.tab-content tbody tr.row-stretch td {
|
|
1295
|
+
background: #E5EACC;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1257
1298
|
/* KPIs: compact grid for column width */
|
|
1258
1299
|
.kpi-strip {
|
|
1259
1300
|
margin: 12px 0; padding: 8px 0;
|
|
@@ -1274,7 +1315,7 @@ var PRINT_CSS = `
|
|
|
1274
1315
|
overflow: hidden;
|
|
1275
1316
|
}
|
|
1276
1317
|
figcaption { font-size: 10px; margin-top: 4px; }
|
|
1277
|
-
.chart-container { height:
|
|
1318
|
+
.chart-container { height: 200px; overflow: hidden; }
|
|
1278
1319
|
|
|
1279
1320
|
.kpi-strip { break-inside: avoid; page-break-inside: avoid; }
|
|
1280
1321
|
thead { display: table-header-group; }
|
|
@@ -1348,15 +1389,33 @@ function renderKpi(table) {
|
|
|
1348
1389
|
${kpis}
|
|
1349
1390
|
</div>`;
|
|
1350
1391
|
}
|
|
1351
|
-
function renderTable(table) {
|
|
1352
|
-
const
|
|
1392
|
+
function renderTable(table, wide) {
|
|
1393
|
+
const colCount = table.headers.length;
|
|
1394
|
+
const totalParts = 1.15 + (colCount - 1);
|
|
1395
|
+
const firstPct = (1.15 / totalParts * 100).toFixed(1);
|
|
1396
|
+
const otherPct = (1 / totalParts * 100).toFixed(1);
|
|
1397
|
+
const thead = table.headers.map((h, i) => {
|
|
1398
|
+
const w = i === 0 ? firstPct : otherPct;
|
|
1399
|
+
return `<th style="width:${w}%">${escapeHtml3(h)}</th>`;
|
|
1400
|
+
}).join("");
|
|
1353
1401
|
const tbody = table.rows.map((row) => {
|
|
1402
|
+
const firstCell = (row[0] || "").toLowerCase();
|
|
1403
|
+
let rowClass = "";
|
|
1404
|
+
if (/baseline/.test(firstCell)) {
|
|
1405
|
+
rowClass = ' class="row-baseline"';
|
|
1406
|
+
} else if (/stretch|\btarget\b/.test(firstCell)) {
|
|
1407
|
+
rowClass = ' class="row-stretch"';
|
|
1408
|
+
} else if (/forecast|projected|plan|budget|estimate/.test(firstCell)) {
|
|
1409
|
+
rowClass = ' class="row-forecast"';
|
|
1410
|
+
} else if (/actual/.test(firstCell)) {
|
|
1411
|
+
rowClass = ' class="row-actual"';
|
|
1412
|
+
}
|
|
1354
1413
|
const cells = row.map((cell) => {
|
|
1355
1414
|
return `<td>${escapeHtml3(cell)}</td>`;
|
|
1356
1415
|
}).join("");
|
|
1357
|
-
return `<tr>${cells}</tr>`;
|
|
1416
|
+
return `<tr${rowClass}>${cells}</tr>`;
|
|
1358
1417
|
}).join("\n ");
|
|
1359
|
-
const wideClass = table.headers.length >= 5 ? " table-wide" : "";
|
|
1418
|
+
const wideClass = wide || table.headers.length >= 5 ? " table-wide" : "";
|
|
1360
1419
|
return ` <div class="table-wrap${wideClass}"><table>
|
|
1361
1420
|
<thead><tr>${thead}</tr></thead>
|
|
1362
1421
|
<tbody>
|
|
@@ -1364,16 +1423,25 @@ function renderTable(table) {
|
|
|
1364
1423
|
</tbody>
|
|
1365
1424
|
</table></div>`;
|
|
1366
1425
|
}
|
|
1367
|
-
function renderChart(chartId, caption, chartType, rowCount) {
|
|
1426
|
+
function renderChart(chartId, caption, chartType, rowCount, seriesCount, wide) {
|
|
1368
1427
|
let styleAttr = "";
|
|
1369
1428
|
if (chartType === "bar" && rowCount) {
|
|
1370
1429
|
const height = Math.max(120, rowCount * 44 + 40);
|
|
1371
1430
|
styleAttr = ` style="height:${height}px"`;
|
|
1431
|
+
} else if (seriesCount && seriesCount > 0) {
|
|
1432
|
+
const height = Math.min(300, 200 + Math.max(0, seriesCount - 1) * 25);
|
|
1433
|
+
styleAttr = ` style="height:${height}px"`;
|
|
1372
1434
|
}
|
|
1373
|
-
|
|
1435
|
+
const figureHtml = ` <figure>
|
|
1374
1436
|
<div class="chart-container" id="${chartId}"${styleAttr}></div>
|
|
1375
1437
|
${caption ? `<figcaption>${escapeHtml3(caption)}</figcaption>` : ""}
|
|
1376
1438
|
</figure>`;
|
|
1439
|
+
if (wide) {
|
|
1440
|
+
return ` <div class="chart-wide">
|
|
1441
|
+
${figureHtml}
|
|
1442
|
+
</div>`;
|
|
1443
|
+
}
|
|
1444
|
+
return figureHtml;
|
|
1377
1445
|
}
|
|
1378
1446
|
function renderElement(el) {
|
|
1379
1447
|
switch (el.kind) {
|
|
@@ -1384,9 +1452,9 @@ function renderElement(el) {
|
|
|
1384
1452
|
case "kpi":
|
|
1385
1453
|
return renderKpi(el.table);
|
|
1386
1454
|
case "chart":
|
|
1387
|
-
return renderChart(el.chartId, el.caption, el.chartType, el.table.rows.length);
|
|
1455
|
+
return renderChart(el.chartId, el.caption, el.chartType, el.table.rows.length, el.table.headers.length - 1, el.wide);
|
|
1388
1456
|
case "table":
|
|
1389
|
-
return renderTable(el.table);
|
|
1457
|
+
return renderTable(el.table, el.wide);
|
|
1390
1458
|
case "aside":
|
|
1391
1459
|
return ` <aside>${el.html}</aside>`;
|
|
1392
1460
|
case "pagebreak":
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
build
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-NWFECGNX.js";
|
|
5
5
|
|
|
6
6
|
// src/cli.ts
|
|
7
7
|
import { Command } from "commander";
|
|
@@ -25,24 +25,28 @@ program.command("prompt").description("Print agent instructions for writing Ghos
|
|
|
25
25
|
- Use # H1 headings to separate major sections (each becomes a tab in the sidebar)
|
|
26
26
|
- Put a blockquote immediately after an H1 to set that tab's subtitle
|
|
27
27
|
- Use ## H2 headings for sections within a tab
|
|
28
|
+
- Use --- (horizontal rule) to force a page break in PDF output
|
|
28
29
|
- Use blockquotes for key insight callouts
|
|
29
30
|
|
|
30
31
|
## Tables \u2192 Charts
|
|
31
32
|
|
|
32
33
|
Write standard markdown tables. Ghost Paper auto-classifies them:
|
|
33
34
|
|
|
34
|
-
**KPI strip** \u2014 2\u20136 rows, 2\u20134 columns
|
|
35
|
+
**KPI strip** \u2014 2\u20136 rows, 2\u20134 columns where the first column header matches a metric/label pattern (e.g. Metric, Name, Label, KPI, Indicator):
|
|
35
36
|
| Metric | Value | Change |
|
|
36
37
|
|--------|-------|--------|
|
|
37
38
|
| Revenue | $4.2M | +23% YoY |
|
|
38
39
|
| Users | 214K | +18% vs Q3 |
|
|
39
40
|
|
|
40
|
-
**Line chart** \u2014 first column is time (months, quarters, years),
|
|
41
|
-
|
|
|
42
|
-
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
|
|
|
41
|
+
**Line chart** \u2014 first column is time-based (months, quarters, years, FY), 4+ data rows, numeric series count \u2264 row count, and no sparse data:
|
|
42
|
+
| Quarter | Revenue ($M) | Users (K) |
|
|
43
|
+
|---------|-------------|-----------|
|
|
44
|
+
| Q1 FY25 | 3.2 | 142 |
|
|
45
|
+
| Q2 FY25 | 3.5 | 148 |
|
|
46
|
+
| Q3 FY25 | 3.8 | 155 |
|
|
47
|
+
| Q4 FY25 | 4.2 | 163 |
|
|
48
|
+
|
|
49
|
+
Note: summary rows like "Total" or "Full Year" in the first column are ignored when detecting time-based columns.
|
|
46
50
|
|
|
47
51
|
**Bar chart** \u2014 first column is categories, one numeric column, 3+ rows:
|
|
48
52
|
| Channel | CPA ($) |
|
|
@@ -57,7 +61,21 @@ Write standard markdown tables. Ghost Paper auto-classifies them:
|
|
|
57
61
|
| Enterprise | 60 |
|
|
58
62
|
| SMB | 40 |
|
|
59
63
|
|
|
60
|
-
Any table that doesn't match these patterns renders as a styled HTML table
|
|
64
|
+
Any table that doesn't match these patterns renders as a styled HTML table.
|
|
65
|
+
|
|
66
|
+
## Manual Override Hints
|
|
67
|
+
|
|
68
|
+
Place an HTML comment immediately before a table to force a specific rendering:
|
|
69
|
+
|
|
70
|
+
<!-- paper: table --> \u2014 force table (skip chart classification)
|
|
71
|
+
<!-- paper: bar --> \u2014 force bar chart
|
|
72
|
+
<!-- paper: line --> \u2014 force line chart
|
|
73
|
+
<!-- paper: pie --> \u2014 force pie chart
|
|
74
|
+
<!-- paper: kpi --> \u2014 force KPI strip
|
|
75
|
+
<!-- paper: bar wide --> \u2014 force bar chart, full-width in PDF (spans both columns)
|
|
76
|
+
<!-- paper: table wide --> \u2014 force table, full-width in PDF
|
|
77
|
+
|
|
78
|
+
The "wide" flag makes the element span both columns in the two-column PDF layout.`);
|
|
61
79
|
});
|
|
62
80
|
var buildCmd = program.command("build").description("Convert a markdown file to HTML or PDF");
|
|
63
81
|
buildCmd.command("html").description("Convert a markdown file to an HTML report").argument("<input>", "Markdown file path").option("-o, --output <path>", "Output HTML file path").action((input, opts) => {
|
|
@@ -72,7 +90,7 @@ buildCmd.command("pdf").description("Convert a markdown file to a PDF report").a
|
|
|
72
90
|
const inputPath = resolve(input);
|
|
73
91
|
const markdown = readFileSync(inputPath, "utf-8");
|
|
74
92
|
const outputPath = opts.output ? resolve(opts.output) : inputPath.replace(/\.md$/, ".pdf");
|
|
75
|
-
const { buildPdf } = await import("./-
|
|
93
|
+
const { buildPdf } = await import("./-5C6AORSD.js");
|
|
76
94
|
await buildPdf(markdown, outputPath, {
|
|
77
95
|
pageSize: opts.pageSize ?? "A4",
|
|
78
96
|
landscape: opts.landscape ?? false
|