@liustack/pagepress 1.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/README.md CHANGED
@@ -1,19 +1,30 @@
1
1
  # PagePress
2
2
 
3
- A CLI toolkit for AI agents to render local HTML files into high-quality PDF documents.
3
+ A CLI toolkit for AI agents to render local Markdown files into high-quality PDF documents.
4
4
 
5
5
  中文说明请见:[README.zh-CN.md](README.zh-CN.md)
6
6
 
7
7
  ## Features
8
8
 
9
9
  - PDF-only command surface (`pagepress`)
10
- - Local HTML file input only (`.html`)
10
+ - Local Markdown file input only (`.md`)
11
+ - Built-in Markdown templates: `default`, `github`, `magazine`
12
+ - Built-in Mermaid rendering for Markdown code blocks
11
13
  - Deterministic output via Playwright + Chromium print engine
12
14
 
13
15
  ## Installation
14
16
 
17
+ Requires Node.js 18+.
18
+
19
+ Install the CLI globally:
20
+
15
21
  ```bash
16
22
  npm install -g @liustack/pagepress
23
+ ```
24
+
25
+ Then install the Chromium browser used for PDF rendering:
26
+
27
+ ```bash
17
28
  npx playwright install chromium
18
29
  ```
19
30
 
@@ -23,17 +34,47 @@ Or run with `npx`:
23
34
  npx @liustack/pagepress [options]
24
35
  ```
25
36
 
37
+ `pagepress` renders PDFs through Playwright + Chromium, so the browser install step is required even if you use `npx`.
38
+
39
+ Or install as an **Agent Skill** — tell any AI coding tool that supports agent skills (Claude Code, Codex, OpenCode, Cursor, Antigravity, etc.):
40
+
41
+ ```text
42
+ Install the skill from liustack/pagepress
43
+ ```
44
+
45
+ Or use the `skills` CLI directly:
46
+
47
+ ```bash
48
+ npx skills add liustack/pagepress --skill pagepress
49
+ ```
50
+
51
+ To check for updates later:
52
+
53
+ ```bash
54
+ pagepress check-update
55
+ pagepress self-update
56
+ ```
57
+
58
+ If `check-update` cannot reach the registry, it returns `checked: false` and you can continue using the local CLI.
59
+
26
60
  ## Usage
27
61
 
28
62
  ```bash
29
- # Local HTML to PDF
30
- pagepress -i page.html -o output.pdf
63
+ # Markdown to PDF
64
+ pagepress -i document.md -o output.pdf --template default
31
65
  ```
32
66
 
67
+ ## Templates
68
+
69
+ - `default` - Clean minimalist style
70
+ - `github` - GitHub style
71
+ - `magazine` - magazine layout
72
+
33
73
  ## Options
34
74
 
35
- - `-i, --input <path>` input HTML file path
75
+ - `-i, --input <path>` input Markdown file path
36
76
  - `-o, --output <path>` output PDF file path
77
+ - `-t, --template <name>` template for Markdown input (default: `default`)
37
78
  - `--wait-until <state>` `load | domcontentloaded | networkidle`
38
79
  - `--timeout <ms>` timeout in milliseconds
39
80
  - `--safe` disable external network requests and JavaScript execution
package/README.zh-CN.md CHANGED
@@ -1,17 +1,28 @@
1
1
  # PagePress
2
2
 
3
- 面向 AI Agent 的 PDF 渲染 CLI,可将本地 HTML 文件输出为高质量 PDF。
3
+ 面向 AI Agent 的 PDF 渲染 CLI,可将本地 Markdown 文件输出为高质量 PDF。
4
4
 
5
5
  ## 特性
6
6
 
7
7
  - 专注 PDF 输出(仅 `pagepress`)
8
- - 仅支持本地 HTML 文件输入(`.html`)
8
+ - 仅支持本地 Markdown 文件输入(`.md`)
9
+ - 内置 Markdown 模板:`default`、`github`、`magazine`
10
+ - 支持 Mermaid 代码块渲染
9
11
  - 基于 Playwright + Chromium 的稳定打印引擎
10
12
 
11
13
  ## 安装
12
14
 
15
+ 需要 Node.js 18+。
16
+
17
+ 全局安装 CLI:
18
+
13
19
  ```bash
14
20
  npm install -g @liustack/pagepress
21
+ ```
22
+
23
+ 然后安装 PDF 渲染所需的 Chromium 浏览器:
24
+
25
+ ```bash
15
26
  npx playwright install chromium
16
27
  ```
17
28
 
@@ -21,17 +32,47 @@ npx playwright install chromium
21
32
  npx @liustack/pagepress [options]
22
33
  ```
23
34
 
35
+ `pagepress` 基于 Playwright + Chromium 导出 PDF,所以即使使用 `npx`,浏览器安装这一步也仍然需要。
36
+
37
+ 也可以作为 **Agent Skill** 安装,在任何支持 Agent Skill 的 AI 编程工具(Claude Code、Codex、OpenCode、Cursor、Antigravity 等)中输入:
38
+
39
+ ```text
40
+ 帮我安装这个 skill:liustack/pagepress
41
+ ```
42
+
43
+ 或使用 `skills` CLI 直接安装:
44
+
45
+ ```bash
46
+ npx skills add liustack/pagepress --skill pagepress
47
+ ```
48
+
49
+ 后续如需检查或升级版本:
50
+
51
+ ```bash
52
+ pagepress check-update
53
+ pagepress self-update
54
+ ```
55
+
56
+ 如果 `check-update` 无法连接 registry,会返回 `checked: false`,此时可继续使用本地 CLI。
57
+
24
58
  ## 用法
25
59
 
