@liustack/pagepress 0.6.1 → 0.8.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/LICENSE +1 -1
- package/README.md +29 -93
- package/README.zh-CN.md +29 -93
- package/dist/main.js +13 -203
- package/package.json +11 -14
- package/src/templates/default.html +253 -0
- package/src/pdf/templates/default.html +0 -555
- /package/src/{pdf/templates → templates}/github.html +0 -0
- /package/src/{pdf/templates → templates}/magazine.html +0 -0
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,123 +1,59 @@
|
|
|
1
1
|
# PagePress
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A CLI toolkit for AI agents to render local Markdown and HTML 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
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
- **Image**: 8 presets — OG cards, infographics, posters, banners, Twitter/X cards, YouTube thumbnails, Xiaohongshu covers, WeChat covers
|
|
15
|
-
- **Brand-aware visuals**: OG / poster / banner outputs are generated based on workspace visual & brand assets (commonly in `assets/`, `public/`, `src/assets/`)
|
|
16
|
-
- **Consistency guarantees**: Deterministic rendering, font waiting, network idle detection, syntax highlighting
|
|
9
|
+
- PDF-only command surface (`pagepress`)
|
|
10
|
+
- Local file input only (`.md` / `.html`)
|
|
11
|
+
- Built-in Markdown templates: `default`, `github`, `magazine`
|
|
12
|
+
- Built-in Mermaid rendering for Markdown code blocks
|
|
13
|
+
- Deterministic output via Playwright + Chromium print engine
|
|
17
14
|
|
|
18
|
-
##
|
|
15
|
+
## Installation
|
|
19
16
|
|
|
20
17
|
```bash
|
|
21
|
-
# Global install
|
|
22
18
|
npm install -g @liustack/pagepress
|
|
23
|
-
|
|
24
|
-
# Install browsers (Playwright)
|
|
25
19
|
npx playwright install chromium
|
|
26
20
|
```
|
|
27
21
|
|
|
28
|
-
Or
|
|
22
|
+
Or run with `npx`:
|
|
29
23
|
|
|
30
24
|
```bash
|
|
31
|
-
npx @liustack/pagepress
|
|
25
|
+
npx @liustack/pagepress [options]
|
|
32
26
|
```
|
|
33
27
|
|
|
34
|
-
##
|
|
35
|
-
|
|
36
|
-
### 1. Generate PDF
|
|
37
|
-
|
|
38
|
-
Convert HTML/Markdown into PDF documents. Supports syntax highlighting.
|
|
28
|
+
## Usage
|
|
39
29
|
|
|
40
30
|
```bash
|
|
41
|
-
# Markdown to PDF
|
|
42
|
-
pagepress
|
|
43
|
-
|
|
44
|
-
# Local HTML file to PDF
|
|
45
|
-
pagepress pdf -i page.html -o output.pdf
|
|
31
|
+
# Markdown to PDF
|
|
32
|
+
pagepress -i document.md -o output.pdf --template default
|
|
46
33
|
|
|
34
|
+
# Local HTML to PDF
|
|
35
|
+
pagepress -i page.html -o output.pdf
|
|
47
36
|
```
|
|
48
37
|
|
|
49
|
-
|
|
50
|
-
- `default` - Apple style, clean and elegant
|
|
51
|
-
- `github` - GitHub style
|
|
52
|
-
- `magazine` - VOGUE/WIRED magazine layout
|
|
53
|
-
|
|
54
|
-
### 2. Generate Images
|
|
38
|
+
## Templates
|
|
55
39
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
**Recommended brand assets layout**:
|
|
61
|
-
|
|
62
|
-
```text
|
|
63
|
-
assets/
|
|
64
|
-
brand/
|
|
65
|
-
logo.svg
|
|
66
|
-
palette.json
|
|
67
|
-
fonts/
|
|
68
|
-
YourBrand-Regular.woff2
|
|
69
|
-
imagery/
|
|
70
|
-
hero.jpg
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
```bash
|
|
74
|
-
# Generate an OG card (1200x630)
|
|
75
|
-
pagepress shot -i card.html -o og.png --preset og
|
|
76
|
-
|
|
77
|
-
# Generate a long infographic
|
|
78
|
-
pagepress shot -i stats.html -o infographic.png --preset infographic
|
|
79
|
-
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
**Supported presets**:
|
|
83
|
-
- `og` (1200x630) - Social card
|
|
84
|
-
- `infographic` (1080x1350) - Infographic
|
|
85
|
-
- `poster` (1200x1500) - Poster
|
|
86
|
-
- `banner` (1600x900) - Banner
|
|
87
|
-
- `twitter` (1200x675) - Twitter/X card
|
|
88
|
-
- `youtube` (1280x720) - YouTube thumbnail
|
|
89
|
-
- `xiaohongshu` (1080x1440) - Xiaohongshu / RedNote cover
|
|
90
|
-
- `wechat` (900x383) - WeChat Official Account cover
|
|
91
|
-
|
|
92
|
-
## 🖼️ Examples
|
|
93
|
-
|
|
94
|
-
### Image Presets
|
|
95
|
-
|
|
96
|
-
| OG Card (1200×630) | Infographic (1080×1350) |
|
|
97
|
-
|:---:|:---:|
|
|
98
|
-
|  |  |
|
|
99
|
-
|
|
100
|
-
| Poster (1200×1500) | Banner (1600×900) |
|
|
101
|
-
|:---:|:---:|
|
|
102
|
-
|  |  |
|
|
103
|
-
|
|
104
|
-
| Twitter/X Card (1200×675) | YouTube Thumbnail (1280×720) |
|
|
105
|
-
|:---:|:---:|
|
|
106
|
-
|  |  |
|
|
107
|
-
|
|
108
|
-
| Xiaohongshu Cover (1080×1440) | WeChat Cover (900×383) |
|
|
109
|
-
|:---:|:---:|
|
|
110
|
-
|  |  |
|
|
40
|
+
- `default` - Clean minimalist style
|
|
41
|
+
- `github` - GitHub style
|
|
42
|
+
- `magazine` - magazine layout
|
|
111
43
|
|
|
112
|
-
##
|
|
44
|
+
## Options
|
|
113
45
|
|
|
114
|
-
|
|
46
|
+
- `-i, --input <path>` input Markdown or HTML file path
|
|
47
|
+
- `-o, --output <path>` output PDF file path
|
|
48
|
+
- `-t, --template <name>` template for Markdown input (default: `default`)
|
|
49
|
+
- `--wait-until <state>` `load | domcontentloaded | networkidle`
|
|
50
|
+
- `--timeout <ms>` timeout in milliseconds
|
|
51
|
+
- `--safe` disable external network requests and JavaScript execution
|
|
115
52
|
|
|
116
|
-
|
|
117
|
-
- [web-to-png/SKILL.md](skills/web-to-png/SKILL.md) — PNG generation (OG cards, infographics, posters, banners, social cards)
|
|
53
|
+
## AI Agent Skill
|
|
118
54
|
|
|
119
|
-
|
|
55
|
+
- [pagepress/SKILL.md](skills/pagepress/SKILL.md)
|
|
120
56
|
|
|
121
|
-
##
|
|
57
|
+
## License
|
|
122
58
|
|
|
123
59
|
MIT
|
package/README.zh-CN.md
CHANGED
|
@@ -1,121 +1,57 @@
|
|
|
1
1
|
# PagePress
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
面向 AI Agent 的 PDF 渲染 CLI,可将本地 Markdown 和 HTML 文件输出为高质量 PDF。
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 特性
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
- **Image**: 8 种预设 — OG 卡片、信息图、海报、横幅、Twitter/X 卡片、YouTube 缩略图、小红书封面、微信公众号封面
|
|
13
|
-
- **品牌一致性**:OG/海报/横幅会基于工作区的视觉与品牌资源生成(常见于 `assets/`、`public/`、`src/assets/`)
|
|
14
|
-
- **一致性保障**:确定性渲染、字体等待、网络空闲检测、代码高亮
|
|
7
|
+
- 专注 PDF 输出(仅 `pagepress`)
|
|
8
|
+
- 仅支持本地文件输入(`.md` / `.html`)
|
|
9
|
+
- 内置 Markdown 模板:`default`、`github`、`magazine`
|
|
10
|
+
- 支持 Mermaid 代码块渲染
|
|
11
|
+
- 基于 Playwright + Chromium 的稳定打印引擎
|
|
15
12
|
|
|
16
|
-
##
|
|
13
|
+
## 安装
|
|
17
14
|
|
|
18
15
|
```bash
|
|
19
|
-
# 全局安装
|
|
20
16
|
npm install -g @liustack/pagepress
|
|
21
|
-
|
|
22
|
-
# 安装浏览器(Playwright)
|
|
23
17
|
npx playwright install chromium
|
|
24
18
|
```
|
|
25
19
|
|
|
26
|
-
|
|
20
|
+
或使用 `npx`:
|
|
27
21
|
|
|
28
22
|
```bash
|
|
29
|
-
npx @liustack/pagepress
|
|
23
|
+
npx @liustack/pagepress [options]
|
|
30
24
|
```
|
|
31
25
|
|
|
32
|
-
##
|
|
33
|
-
|
|
34
|
-
### 1. 生成 PDF
|
|
35
|
-
|
|
36
|
-
将 HTML/Markdown 转换为 PDF 文档。支持代码高亮。
|
|
26
|
+
## 用法
|
|
37
27
|
|
|
38
28
|
```bash
|
|
39
|
-
# Markdown 转 PDF
|
|
40
|
-
pagepress
|
|
41
|
-
|
|
42
|
-
# 本地 HTML 文件转 PDF
|
|
43
|
-
pagepress pdf -i page.html -o output.pdf
|
|
29
|
+
# Markdown 转 PDF
|
|
30
|
+
pagepress -i document.md -o output.pdf --template default
|
|
44
31
|
|
|
32
|
+
# 本地 HTML 转 PDF
|
|
33
|
+
pagepress -i page.html -o output.pdf
|
|
45
34
|
```
|
|
46
35
|
|
|
47
|
-
|
|
48
|
-
- `default` - Apple 风格,简洁优雅
|
|
49
|
-
- `github` - GitHub 风格
|
|
50
|
-
- `magazine` - VOGUE/WIRED 杂志排版
|
|
51
|
-
|
|
52
|
-
### 2. 生成图像
|
|
36
|
+
## 模板
|
|
53
37
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
**品牌资源目录建议**:
|
|
59
|
-
|
|
60
|
-
```text
|
|
61
|
-
assets/
|
|
62
|
-
brand/
|
|
63
|
-
logo.svg
|
|
64
|
-
palette.json
|
|
65
|
-
fonts/
|
|
66
|
-
YourBrand-Regular.woff2
|
|
67
|
-
imagery/
|
|
68
|
-
hero.jpg
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
# 生成 OG 卡片(1200×630)
|
|
73
|
-
pagepress shot -i card.html -o og.png --preset og
|
|
74
|
-
|
|
75
|
-
# 生成信息长图
|
|
76
|
-
pagepress shot -i stats.html -o infographic.png --preset infographic
|
|
77
|
-
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
**支持的预设**:
|
|
81
|
-
- `og` (1200×630) - 社交卡片
|
|
82
|
-
- `infographic` (1080×1350) - 信息图
|
|
83
|
-
- `poster` (1200×1500) - 海报
|
|
84
|
-
- `banner` (1600×900) - 横幅
|
|
85
|
-
- `twitter` (1200×675) - Twitter/X 卡片
|
|
86
|
-
- `youtube` (1280×720) - YouTube 缩略图
|
|
87
|
-
- `xiaohongshu` (1080×1440) - 小红书封面
|
|
88
|
-
- `wechat` (900×383) - 微信公众号封面
|
|
89
|
-
|
|
90
|
-
## 🖼️ 示例
|
|
91
|
-
|
|
92
|
-
### 图像预设
|
|
93
|
-
|
|
94
|
-
| OG 卡片 (1200×630) | 信息图 (1080×1350) |
|
|
95
|
-
|:---:|:---:|
|
|
96
|
-
|  |  |
|
|
97
|
-
|
|
98
|
-
| 海报 (1200×1500) | 横幅 (1600×900) |
|
|
99
|
-
|:---:|:---:|
|
|
100
|
-
|  |  |
|
|
101
|
-
|
|
102
|
-
| Twitter/X 卡片 (1200×675) | YouTube 缩略图 (1280×720) |
|
|
103
|
-
|:---:|:---:|
|
|
104
|
-
|  |  |
|
|
105
|
-
|
|
106
|
-
| 小红书封面 (1080×1440) | 微信公众号封面 (900×383) |
|
|
107
|
-
|:---:|:---:|
|
|
108
|
-
|  |  |
|
|
38
|
+
- `default` - 简洁风格
|
|
39
|
+
- `github` - GitHub 风格
|
|
40
|
+
- `magazine` - 杂志排版
|
|
109
41
|
|
|
110
|
-
##
|
|
42
|
+
## 参数
|
|
111
43
|
|
|
112
|
-
|
|
44
|
+
- `-i, --input <path>` 输入 Markdown 或 HTML 路径
|
|
45
|
+
- `-o, --output <path>` 输出 PDF 路径
|
|
46
|
+
- `-t, --template <name>` Markdown 模板(默认 `default`)
|
|
47
|
+
- `--wait-until <state>` `load | domcontentloaded | networkidle`
|
|
48
|
+
- `--timeout <ms>` 超时时间(毫秒)
|
|
49
|
+
- `--safe` 禁用外部网络请求和 JavaScript 执行
|
|
113
50
|
|
|
114
|
-
|
|
115
|
-
- [web-to-png/SKILL.md](skills/web-to-png/SKILL.md) — PNG 生成(OG 卡片、信息图、海报、横幅、社交卡片)
|
|
51
|
+
## AI Agent Skill
|
|
116
52
|
|
|
117
|
-
|
|
53
|
+
- [pagepress/SKILL.md](skills/pagepress/SKILL.md)
|
|
118
54
|
|
|
119
|
-
##
|
|
55
|
+
## License
|
|
120
56
|
|
|
121
57
|
MIT
|
package/dist/main.js
CHANGED
|
@@ -11,7 +11,7 @@ import hljs from "highlight.js";
|
|
|
11
11
|
const pdfTemplates = {
|
|
12
12
|
default: {
|
|
13
13
|
name: "default",
|
|
14
|
-
description: "
|
|
14
|
+
description: "Clean minimalist design",
|
|
15
15
|
file: "default.html"
|
|
16
16
|
},
|
|
17
17
|
github: {
|
|
@@ -73,15 +73,15 @@ function getFontCSS(template) {
|
|
|
73
73
|
const __filename$1 = fileURLToPath(import.meta.url);
|
|
74
74
|
const __dirname$1 = path.dirname(__filename$1);
|
|
75
75
|
const require$1 = createRequire(import.meta.url);
|
|
76
|
-
const WAIT_UNTIL_STATES
|
|
77
|
-
function normalizeWaitUntil
|
|
76
|
+
const WAIT_UNTIL_STATES = /* @__PURE__ */ new Set(["load", "domcontentloaded", "networkidle"]);
|
|
77
|
+
function normalizeWaitUntil(value) {
|
|
78
78
|
if (!value) return "networkidle";
|
|
79
|
-
if (WAIT_UNTIL_STATES
|
|
79
|
+
if (WAIT_UNTIL_STATES.has(value)) {
|
|
80
80
|
return value;
|
|
81
81
|
}
|
|
82
82
|
throw new Error(`Invalid waitUntil: ${value}. Allowed: load, domcontentloaded, networkidle.`);
|
|
83
83
|
}
|
|
84
|
-
function normalizeTimeout
|
|
84
|
+
function normalizeTimeout(value) {
|
|
85
85
|
if (value === void 0) return 3e4;
|
|
86
86
|
if (!Number.isFinite(value) || value < 0) {
|
|
87
87
|
throw new Error("Invalid timeout. Use a non-negative number of milliseconds.");
|
|
@@ -91,9 +91,9 @@ function normalizeTimeout$1(value) {
|
|
|
91
91
|
function findTemplatesDir() {
|
|
92
92
|
const candidates = [
|
|
93
93
|
// When running from project root (dev/installed)
|
|
94
|
-
path.resolve(process.cwd(), "src/
|
|
94
|
+
path.resolve(process.cwd(), "src/templates"),
|
|
95
95
|
// Relative to dist/main.js
|
|
96
|
-
path.resolve(__dirname$1, "..", "src", "
|
|
96
|
+
path.resolve(__dirname$1, "..", "src", "templates"),
|
|
97
97
|
// Relative to source file location
|
|
98
98
|
path.resolve(__dirname$1, "templates")
|
|
99
99
|
];
|
|
@@ -102,11 +102,11 @@ function findTemplatesDir() {
|
|
|
102
102
|
}
|
|
103
103
|
throw new Error(`Templates directory not found. Searched: ${candidates.join(", ")}`);
|
|
104
104
|
}
|
|
105
|
-
async function render
|
|
105
|
+
async function render(options) {
|
|
106
106
|
const template = getTemplate(options.template ?? "default");
|
|
107
107
|
const safeMode = options.safe ?? false;
|
|
108
|
-
const waitUntil = normalizeWaitUntil
|
|
109
|
-
const timeout = normalizeTimeout
|
|
108
|
+
const waitUntil = normalizeWaitUntil(options.waitUntil);
|
|
109
|
+
const timeout = normalizeTimeout(options.timeout);
|
|
110
110
|
const templatesDir = findTemplatesDir();
|
|
111
111
|
const templatePath = path.join(templatesDir, template.file);
|
|
112
112
|
if (!fs.existsSync(templatePath)) {
|
|
@@ -316,191 +316,8 @@ async function render$1(options) {
|
|
|
316
316
|
}
|
|
317
317
|
}
|
|
318
318
|
}
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
const timeout = Number.parseInt(options.timeout, 10);
|
|
322
|
-
if (!Number.isFinite(timeout) || timeout < 0) {
|
|
323
|
-
throw new Error("Invalid --timeout value. Use a non-negative integer in milliseconds.");
|
|
324
|
-
}
|
|
325
|
-
const result = await render$1({
|
|
326
|
-
input: options.input,
|
|
327
|
-
output: options.output,
|
|
328
|
-
template: options.template,
|
|
329
|
-
waitUntil: options.waitUntil,
|
|
330
|
-
timeout,
|
|
331
|
-
safe: options.safe
|
|
332
|
-
});
|
|
333
|
-
console.log(JSON.stringify(result, null, 2));
|
|
334
|
-
} catch (error) {
|
|
335
|
-
console.error("Error:", error instanceof Error ? error.message : error);
|
|
336
|
-
process.exit(1);
|
|
337
|
-
}
|
|
338
|
-
});
|
|
339
|
-
const imagePresets = {
|
|
340
|
-
og: {
|
|
341
|
-
name: "og",
|
|
342
|
-
width: 1200,
|
|
343
|
-
height: 630,
|
|
344
|
-
description: "Open Graph / Social Card (1200×630)"
|
|
345
|
-
},
|
|
346
|
-
infographic: {
|
|
347
|
-
name: "infographic",
|
|
348
|
-
width: 1080,
|
|
349
|
-
height: 1350,
|
|
350
|
-
description: "Infographic (1080×1350)"
|
|
351
|
-
},
|
|
352
|
-
poster: {
|
|
353
|
-
name: "poster",
|
|
354
|
-
width: 1200,
|
|
355
|
-
height: 1500,
|
|
356
|
-
description: "Poster (1200×1500)"
|
|
357
|
-
},
|
|
358
|
-
banner: {
|
|
359
|
-
name: "banner",
|
|
360
|
-
width: 1600,
|
|
361
|
-
height: 900,
|
|
362
|
-
description: "Banner (1600×900)"
|
|
363
|
-
},
|
|
364
|
-
twitter: {
|
|
365
|
-
name: "twitter",
|
|
366
|
-
width: 1200,
|
|
367
|
-
height: 675,
|
|
368
|
-
description: "Twitter Card (1200×675)"
|
|
369
|
-
},
|
|
370
|
-
youtube: {
|
|
371
|
-
name: "youtube",
|
|
372
|
-
width: 1280,
|
|
373
|
-
height: 720,
|
|
374
|
-
description: "YouTube Thumbnail (1280×720)"
|
|
375
|
-
},
|
|
376
|
-
xiaohongshu: {
|
|
377
|
-
name: "xiaohongshu",
|
|
378
|
-
width: 1080,
|
|
379
|
-
height: 1440,
|
|
380
|
-
description: "Xiaohongshu / RedNote Cover (1080×1440)"
|
|
381
|
-
},
|
|
382
|
-
wechat: {
|
|
383
|
-
name: "wechat",
|
|
384
|
-
width: 900,
|
|
385
|
-
height: 383,
|
|
386
|
-
description: "WeChat Official Account Cover (900×383)"
|
|
387
|
-
}
|
|
388
|
-
};
|
|
389
|
-
function getPreset(name) {
|
|
390
|
-
const preset = imagePresets[name];
|
|
391
|
-
if (!preset) {
|
|
392
|
-
throw new Error(`Unknown image preset: ${name}. Available: ${Object.keys(imagePresets).join(", ")}`);
|
|
393
|
-
}
|
|
394
|
-
return preset;
|
|
395
|
-
}
|
|
396
|
-
const WAIT_UNTIL_STATES = /* @__PURE__ */ new Set(["load", "domcontentloaded", "networkidle"]);
|
|
397
|
-
const MIN_DIMENSION = 100;
|
|
398
|
-
const MAX_DIMENSION = 5e3;
|
|
399
|
-
const MIN_SCALE = 1;
|
|
400
|
-
const MAX_SCALE = 4;
|
|
401
|
-
function normalizeWaitUntil(value) {
|
|
402
|
-
if (!value) return "networkidle";
|
|
403
|
-
if (WAIT_UNTIL_STATES.has(value)) {
|
|
404
|
-
return value;
|
|
405
|
-
}
|
|
406
|
-
throw new Error(`Invalid waitUntil: ${value}. Allowed: load, domcontentloaded, networkidle.`);
|
|
407
|
-
}
|
|
408
|
-
function normalizeTimeout(value) {
|
|
409
|
-
if (value === void 0) return 3e4;
|
|
410
|
-
if (!Number.isFinite(value) || value < 0) {
|
|
411
|
-
throw new Error("Invalid timeout. Use a non-negative number of milliseconds.");
|
|
412
|
-
}
|
|
413
|
-
return value;
|
|
414
|
-
}
|
|
415
|
-
function validateRange(name, value, min, max) {
|
|
416
|
-
if (!Number.isFinite(value)) {
|
|
417
|
-
throw new Error(`${name} must be a finite number.`);
|
|
418
|
-
}
|
|
419
|
-
if (value < min || value > max) {
|
|
420
|
-
throw new Error(`${name} must be between ${min} and ${max}. Received: ${value}.`);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
async function render(options) {
|
|
424
|
-
const preset = options.preset ? getPreset(options.preset) : getPreset("og");
|
|
425
|
-
const width = options.width ?? preset.width;
|
|
426
|
-
const height = options.height ?? preset.height;
|
|
427
|
-
const scale = options.scale ?? 2;
|
|
428
|
-
const safeMode = options.safe ?? false;
|
|
429
|
-
const waitUntil = normalizeWaitUntil(options.waitUntil);
|
|
430
|
-
const timeout = normalizeTimeout(options.timeout);
|
|
431
|
-
validateRange("Width", width, MIN_DIMENSION, MAX_DIMENSION);
|
|
432
|
-
validateRange("Height", height, MIN_DIMENSION, MAX_DIMENSION);
|
|
433
|
-
validateRange("Scale", scale, MIN_SCALE, MAX_SCALE);
|
|
434
|
-
let browser = null;
|
|
435
|
-
try {
|
|
436
|
-
browser = await chromium.launch({ headless: true });
|
|
437
|
-
const context = await browser.newContext({
|
|
438
|
-
viewport: { width, height },
|
|
439
|
-
deviceScaleFactor: scale,
|
|
440
|
-
javaScriptEnabled: !safeMode
|
|
441
|
-
});
|
|
442
|
-
if (safeMode) {
|
|
443
|
-
await context.route("**/*", (route) => {
|
|
444
|
-
const url = route.request().url();
|
|
445
|
-
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
446
|
-
return route.abort();
|
|
447
|
-
}
|
|
448
|
-
return route.continue();
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
const page = await context.newPage();
|
|
452
|
-
const inputPath = path.resolve(options.input);
|
|
453
|
-
if (options.input.startsWith("http://") || options.input.startsWith("https://")) {
|
|
454
|
-
throw new Error("Remote URL inputs are not supported. Please provide a local HTML file path.");
|
|
455
|
-
} else if (fs.existsSync(inputPath)) {
|
|
456
|
-
const content = fs.readFileSync(inputPath, "utf-8");
|
|
457
|
-
await page.setContent(content, { waitUntil, timeout });
|
|
458
|
-
} else {
|
|
459
|
-
throw new Error(`Input not found: ${options.input}`);
|
|
460
|
-
}
|
|
461
|
-
await page.addStyleTag({
|
|
462
|
-
content: `
|
|
463
|
-
html, body {
|
|
464
|
-
width: ${width}px;
|
|
465
|
-
height: ${height}px;
|
|
466
|
-
margin: 0;
|
|
467
|
-
padding: 0;
|
|
468
|
-
overflow: hidden;
|
|
469
|
-
}
|
|
470
|
-
`
|
|
471
|
-
});
|
|
472
|
-
await page.waitForTimeout(500);
|
|
473
|
-
const cardContainer = await page.$("#container");
|
|
474
|
-
if (!cardContainer) {
|
|
475
|
-
throw new Error("Missing #container element. PNG output requires a #container.");
|
|
476
|
-
}
|
|
477
|
-
const box = await cardContainer.boundingBox();
|
|
478
|
-
if (!box || box.width <= 0 || box.height <= 0) {
|
|
479
|
-
throw new Error("Invalid #container size. Ensure it has a positive width and height.");
|
|
480
|
-
}
|
|
481
|
-
const outputPath = path.resolve(options.output);
|
|
482
|
-
await page.screenshot({
|
|
483
|
-
path: outputPath,
|
|
484
|
-
type: "png",
|
|
485
|
-
clip: { x: box.x, y: box.y, width: box.width, height: box.height }
|
|
486
|
-
});
|
|
487
|
-
return {
|
|
488
|
-
pngPath: outputPath,
|
|
489
|
-
meta: {
|
|
490
|
-
preset: preset.name,
|
|
491
|
-
width,
|
|
492
|
-
height,
|
|
493
|
-
deviceScaleFactor: scale,
|
|
494
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
495
|
-
}
|
|
496
|
-
};
|
|
497
|
-
} finally {
|
|
498
|
-
if (browser) {
|
|
499
|
-
await browser.close();
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
const command = new Command("shot").description("Capture HTML to PNG image").requiredOption("-i, --input <path>", "Input HTML file path").requiredOption("-o, --output <path>", "Output PNG file path").option("-p, --preset <name>", "Image preset (og, infographic, poster, banner, twitter, youtube, xiaohongshu, wechat)", "og").option("--width <number>", "Custom width in pixels").option("--height <number>", "Custom height in pixels").option("--scale <number>", "Device scale factor", "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) => {
|
|
319
|
+
const program = new Command();
|
|
320
|
+
program.name("pagepress").description("Render local Markdown and HTML files to PDF").version("0.8.0").requiredOption("-i, --input <path>", "Input Markdown or HTML 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) => {
|
|
504
321
|
try {
|
|
505
322
|
const timeout = Number.parseInt(options.timeout, 10);
|
|
506
323
|
if (!Number.isFinite(timeout) || timeout < 0) {
|
|
@@ -509,10 +326,7 @@ const command = new Command("shot").description("Capture HTML to PNG image").req
|
|
|
509
326
|
const result = await render({
|
|
510
327
|
input: options.input,
|
|
511
328
|
output: options.output,
|
|
512
|
-
|
|
513
|
-
width: options.width ? parseInt(options.width) : void 0,
|
|
514
|
-
height: options.height ? parseInt(options.height) : void 0,
|
|
515
|
-
scale: parseFloat(options.scale),
|
|
329
|
+
template: options.template,
|
|
516
330
|
waitUntil: options.waitUntil,
|
|
517
331
|
timeout,
|
|
518
332
|
safe: options.safe
|
|
@@ -523,8 +337,4 @@ const command = new Command("shot").description("Capture HTML to PNG image").req
|
|
|
523
337
|
process.exit(1);
|
|
524
338
|
}
|
|
525
339
|
});
|
|
526
|
-
const program = new Command();
|
|
527
|
-
program.name("pagepress").description("Convert web pages and Markdown to PDF and images").version("0.6.1");
|
|
528
|
-
program.addCommand(command$1);
|
|
529
|
-
program.addCommand(command);
|
|
530
340
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,28 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liustack/pagepress",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CLI tool to
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "CLI tool to render local Markdown and HTML files into PDF",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"pagepress": "./dist/main.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
|
-
"src/
|
|
11
|
+
"src/templates"
|
|
12
12
|
],
|
|
13
13
|
"keywords": [
|
|
14
14
|
"pdf",
|
|
15
|
-
"png",
|
|
16
|
-
"screenshot",
|
|
17
|
-
"og-image",
|
|
18
|
-
"social-card",
|
|
19
|
-
"banner",
|
|
20
|
-
"poster",
|
|
21
|
-
"infographic",
|
|
22
15
|
"markdown-to-pdf",
|
|
23
16
|
"html-to-pdf",
|
|
24
|
-
"html-to-image",
|
|
25
|
-
"web-to-png",
|
|
26
17
|
"pdf-report",
|
|
27
18
|
"cli",
|
|
28
19
|
"playwright",
|
|
@@ -55,13 +46,19 @@
|
|
|
55
46
|
},
|
|
56
47
|
"devDependencies": {
|
|
57
48
|
"@types/node": "^22.19.7",
|
|
49
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
58
50
|
"typescript": "^5.9.3",
|
|
59
51
|
"vite": "^6.4.1",
|
|
60
|
-
"vite-plugin-node": "^5.0.1"
|
|
52
|
+
"vite-plugin-node": "^5.0.1",
|
|
53
|
+
"vitest": "^4.0.18"
|
|
61
54
|
},
|
|
62
55
|
"scripts": {
|
|
63
56
|
"dev": "vite build --watch",
|
|
64
57
|
"build": "vite build",
|
|
65
|
-
"typecheck": "tsc --noEmit"
|
|
58
|
+
"typecheck": "tsc --noEmit",
|
|
59
|
+
"test": "vitest run",
|
|
60
|
+
"test:watch": "vitest",
|
|
61
|
+
"test:coverage": "vitest run --coverage",
|
|
62
|
+
"docs:list": "node scripts/docs-list.js"
|
|
66
63
|
}
|
|
67
64
|
}
|