@liustack/pagepress 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js ADDED
@@ -0,0 +1,528 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { chromium } from "playwright";
4
+ import * as fs from "fs";
5
+ import * as path from "path";
6
+ import { fileURLToPath } 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: "Apple-inspired clean 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
+ const __filename$1 = fileURLToPath(import.meta.url);
74
+ const __dirname$1 = path.dirname(__filename$1);
75
+ const require$1 = createRequire(import.meta.url);
76
+ const WAIT_UNTIL_STATES$1 = /* @__PURE__ */ new Set(["load", "domcontentloaded", "networkidle"]);
77
+ function normalizeWaitUntil$1(value) {
78
+ if (!value) return "networkidle";
79
+ if (WAIT_UNTIL_STATES$1.has(value)) {
80
+ return value;
81
+ }
82
+ throw new Error(`Invalid waitUntil: ${value}. Allowed: load, domcontentloaded, networkidle.`);
83
+ }
84
+ function normalizeTimeout$1(value) {
85
+ if (value === void 0) return 3e4;
86
+ if (!Number.isFinite(value) || value < 0) {
87
+ throw new Error("Invalid timeout. Use a non-negative number of milliseconds.");
88
+ }
89
+ return value;
90
+ }
91
+ function findTemplatesDir() {
92
+ const candidates = [
93
+ // When running from project root (dev/installed)
94
+ path.resolve(process.cwd(), "src/pdf/templates"),
95
+ // Relative to dist/main.js
96
+ path.resolve(__dirname$1, "..", "src", "pdf", "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
+ async function render$1(options) {
106
+ const template = getTemplate(options.template ?? "default");
107
+ const safeMode = options.safe ?? false;
108
+ const waitUntil = normalizeWaitUntil$1(options.waitUntil);
109
+ const timeout = normalizeTimeout$1(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
+ if (options.input.startsWith("http")) {
119
+ if (safeMode) {
120
+ throw new Error("Safe mode does not allow remote URL inputs.");
121
+ }
122
+ htmlContent = "";
123
+ } else if (options.input.endsWith(".md")) {
124
+ const raw = fs.readFileSync(inputPath, "utf-8");
125
+ const { data: frontmatter, content } = matter(raw);
126
+ hasMermaid = /```mermaid/i.test(content);
127
+ const renderer = new Renderer();
128
+ renderer.code = function({ text, lang }) {
129
+ if (lang === "mermaid") {
130
+ return `<pre class="mermaid">${text}</pre>`;
131
+ }
132
+ const highlighted = lang && hljs.getLanguage(lang) ? hljs.highlight(text, { language: lang }).value : hljs.highlightAuto(text).value;
133
+ const langClass = lang ? `hljs language-${lang}` : "hljs";
134
+ return `<pre><code class="${langClass}">${highlighted}</code></pre>`;
135
+ };
136
+ marked.use({ renderer });
137
+ const bodyHtml = await marked.parse(content);
138
+ const title = frontmatter.title || "Document";
139
+ const isDarkTheme = template.name === "magazine";
140
+ const hljsLightStyles = `
141
+ <style>
142
+ /* highlight.js GitHub Light Theme */
143
+ .hljs { color: #24292e; }
144
+ .hljs-comment, .hljs-quote { color: #6a737d; font-style: italic; }
145
+ .hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-type { color: #d73a49; font-weight: 600; }
146
+ .hljs-string, .hljs-attr, .hljs-symbol, .hljs-bullet, .hljs-addition { color: #032f62; }
147
+ .hljs-number { color: #005cc5; }
148
+ .hljs-title, .hljs-section, .hljs-function .hljs-title { color: #6f42c1; }
149
+ .hljs-built_in, .hljs-name { color: #005cc5; }
150
+ .hljs-class .hljs-title { color: #e36209; }
151
+ .hljs-meta { color: #6f42c1; }
152
+ .hljs-variable, .hljs-template-variable { color: #e36209; }
153
+ .hljs-params { color: #24292e; }
154
+ </style>
155
+ `;
156
+ const hljsDarkStyles = `
157
+ <style>
158
+ /* highlight.js Dark Theme for Magazine */
159
+ .hljs { color: #e8e8e8; }
160
+ .hljs-comment, .hljs-quote { color: #6b7280; font-style: italic; }
161
+ .hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-type { color: #f0abfc; }
162
+ .hljs-string, .hljs-attr, .hljs-symbol, .hljs-bullet, .hljs-addition { color: #86efac; }
163
+ .hljs-number { color: #fcd34d; }
164
+ .hljs-title, .hljs-section, .hljs-function .hljs-title { color: #93c5fd; }
165
+ .hljs-built_in, .hljs-name { color: #93c5fd; }
166
+ .hljs-class .hljs-title { color: #67e8f9; }
167
+ .hljs-meta { color: #f0abfc; }
168
+ .hljs-variable, .hljs-template-variable { color: #fcd34d; }
169
+ .hljs-params { color: #e8e8e8; }
170
+ </style>
171
+ `;
172
+ const hljsStyles = isDarkTheme ? hljsDarkStyles : hljsLightStyles;
173
+ let templateHtml = fs.readFileSync(templatePath, "utf-8");
174
+ htmlContent = templateHtml.replace(/\{\{title\}\}/g, title).replace(/\{\{body\}\}/g, bodyHtml).replace(/\{\{styles\}\}/g, hljsStyles);
175
+ } else if (options.input.endsWith(".html")) {
176
+ htmlContent = fs.readFileSync(inputPath, "utf-8");
177
+ } else {
178
+ throw new Error(`Unsupported input format: ${options.input}`);
179
+ }
180
+ let browser = null;
181
+ try {
182
+ browser = await chromium.launch({ headless: true });
183
+ const context = await browser.newContext({ javaScriptEnabled: !safeMode });
184
+ if (safeMode) {
185
+ await context.route("**/*", (route) => {
186
+ const url = route.request().url();
187
+ if (url.startsWith("http://") || url.startsWith("https://")) {
188
+ return route.abort();
189
+ }
190
+ return route.continue();
191
+ });
192
+ }
193
+ const page = await context.newPage();
194
+ if (options.input.startsWith("http")) {
195
+ await page.goto(options.input, { waitUntil, timeout });
196
+ } else {
197
+ await page.setContent(htmlContent, { waitUntil, timeout });
198
+ }
199
+ const fontCSS = getFontCSS(template.name);
200
+ if (fontCSS) {
201
+ await page.addStyleTag({ content: fontCSS });
202
+ }
203
+ if (hasMermaid && !safeMode) {
204
+ const mermaidPath = require$1.resolve("mermaid/dist/mermaid.min.js");
205
+ await page.addScriptTag({ path: mermaidPath });
206
+ const templateName = template.name;
207
+ await page.evaluate((tpl) => {
208
+ const defaultTheme = {
209
+ primaryColor: "#f5f5f7",
210
+ primaryTextColor: "#1d1d1f",
211
+ primaryBorderColor: "#d2d2d7",
212
+ lineColor: "#86868b",
213
+ secondaryColor: "#f5f5f7",
214
+ tertiaryColor: "#ffffff",
215
+ background: "#ffffff",
216
+ mainBkg: "#f5f5f7",
217
+ nodeBorder: "#d2d2d7",
218
+ nodeTextColor: "#1d1d1f",
219
+ clusterBkg: "rgba(0, 0, 0, 0.02)",
220
+ clusterBorder: "rgba(0, 0, 0, 0.1)",
221
+ titleColor: "#1d1d1f"
222
+ };
223
+ const githubTheme = {
224
+ primaryColor: "#f6f8fa",
225
+ primaryTextColor: "#1f2328",
226
+ primaryBorderColor: "#d0d7de",
227
+ lineColor: "#656d76",
228
+ secondaryColor: "#f6f8fa",
229
+ tertiaryColor: "#ffffff",
230
+ background: "#ffffff",
231
+ mainBkg: "#f6f8fa",
232
+ nodeBorder: "#d0d7de",
233
+ nodeTextColor: "#1f2328",
234
+ clusterBkg: "rgba(208, 215, 222, 0.2)",
235
+ clusterBorder: "#d0d7de",
236
+ titleColor: "#1f2328"
237
+ };
238
+ const magazineTheme = {
239
+ primaryColor: "#2a2a2a",
240
+ primaryTextColor: "#ffffff",
241
+ primaryBorderColor: "#c41e3a",
242
+ lineColor: "#666666",
243
+ secondaryColor: "#1a1a1a",
244
+ tertiaryColor: "#0f0f0f",
245
+ background: "#0f0f0f",
246
+ mainBkg: "#2a2a2a",
247
+ nodeBorder: "#c41e3a",
248
+ nodeTextColor: "#ffffff",
249
+ clusterBkg: "rgba(196, 30, 58, 0.1)",
250
+ clusterBorder: "rgba(196, 30, 58, 0.4)",
251
+ titleColor: "#ffffff"
252
+ };
253
+ const themes = {
254
+ default: defaultTheme,
255
+ github: githubTheme,
256
+ magazine: magazineTheme
257
+ };
258
+ const activeTheme = themes[tpl] || defaultTheme;
259
+ window.mermaid.initialize({
260
+ startOnLoad: false,
261
+ theme: "base",
262
+ themeVariables: {
263
+ ...activeTheme,
264
+ fontFamily: '"SF Mono", "Fira Code", Menlo, monospace',
265
+ fontSize: "13px"
266
+ },
267
+ flowchart: {
268
+ curve: "basis",
269
+ nodeSpacing: 40,
270
+ rankSpacing: 50,
271
+ htmlLabels: true,
272
+ useMaxWidth: true,
273
+ subGraphTitleMargin: { top: 15, bottom: 15 },
274
+ padding: 20
275
+ }
276
+ });
277
+ }, templateName);
278
+ await page.evaluate(() => {
279
+ return window.mermaid.run();
280
+ });
281
+ await page.waitForSelector(".mermaid svg", { timeout: 5e3 });
282
+ await page.waitForTimeout(300);
283
+ await page.evaluate(() => {
284
+ document.querySelectorAll(".node rect").forEach((rect) => {
285
+ rect.setAttribute("rx", "12");
286
+ rect.setAttribute("ry", "12");
287
+ });
288
+ document.querySelectorAll(".cluster rect").forEach((rect) => {
289
+ rect.setAttribute("rx", "16");
290
+ rect.setAttribute("ry", "16");
291
+ });
292
+ document.querySelectorAll(".cluster-label").forEach((label) => {
293
+ const transform = label.getAttribute("transform");
294
+ if (transform) {
295
+ const match = transform.match(/translate\(([\d.]+),\s*([\d.]+)\)/);
296
+ if (match) {
297
+ const x = parseFloat(match[1]);
298
+ const y = parseFloat(match[2]) + 8;
299
+ label.setAttribute("transform", `translate(${x}, ${y})`);
300
+ }
301
+ }
302
+ });
303
+ });
304
+ }
305
+ await page.waitForTimeout(200);
306
+ const outputPath = path.resolve(options.output);
307
+ await page.pdf({
308
+ path: outputPath,
309
+ format: "A4",
310
+ margin: { top: "0", right: "0", bottom: "0", left: "0" },
311
+ printBackground: true
312
+ });
313
+ return {
314
+ pdfPath: outputPath,
315
+ meta: {
316
+ template: template.name,
317
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
318
+ }
319
+ };
320
+ } finally {
321
+ if (browser) {
322
+ await browser.close();
323
+ }
324
+ }
325
+ }
326
+ const command$1 = new Command("pdf").description("Render Markdown or HTML to PDF").requiredOption("-i, --input <path>", "Input file path or URL").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) => {
327
+ try {
328
+ const timeout = Number.parseInt(options.timeout, 10);
329
+ if (!Number.isFinite(timeout) || timeout < 0) {
330
+ throw new Error("Invalid --timeout value. Use a non-negative integer in milliseconds.");
331
+ }
332
+ const result = await render$1({
333
+ input: options.input,
334
+ output: options.output,
335
+ template: options.template,
336
+ waitUntil: options.waitUntil,
337
+ timeout,
338
+ safe: options.safe
339
+ });
340
+ console.log(JSON.stringify(result, null, 2));
341
+ } catch (error) {
342
+ console.error("Error:", error instanceof Error ? error.message : error);
343
+ process.exit(1);
344
+ }
345
+ });
346
+ const imagePresets = {
347
+ og: {
348
+ name: "og",
349
+ width: 1200,
350
+ height: 630,
351
+ description: "Open Graph / Social Card (1200×630)"
352
+ },
353
+ infographic: {
354
+ name: "infographic",
355
+ width: 1080,
356
+ height: 1350,
357
+ description: "Infographic (1080×1350)"
358
+ },
359
+ poster: {
360
+ name: "poster",
361
+ width: 1200,
362
+ height: 1500,
363
+ description: "Poster (1200×1500)"
364
+ },
365
+ banner: {
366
+ name: "banner",
367
+ width: 1600,
368
+ height: 900,
369
+ description: "Banner (1600×900)"
370
+ },
371
+ twitter: {
372
+ name: "twitter",
373
+ width: 1200,
374
+ height: 675,
375
+ description: "Twitter Card (1200×675)"
376
+ },
377
+ youtube: {
378
+ name: "youtube",
379
+ width: 1280,
380
+ height: 720,
381
+ description: "YouTube Thumbnail (1280×720)"
382
+ }
383
+ };
384
+ function getPreset(name) {
385
+ const preset = imagePresets[name];
386
+ if (!preset) {
387
+ throw new Error(`Unknown image preset: ${name}. Available: ${Object.keys(imagePresets).join(", ")}`);
388
+ }
389
+ return preset;
390
+ }
391
+ const WAIT_UNTIL_STATES = /* @__PURE__ */ new Set(["load", "domcontentloaded", "networkidle"]);
392
+ const MIN_DIMENSION = 100;
393
+ const MAX_DIMENSION = 5e3;
394
+ const MIN_SCALE = 1;
395
+ const MAX_SCALE = 4;
396
+ function normalizeWaitUntil(value) {
397
+ if (!value) return "networkidle";
398
+ if (WAIT_UNTIL_STATES.has(value)) {
399
+ return value;
400
+ }
401
+ throw new Error(`Invalid waitUntil: ${value}. Allowed: load, domcontentloaded, networkidle.`);
402
+ }
403
+ function normalizeTimeout(value) {
404
+ if (value === void 0) return 3e4;
405
+ if (!Number.isFinite(value) || value < 0) {
406
+ throw new Error("Invalid timeout. Use a non-negative number of milliseconds.");
407
+ }
408
+ return value;
409
+ }
410
+ function validateRange(name, value, min, max) {
411
+ if (!Number.isFinite(value)) {
412
+ throw new Error(`${name} must be a finite number.`);
413
+ }
414
+ if (value < min || value > max) {
415
+ throw new Error(`${name} must be between ${min} and ${max}. Received: ${value}.`);
416
+ }
417
+ }
418
+ async function render(options) {
419
+ const preset = options.preset ? getPreset(options.preset) : getPreset("og");
420
+ const width = options.width ?? preset.width;
421
+ const height = options.height ?? preset.height;
422
+ const scale = options.scale ?? 2;
423
+ const safeMode = options.safe ?? false;
424
+ const waitUntil = normalizeWaitUntil(options.waitUntil);
425
+ const timeout = normalizeTimeout(options.timeout);
426
+ validateRange("Width", width, MIN_DIMENSION, MAX_DIMENSION);
427
+ validateRange("Height", height, MIN_DIMENSION, MAX_DIMENSION);
428
+ validateRange("Scale", scale, MIN_SCALE, MAX_SCALE);
429
+ let browser = null;
430
+ try {
431
+ browser = await chromium.launch({ headless: true });
432
+ const context = await browser.newContext({
433
+ viewport: { width, height },
434
+ deviceScaleFactor: scale,
435
+ javaScriptEnabled: !safeMode
436
+ });
437
+ if (safeMode) {
438
+ await context.route("**/*", (route) => {
439
+ const url = route.request().url();
440
+ if (url.startsWith("http://") || url.startsWith("https://")) {
441
+ return route.abort();
442
+ }
443
+ return route.continue();
444
+ });
445
+ }
446
+ const page = await context.newPage();
447
+ const inputPath = path.resolve(options.input);
448
+ if (options.input.startsWith("http://") || options.input.startsWith("https://")) {
449
+ if (safeMode) {
450
+ throw new Error("Safe mode does not allow remote URL inputs.");
451
+ }
452
+ await page.goto(options.input, { waitUntil, timeout });
453
+ } else if (fs.existsSync(inputPath)) {
454
+ const content = fs.readFileSync(inputPath, "utf-8");
455
+ await page.setContent(content, { waitUntil, timeout });
456
+ } else {
457
+ throw new Error(`Input not found: ${options.input}`);
458
+ }
459
+ await page.addStyleTag({
460
+ content: `
461
+ html, body {
462
+ width: ${width}px;
463
+ height: ${height}px;
464
+ margin: 0;
465
+ padding: 0;
466
+ overflow: hidden;
467
+ }
468
+ `
469
+ });
470
+ await page.waitForTimeout(500);
471
+ const cardContainer = await page.$("#container");
472
+ if (!cardContainer) {
473
+ throw new Error("Missing #container element. PNG output requires a #container.");
474
+ }
475
+ const box = await cardContainer.boundingBox();
476
+ if (!box || box.width <= 0 || box.height <= 0) {
477
+ throw new Error("Invalid #container size. Ensure it has a positive width and height.");
478
+ }
479
+ const outputPath = path.resolve(options.output);
480
+ await page.screenshot({
481
+ path: outputPath,
482
+ type: "png",
483
+ clip: { x: box.x, y: box.y, width: box.width, height: box.height }
484
+ });
485
+ return {
486
+ pngPath: outputPath,
487
+ meta: {
488
+ preset: preset.name,
489
+ width,
490
+ height,
491
+ deviceScaleFactor: scale,
492
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
493
+ }
494
+ };
495
+ } finally {
496
+ if (browser) {
497
+ await browser.close();
498
+ }
499
+ }
500
+ }
501
+ const command = new Command("shot").description("Capture HTML to PNG image").requiredOption("-i, --input <path>", "Input HTML file path or URL").requiredOption("-o, --output <path>", "Output PNG file path").option("-p, --preset <name>", "Image preset (og, infographic, poster, banner)", "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) => {
502
+ try {
503
+ const timeout = Number.parseInt(options.timeout, 10);
504
+ if (!Number.isFinite(timeout) || timeout < 0) {
505
+ throw new Error("Invalid --timeout value. Use a non-negative integer in milliseconds.");
506
+ }
507
+ const result = await render({
508
+ input: options.input,
509
+ output: options.output,
510
+ preset: options.preset,
511
+ width: options.width ? parseInt(options.width) : void 0,
512
+ height: options.height ? parseInt(options.height) : void 0,
513
+ scale: parseFloat(options.scale),
514
+ waitUntil: options.waitUntil,
515
+ timeout,
516
+ safe: options.safe
517
+ });
518
+ console.log(JSON.stringify(result, null, 2));
519
+ } catch (error) {
520
+ console.error("Error:", error instanceof Error ? error.message : error);
521
+ process.exit(1);
522
+ }
523
+ });
524
+ const program = new Command();
525
+ program.name("pagepress").description("Convert web pages and Markdown to PDF and images").version("0.1.0");
526
+ program.addCommand(command$1);
527
+ program.addCommand(command);
528
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@liustack/pagepress",
3
+ "version": "0.1.0",
4
+ "description": "CLI tool to convert web pages and Markdown to PDF and images",
5
+ "type": "module",
6
+ "bin": {
7
+ "pagepress": "./dist/main.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "src/pdf/templates"
12
+ ],
13
+ "keywords": [
14
+ "pdf",
15
+ "screenshot",
16
+ "html-to-pdf",
17
+ "html-to-image",
18
+ "markdown-to-pdf",
19
+ "cli",
20
+ "playwright",
21
+ "mermaid"
22
+ ],
23
+ "author": "Leon Liu",
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/liustack/pagepress.git"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/liustack/pagepress/issues"
31
+ },
32
+ "homepage": "https://github.com/liustack/pagepress#readme",
33
+ "engines": {
34
+ "node": ">=18"
35
+ },
36
+ "dependencies": {
37
+ "@fontsource/bebas-neue": "^5.2.7",
38
+ "@fontsource/cormorant-garamond": "^5.2.11",
39
+ "@fontsource/inter": "^5.2.8",
40
+ "commander": "^13.1.0",
41
+ "gray-matter": "^4.0.3",
42
+ "highlight.js": "^11.11.1",
43
+ "marked": "^15.0.6",
44
+ "marked-highlight": "^2.2.3",
45
+ "mermaid": "^11.12.2",
46
+ "playwright": "^1.50.1"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^22.13.1",
50
+ "typescript": "^5.7.3",
51
+ "vite": "^6.1.0",
52
+ "vite-plugin-node": "^5.0.0"
53
+ },
54
+ "scripts": {
55
+ "dev": "vite build --watch",
56
+ "build": "vite build",
57
+ "typecheck": "tsc --noEmit"
58
+ }
59
+ }