ghost-paper 0.3.2 → 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.
@@ -2,7 +2,7 @@ import {
2
2
  build,
3
3
  buildPdf,
4
4
  buildPrintHtml
5
- } from "./chunk-VH6KYYQG.js";
5
+ } from "./chunk-BYWWJNGV.js";
6
6
  export {
7
7
  build,
8
8
  buildPdf,
@@ -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 headerStr = headers.join(" ").toLowerCase();
72
- const hasMetricHeader = /metric|name|label|kpi|measure|indicator/i.test(headerStr);
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;
@@ -1238,6 +1248,10 @@ var PRINT_CSS = `
1238
1248
  border-radius: 4px;
1239
1249
  }
1240
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; }
1241
1255
  .tab-content table {
1242
1256
  font-size: 10px;
1243
1257
  margin: 0;
@@ -1274,7 +1288,7 @@ var PRINT_CSS = `
1274
1288
  overflow: hidden;
1275
1289
  }
1276
1290
  figcaption { font-size: 10px; margin-top: 4px; }
1277
- .chart-container { height: 190px; overflow: hidden; }
1291
+ .chart-container { height: 200px; overflow: hidden; }
1278
1292
 
1279
1293
  .kpi-strip { break-inside: avoid; page-break-inside: avoid; }
1280
1294
  thead { display: table-header-group; }
@@ -1348,7 +1362,7 @@ function renderKpi(table) {
1348
1362
  ${kpis}
1349
1363
  </div>`;
1350
1364
  }
1351
- function renderTable(table) {
1365
+ function renderTable(table, wide) {
1352
1366
  const thead = table.headers.map((h) => `<th>${escapeHtml3(h)}</th>`).join("");
1353
1367
  const tbody = table.rows.map((row) => {
1354
1368
  const cells = row.map((cell) => {
@@ -1356,7 +1370,7 @@ function renderTable(table) {
1356
1370
  }).join("");
1357
1371
  return `<tr>${cells}</tr>`;
1358
1372
  }).join("\n ");
1359
- const wideClass = table.headers.length >= 5 ? " table-wide" : "";
1373
+ const wideClass = wide || table.headers.length >= 5 ? " table-wide" : "";
1360
1374
  return ` <div class="table-wrap${wideClass}"><table>
1361
1375
  <thead><tr>${thead}</tr></thead>
1362
1376
  <tbody>
@@ -1364,16 +1378,25 @@ function renderTable(table) {
1364
1378
  </tbody>
1365
1379
  </table></div>`;
1366
1380
  }
1367
- function renderChart(chartId, caption, chartType, rowCount) {
1381
+ function renderChart(chartId, caption, chartType, rowCount, seriesCount, wide) {
1368
1382
  let styleAttr = "";
1369
1383
  if (chartType === "bar" && rowCount) {
1370
1384
  const height = Math.max(120, rowCount * 44 + 40);
1371
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"`;
1372
1389
  }
1373
- return ` <figure>
1390
+ const figureHtml = ` <figure>
1374
1391
  <div class="chart-container" id="${chartId}"${styleAttr}></div>
1375
1392
  ${caption ? `<figcaption>${escapeHtml3(caption)}</figcaption>` : ""}
1376
1393
  </figure>`;
1394
+ if (wide) {
1395
+ return ` <div class="chart-wide">
1396
+ ${figureHtml}
1397
+ </div>`;
1398
+ }
1399
+ return figureHtml;
1377
1400
  }
1378
1401
  function renderElement(el) {
1379
1402
  switch (el.kind) {
@@ -1384,9 +1407,9 @@ function renderElement(el) {
1384
1407
  case "kpi":
1385
1408
  return renderKpi(el.table);
1386
1409
  case "chart":
1387
- 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);
1388
1411
  case "table":
1389
- return renderTable(el.table);
1412
+ return renderTable(el.table, el.wide);
1390
1413
  case "aside":
1391
1414
  return ` <aside>${el.html}</aside>`;
1392
1415
  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-VH6KYYQG.js";
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 with headers like Metric/Value/Change:
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), other columns numeric:
41
- | Month | Revenue ($K) | Users (K) |
42
- |-------|-------------|-----------|
43
- | Jan | 280 | 142 |
44
- | Feb | 295 | 148 |
45
- | Mar | 310 | 155 |
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("./-3QXRJ2CJ.js");
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ghost-paper",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Turn markdown into beautiful HTML reports",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",