@liustack/pagepress 0.9.0 → 1.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,15 +1,13 @@
1
1
  # PagePress
2
2
 
3
- A CLI toolkit for AI agents to render local Markdown and HTML files into high-quality PDF documents.
3
+ A CLI toolkit for AI agents to render local 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
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
10
+ - Local HTML file input only (`.html`)
13
11
  - Deterministic output via Playwright + Chromium print engine
14
12
 
15
13
  ## Installation
@@ -28,24 +26,14 @@ npx @liustack/pagepress [options]
28
26
  ## Usage
29
27
 
30
28
  ```bash
31
- # Markdown to PDF
32
- pagepress -i document.md -o output.pdf --template default
33
-
34
29
  # Local HTML to PDF
35
30
  pagepress -i page.html -o output.pdf
36
31
  ```
37
32
 
38
- ## Templates
39
-
40
- - `default` - Clean minimalist style
41
- - `github` - GitHub style
42
- - `magazine` - magazine layout
43
-
44
33
  ## Options
45
34
 
46
- - `-i, --input <path>` input Markdown or HTML file path
35
+ - `-i, --input <path>` input HTML file path
47
36
  - `-o, --output <path>` output PDF file path
48
- - `-t, --template <name>` template for Markdown input (default: `default`)
49
37
  - `--wait-until <state>` `load | domcontentloaded | networkidle`
50
38
  - `--timeout <ms>` timeout in milliseconds
51
39
  - `--safe` disable external network requests and JavaScript execution
package/README.zh-CN.md CHANGED
@@ -1,13 +1,11 @@
1
1
  # PagePress
2
2
 
3
- 面向 AI Agent 的 PDF 渲染 CLI,可将本地 Markdown 和 HTML 文件输出为高质量 PDF。
3
+ 面向 AI Agent 的 PDF 渲染 CLI,可将本地 HTML 文件输出为高质量 PDF。
4
4
 
5
5
  ## 特性
6
6
 
7
7
  - 专注 PDF 输出(仅 `pagepress`)
8
- - 仅支持本地文件输入(`.md` / `.html`)
9
- - 内置 Markdown 模板:`default`、`github`、`magazine`
10
- - 支持 Mermaid 代码块渲染
8
+ - 仅支持本地 HTML 文件输入(`.html`)
11
9
  - 基于 Playwright + Chromium 的稳定打印引擎
12
10
 
13
11
  ## 安装
@@ -26,24 +24,14 @@ npx @liustack/pagepress [options]
26
24
  ## 用法
27
25
 
28
26
  ```bash
29
- # Markdown 转 PDF
30
- pagepress -i document.md -o output.pdf --template default
31
-
32
27
  # 本地 HTML 转 PDF
33
28
  pagepress -i page.html -o output.pdf
34
29
  ```
35
30
 
36
- ## 模板
37
-
38
- - `default` - 简洁风格
39
- - `github` - GitHub 风格
40
- - `magazine` - 杂志排版
41
-
42
31
  ## 参数
43
32
 
44
- - `-i, --input <path>` 输入 Markdown 或 HTML 路径
33
+ - `-i, --input <path>` 输入 HTML 路径
45
34
  - `-o, --output <path>` 输出 PDF 路径
46
- - `-t, --template <name>` Markdown 模板(默认 `default`)
47
35
  - `--wait-until <state>` `load | domcontentloaded | networkidle`
48
36
  - `--timeout <ms>` 超时时间(毫秒)
49
37
  - `--safe` 禁用外部网络请求和 JavaScript 执行
