@liustack/pagepress 2.0.0 → 3.0.0

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/main.js CHANGED
@@ -3,12 +3,78 @@ import { Command } from "commander";
3
3
  import { chromium } from "playwright";
4
4
  import * as fs from "fs";
5
5
  import * as path from "path";
6
- import { pathToFileURL } from "url";
6
+ import { fileURLToPath, pathToFileURL } from "url";
7
+ import { createRequire } from "module";
8
+ import { Renderer, marked } from "marked";
9
+ import matter from "gray-matter";
10
+ import hljs from "highlight.js";
11
+ import { execFile } from "child_process";
12
+ const pdfTemplates = {
13
+ default: {
14
+ name: "default",
15
+ description: "Clean minimalist design",
16
+ file: "default.html"
17
+ },
18
+ github: {
19
+ name: "github",
20
+ description: "GitHub-flavored markdown style",
21
+ file: "github.html"
22
+ },
23
+ magazine: {
24
+ name: "magazine",
25
+ description: "VOGUE/WIRED premium magazine style",
26
+ file: "magazine.html"
27
+ }
28
+ };
29
+ function getTemplate(name) {
30
+ const template = pdfTemplates[name];
31
+ if (!template) {
32
+ throw new Error(`Unknown PDF template: ${name}. Available: ${Object.keys(pdfTemplates).join(", ")}`);
33
+ }
34
+ return template;
35
+ }
36
+ const require$2 = createRequire(import.meta.url);
37
+ const MAGAZINE_FONTS = [
38
+ "@fontsource/bebas-neue/400.css",
39
+ "@fontsource/cormorant-garamond/400.css",
40
+ "@fontsource/cormorant-garamond/400-italic.css",
41
+ "@fontsource/cormorant-garamond/600.css",
42
+ "@fontsource/inter/300.css",
43
+ "@fontsource/inter/400.css",
44
+ "@fontsource/inter/500.css"
45
+ ];
46
+ function getFontCSS(template) {
47
+ if (template !== "magazine") {
48
+ return "";
49
+ }
50
+ const cssChunks = [];
51
+ for (const fontPath of MAGAZINE_FONTS) {
52
+ try {
53
+ const cssFilePath = require$2.resolve(fontPath);
54
+ let css = fs.readFileSync(cssFilePath, "utf-8");
55
+ const cssDir = path.dirname(cssFilePath);
56
+ css = css.replace(/url\(\.\/files\/([^)]+)\)/g, (match, filename) => {
57
+ const fontFilePath = path.join(cssDir, "files", filename);
58
+ if (fs.existsSync(fontFilePath)) {
59
+ const fontData = fs.readFileSync(fontFilePath);
60
+ const base64 = fontData.toString("base64");
61
+ const ext = path.extname(filename).slice(1);
62
+ const mimeType = ext === "woff2" ? "font/woff2" : `font/${ext}`;
63
+ return `url(data:${mimeType};base64,${base64})`;
64
+ }
65
+ return match;
66
+ });
67
+ cssChunks.push(css);
68
+ } catch (e) {
69
+ console.warn(`Warning: Could not load font ${fontPath}:`, e);
70
+ }
71
+ }
72
+ return cssChunks.join("\n");
73
+ }
74
+ const __filename$1 = fileURLToPath(import.meta.url);
75
+ const __dirname$1 = path.dirname(__filename$1);
76
+ const require$1 = createRequire(import.meta.url);
7
77
  const WAIT_UNTIL_STATES = /* @__PURE__ */ new Set(["load", "domcontentloaded", "networkidle"]);