26
60
  ```bash
27
- # 本地 HTML 转 PDF
28
- pagepress -i page.html -o output.pdf
61
+ # Markdown 转 PDF
62
+ pagepress -i document.md -o output.pdf --template default
29
63
  ```
30
64
 
65
+ ## 模板
66
+
67
+ - `default` - 简洁风格
68
+ - `github` - GitHub 风格
69
+ - `magazine` - 杂志排版
70
+
31
71
  ## 参数
32
72
 
33
- - `-i, --input <path>` 输入 HTML 路径
73
+ - `-i, --input <path>` 输入 Markdown 路径
34
74
  - `-o, --output <path>` 输出 PDF 路径
75
+ - `-t, --template <name>` Markdown 模板(默认 `default`)
35
76
  - `--wait-until <state>` `load | domcontentloaded | networkidle`
36
77
  - `--timeout <ms>` 超时时间(毫秒)
37
78
  - `--safe` 禁用外部网络请求和 JavaScript 执行
package/dist/main.js CHANGED
@@ -4,8 +4,76 @@ import { chromium } from "playwright";
4
4
  import * as fs from "fs";
5
5
  import * as path from "path";
6
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
+ }
7
74
  const __filename$1 = fileURLToPath(import.meta.url);
8
- path.dirname(__filename$1);
75
+ const __dirname$1 = path.dirname(__filename$1);
76
+ const require$1 = createRequire(import.meta.url);
9
77
  const WAIT_UNTIL_STATES = /* @__PURE__ */ new Set(["load", "domcontentloaded", "networkidle"]);
10
78
  function normalizeWaitUntil(value) {
11
79
  if (!value) return "networkidle";
@@ -21,18 +89,88 @@ function normalizeTimeout(value) {
21
89
  }
22
90
  return value;
23
91
  }
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;
103
+ }
104
+ throw new Error(`Templates directory not found. Searched: ${candidates.join(", ")}`);
105
+ }
24
106
  async function render(options) {
107
+ const template = getTemplate(options.template ?? "default");
25
108
  const safeMode = options.safe ?? false;
26
109
  const waitUntil = normalizeWaitUntil(options.waitUntil);
27
110
  const timeout = normalizeTimeout(options.timeout);
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);
28
117
  if (options.input.startsWith("http://") || options.input.startsWith("https://")) {
29
- 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.");
30
119
  }
31
- if (!options.input.endsWith(".html")) {
32
- 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.`);
33
122
  }
34
- const inputPath = path.resolve(options.input);
35
- 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);
36
174
  let browser = null;
37
175
  try {
38
176
  browser = await chromium.launch({ headless: true });
@@ -49,6 +187,112 @@ async function render(options) {
49
187
  const page = await context.newPage();
50
188
  await page.goto(pathToFileURL(inputPath).href, { waitUntil: "domcontentloaded", timeout });
51
189
  await page.setContent(htmlContent, { waitUntil, timeout });
190
+ const fontCSS = getFontCSS(template.name);
191
+ if (fontCSS) {
192
+ await page.addStyleTag({ content: fontCSS });
193
+ }
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
+ });
295
+ }
52
296
  await page.waitForTimeout(200);
53
297
  const outputPath = path.resolve(options.output);
54
298
  await page.pdf({
@@ -60,6 +304,7 @@ async function render(options) {
60
304
  return {
61
305
  pdfPath: outputPath,
62
306
  meta: {
307
+ template: template.name,
63
308
  generatedAt: (/* @__PURE__ */ new Date()).toISOString()
64
309
  }
65
310
  };
@@ -69,9 +314,107 @@ async function render(options) {
69
314
  }
70
315
  }
71
316
  }
72
- const program = new Command();
73
- program.name("pagepress").description("Render local HTML files to PDF").version("1.0.0").requiredOption("-i, --input <path>", "Input HTML file path").requiredOption("-o, --output <path>", "Output PDF file path").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);
74
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) => {
75
418
  const timeout = Number.parseInt(options.timeout, 10);
76
419
  if (!Number.isFinite(timeout) || timeout < 0) {
77
420
  throw new Error("Invalid --timeout value. Use a non-negative integer in milliseconds.");
@@ -79,14 +422,37 @@ program.name("pagepress").description("Render local HTML files to PDF").version(
79
422
  const result = await render({
80
423
  input: options.input,
81
424
  output: options.output,
425
+ template: options.template,
82
426
  waitUntil: options.waitUntil,
83
427
  timeout,
84
428
  safe: options.safe
85
429
  });
86
430
  console.log(JSON.stringify(result, null, 2));
87
- } catch (error) {
88
- console.error("Error:", error instanceof Error ? error.message : error);
89
- 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;
90
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);
91
458
  });
92
- program.parse();
package/package.json CHANGED
@@ -1,20 +1,22 @@
1
1
  {
2
2
  "name": "@liustack/pagepress",
3
- "version": "1.0.0",
4
- "description": "CLI tool to render local HTML files into PDF",
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
9
  "files": [
10
- "dist"
10
+ "dist",
11
+ "src/templates"
11
12
  ],
12
13
  "keywords": [
13
14
  "pdf",
14
- "html-to-pdf",
15
+ "markdown-to-pdf",
15
16
  "pdf-report",
16
17
  "cli",
17
- "playwright"
18
+ "playwright",
19
+ "mermaid"
18
20
  ],
19
21
  "author": "Leon Liu",
20
22
  "license": "MIT",
@@ -30,7 +32,15 @@
30
32
  "node": ">=18"
31
33
  },
32
34
  "dependencies": {
35
+ "@fontsource/bebas-neue": "^5.2.7",
36
+ "@fontsource/cormorant-garamond": "^5.2.11",
37
+ "@fontsource/inter": "^5.2.8",
33
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",
34
44
  "playwright": "^1.58.1"
35
45
  },
36
46
  "devDependencies": {