package/dist/main.js CHANGED
@@ -4,75 +4,8 @@ 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
- const pdfTemplates = {
12
- default: {
13
- name: "default",
14
- description: "Clean minimalist design",
15
- file: "default.html"
16
- },
17
- github: {
18
- name: "github",
19
- description: "GitHub-flavored markdown style",
20
- file: "github.html"
21
- },
22
- magazine: {
23
- name: "magazine",
24
- description: "VOGUE/WIRED premium magazine style",
25
- file: "magazine.html"
26
- }
27
- };
28
- function getTemplate(name) {
29
- const template = pdfTemplates[name];
30
- if (!template) {
31
- throw new Error(`Unknown PDF template: ${name}. Available: ${Object.keys(pdfTemplates).join(", ")}`);
32
- }
33
- return template;
34
- }
35
- const require$2 = createRequire(import.meta.url);
36
- const MAGAZINE_FONTS = [
37
- "@fontsource/bebas-neue/400.css",
38
- "@fontsource/cormorant-garamond/400.css",
39
- "@fontsource/cormorant-garamond/400-italic.css",
40
- "@fontsource/cormorant-garamond/600.css",
41
- "@fontsource/inter/300.css",
42
- "@fontsource/inter/400.css",
43
- "@fontsource/inter/500.css"
44
- ];
45
- function getFontCSS(template) {
46
- if (template !== "magazine") {
47
- return "";
48
- }
49
- const cssChunks = [];
50
- for (const fontPath of MAGAZINE_FONTS) {
51
- try {
52
- const cssFilePath = require$2.resolve(fontPath);
53
- let css = fs.readFileSync(cssFilePath, "utf-8");
54
- const cssDir = path.dirname(cssFilePath);
55
- css = css.replace(/url\(\.\/files\/([^)]+)\)/g, (match, filename) => {
56
- const fontFilePath = path.join(cssDir, "files", filename);
57
- if (fs.existsSync(fontFilePath)) {
58
- const fontData = fs.readFileSync(fontFilePath);
59
- const base64 = fontData.toString("base64");
60
- const ext = path.extname(filename).slice(1);
61
- const mimeType = ext === "woff2" ? "font/woff2" : `font/${ext}`;
62
- return `url(data:${mimeType};base64,${base64})`;
63
- }
64
- return match;
65
- });
66
- cssChunks.push(css);
67
- } catch (e) {
68
- console.warn(`Warning: Could not load font ${fontPath}:`, e);
69
- }
70
- }
71
- return cssChunks.join("\n");
72
- }
73
7
  const __filename$1 = fileURLToPath(import.meta.url);
74
- const __dirname$1 = path.dirname(__filename$1);
75
- const require$1 = createRequire(import.meta.url);
8
+ path.dirname(__filename$1);
76
9
  const WAIT_UNTIL_STATES = /* @__PURE__ */ new Set(["load", "domcontentloaded", "networkidle"]);
77
10
  function normalizeWaitUntil(value) {
78
11
  if (!value) return "networkidle";
@@ -88,92 +21,18 @@ function normalizeTimeout(value) {
88
21
  }
89
22
  return value;
90
23
  }