8
- const MIN_SCALE = 1;
9
- const MAX_SCALE = 4;
10
- const DEFAULT_WIDTH = 1200;
11
- const DEFAULT_HEIGHT = 630;
12
78
  function normalizeWaitUntil(value) {
13
79
  if (!value) return "networkidle";
14
80
  if (WAIT_UNTIL_STATES.has(value)) {
@@ -23,40 +89,92 @@ function normalizeTimeout(value) {
23
89
  }
24
90
  return value;
25
91
  }
26
- function validateRange(name, value, min, max) {
27
- if (!Number.isFinite(value)) {
28
- throw new Error(`${name} must be a finite number.`);
29
- }
30
- if (value < min || value > max) {
31
- throw new Error(`${name} must be between ${min} and ${max}. Received: ${value}.`);
92
+ function findTemplatesDir() {
93
+ const candidates = [
94
+ // When running from project root (dev/installed)
95
+ path.resolve(process.cwd(), "src/templates"),
96
+ // Relative to dist/main.js
97
+ path.resolve(__dirname$1, "..", "src", "templates"),
98
+ // Relative to source file location
99
+ path.resolve(__dirname$1, "templates")
100
+ ];
101
+ for (const dir of candidates) {
102
+ if (fs.existsSync(dir)) return dir;
32
103
  }
104
+ throw new Error(`Templates directory not found. Searched: ${candidates.join(", ")}`);
33
105
  }
34
106
  async function render(options) {
35
- const width = options.width ?? DEFAULT_WIDTH;
36
- const height = options.height ?? DEFAULT_HEIGHT;
37
- const scale = options.scale ?? 2;
107
+ const template = getTemplate(options.template ?? "default");
38
108
  const safeMode = options.safe ?? false;
39
109
  const waitUntil = normalizeWaitUntil(options.waitUntil);
40
110
  const timeout = normalizeTimeout(options.timeout);
41
- validateRange("Width", width, 1, 1e4);
42
- validateRange("Height", height, 1, 1e4);
43
- validateRange("Scale", scale, MIN_SCALE, MAX_SCALE);
111
+ const templatesDir = findTemplatesDir();
112
+ const templatePath = path.join(templatesDir, template.file);
113
+ if (!fs.existsSync(templatePath)) {
114
+ throw new Error(`Template not found: ${templatePath}`);
115
+ }
116
+ const inputPath = path.resolve(options.input);
44
117
  if (options.input.startsWith("http://") || options.input.startsWith("https://")) {
45
- throw new Error("Remote URL inputs are not supported. Please provide a local HTML file path.");
118
+ throw new Error("Remote URL inputs are not supported. Please provide a local Markdown file path.");
46
119
  }
47
- if (!options.input.endsWith(".html")) {
48
- throw new Error(`Unsupported input format: ${options.input}. Only HTML (.html) files are supported.`);
120
+ if (!options.input.endsWith(".md")) {
121
+ throw new Error(`Unsupported input format: ${options.input}. Only Markdown (.md) files are supported.`);
49
122
  }
50
- const inputPath = path.resolve(options.input);
51
- const htmlContent = fs.readFileSync(inputPath, "utf-8");
123
+ const raw = fs.readFileSync(inputPath, "utf-8");
124
+ const { data: frontmatter, content } = matter(raw);
125
+ const hasMermaid = /```mermaid/i.test(content);
126
+ const renderer = new Renderer();
127
+ renderer.code = function({ text, lang }) {
128
+ if (lang === "mermaid") {
129
+ return `<pre class="mermaid">${text}</pre>`;
130
+ }
131
+ const highlighted = lang && hljs.getLanguage(lang) ? hljs.highlight(text, { language: lang }).value : hljs.highlightAuto(text).value;
132
+ const langClass = lang ? `hljs language-${lang}` : "hljs";
133
+ return `<pre><code class="${langClass}">${highlighted}</code></pre>`;
134
+ };
135
+ marked.use({ renderer });
136
+ const bodyHtml = await marked.parse(content);
137
+ const title = frontmatter.title || "Document";
138
+ const isDarkTheme = template.name === "magazine";
139
+ const hljsLightStyles = `
140
+ <style>
141
+ /* highlight.js GitHub Light Theme */
142
+ .hljs { color: #24292e; }
143
+ .hljs-comment, .hljs-quote { color: #6a737d; font-style: italic; }
144
+ .hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-type { color: #d73a49; font-weight: 600; }
145
+ .hljs-string, .hljs-attr, .hljs-symbol, .hljs-bullet, .hljs-addition { color: #032f62; }
146
+ .hljs-number { color: #005cc5; }
147
+ .hljs-title, .hljs-section, .hljs-function .hljs-title { color: #6f42c1; }
148
+ .hljs-built_in, .hljs-name { color: #005cc5; }
149
+ .hljs-class .hljs-title { color: #e36209; }
150
+ .hljs-meta { color: #6f42c1; }
151
+ .hljs-variable, .hljs-template-variable { color: #e36209; }
152
+ .hljs-params { color: #24292e; }
153
+ </style>
154
+ `;
155
+ const hljsDarkStyles = `
156
+ <style>
157
+ /* highlight.js Dark Theme for Magazine */
158
+ .hljs { color: #e8e8e8; }
159
+ .hljs-comment, .hljs-quote { color: #6b7280; font-style: italic; }
160
+ .hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-type { color: #f0abfc; }
161
+ .hljs-string, .hljs-attr, .hljs-symbol, .hljs-bullet, .hljs-addition { color: #86efac; }
162
+ .hljs-number { color: #fcd34d; }
163
+ .hljs-title, .hljs-section, .hljs-function .hljs-title { color: #93c5fd; }
164
+ .hljs-built_in, .hljs-name { color: #93c5fd; }
165
+ .hljs-class .hljs-title { color: #67e8f9; }
166
+ .hljs-meta { color: #f0abfc; }
167
+ .hljs-variable, .hljs-template-variable { color: #fcd34d; }
168
+ .hljs-params { color: #e8e8e8; }
169
+ </style>
170
+ `;
171
+ const hljsStyles = isDarkTheme ? hljsDarkStyles : hljsLightStyles;
172
+ let templateHtml = fs.readFileSync(templatePath, "utf-8");
173
+ const htmlContent = templateHtml.replace(/\{\{title\}\}/g, title).replace(/\{\{body\}\}/g, bodyHtml).replace(/\{\{styles\}\}/g, hljsStyles);
52
174
  let browser = null;
53
175
  try {
54
176
  browser = await chromium.launch({ headless: true });
55
- const context = await browser.newContext({
56
- viewport: { width, height },
57
- deviceScaleFactor: scale,
58
- javaScriptEnabled: !safeMode
59
- });
177
+ const context = await browser.newContext({ javaScriptEnabled: !safeMode });
60
178
  if (safeMode) {
61
179
  await context.route("**/*", (route) => {
62
180
  const url = route.request().url();
@@ -69,38 +187,124 @@ async function render(options) {
69
187
  const page = await context.newPage();
70
188
  await page.goto(pathToFileURL(inputPath).href, { waitUntil: "domcontentloaded", timeout });
71
189
  await page.setContent(htmlContent, { waitUntil, timeout });
72
- await page.addStyleTag({
73
- content: `
74
- html, body {
75
- width: ${width}px;
76
- height: ${height}px;
77
- margin: 0;
78
- padding: 0;
79
- overflow: hidden;
80
- }
81
- `
82
- });
83
- await page.waitForTimeout(500);
84
- const cardContainer = await page.$("#container");
85
- if (!cardContainer) {
86
- throw new Error('Missing #container element. PNG output requires a <div id="container"> wrapper.');
190
+ const fontCSS = getFontCSS(template.name);
191
+ if (fontCSS) {
192
+ await page.addStyleTag({ content: fontCSS });
87
193
  }
88
- const box = await cardContainer.boundingBox();
89
- if (!box || box.width <= 0 || box.height <= 0) {
90
- throw new Error("Invalid #container size. Ensure it has a positive width and height.");
194
+ if (hasMermaid && !safeMode) {
195
+ const mermaidPath = require$1.resolve("mermaid/dist/mermaid.min.js");
196
+ await page.addScriptTag({ path: mermaidPath });
197
+ const templateName = template.name;
198
+ await page.evaluate((tpl) => {
199
+ const defaultTheme = {
200
+ primaryColor: "#f5f5f7",
201
+ primaryTextColor: "#1d1d1f",
202
+ primaryBorderColor: "#d2d2d7",
203
+ lineColor: "#86868b",
204
+ secondaryColor: "#f5f5f7",
205
+ tertiaryColor: "#ffffff",
206
+ background: "#ffffff",
207
+ mainBkg: "#f5f5f7",
208
+ nodeBorder: "#d2d2d7",
209
+ nodeTextColor: "#1d1d1f",
210
+ clusterBkg: "rgba(0, 0, 0, 0.02)",
211
+ clusterBorder: "rgba(0, 0, 0, 0.1)",
212
+ titleColor: "#1d1d1f"
213
+ };
214
+ const githubTheme = {
215
+ primaryColor: "#f6f8fa",
216
+ primaryTextColor: "#1f2328",
217
+ primaryBorderColor: "#d0d7de",
218
+ lineColor: "#656d76",
219
+ secondaryColor: "#f6f8fa",
220
+ tertiaryColor: "#ffffff",
221
+ background: "#ffffff",
222
+ mainBkg: "#f6f8fa",
223
+ nodeBorder: "#d0d7de",
224
+ nodeTextColor: "#1f2328",
225
+ clusterBkg: "rgba(208, 215, 222, 0.2)",
226
+ clusterBorder: "#d0d7de",
227
+ titleColor: "#1f2328"
228
+ };
229
+ const magazineTheme = {
230
+ primaryColor: "#2a2a2a",
231
+ primaryTextColor: "#ffffff",
232
+ primaryBorderColor: "#c41e3a",
233
+ lineColor: "#666666",
234
+ secondaryColor: "#1a1a1a",
235
+ tertiaryColor: "#0f0f0f",
236
+ background: "#0f0f0f",
237
+ mainBkg: "#2a2a2a",
238
+ nodeBorder: "#c41e3a",
239
+ nodeTextColor: "#ffffff",
240
+ clusterBkg: "rgba(196, 30, 58, 0.1)",
241
+ clusterBorder: "rgba(196, 30, 58, 0.4)",
242
+ titleColor: "#ffffff"
243
+ };
244
+ const themes = {
245
+ default: defaultTheme,
246
+ github: githubTheme,
247
+ magazine: magazineTheme
248
+ };
249
+ const activeTheme = themes[tpl] || defaultTheme;
250
+ window.mermaid.initialize({
251
+ startOnLoad: false,
252
+ theme: "base",
253
+ themeVariables: {
254
+ ...activeTheme,
255
+ fontFamily: '"SF Mono", "Fira Code", Menlo, monospace',
256
+ fontSize: "13px"
257
+ },
258
+ flowchart: {
259
+ curve: "basis",
260
+ nodeSpacing: 40,
261
+ rankSpacing: 50,
262
+ htmlLabels: true,
263
+ useMaxWidth: true,
264
+ subGraphTitleMargin: { top: 15, bottom: 15 },
265
+ padding: 20
266
+ }
267
+ });
268
+ }, templateName);
269
+ await page.evaluate(() => {
270
+ return window.mermaid.run();
271
+ });
272
+ await page.waitForSelector(".mermaid svg", { timeout: 5e3 });
273
+ await page.waitForTimeout(300);
274
+ await page.evaluate(() => {
275
+ document.querySelectorAll(".node rect").forEach((rect) => {
276
+ rect.setAttribute("rx", "12");
277
+ rect.setAttribute("ry", "12");
278
+ });
279
+ document.querySelectorAll(".cluster rect").forEach((rect) => {
280
+ rect.setAttribute("rx", "16");
281
+ rect.setAttribute("ry", "16");
282
+ });
283
+ document.querySelectorAll(".cluster-label").forEach((label) => {
284
+ const transform = label.getAttribute("transform");
285
+ if (transform) {
286
+ const match = transform.match(/translate\(([\d.]+),\s*([\d.]+)\)/);
287
+ if (match) {
288
+ const x = parseFloat(match[1]);
289
+ const y = parseFloat(match[2]) + 8;
290
+ label.setAttribute("transform", `translate(${x}, ${y})`);
291
+ }
292
+ }
293
+ });
294
+ });
91
295
  }
296
+ await page.waitForTimeout(200);
92
297
  const outputPath = path.resolve(options.output);
93
- await page.screenshot({
298
+ await page.pdf({
94
299
  path: outputPath,
95
- type: "png",
96
- clip: { x: box.x, y: box.y, width: box.width, height: box.height }
300
+ format: "A4",
301
+ margin: { top: "0", right: "0", bottom: "0", left: "0" },
302
+ printBackground: true
97
303
  });
98
304
  return {
99
- pngPath: outputPath,
305
+ pdfPath: outputPath,
100
306
  meta: {
101
- width,
102
- height,
103
- deviceScaleFactor: scale,
307
+ template: template.name,
104
308
  generatedAt: (/* @__PURE__ */ new Date()).toISOString()
105
309
  }
106
310
  };
@@ -110,9 +314,107 @@ async function render(options) {
110
314
  }
111
315
  }
112
316
  }
113
- const program = new Command();
114
- program.name("pagepress").description("Generate one-page visual reports, dashboards, and infographics from HTML").version("2.0.0").requiredOption("-i, --input <path>", "Input HTML file path").requiredOption("-o, --output <path>", "Output PNG file path").option("-w, --width <pixels>", "Viewport width in pixels", "1200").option("-h, --height <pixels>", "Viewport height in pixels", "630").option("--scale <number>", "Device scale factor (1-4)", "2").option("--wait-until <state>", "Navigation waitUntil (load, domcontentloaded, networkidle)", "networkidle").option("--timeout <ms>", "Navigation timeout in milliseconds", "30000").option("--safe", "Disable external network requests and JavaScript execution").action(async (options) => {
317
+ const PACKAGE_NAME = "@liustack/pagepress";
318
+ function normalizeVersion(version) {
319
+ const normalized = version.trim().replace(/^v/i, "").split("-")[0];
320
+ if (!normalized) {
321
+ throw new Error(`Invalid version: ${version}`);
322
+ }
323
+ return normalized;
324
+ }
325
+ function parseVersion(version) {
326
+ return normalizeVersion(version).split(".").map((segment) => {
327
+ const value = Number.parseInt(segment, 10);
328
+ if (!Number.isFinite(value)) {
329
+ throw new Error(`Invalid version segment: ${segment}`);
330
+ }
331
+ return value;
332
+ });
333
+ }
334
+ function compareVersions(left, right) {
335
+ const leftParts = parseVersion(left);
336
+ const rightParts = parseVersion(right);
337
+ const length = Math.max(leftParts.length, rightParts.length);
338
+ for (let index = 0; index < length; index += 1) {
339
+ const leftValue = leftParts[index] ?? 0;
340
+ const rightValue = rightParts[index] ?? 0;
341
+ if (leftValue < rightValue) return -1;
342
+ if (leftValue > rightValue) return 1;
343
+ }
344
+ return 0;
345
+ }
346
+ const runCommand = (command, args) => new Promise((resolve, reject) => {
347
+ execFile(command, args, { encoding: "utf-8" }, (error, stdout, stderr) => {
348
+ if (error) {
349
+ const message = stderr.trim() || error.message;
350
+ reject(new Error(message));
351
+ return;
352
+ }
353
+ resolve(stdout.trim());
354
+ });
355
+ });
356
+ const resolvePlaywrightCliPath = () => {
357
+ const require2 = createRequire(import.meta.url);
358
+ const playwrightPackagePath = require2.resolve("playwright/package.json");
359
+ const playwrightPackage = JSON.parse(fs.readFileSync(playwrightPackagePath, "utf-8"));
360
+ const cliRelativePath = playwrightPackage.bin?.playwright;
361
+ if (!cliRelativePath) {
362
+ throw new Error("Unable to resolve Playwright CLI.");
363
+ }
364
+ return path.resolve(path.dirname(playwrightPackagePath), cliRelativePath);
365
+ };
366
+ async function checkForUpdate({
367
+ currentVersion,
368
+ packageName = PACKAGE_NAME,
369
+ run = runCommand
370
+ }) {
371
+ const normalizedCurrentVersion = normalizeVersion(currentVersion);
115
372
  try {
373
+ const latestVersion = normalizeVersion(await run("npm", ["view", packageName, "version"]));
374
+ return {
375
+ packageName,
376
+ currentVersion: normalizedCurrentVersion,
377
+ latestVersion,
378
+ updateAvailable: compareVersions(normalizedCurrentVersion, latestVersion) < 0,
379
+ checked: true
380
+ };
381
+ } catch (error) {
382
+ return {
383
+ packageName,
384
+ currentVersion: normalizedCurrentVersion,
385
+ latestVersion: null,
386
+ updateAvailable: false,
387
+ checked: false,
388
+ error: error instanceof Error ? error.message : String(error)
389
+ };
390
+ }
391
+ }
392
+ function createSelfUpdater(run = runCommand, resolvePlaywrightCli = resolvePlaywrightCliPath) {
393
+ return async ({
394
+ currentVersion,
395
+ packageName = PACKAGE_NAME
396
+ }) => {
397
+ const updateInfo = await checkForUpdate({ currentVersion, packageName, run });
398
+ if (!updateInfo.checked) {
399
+ throw new Error(`Unable to check for updates: ${updateInfo.error ?? "unknown error"}`);
400
+ }
401
+ if (!updateInfo.updateAvailable) {
402
+ return {
403
+ ...updateInfo,
404
+ updated: false
405
+ };
406
+ }
407
+ await run("npm", ["install", "-g", `${packageName}@latest`]);
408
+ await run(process.execPath, [resolvePlaywrightCli(), "install", "chromium"]);
409
+ return {
410
+ ...updateInfo,
411
+ updated: true
412
+ };
413
+ };
414
+ }
415
+ async function runRenderCommand() {
416
+ const program = new Command();
417
+ program.name("pagepress").description("Render local Markdown files to PDF").version("3.0.0").requiredOption("-i, --input <path>", "Input Markdown file path").requiredOption("-o, --output <path>", "Output PDF file path").option("-t, --template <name>", "PDF template (default, github, magazine)", "default").option("--wait-until <state>", "Navigation waitUntil (load, domcontentloaded, networkidle)", "networkidle").option("--timeout <ms>", "Navigation timeout in milliseconds", "30000").option("--safe", "Disable external network requests and JavaScript execution").action(async (options) => {
116
418
  const timeout = Number.parseInt(options.timeout, 10);
117
419
  if (!Number.isFinite(timeout) || timeout < 0) {
118
420
  throw new Error("Invalid --timeout value. Use a non-negative integer in milliseconds.");
@@ -120,17 +422,37 @@ program.name("pagepress").description("Generate one-page visual reports, dashboa
120
422
  const result = await render({
121
423
  input: options.input,
122
424
  output: options.output,
123
- width: Number.parseInt(options.width, 10),
124
- height: Number.parseInt(options.height, 10),
125
- scale: Number.parseFloat(options.scale),
425
+ template: options.template,
126
426
  waitUntil: options.waitUntil,
127
427
  timeout,
128
428
  safe: options.safe
129
429
  });
130
430
  console.log(JSON.stringify(result, null, 2));
131
- } catch (error) {
132
- console.error("Error:", error instanceof Error ? error.message : error);
133
- process.exit(1);
431
+ });
432
+ await program.parseAsync(process.argv);
433
+ }
434
+ async function runCheckUpdateCommand() {
435
+ const result = await checkForUpdate({ currentVersion: "3.0.0" });
436
+ console.log(JSON.stringify(result, null, 2));
437
+ }
438
+ async function runSelfUpdateCommand() {
439
+ const selfUpdate = createSelfUpdater();
440
+ const result = await selfUpdate({ currentVersion: "3.0.0" });
441
+ console.log(JSON.stringify(result, null, 2));
442
+ }
443
+ async function main() {
444
+ const command = process.argv[2];
445
+ if (command === "check-update") {
446
+ await runCheckUpdateCommand();
447
+ return;
134
448
  }
449
+ if (command === "self-update") {
450
+ await runSelfUpdateCommand();
451
+ return;
452
+ }
453
+ await runRenderCommand();
454
+ }
455
+ main().catch((error) => {
456
+ console.error("Error:", error instanceof Error ? error.message : error);
457
+ process.exit(1);
135
458
  });
136
- program.parse();
package/package.json CHANGED
@@ -1,32 +1,22 @@
1
1
  {
2
2
  "name": "@liustack/pagepress",
3
- "version": "2.0.0",
4
- "description": "CLI tool to generate one-page visual reports, dashboards, and infographics from HTML",
3
+ "version": "3.0.0",
4
+ "description": "CLI tool to render local Markdown files into polished PDF documents",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "pagepress": "./dist/main.js"
8
8
  },
9
- "scripts": {
10
- "dev": "vite build --watch",
11
- "build": "vite build",
12
- "typecheck": "tsc --noEmit",
13
- "test": "vitest run",
14
- "test:watch": "vitest",
15
- "test:coverage": "vitest run --coverage",
16
- "prepublishOnly": "pnpm build",
17
- "docs:list": "node scripts/docs-list.js"
18
- },
19
9
  "files": [
20
- "dist"
10
+ "dist",
11
+ "src/templates"
21
12
  ],
22
13
  "keywords": [
23
- "infographic",
24
- "html-to-png",
25
- "visual-report",
26
- "dashboard",
27
- "screenshot",
14
+ "pdf",
15
+ "markdown-to-pdf",
16
+ "pdf-report",
28
17
  "cli",
29
- "playwright"
18
+ "playwright",
19
+ "mermaid"
30
20
  ],
31
21
  "author": "Leon Liu",
32
22
  "license": "MIT",
@@ -42,7 +32,15 @@
42
32
  "node": ">=18"
43
33
  },
44
34
  "dependencies": {
35
+ "@fontsource/bebas-neue": "^5.2.7",
36
+ "@fontsource/cormorant-garamond": "^5.2.11",
37
+ "@fontsource/inter": "^5.2.8",
45
38
  "commander": "^13.1.0",
39
+ "gray-matter": "^4.0.3",
40
+ "highlight.js": "^11.11.1",
41
+ "marked": "^15.0.12",
42
+ "marked-highlight": "^2.2.3",
43
+ "mermaid": "^11.12.2",
46
44
  "playwright": "^1.58.1"
47
45
  },
48
46
  "devDependencies": {
@@ -52,5 +50,14 @@
52
50
  "vite": "^6.4.1",
53
51
  "vite-plugin-node": "^5.0.1",
54
52
  "vitest": "^4.0.18"
53
+ },
54
+ "scripts": {
55
+ "dev": "vite build --watch",
56
+ "build": "vite build",
57
+ "typecheck": "tsc --noEmit",
58
+ "test": "vitest run",
59
+ "test:watch": "vitest",
60
+ "test:coverage": "vitest run --coverage",
61
+ "docs:list": "node scripts/docs-list.js"
55
62
  }
56
- }
63
+ }