@pablo_clueless/printr 0.1.2

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/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ All notable changes to **printr** are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-06-22
11
+
12
+ ### Added
13
+ - Initial release: a CLI that prints Markdown and plain-text files as PDFs.
14
+ - Markdown rendering with GitHub-flavored styling (`markdown-it`) and
15
+ syntax-highlighted code blocks (`highlight.js`).
16
+ - High-fidelity PDF output through headless Chrome (`puppeteer`); generated HTML
17
+ is fully self-contained so no external resources are fetched while rendering.
18
+ - Plain-text files (any non-Markdown extension) rendered verbatim in a
19
+ monospace layout.
20
+ - Batch conversion with glob support (e.g. `printr "docs/**/*.md"`), reusing a
21
+ single browser instance across files for speed.
22
+ - `--watch` / `-w` mode: re-renders a file whenever it changes, debounced and
23
+ serialized, with recursive directory watching for `**` patterns and pickup of
24
+ newly created files matching a glob.
25
+ - CLI options: `--output`/`-o`, `--out-dir`/`-d`, `--format`/`-f`,
26
+ `--margin`/`-m`, `--title`/`-t`.
27
+
28
+ [Unreleased]: https://example.com/printr/compare/v0.1.0...HEAD
29
+ [0.1.0]: https://example.com/printr/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 smsnmicheal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # printr
2
+
3
+ > Print **Markdown** and **plain-text** files as nicely styled PDFs โ€” straight from your terminal.
4
+
5
+ Markdown is rendered with GitHub-flavored styling and syntax-highlighted code
6
+ blocks, then printed to PDF through headless Chrome (Puppeteer) for high
7
+ fidelity. Plain-text files are rendered verbatim in a clean monospace layout.
8
+ The generated HTML is fully self-contained, so no external resources are
9
+ fetched while rendering.
10
+
11
+ ## Features
12
+
13
+ - ๐Ÿ“„ GitHub-flavored Markdown styling with print-tuned page breaks
14
+ - ๐ŸŽจ Syntax highlighting for code blocks via `highlight.js`
15
+ - ๐Ÿ–จ๏ธ High-fidelity PDF output through headless Chrome
16
+ - ๐Ÿ“ Batch conversion with glob patterns, reusing one browser for speed
17
+ - ๐Ÿ‘€ `--watch` mode that re-renders on every save
18
+ - ๐Ÿงพ Plain-text files rendered verbatim in monospace
19
+
20
+ ## Install
21
+
22
+ Run it on demand without installing:
23
+
24
+ ```bash
25
+ npx @pablo_clueless/printr report.md
26
+ ```
27
+
28
+ Or install globally to get the `printr` command everywhere:
29
+
30
+ ```bash
31
+ npm install -g @pablo_clueless/printr
32
+ ```
33
+
34
+ > **Chromium download:** on first install Puppeteer downloads a matching
35
+ > Chromium build (~150 MB). To reuse an existing Chrome instead, set
36
+ > `PUPPETEER_EXECUTABLE_PATH` to its path and install with
37
+ > `PUPPETEER_SKIP_DOWNLOAD=1`.
38
+
39
+ **Requirements:** Node.js >= 18.
40
+
41
+ ## Usage
42
+
43
+ ```bash
44
+ # Single file โ†’ writes report.pdf beside the source
45
+ printr report.md
46
+
47
+ # Explicit output path
48
+ printr report.md -o ~/Desktop/report.pdf
49
+
50
+ # Batch convert with a glob, into one folder
51
+ printr "docs/**/*.md" -d out/
52
+
53
+ # Letter paper, tighter margins
54
+ printr notes.txt -f Letter -m 15mm
55
+
56
+ # Watch and re-render on every save (Ctrl+C to stop)
57
+ printr "docs/**/*.md" -d out/ --watch
58
+ ```
59
+
60
+ Quote glob patterns (`"docs/**/*.md"`) so your shell passes them to `printr`
61
+ rather than expanding them itself.
62
+
63
+ ## Options
64
+
65
+ | Flag | Description | Default |
66
+ | ---- | ----------- | ------- |
67
+ | `-o, --output <file>` | Output PDF path (single input only) | beside source |
68
+ | `-d, --out-dir <dir>` | Folder for output PDFs | source folder |
69
+ | `-f, --format <fmt>` | Paper format: `A4`, `Letter`, `Legal`, โ€ฆ | `A4` |
70
+ | `-m, --margin <size>` | Page margin on all sides, e.g. `20mm`, `1in` | `20mm` |
71
+ | `-t, --title <title>` | Document title (single input only) | filename |
72
+ | `-w, --watch` | Re-render whenever an input file changes | off |
73
+ | `-h, --help` | Show help | |
74
+
75
+ ## Supported inputs
76
+
77
+ - **Markdown:** `.md`, `.markdown`, `.mdown`, `.mkd`
78
+ - **Plain text:** anything else is rendered verbatim in monospace.
79
+
80
+ ## Examples
81
+
82
+ A ready-to-try sample lives in [`examples/`](examples/):
83
+
84
+ ```bash
85
+ printr examples/sample.md -d examples/out/
86
+ ```
87
+
88
+ ## Contributing
89
+
90
+ ```bash
91
+ git clone https://github.com/pablo-clueless/printr.git
92
+ cd printr
93
+ npm install # installs deps and downloads Chromium
94
+ npm run dev -- examples/sample.md # run from source without building
95
+ npm run build # compile TypeScript to dist/
96
+ ```
97
+
98
+ Issues and pull requests are welcome. See [CHANGELOG.md](CHANGELOG.md) for the
99
+ release history.
100
+
101
+ ## License
102
+
103
+ [MIT](LICENSE)
package/dist/cli.js ADDED
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+ import { renderFileToPdf } from "./convert.js";
3
+ import { writeFile, mkdir, stat } from "node:fs/promises";
4
+ import puppeteer from "puppeteer";
5
+ import { watch } from "node:fs";
6
+ import { Command } from "commander";
7
+ import path from "node:path";
8
+ import { glob } from "glob";
9
+ const program = new Command();
10
+ program
11
+ .name("printr")
12
+ .description("Print Markdown and text files as nicely styled PDFs.")
13
+ .argument("<inputs...>", "files or globs to convert (.md, .markdown, .txt, โ€ฆ)")
14
+ .option("-o, --output <file>", "output PDF path (single input only)")
15
+ .option("-d, --out-dir <dir>", "directory for output PDFs (defaults beside each source)")
16
+ .option("-f, --format <format>", "paper format: A4, Letter, Legal, โ€ฆ", "A4")
17
+ .option("-m, --margin <size>", "page margin on all sides, e.g. 20mm or 1in", "20mm")
18
+ .option("-t, --title <title>", "document title (single input only)")
19
+ .option("-w, --watch", "watch inputs and re-render on change (Ctrl+C to stop)")
20
+ .showHelpAfterError()
21
+ .action(async (inputs, opts) => {
22
+ try {
23
+ await run(inputs, opts);
24
+ }
25
+ catch (err) {
26
+ console.error(`printr: ${err.message}`);
27
+ process.exitCode = 1;
28
+ }
29
+ });
30
+ async function resolveInputs(inputs) {
31
+ const resolved = new Set();
32
+ for (const input of inputs) {
33
+ // A literal existing path should be used as-is (handles names with glob chars).
34
+ const isFile = await stat(input).then((s) => s.isFile()).catch(() => false);
35
+ if (isFile) {
36
+ resolved.add(path.resolve(input));
37
+ continue;
38
+ }
39
+ const matches = await glob(input, { nodir: true, windowsPathsNoEscape: true });
40
+ for (const m of matches)
41
+ resolved.add(path.resolve(m));
42
+ }
43
+ return [...resolved].sort();
44
+ }
45
+ function outputPathFor(file, opts) {
46
+ if (opts.output)
47
+ return path.resolve(opts.output);
48
+ const base = path.basename(file, path.extname(file)) + ".pdf";
49
+ const dir = opts.outDir ? path.resolve(opts.outDir) : path.dirname(file);
50
+ return path.join(dir, base);
51
+ }
52
+ /** Render a single source file and write its PDF, logging the result. */
53
+ async function renderOne(browser, file, opts, renderOpts) {
54
+ const out = outputPathFor(file, opts);
55
+ await mkdir(path.dirname(out), { recursive: true });
56
+ const pdf = await renderFileToPdf(browser, file, renderOpts);
57
+ await writeFile(out, pdf);
58
+ console.log(`${path.relative(process.cwd(), file)} โ†’ ${path.relative(process.cwd(), out)}`);
59
+ }
60
+ /**
61
+ * For a single input pattern, determine which directory to watch and whether
62
+ * it must be watched recursively. The watch root is the leading portion of the
63
+ * pattern before the first segment containing glob magic.
64
+ */
65
+ function watchRootFor(input) {
66
+ const recursive = input.includes("**");
67
+ const parts = input.split(/[\\/]/);
68
+ const base = [];
69
+ for (const part of parts) {
70
+ if (/[*?[\]{}!()+@]/.test(part))
71
+ break;
72
+ base.push(part);
73
+ }
74
+ const basePath = base.length ? base.join(path.sep) : ".";
75
+ return { dir: path.resolve(basePath), recursive };
76
+ }
77
+ async function run(inputs, opts) {
78
+ const files = await resolveInputs(inputs);
79
+ if (files.length === 0) {
80
+ throw new Error("no matching files found");
81
+ }
82
+ if (opts.output && files.length > 1) {
83
+ throw new Error("--output can only be used with a single input file");
84
+ }
85
+ if (opts.title && files.length > 1) {
86
+ throw new Error("--title can only be used with a single input file");
87
+ }
88
+ const renderOpts = {
89
+ format: opts.format,
90
+ margin: opts.margin,
91
+ title: opts.title,
92
+ };
93
+ const browser = await puppeteer.launch({
94
+ headless: true,
95
+ args: ["--no-sandbox", "--disable-setuid-sandbox"],
96
+ });
97
+ // Initial render of everything that currently matches.
98
+ for (const file of files) {
99
+ await renderOne(browser, file, opts, renderOpts);
100
+ }
101
+ console.log(`Done. Converted ${files.length} file${files.length === 1 ? "" : "s"}.`);
102
+ if (!opts.watch) {
103
+ await browser.close();
104
+ return;
105
+ }
106
+ await startWatch(browser, inputs, opts, renderOpts);
107
+ }
108
+ /**
109
+ * Watch the directories backing each input pattern and re-render a file
110
+ * whenever it changes. Re-resolving the patterns on each event means newly
111
+ * created files that match a glob are picked up too. The browser is kept open
112
+ * for the lifetime of the watch.
113
+ */
114
+ async function startWatch(browser, inputs, opts, renderOpts) {
115
+ // Deduplicate watch roots; a recursive root supersedes a non-recursive one
116
+ // for the same directory.
117
+ const roots = new Map();
118
+ for (const input of inputs) {
119
+ const isFile = await stat(input).then((s) => s.isFile()).catch(() => false);
120
+ const { dir, recursive } = isFile
121
+ ? { dir: path.dirname(path.resolve(input)), recursive: false }
122
+ : watchRootFor(input);
123
+ roots.set(dir, (roots.get(dir) ?? false) || recursive);
124
+ }
125
+ const watchers = [];
126
+ const debounce = new Map();
127
+ let rendering = Promise.resolve();
128
+ const handleChange = (root, filename) => {
129
+ if (!filename)
130
+ return;
131
+ const full = path.resolve(root, filename);
132
+ const prev = debounce.get(full);
133
+ if (prev)
134
+ clearTimeout(prev);
135
+ debounce.set(full, setTimeout(() => {
136
+ debounce.delete(full);
137
+ // Serialize renders so concurrent saves don't open many pages at once.
138
+ rendering = rendering.then(async () => {
139
+ const matched = await resolveInputs(inputs);
140
+ if (!matched.includes(full))
141
+ return; // not one of our inputs
142
+ const exists = await stat(full).then((s) => s.isFile()).catch(() => false);
143
+ if (!exists)
144
+ return; // file was deleted mid-edit
145
+ try {
146
+ await renderOne(browser, full, opts, renderOpts);
147
+ }
148
+ catch (err) {
149
+ console.error(`printr: failed to render ${filename}: ${err.message}`);
150
+ }
151
+ });
152
+ }, 120));
153
+ };
154
+ for (const [dir, recursive] of roots) {
155
+ try {
156
+ const w = watch(dir, { recursive }, (_event, filename) => handleChange(dir, filename));
157
+ watchers.push(w);
158
+ console.log(`Watching ${path.relative(process.cwd(), dir) || "."}${recursive ? " (recursive)" : ""} โ€ฆ`);
159
+ }
160
+ catch (err) {
161
+ console.error(`printr: cannot watch ${dir}: ${err.message}`);
162
+ }
163
+ }
164
+ console.log("Press Ctrl+C to stop.");
165
+ // Keep the process alive until interrupted, then clean up.
166
+ await new Promise((resolve) => {
167
+ const shutdown = () => {
168
+ for (const w of watchers)
169
+ w.close();
170
+ browser.close().finally(() => resolve());
171
+ };
172
+ process.once("SIGINT", shutdown);
173
+ process.once("SIGTERM", shutdown);
174
+ });
175
+ console.log("\nStopped.");
176
+ }
177
+ program.parseAsync();
@@ -0,0 +1,71 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import MarkdownIt from "markdown-it";
3
+ import hljs from "highlight.js";
4
+ import path from "node:path";
5
+ import { githubMarkdownCss } from "./styles.js";
6
+ const MARKDOWN_EXTS = new Set([".md", ".markdown", ".mdown", ".mkd"]);
7
+ const md = new MarkdownIt({
8
+ html: true,
9
+ linkify: true,
10
+ typographer: true,
11
+ highlight(code, lang) {
12
+ if (lang && hljs.getLanguage(lang)) {
13
+ try {
14
+ return hljs.highlight(code, { language: lang, ignoreIllegals: true })
15
+ .value;
16
+ }
17
+ catch {
18
+ /* fall through to auto */
19
+ }
20
+ }
21
+ return hljs.highlightAuto(code).value;
22
+ },
23
+ });
24
+ function escapeHtml(s) {
25
+ return s
26
+ .replace(/&/g, "&amp;")
27
+ .replace(/</g, "&lt;")
28
+ .replace(/>/g, "&gt;");
29
+ }
30
+ /** Build the full self-contained HTML document for a source file. */
31
+ export function buildHtml(source, filePath, options = {}) {
32
+ const ext = path.extname(filePath).toLowerCase();
33
+ const isMarkdown = MARKDOWN_EXTS.has(ext);
34
+ const title = options.title ?? path.basename(filePath);
35
+ const bodyHtml = isMarkdown
36
+ ? md.render(source)
37
+ : // Plain text: preserve it verbatim inside a code block.
38
+ `<pre class="plain-text">${escapeHtml(source)}</pre>`;
39
+ return `<!DOCTYPE html>
40
+ <html lang="en">
41
+ <head>
42
+ <meta charset="utf-8">
43
+ <title>${escapeHtml(title)}</title>
44
+ <style>${githubMarkdownCss}
45
+ .plain-text { background: transparent; padding: 0; font-size: 0.9em; }
46
+ ${options.extraCss ?? ""}</style>
47
+ </head>
48
+ <body><article class="markdown-body">${bodyHtml}</article></body>
49
+ </html>`;
50
+ }
51
+ /**
52
+ * Render a single source file to a PDF buffer using an already-launched
53
+ * browser. Reusing the browser across files keeps batch conversion fast.
54
+ */
55
+ export async function renderFileToPdf(browser, filePath, options = {}) {
56
+ const source = await readFile(filePath, "utf8");
57
+ const html = buildHtml(source, filePath, options);
58
+ const page = await browser.newPage();
59
+ try {
60
+ await page.setContent(html, { waitUntil: "load" });
61
+ const margin = options.margin ?? "20mm";
62
+ return await page.pdf({
63
+ format: (options.format ?? "A4"),
64
+ printBackground: true,
65
+ margin: { top: margin, bottom: margin, left: margin, right: margin },
66
+ });
67
+ }
68
+ finally {
69
+ await page.close();
70
+ }
71
+ }
package/dist/styles.js ADDED
@@ -0,0 +1,124 @@
1
+ /**
2
+ * GitHub-flavored Markdown stylesheet plus a highlight.js theme, scoped for
3
+ * print. Kept inline so the produced HTML is fully self-contained and Chrome
4
+ * never has to fetch external resources while rendering the PDF.
5
+ */
6
+ export const githubMarkdownCss = `
7
+ :root {
8
+ --fg: #1f2328;
9
+ --muted: #59636e;
10
+ --border: #d1d9e0;
11
+ --border-muted: #d1d9e0b3;
12
+ --accent: #0969da;
13
+ --code-bg: #f6f8fa;
14
+ --code-fg: #1f2328;
15
+ }
16
+
17
+ * { box-sizing: border-box; }
18
+
19
+ body {
20
+ color: var(--fg);
21
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans",
22
+ Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
23
+ font-size: 11pt;
24
+ line-height: 1.55;
25
+ word-wrap: break-word;
26
+ margin: 0;
27
+ }
28
+
29
+ .markdown-body > *:first-child { margin-top: 0 !important; }
30
+ .markdown-body > *:last-child { margin-bottom: 0 !important; }
31
+
32
+ h1, h2, h3, h4, h5, h6 {
33
+ margin-top: 1.4em;
34
+ margin-bottom: 0.6em;
35
+ font-weight: 600;
36
+ line-height: 1.25;
37
+ }
38
+ h1 { font-size: 1.9em; padding-bottom: 0.3em; border-bottom: 1px solid var(--border-muted); }
39
+ h2 { font-size: 1.45em; padding-bottom: 0.3em; border-bottom: 1px solid var(--border-muted); }
40
+ h3 { font-size: 1.2em; }
41
+ h4 { font-size: 1em; }
42
+ h5 { font-size: 0.9em; }
43
+ h6 { font-size: 0.85em; color: var(--muted); }
44
+
45
+ p, blockquote, ul, ol, dl, table, pre { margin-top: 0; margin-bottom: 1em; }
46
+
47
+ a { color: var(--accent); text-decoration: none; }
48
+ a:hover { text-decoration: underline; }
49
+
50
+ blockquote {
51
+ padding: 0 1em;
52
+ color: var(--muted);
53
+ border-left: 0.25em solid var(--border);
54
+ margin-left: 0;
55
+ }
56
+
57
+ ul, ol { padding-left: 2em; }
58
+ li + li { margin-top: 0.25em; }
59
+ li > p { margin-top: 0.5em; }
60
+
61
+ code, kbd, pre, samp {
62
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
63
+ "Liberation Mono", monospace;
64
+ }
65
+
66
+ code {
67
+ font-size: 0.88em;
68
+ background: var(--code-bg);
69
+ padding: 0.2em 0.4em;
70
+ border-radius: 6px;
71
+ }
72
+
73
+ pre {
74
+ padding: 1em;
75
+ overflow: auto;
76
+ font-size: 0.85em;
77
+ line-height: 1.45;
78
+ background: var(--code-bg);
79
+ border-radius: 6px;
80
+ white-space: pre-wrap;
81
+ word-break: break-word;
82
+ }
83
+ pre code {
84
+ background: transparent;
85
+ padding: 0;
86
+ font-size: inherit;
87
+ border-radius: 0;
88
+ }
89
+
90
+ table {
91
+ border-collapse: collapse;
92
+ display: block;
93
+ width: max-content;
94
+ max-width: 100%;
95
+ overflow: auto;
96
+ }
97
+ table th, table td { padding: 6px 13px; border: 1px solid var(--border); }
98
+ table th { font-weight: 600; }
99
+ table tr:nth-child(2n) { background: var(--code-bg); }
100
+
101
+ img { max-width: 100%; }
102
+
103
+ hr {
104
+ height: 1px;
105
+ border: 0;
106
+ background: var(--border);
107
+ margin: 1.6em 0;
108
+ }
109
+
110
+ /* Page-break behavior for print */
111
+ h1, h2, h3, h4, h5, h6 { break-after: avoid-page; }
112
+ pre, blockquote, table, img { break-inside: avoid; }
113
+
114
+ /* highlight.js โ€” GitHub light theme */
115
+ .hljs { color: var(--code-fg); background: var(--code-bg); }
116
+ .hljs-comment, .hljs-quote { color: #6a737d; }
117
+ .hljs-keyword, .hljs-selector-tag, .hljs-doctag, .hljs-type, .hljs-name, .hljs-strong { color: #d73a49; }
118
+ .hljs-literal, .hljs-number, .hljs-variable, .hljs-template-variable, .hljs-tag .hljs-attr { color: #005cc5; }
119
+ .hljs-string, .hljs-regexp, .hljs-addition, .hljs-attribute, .hljs-meta .hljs-string { color: #032f62; }
120
+ .hljs-title, .hljs-section, .hljs-built_in, .hljs-title.class_, .hljs-title.function_ { color: #6f42c1; }
121
+ .hljs-attr, .hljs-attribute, .hljs-symbol, .hljs-bullet, .hljs-link { color: #e36209; }
122
+ .hljs-emphasis { font-style: italic; }
123
+ .hljs-strong { font-weight: 600; }
124
+ `;
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@pablo_clueless/printr",
3
+ "version": "0.1.2",
4
+ "description": "Print Markdown and text files as nicely styled PDFs.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "smsnmicheal <smsnmicheal@gmail.com>",
8
+ "homepage": "https://github.com/pablo-clueless/printr#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/pablo-clueless/printr.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/pablo-clueless/printr/issues"
15
+ },
16
+ "keywords": [
17
+ "markdown",
18
+ "pdf",
19
+ "cli",
20
+ "puppeteer",
21
+ "markdown-to-pdf",
22
+ "print",
23
+ "highlight.js",
24
+ "github-markdown"
25
+ ],
26
+ "bin": {
27
+ "printr": "dist/cli.js"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "README.md",
35
+ "CHANGELOG.md",
36
+ "LICENSE"
37
+ ],
38
+ "scripts": {
39
+ "build": "tsc",
40
+ "dev": "tsx src/cli.ts",
41
+ "start": "node dist/cli.js",
42
+ "prepublishOnly": "npm run build"
43
+ },
44
+ "engines": {
45
+ "node": ">=18"
46
+ },
47
+ "dependencies": {
48
+ "commander": "^12.1.0",
49
+ "glob": "^11.0.0",
50
+ "highlight.js": "^11.10.0",
51
+ "markdown-it": "^14.1.0",
52
+ "puppeteer": "^24.15.0"
53
+ },
54
+ "devDependencies": {
55
+ "@types/markdown-it": "^14.1.2",
56
+ "@types/node": "^22.0.0",
57
+ "tsx": "^4.19.0",
58
+ "typescript": "^5.6.0"
59
+ }
60
+ }