91
- function findTemplatesDir() {
92
- const candidates = [
93
- // When running from project root (dev/installed)
94
- path.resolve(process.cwd(), "src/templates"),
95
- // Relative to dist/main.js
96
- path.resolve(__dirname$1, "..", "src", "templates"),
97
- // Relative to source file location
98
- path.resolve(__dirname$1, "templates")
99
- ];
100
- for (const dir of candidates) {
101
- if (fs.existsSync(dir)) return dir;
102
- }
103
- throw new Error(`Templates directory not found. Searched: ${candidates.join(", ")}`);
104
- }
105
24
  async function render(options) {
106
- const template = getTemplate(options.template ?? "default");
107
25
  const safeMode = options.safe ?? false;
108
26
  const waitUntil = normalizeWaitUntil(options.waitUntil);
109
27
  const timeout = normalizeTimeout(options.timeout);
110
- const templatesDir = findTemplatesDir();
111
- const templatePath = path.join(templatesDir, template.file);
112
- if (!fs.existsSync(templatePath)) {
113
- throw new Error(`Template not found: ${templatePath}`);
114
- }
115
- const inputPath = path.resolve(options.input);
116
- let htmlContent;
117
- let hasMermaid = false;
118
28
  if (options.input.startsWith("http://") || options.input.startsWith("https://")) {
119
- throw new Error("Remote URL inputs are not supported. Please provide a local Markdown or HTML file path.");
120
- } else if (options.input.endsWith(".md")) {
121
- const raw = fs.readFileSync(inputPath, "utf-8");
122
- const { data: frontmatter, content } = matter(raw);
123
- hasMermaid = /```mermaid/i.test(content);
124
- const renderer = new Renderer();
125
- renderer.code = function({ text, lang }) {
126
- if (lang === "mermaid") {
127
- return `<pre class="mermaid">${text}</pre>`;
128
- }
129
- const highlighted = lang && hljs.getLanguage(lang) ? hljs.highlight(text, { language: lang }).value : hljs.highlightAuto(text).value;
130
- const langClass = lang ? `hljs language-${lang}` : "hljs";
131
- return `<pre><code class="${langClass}">${highlighted}</code></pre>`;
132
- };
133
- marked.use({ renderer });
134
- const bodyHtml = await marked.parse(content);
135
- const title = frontmatter.title || "Document";
136
- const isDarkTheme = template.name === "magazine";
137
- const hljsLightStyles = `
138
- <style>
139
- /* highlight.js GitHub Light Theme */
140
- .hljs { color: #24292e; }
141
- .hljs-comment, .hljs-quote { color: #6a737d; font-style: italic; }
142
- .hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-type { color: #d73a49; font-weight: 600; }
143
- .hljs-string, .hljs-attr, .hljs-symbol, .hljs-bullet, .hljs-addition { color: #032f62; }
144
- .hljs-number { color: #005cc5; }
145
- .hljs-title, .hljs-section, .hljs-function .hljs-title { color: #6f42c1; }
146
- .hljs-built_in, .hljs-name { color: #005cc5; }
147
- .hljs-class .hljs-title { color: #e36209; }
148
- .hljs-meta { color: #6f42c1; }
149
- .hljs-variable, .hljs-template-variable { color: #e36209; }
150
- .hljs-params { color: #24292e; }
151
- </style>
152
- `;
153
- const hljsDarkStyles = `
154
- <style>
155
- /* highlight.js Dark Theme for Magazine */
156
- .hljs { color: #e8e8e8; }
157
- .hljs-comment, .hljs-quote { color: #6b7280; font-style: italic; }
158
- .hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-type { color: #f0abfc; }
159
- .hljs-string, .hljs-attr, .hljs-symbol, .hljs-bullet, .hljs-addition { color: #86efac; }
160
- .hljs-number { color: #fcd34d; }
161
- .hljs-title, .hljs-section, .hljs-function .hljs-title { color: #93c5fd; }
162
- .hljs-built_in, .hljs-name { color: #93c5fd; }
163
- .hljs-class .hljs-title { color: #67e8f9; }
164
- .hljs-meta { color: #f0abfc; }
165
- .hljs-variable, .hljs-template-variable { color: #fcd34d; }
166
- .hljs-params { color: #e8e8e8; }
167
- </style>
168
- `;
169
- const hljsStyles = isDarkTheme ? hljsDarkStyles : hljsLightStyles;
170
- let templateHtml = fs.readFileSync(templatePath, "utf-8");
171
- htmlContent = templateHtml.replace(/\{\{title\}\}/g, title).replace(/\{\{body\}\}/g, bodyHtml).replace(/\{\{styles\}\}/g, hljsStyles);
172
- } else if (options.input.endsWith(".html")) {
173
- htmlContent = fs.readFileSync(inputPath, "utf-8");
174
- } else {
175
- throw new Error(`Unsupported input format: ${options.input}`);
29
+ throw new Error("Remote URL inputs are not supported. Please provide a local HTML file path.");
30
+ }
31
+ if (!options.input.endsWith(".html")) {
32
+ throw new Error(`Unsupported input format: ${options.input}. Only HTML (.html) files are supported.`);
176
33
  }
34
+ const inputPath = path.resolve(options.input);
35
+ const htmlContent = fs.readFileSync(inputPath, "utf-8");
177
36
  let browser = null;
178
37
  try {
179
38
  browser = await chromium.launch({ headless: true });
@@ -190,112 +49,6 @@ async function render(options) {
190
49
  const page = await context.newPage();
191
50
  await page.goto(pathToFileURL(inputPath).href, { waitUntil: "domcontentloaded", timeout });
192
51
  await page.setContent(htmlContent, { waitUntil, timeout });
193
- const fontCSS = getFontCSS(template.name);
194
- if (fontCSS) {
195
- await page.addStyleTag({ content: fontCSS });
196
- }
197
- if (hasMermaid && !safeMode) {
198
- const mermaidPath = require$1.resolve("mermaid/dist/mermaid.min.js");
199
- await page.addScriptTag({ path: mermaidPath });
200
- const templateName = template.name;
201
- await page.evaluate((tpl) => {
202
- const defaultTheme = {
203
- primaryColor: "#f5f5f7",
204
- primaryTextColor: "#1d1d1f",
205
- primaryBorderColor: "#d2d2d7",
206
- lineColor: "#86868b",
207
- secondaryColor: "#f5f5f7",
208
- tertiaryColor: "#ffffff",
209
- background: "#ffffff",
210
- mainBkg: "#f5f5f7",
211
- nodeBorder: "#d2d2d7",
212
- nodeTextColor: "#1d1d1f",
213
- clusterBkg: "rgba(0, 0, 0, 0.02)",
214
- clusterBorder: "rgba(0, 0, 0, 0.1)",
215
- titleColor: "#1d1d1f"
216
- };
217
- const githubTheme = {
218
- primaryColor: "#f6f8fa",
219
- primaryTextColor: "#1f2328",
220
- primaryBorderColor: "#d0d7de",
221
- lineColor: "#656d76",
222
- secondaryColor: "#f6f8fa",
223
- tertiaryColor: "#ffffff",
224
- background: "#ffffff",
225
- mainBkg: "#f6f8fa",
226
- nodeBorder: "#d0d7de",
227
- nodeTextColor: "#1f2328",
228
- clusterBkg: "rgba(208, 215, 222, 0.2)",
229
- clusterBorder: "#d0d7de",
230
- titleColor: "#1f2328"
231
- };
232
- const magazineTheme = {
233
- primaryColor: "#2a2a2a",
234
- primaryTextColor: "#ffffff",
235
- primaryBorderColor: "#c41e3a",
236
- lineColor: "#666666",
237
- secondaryColor: "#1a1a1a",
238
- tertiaryColor: "#0f0f0f",
239
- background: "#0f0f0f",
240
- mainBkg: "#2a2a2a",
241
- nodeBorder: "#c41e3a",
242
- nodeTextColor: "#ffffff",
243
- clusterBkg: "rgba(196, 30, 58, 0.1)",
244
- clusterBorder: "rgba(196, 30, 58, 0.4)",
245
- titleColor: "#ffffff"
246
- };
247
- const themes = {
248
- default: defaultTheme,
249
- github: githubTheme,
250
- magazine: magazineTheme
251
- };
252
- const activeTheme = themes[tpl] || defaultTheme;
253
- window.mermaid.initialize({
254
- startOnLoad: false,
255
- theme: "base",
256
- themeVariables: {
257
- ...activeTheme,
258
- fontFamily: '"SF Mono", "Fira Code", Menlo, monospace',
259
- fontSize: "13px"
260
- },
261
- flowchart: {
262
- curve: "basis",
263
- nodeSpacing: 40,
264
- rankSpacing: 50,
265
- htmlLabels: true,
266
- useMaxWidth: true,
267
- subGraphTitleMargin: { top: 15, bottom: 15 },
268
- padding: 20
269
- }
270
- });
271
- }, templateName);
272
- await page.evaluate(() => {
273
- return window.mermaid.run();
274
- });
275
- await page.waitForSelector(".mermaid svg", { timeout: 5e3 });
276
- await page.waitForTimeout(300);
277
- await page.evaluate(() => {
278
- document.querySelectorAll(".node rect").forEach((rect) => {
279
- rect.setAttribute("rx", "12");
280
- rect.setAttribute("ry", "12");
281
- });
282
- document.querySelectorAll(".cluster rect").forEach((rect) => {
283
- rect.setAttribute("rx", "16");
284
- rect.setAttribute("ry", "16");
285
- });
286
- document.querySelectorAll(".cluster-label").forEach((label) => {
287
- const transform = label.getAttribute("transform");
288
- if (transform) {
289
- const match = transform.match(/translate\(([\d.]+),\s*([\d.]+)\)/);
290
- if (match) {
291
- const x = parseFloat(match[1]);
292
- const y = parseFloat(match[2]) + 8;
293
- label.setAttribute("transform", `translate(${x}, ${y})`);
294
- }
295
- }
296
- });
297
- });
298
- }
299
52
  await page.waitForTimeout(200);
300
53
  const outputPath = path.resolve(options.output);
301
54
  await page.pdf({
@@ -307,7 +60,6 @@ async function render(options) {
307
60
  return {
308
61
  pdfPath: outputPath,
309
62
  meta: {
310
- template: template.name,
311
63
  generatedAt: (/* @__PURE__ */ new Date()).toISOString()
312
64
  }
313
65
  };
@@ -318,7 +70,7 @@ async function render(options) {
318
70
  }
319
71
  }
320
72
  const program = new Command();
321
- program.name("pagepress").description("Render local Markdown and HTML files to PDF").version("0.9.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) => {
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) => {
322
74
  try {
323
75
  const timeout = Number.parseInt(options.timeout, 10);
324
76
  if (!Number.isFinite(timeout) || timeout < 0) {
@@ -327,7 +79,6 @@ program.name("pagepress").description("Render local Markdown and HTML files to P
327
79
  const result = await render({
328
80
  input: options.input,
329
81
  output: options.output,
330
- template: options.template,
331
82
  waitUntil: options.waitUntil,
332
83
  timeout,
333
84
  safe: options.safe
package/package.json CHANGED
@@ -1,23 +1,20 @@
1
1
  {
2
2
  "name": "@liustack/pagepress",
3
- "version": "0.9.0",
4
- "description": "CLI tool to render local Markdown and HTML files into PDF",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool to render local HTML files into PDF",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "pagepress": "./dist/main.js"
8
8
  },
9
9
  "files": [
10
- "dist",
11
- "src/templates"
10
+ "dist"
12
11
  ],
13
12
  "keywords": [
14
13
  "pdf",
15
- "markdown-to-pdf",
16
14
  "html-to-pdf",
17
15
  "pdf-report",
18
16
  "cli",
19
- "playwright",
20
- "mermaid"
17
+ "playwright"
21
18
  ],
22
19
  "author": "Leon Liu",
23
20
  "license": "MIT",
@@ -33,15 +30,7 @@
33
30
  "node": ">=18"
34
31
  },
35
32
  "dependencies": {
36
- "@fontsource/bebas-neue": "^5.2.7",
37
- "@fontsource/cormorant-garamond": "^5.2.11",
38
- "@fontsource/inter": "^5.2.8",
39
33
  "commander": "^13.1.0",
40
- "gray-matter": "^4.0.3",
41
- "highlight.js": "^11.11.1",
42
- "marked": "^15.0.12",
43
- "marked-highlight": "^2.2.3",
44
- "mermaid": "^11.12.2",
45
34
  "playwright": "^1.58.1"
46
35
  },
47
36
  "devDependencies": {
@@ -1,253 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
-
4
- <head>
5
- <meta charset="UTF-8" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>{{title}}</title>
8
- <style>
9
- /**
10
- * Default Theme — Clean minimalist design
11
- */
12
-
13
- :root {
14
- --text-primary: #1d1d1f;
15
- --text-secondary: #6e6e73;
16
- --text-tertiary: #86868b;
17
- --bg-primary: #ffffff;
18
- --border-color: #d2d2d7;
19
- --border-light: #e5e5ea;
20
- --accent: #0071e3;
21
- --code-bg: #f5f5f7;
22
- --code-block-bg: #f6f8fa;
23
- }
24
-
25
- * {
26
- box-sizing: border-box;
27
- margin: 0;
28
- padding: 0;
29
- }
30
-
31
- body {
32
- font-family: system-ui, -apple-system, BlinkMacSystemFont,
33
- "PingFang SC", "Hiragino Sans GB", "Noto Sans CJK SC", sans-serif;
34
- font-size: 16px;
35
- line-height: 1.75;
36
- color: var(--text-primary);
37
- background: var(--bg-primary);
38
- -webkit-font-smoothing: antialiased;
39
- -moz-osx-font-smoothing: grayscale;
40
- -webkit-print-color-adjust: exact;
41
- print-color-adjust: exact;
42
- }
43
-
44
- .container {
45
- max-width: 900px;
46
- margin: 0 auto;
47
- padding: 64px 56px 120px;
48
- }
49
-
50
- /* Heading System */
51
- h1, h2, h3, h4, h5, h6 {
52
- font-weight: 600;
53
- line-height: 1.25;
54
- margin-top: 1.5em;
55
- margin-bottom: 0.5em;
56
- }
57
-
58
- h1 {
59
- font-size: 32px;
60
- line-height: 1.15;
61
- margin-top: 0;
62
- margin-bottom: 20px;
63
- }
64
-
65
- h2 {
66
- font-size: 24px;
67
- margin-top: 40px;
68
- }
69
-
70
- h3 {
71
- font-size: 18px;
72
- }
73
-
74
- h4 {
75
- font-size: 16px;
76
- color: var(--text-secondary);
77
- }
78
-
79
- /* Paragraph */
80
- p {
81
- margin-bottom: 1.25em;
82
- }
83
-
84
- strong {
85
- font-weight: 600;
86
- }
87
-
88
- a {
89
- color: var(--accent);
90
- text-decoration: none;
91
- }
92
-
93
- a:hover {
94
- text-decoration: underline;
95
- }
96
-
97
- /* Blockquote */
98
- blockquote {
99
- margin: 1.5em 0;
100
- padding: 0 0 0 1em;
101
- border-left: 3px solid var(--border-color);
102
- color: var(--text-secondary);
103
- font-style: italic;
104
- }
105
-
106
- blockquote p:last-child {
107
- margin-bottom: 0;
108
- }
109
-
110
- /* Code */
111
- code {
112
- font-family: ui-monospace, Menlo, Consolas, monospace;
113
- font-size: 0.875em;
114
- padding: 0.15em 0.4em;
115
- background: var(--code-bg);
116
- border-radius: 4px;
117
- color: var(--text-primary);
118
- }
119
-
120
- pre {
121
- margin: 1.5em 0;
122
- padding: 20px 24px;
123
- background: var(--code-block-bg);
124
- border: 1px solid var(--border-light);
125
- border-radius: 8px;
126
- overflow-x: auto;
127
- font-family: ui-monospace, Menlo, Consolas, monospace;
128
- font-size: 13px;
129
- line-height: 1.6;
130
- color: var(--text-primary);
131
- }
132
-
133
- pre code {
134
- display: block;
135
- padding: 0;
136
- background: transparent;
137
- color: inherit;
138
- border-radius: 0;
139
- font-size: inherit;
140
- font-family: inherit;
141
- }
142
-
143
- /* Syntax highlighting — light theme */
144
- pre .hljs { color: #24292e; }
145
- pre .hljs-comment, pre .hljs-quote { color: #6a737d; font-style: italic; }
146
- pre .hljs-keyword, pre .hljs-selector-tag, pre .hljs-literal, pre .hljs-type { color: #d73a49; font-weight: 600; }
147
- pre .hljs-string, pre .hljs-attr, pre .hljs-symbol, pre .hljs-bullet, pre .hljs-addition { color: #032f62; }
148
- pre .hljs-number { color: #005cc5; }
149
- pre .hljs-title, pre .hljs-section { color: #6f42c1; }
150
- pre .hljs-built_in, pre .hljs-name { color: #005cc5; }
151
- pre .hljs-variable, pre .hljs-template-variable { color: #e36209; }
152
- pre .hljs-meta { color: #6f42c1; }
153
-
154
- /* codehilite compatibility */
155
- pre .c, pre .ch, pre .cm, pre .c1, pre .cs { color: #6a737d; font-style: italic; }
156
- pre .k, pre .kc, pre .kd, pre .kn, pre .kp, pre .kr, pre .kt { color: #d73a49; font-weight: 600; }
157
- pre .s, pre .s1, pre .s2, pre .sa, pre .sb, pre .sc, pre .sd, pre .se, pre .sh, pre .si, pre .sr, pre .ss, pre .sx { color: #032f62; }
158
- pre .nf, pre .fm { color: #6f42c1; }
159
- pre .mi, pre .mf, pre .mh, pre .mo, pre .mb, pre .il { color: #005cc5; }
160
- pre .bp { color: #005cc5; }
161
-
162
- /* Table */
163
- table {
164
- width: 100%;
165
- margin: 1.5em 0;
166
- border-collapse: collapse;
167
- font-size: 15px;
168
- }
169
-
170
- th, td {
171
- padding: 10px 14px;
172
- text-align: left;
173
- border-bottom: 1px solid var(--border-light);
174
- }
175
-
176
- th {
177
- font-weight: 600;
178
- color: var(--text-primary);
179
- border-bottom-color: var(--border-color);
180
- }
181
-
182
- tr:last-child td {
183
- border-bottom: none;
184
- }
185
-
186
- /* Lists */
187
- ul, ol {
188
- margin: 1em 0;
189
- padding-left: 1.5em;
190
- }
191
-
192
- li {
193
- margin: 0.4em 0;
194
- line-height: 1.75;
195
- }
196
-
197
- li::marker {
198
- color: var(--text-secondary);
199
- }
200
-
201
- /* Images */
202
- img {
203
- max-width: 100%;
204
- height: auto;
205
- display: block;
206
- margin: 1.5em auto;
207
- border-radius: 8px;
208
- }
209
-
210
- /* Divider */
211
- hr {
212
- border: none;
213
- height: 1px;
214
- background: var(--border-color);
215
- margin: 2em 0;
216
- }
217
-
218
- /* Print */
219
- @page {
220
- size: A4;
221
- margin: 24mm 20mm;
222
- }
223
-
224
- @media print {
225
- .container {
226
- padding: 0;
227
- max-width: none;
228
- }
229
-
230
- h1 { font-size: 24px; }
231
- h2 { font-size: 18px; }
232
-
233
- pre {
234
- background: var(--code-block-bg) !important;
235
- color: var(--text-primary) !important;
236
- border: 1px solid var(--border-light);
237
- -webkit-print-color-adjust: exact;
238
- print-color-adjust: exact;
239
- }
240
-
241
- pre code { color: inherit !important; }
242
- }
243
- </style>
244
- {{styles}}
245
- </head>
246
-
247
- <body>
248
- <article class="container">
249
- {{body}}
250
- </article>
251
- </body>
252
-
253
- </html>