ghost-paper 0.3.1 → 0.3.3
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;
|
|
@@ -1138,31 +1148,26 @@ var PRINT_CSS = `
|
|
|
1138
1148
|
.sidebar, .mobile-header, .mobile-dropdown, .mobile-overlay { display: none !important; }
|
|
1139
1149
|
.main { margin-left: 0; min-height: auto; }
|
|
1140
1150
|
|
|
1141
|
-
/* \u2500\u2500 Cover page \u2014 full bleed dark background \u2500\u2500 */
|
|
1151
|
+
/* \u2500\u2500 Cover page \u2014 full bleed dark background, content at bottom \u2500\u2500 */
|
|
1142
1152
|
.print-header {
|
|
1143
1153
|
min-height: 100vh;
|
|
1144
1154
|
display: flex;
|
|
1145
1155
|
flex-direction: column;
|
|
1146
|
-
justify-content: flex-
|
|
1156
|
+
justify-content: flex-end;
|
|
1147
1157
|
background: var(--charcoal);
|
|
1148
1158
|
margin: -0.7in -0.65in 0;
|
|
1149
|
-
padding: calc(0.
|
|
1159
|
+
padding: 56px calc(0.65in + 48px) calc(0.7in + 56px);
|
|
1150
1160
|
page-break-after: always;
|
|
1151
1161
|
break-after: page;
|
|
1152
1162
|
}
|
|
1153
1163
|
.print-title {
|
|
1154
|
-
font-size:
|
|
1164
|
+
font-size: 72px;
|
|
1155
1165
|
font-weight: 700;
|
|
1156
|
-
letter-spacing: -
|
|
1166
|
+
letter-spacing: -3px;
|
|
1157
1167
|
color: #fff;
|
|
1158
|
-
padding-bottom:
|
|
1168
|
+
padding-bottom: 24px;
|
|
1159
1169
|
border-bottom: 5px solid var(--accent);
|
|
1160
|
-
line-height:
|
|
1161
|
-
word-spacing: 100vw;
|
|
1162
|
-
}
|
|
1163
|
-
.print-title .title-minor {
|
|
1164
|
-
color: rgba(255,255,255,0.4);
|
|
1165
|
-
font-weight: 400;
|
|
1170
|
+
line-height: 1.05;
|
|
1166
1171
|
}
|
|
1167
1172
|
.print-subtitle {
|
|
1168
1173
|
font-size: 15px;
|
|
@@ -1235,19 +1240,32 @@ var PRINT_CSS = `
|
|
|
1235
1240
|
}
|
|
1236
1241
|
|
|
1237
1242
|
/* Tables: in-column with light grey box */
|
|
1238
|
-
.
|
|
1239
|
-
|
|
1243
|
+
.table-wrap {
|
|
1244
|
+
overflow: hidden;
|
|
1240
1245
|
margin: 12px 0;
|
|
1241
1246
|
break-inside: avoid;
|
|
1242
|
-
width: 100%;
|
|
1243
1247
|
background: #F6F6F4;
|
|
1248
|
+
border-radius: 4px;
|
|
1249
|
+
}
|
|
1250
|
+
.table-wrap.table-wide { column-span: all; }
|
|
1251
|
+
.table-wrap.table-wide table { font-size: 12px; }
|
|
1252
|
+
.table-wrap.table-wide thead th { font-size: 9px; }
|
|
1253
|
+
.table-wrap.table-wide tbody td { padding: 8px 12px; }
|
|
1254
|
+
.chart-wide { column-span: all; }
|
|
1255
|
+
.tab-content table {
|
|
1256
|
+
font-size: 10px;
|
|
1257
|
+
margin: 0;
|
|
1258
|
+
width: 100%;
|
|
1259
|
+
table-layout: fixed;
|
|
1244
1260
|
border-collapse: separate;
|
|
1245
1261
|
border-spacing: 0;
|
|
1246
|
-
padding:
|
|
1247
|
-
|
|
1262
|
+
padding: 0;
|
|
1263
|
+
background: transparent;
|
|
1264
|
+
border-radius: 0;
|
|
1248
1265
|
}
|
|
1249
|
-
.tab-content
|
|
1250
|
-
.tab-content
|
|
1266
|
+
.tab-content tbody td { word-wrap: break-word; overflow-wrap: break-word; }
|
|
1267
|
+
.tab-content thead th { font-size: 8px; letter-spacing: 0.8px; padding: 8px 10px; border-bottom: 2px solid #D0D0CB; }
|
|
1268
|
+
.tab-content tbody td { padding: 8px 10px; font-weight: 400; border-bottom: 1px solid #E8E8E4; }
|
|
1251
1269
|
.tab-content tbody tr:last-child td { border-bottom: none; }
|
|
1252
1270
|
|
|
1253
1271
|
/* KPIs: compact grid for column width */
|
|
@@ -1270,10 +1288,9 @@ var PRINT_CSS = `
|
|
|
1270
1288
|
overflow: hidden;
|
|
1271
1289
|
}
|
|
1272
1290
|
figcaption { font-size: 10px; margin-top: 4px; }
|
|
1273
|
-
.chart-container { height:
|
|
1291
|
+
.chart-container { height: 200px; overflow: hidden; }
|
|
1274
1292
|
|
|
1275
1293
|
.kpi-strip { break-inside: avoid; page-break-inside: avoid; }
|
|
1276
|
-
table { break-inside: auto; }
|
|
1277
1294
|
thead { display: table-header-group; }
|
|
1278
1295
|
tr { break-inside: avoid; }
|
|
1279
1296
|
|
|
@@ -1345,7 +1362,7 @@ function renderKpi(table) {
|
|
|
1345
1362
|
${kpis}
|
|
1346
1363
|
</div>`;
|
|
1347
1364
|
}
|
|
1348
|
-
function renderTable(table) {
|
|
1365
|
+
function renderTable(table, wide) {
|
|
1349
1366
|
const thead = table.headers.map((h) => `<th>${escapeHtml3(h)}</th>`).join("");
|
|
1350
1367
|
const tbody = table.rows.map((row) => {
|
|
1351
1368
|
const cells = row.map((cell) => {
|
|
@@ -1353,23 +1370,33 @@ function renderTable(table) {
|
|
|
1353
1370
|
}).join("");
|
|
1354
1371
|
return `<tr>${cells}</tr>`;
|
|
1355
1372
|
}).join("\n ");
|
|
1356
|
-
|
|
1373
|
+
const wideClass = wide || table.headers.length >= 5 ? " table-wide" : "";
|
|
1374
|
+
return ` <div class="table-wrap${wideClass}"><table>
|
|
1357
1375
|
<thead><tr>${thead}</tr></thead>
|
|
1358
1376
|
<tbody>
|
|
1359
1377
|
${tbody}
|
|
1360
1378
|
</tbody>
|
|
1361
|
-
</table>`;
|
|
1379
|
+
</table></div>`;
|
|
1362
1380
|
}
|
|
1363
|
-
function renderChart(chartId, caption, chartType, rowCount) {
|
|
1381
|
+
function renderChart(chartId, caption, chartType, rowCount, seriesCount, wide) {
|
|
1364
1382
|
let styleAttr = "";
|
|
1365
1383
|
if (chartType === "bar" && rowCount) {
|
|
1366
1384
|
const height = Math.max(120, rowCount * 44 + 40);
|
|
1367
1385
|
styleAttr = ` style="height:${height}px"`;
|
|
1386
|
+
} else if (seriesCount && seriesCount > 0) {
|
|
1387
|
+
const height = Math.min(300, 200 + Math.max(0, seriesCount - 1) * 25);
|
|
1388
|
+
styleAttr = ` style="height:${height}px"`;
|
|
1368
1389
|
}
|
|
1369
|
-
|
|
1390
|
+
const figureHtml = ` <figure>
|
|
1370
1391
|
<div class="chart-container" id="${chartId}"${styleAttr}></div>
|
|
1371
1392
|
${caption ? `<figcaption>${escapeHtml3(caption)}</figcaption>` : ""}
|
|
1372
1393
|
</figure>`;
|
|
1394
|
+
if (wide) {
|
|
1395
|
+
return ` <div class="chart-wide">
|
|
1396
|
+
${figureHtml}
|
|
1397
|
+
</div>`;
|
|
1398
|
+
}
|
|
1399
|
+
return figureHtml;
|
|
1373
1400
|
}
|
|
1374
1401
|
function renderElement(el) {
|
|
1375
1402
|
switch (el.kind) {
|
|
@@ -1380,9 +1407,9 @@ function renderElement(el) {
|
|
|
1380
1407
|
case "kpi":
|
|
1381
1408
|
return renderKpi(el.table);
|
|
1382
1409
|
case "chart":
|
|
1383
|
-
return renderChart(el.chartId, el.caption, el.chartType, el.table.rows.length);
|
|
1410
|
+
return renderChart(el.chartId, el.caption, el.chartType, el.table.rows.length, el.table.headers.length - 1, el.wide);
|
|
1384
1411
|
case "table":
|
|
1385
|
-
return renderTable(el.table);
|
|
1412
|
+
return renderTable(el.table, el.wide);
|
|
1386
1413
|
case "aside":
|
|
1387
1414
|
return ` <aside>${el.html}</aside>`;
|
|
1388
1415
|
case "pagebreak":
|
|
@@ -1469,32 +1496,11 @@ function buildChartScript(doc, printMode = false) {
|
|
|
1469
1496
|
if (printMode) return script + "\nwindow.__ghostPaperChartsReady = true;";
|
|
1470
1497
|
return script;
|
|
1471
1498
|
}
|
|
1472
|
-
function stylizePrintTitle(raw) {
|
|
1473
|
-
const MINOR = /* @__PURE__ */ new Set(["the", "a", "an", "for", "of", "in", "and", "or", "to", "with", "by", "on", "at", "is"]);
|
|
1474
|
-
const words = escapeHtml3(raw).split(/\s+/);
|
|
1475
|
-
const result = [];
|
|
1476
|
-
let i = 0;
|
|
1477
|
-
while (i < words.length) {
|
|
1478
|
-
const lower = words[i].toLowerCase();
|
|
1479
|
-
if (i + 1 < words.length && MINOR.has(lower) && MINOR.has(words[i + 1].toLowerCase())) {
|
|
1480
|
-
result.push(`<span class="title-minor" style="white-space:nowrap;word-spacing:normal">${words[i]} ${words[i + 1]}</span>`);
|
|
1481
|
-
i += 2;
|
|
1482
|
-
} else if (MINOR.has(lower)) {
|
|
1483
|
-
result.push(`<span class="title-minor">${words[i]}</span>`);
|
|
1484
|
-
i++;
|
|
1485
|
-
} else {
|
|
1486
|
-
result.push(words[i]);
|
|
1487
|
-
i++;
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1490
|
-
return result.join(" ");
|
|
1491
|
-
}
|
|
1492
1499
|
function buildPrintContent(doc) {
|
|
1493
1500
|
const subtitle = doc.frontmatter.subtitle ? `
|
|
1494
1501
|
<div class="print-subtitle">${escapeHtml3(doc.frontmatter.subtitle)}</div>` : "";
|
|
1495
|
-
const styledTitle = stylizePrintTitle(doc.frontmatter.title);
|
|
1496
1502
|
const header = ` <div class="print-header">
|
|
1497
|
-
<div class="print-title">${
|
|
1503
|
+
<div class="print-title">${escapeHtml3(doc.frontmatter.title)}</div>${subtitle}
|
|
1498
1504
|
</div>`;
|
|
1499
1505
|
const tabs = doc.tabs.map((tab, i) => {
|
|
1500
1506
|
const elements = tab.elements.map(renderElement).join("\n\n");
|
|
@@ -1539,7 +1545,7 @@ function buildPrintHtml(markdown) {
|
|
|
1539
1545
|
async function buildPdf(markdown, outputPath, options) {
|
|
1540
1546
|
const doc = parse(markdown);
|
|
1541
1547
|
const html = render(doc, { printMode: true });
|
|
1542
|
-
const { generatePdf } = await import("./pdf-
|
|
1548
|
+
const { generatePdf } = await import("./pdf-CVTVSJBX.js");
|
|
1543
1549
|
await generatePdf({
|
|
1544
1550
|
html,
|
|
1545
1551
|
outputPath,
|
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-BYWWJNGV.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("./-EDFCYK7D.js");
|
|
76
94
|
await buildPdf(markdown, outputPath, {
|
|
77
95
|
pageSize: opts.pageSize ?? "A4",
|
|
78
96
|
landscape: opts.landscape ?? false
|
|
@@ -52,8 +52,9 @@ async function generatePdf(opts) {
|
|
|
52
52
|
});
|
|
53
53
|
await new Promise((r) => setTimeout(r, 300));
|
|
54
54
|
const title = escapeHtml(opts.title ?? "");
|
|
55
|
-
const
|
|
56
|
-
const
|
|
55
|
+
const hideOnPage1 = `<script>if(document.querySelector('.pageNumber').textContent==='1')document.querySelector('#c').style.display='none';</script>`;
|
|
56
|
+
const headerTemplate = `<div style="font-size: 8px; font-family: -apple-system, system-ui, sans-serif; color: #999; width: 100%; padding: 0 0.65in; letter-spacing: 0.5px; text-transform: uppercase;"><span id="c">${title}</span>${hideOnPage1}</div>`;
|
|
57
|
+
const footerTemplate = `<div style="font-size: 8px; font-family: -apple-system, system-ui, sans-serif; color: #999; width: 100%; text-align: right; padding: 0 0.65in;"><span id="c"><span class="pageNumber"></span> / <span class="totalPages"></span></span>${hideOnPage1}</div>`;
|
|
57
58
|
const pdfBuffer = await page.pdf({
|
|
58
59
|
format: opts.pageSize ?? "A4",
|
|
59
60
|
landscape: opts.landscape ?? false,
|