@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 +46 -5
- package/README.zh-CN.md +46 -5
- package/dist/main.js +378 -12
- package/package.json +15 -5
- package/src/templates/default.html +262 -0
- package/src/templates/github.html +326 -0
- package/src/templates/magazine.html +462 -0
package/README.md
CHANGED
|
@@ -1,19 +1,30 @@
|
|
|
1
1
|
# PagePress
|
|
2
2
|
|
|
3
|
-
A CLI toolkit for AI agents to render local
|
|
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
|
|
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
|
-
#
|
|
30
|
-
pagepress -i
|
|
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
|
|
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,可将本地
|
|
3
|
+
面向 AI Agent 的 PDF 渲染 CLI,可将本地 Markdown 文件输出为高质量 PDF。
|
|
4
4
|
|
|
5
5
|
## 特性
|
|
6
6
|
|
|
7
7
|
- 专注 PDF 输出(仅 `pagepress`)
|
|
8
|
-
- 仅支持本地
|
|
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
|
-
#
|
|
28
|
-
pagepress -i
|
|
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>` 输入
|
|
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
|
|
118
|
+
throw new Error("Remote URL inputs are not supported. Please provide a local Markdown file path.");
|
|
30
119
|
}
|
|
31
|
-
if (!options.input.endsWith(".
|
|
32
|
-
throw new Error(`Unsupported input format: ${options.input}. Only
|
|
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
|
|
35
|
-
const
|
|
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
|
|
73
|
-
|
|
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
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
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": "
|
|
4
|
-
"description": "CLI tool to render local
|
|
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
|
-
"
|
|
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": {
|