@speajus/markdown-to-pdf 1.0.13 → 1.0.15
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 +19 -19
- package/README.pdf +0 -0
- package/dist/cli.js +3 -0
- package/dist/renderer.js +66 -13
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A lightweight TypeScript library that converts Markdown files into styled PDF documents. Built on [marked](https://github.com/markedjs/marked) for parsing and [PDFKit](https://pdfkit.org/) for PDF generation. [README.pdf](./README.pdf) | [Live Demo](https://speajus.github.io/markdown-to-pdf/)
|
|
4
4
|
|
|
5
|
-
[](https://speajus.github.io/markdown-to-pdf/)
|
|
5
|
+
-[]([https://speajus.github.io/markdown-to-pdf/](https://speajus.github.io/markdown-to-pdf/))
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
@@ -128,16 +128,16 @@ await generatePdf(markdown, {
|
|
|
128
128
|
|
|
129
129
|
The full `ThemeConfig` interface exposes styles for:
|
|
130
130
|
|
|
131
|
-
| Section
|
|
132
|
-
|
|
|
133
|
-
|
|
|
134
|
-
|
|
|
135
|
-
|
|
|
136
|
-
|
|
|
137
|
-
|
|
|
138
|
-
|
|
|
139
|
-
|
|
|
140
|
-
|
|
|
131
|
+
| Section | Configurable properties |
|
|
132
|
+
| --- | --- |
|
|
133
|
+
| headings | Font, size, and color for each level (h1–h6) |
|
|
134
|
+
| body | Font, size, color, and line gap |
|
|
135
|
+
| code.inline | Font, size, color, and background color |
|
|
136
|
+
| code.block | Font, size, color, background color, and padding |
|
|
137
|
+
| blockquote | Border color, border width, italic flag, and indent |
|
|
138
|
+
| table | Header background, border color, cell padding, and zebra color |
|
|
139
|
+
| linkColor | Color for hyperlink text |
|
|
140
|
+
| horizontalRuleColor | Color for --- dividers |
|
|
141
141
|
|
|
142
142
|
## Project Structure
|
|
143
143
|
|
|
@@ -163,18 +163,18 @@ The full `ThemeConfig` interface exposes styles for:
|
|
|
163
163
|
## Dependencies
|
|
164
164
|
|
|
165
165
|
| Package | Purpose |
|
|
166
|
-
|
|
|
167
|
-
|
|
|
168
|
-
|
|
|
169
|
-
|
|
|
166
|
+
| --- | --- |
|
|
167
|
+
| marked | Markdown parsing and tokenization |
|
|
168
|
+
| pdfkit | PDF document generation |
|
|
169
|
+
| @resvg/resvg-js | SVG-to-PNG rasterization for image embedding |
|
|
170
170
|
|
|
171
171
|
## Scripts
|
|
172
172
|
|
|
173
173
|
| Command | Description |
|
|
174
|
-
|
|
|
175
|
-
|
|
|
176
|
-
|
|
|
174
|
+
| --- | --- |
|
|
175
|
+
| npm run build | Compile TypeScript to dist/ |
|
|
176
|
+
| npm run generate | Generate sample PDFs from samples/*.md into output/ |
|
|
177
177
|
|
|
178
178
|
## License
|
|
179
179
|
|
|
180
|
-
ISC
|
|
180
|
+
ISC
|
package/README.pdf
CHANGED
|
Binary file
|
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,9 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
10
|
const index_js_1 = require("./index.js");
|
|
11
11
|
async function readMdWritePdf(inputPath, outputPath, extraOptions) {
|
|
12
|
+
if (inputPath === outputPath) {
|
|
13
|
+
throw new Error(`input path can not be the same as output path.`);
|
|
14
|
+
}
|
|
12
15
|
console.log(`Converting ${inputPath} → ${outputPath}`);
|
|
13
16
|
const resolvedInput = path_1.default.resolve(inputPath);
|
|
14
17
|
const markdown = fs_1.default.readFileSync(resolvedInput, "utf-8");
|
package/dist/renderer.js
CHANGED
|
@@ -14,6 +14,13 @@ const emoji_js_1 = require("./emoji.js");
|
|
|
14
14
|
const color_emoji_js_1 = require("./color-emoji.js");
|
|
15
15
|
/** Name used to register the emoji font with PDFKit */
|
|
16
16
|
const EMOJI_FONT_NAME = 'NotoEmoji';
|
|
17
|
+
/** Standard PDF fonts built into PDFKit — always available without filesystem access. */
|
|
18
|
+
const STANDARD_PDF_FONTS = new Set([
|
|
19
|
+
'Helvetica', 'Helvetica-Bold', 'Helvetica-Oblique', 'Helvetica-BoldOblique',
|
|
20
|
+
'Courier', 'Courier-Bold', 'Courier-Oblique', 'Courier-BoldOblique',
|
|
21
|
+
'Times-Roman', 'Times-Bold', 'Times-Italic', 'Times-BoldItalic',
|
|
22
|
+
'Symbol', 'ZapfDingbats',
|
|
23
|
+
]);
|
|
17
24
|
async function renderMarkdownToPdf(markdown, options) {
|
|
18
25
|
const theme = options?.theme ?? styles_js_1.defaultTheme;
|
|
19
26
|
const layout = options?.pageLayout ?? styles_js_1.defaultPageLayout;
|
|
@@ -87,6 +94,20 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
87
94
|
}
|
|
88
95
|
}
|
|
89
96
|
}
|
|
97
|
+
// ── Browser font safety ────────────────────────────────────────────────
|
|
98
|
+
// In browser environments, `fs.readFileSync` doesn't exist. When PDFKit
|
|
99
|
+
// encounters an unknown font name it tries to load it from disk via
|
|
100
|
+
// readFileSync, which crashes in the browser. Detect whether we're in a
|
|
101
|
+
// filesystem-capable environment so we can guard calls to `doc.font()`.
|
|
102
|
+
let fsAvailable = false;
|
|
103
|
+
try {
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
105
|
+
const nodeFs = require('fs');
|
|
106
|
+
fsAvailable = typeof nodeFs.readFileSync === 'function';
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Node.js `fs` not available — we're in a browser environment.
|
|
110
|
+
}
|
|
90
111
|
// ── Color emoji pre-render ─────────────────────────────────────────────
|
|
91
112
|
// When a colorEmoji renderer is provided, pre-scan the entire markdown for
|
|
92
113
|
// every unique emoji and convert them all to PNG buffers up-front. This
|
|
@@ -122,6 +143,38 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
122
143
|
return customFontNames.has(font.substring(0, dash));
|
|
123
144
|
return false;
|
|
124
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Return a safe font name that will not crash `doc.font()`.
|
|
148
|
+
*
|
|
149
|
+
* In Node.js (`fsAvailable === true`) this is a no-op — PDFKit can load
|
|
150
|
+
* fonts from the filesystem. In browser environments, if the font is
|
|
151
|
+
* neither a standard PDF font nor a registered custom / emoji font, we
|
|
152
|
+
* return a standard fallback and log a warning so the user knows which
|
|
153
|
+
* font was unavailable.
|
|
154
|
+
*/
|
|
155
|
+
function safeFont(name) {
|
|
156
|
+
if (fsAvailable)
|
|
157
|
+
return name;
|
|
158
|
+
if (STANDARD_PDF_FONTS.has(name) || isCustomFont(name) || name === EMOJI_FONT_NAME)
|
|
159
|
+
return name;
|
|
160
|
+
// Determine the closest standard fallback, preserving bold/italic intent
|
|
161
|
+
// by inspecting the variant suffix added by resolveFont().
|
|
162
|
+
let fallback;
|
|
163
|
+
if (name.endsWith('-BoldOblique') || name.endsWith('-BoldItalic')) {
|
|
164
|
+
fallback = 'Helvetica-BoldOblique';
|
|
165
|
+
}
|
|
166
|
+
else if (name.endsWith('-Bold')) {
|
|
167
|
+
fallback = 'Helvetica-Bold';
|
|
168
|
+
}
|
|
169
|
+
else if (name.endsWith('-Oblique') || name.endsWith('-Italic')) {
|
|
170
|
+
fallback = 'Helvetica-Oblique';
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
fallback = 'Helvetica';
|
|
174
|
+
}
|
|
175
|
+
console.warn(`[markdown-to-pdf] Font "${name}" is not available; falling back to "${fallback}"`);
|
|
176
|
+
return fallback;
|
|
177
|
+
}
|
|
125
178
|
/** Return the base (registered) name of a custom font, stripping any variant suffix. */
|
|
126
179
|
function customFontBase(font) {
|
|
127
180
|
if (customFontNames.has(font))
|
|
@@ -190,18 +243,18 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
190
243
|
let font = headingCtx.font;
|
|
191
244
|
if (bold || italic)
|
|
192
245
|
font = resolveFont(font, bold, italic);
|
|
193
|
-
doc.font(font).fontSize(headingCtx.fontSize).fillColor(headingCtx.color);
|
|
246
|
+
doc.font(safeFont(font)).fontSize(headingCtx.fontSize).fillColor(headingCtx.color);
|
|
194
247
|
return;
|
|
195
248
|
}
|
|
196
249
|
const font = resolveFont(theme.body.font, bold, italic);
|
|
197
|
-
doc.font(font).fontSize(theme.body.fontSize).fillColor(theme.body.color);
|
|
250
|
+
doc.font(safeFont(font)).fontSize(theme.body.fontSize).fillColor(theme.body.color);
|
|
198
251
|
}
|
|
199
252
|
function resetBodyFont() {
|
|
200
253
|
if (headingCtx) {
|
|
201
|
-
doc.font(headingCtx.font).fontSize(headingCtx.fontSize).fillColor(headingCtx.color);
|
|
254
|
+
doc.font(safeFont(headingCtx.font)).fontSize(headingCtx.fontSize).fillColor(headingCtx.color);
|
|
202
255
|
return;
|
|
203
256
|
}
|
|
204
|
-
doc.font(theme.body.font).fontSize(theme.body.fontSize).fillColor(theme.body.color);
|
|
257
|
+
doc.font(safeFont(theme.body.font)).fontSize(theme.body.fontSize).fillColor(theme.body.color);
|
|
205
258
|
}
|
|
206
259
|
/**
|
|
207
260
|
* Render a text string switching to the emoji font for emoji characters.
|
|
@@ -249,7 +302,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
249
302
|
return;
|
|
250
303
|
}
|
|
251
304
|
// Remember the caller's font state so we can restore after emoji runs.
|
|
252
|
-
const prevFont = doc._font?.name ?? theme.body.font;
|
|
305
|
+
const prevFont = doc._font?.name ?? safeFont(theme.body.font);
|
|
253
306
|
const prevSize = doc._fontSize ?? theme.body.fontSize;
|
|
254
307
|
const segments = (0, emoji_js_1.splitEmojiSegments)(text);
|
|
255
308
|
// ── Color emoji path: render emoji as inline PNG images ──────────────
|
|
@@ -399,7 +452,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
399
452
|
const cs = theme.code.inline;
|
|
400
453
|
const hPad = 2; // horizontal padding each side
|
|
401
454
|
const vPad = 1; // vertical padding each side
|
|
402
|
-
doc.font(cs.font).fontSize(cs.fontSize);
|
|
455
|
+
doc.font(safeFont(cs.font)).fontSize(cs.fontSize);
|
|
403
456
|
const textW = doc.widthOfString(text);
|
|
404
457
|
const textH = doc.currentLineHeight();
|
|
405
458
|
// Determine flow position. If this is the first output in a table cell,
|
|
@@ -429,7 +482,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
429
482
|
.fill(cs.backgroundColor);
|
|
430
483
|
doc.restore();
|
|
431
484
|
// Render inline — use positioned form for cell context, flow form otherwise
|
|
432
|
-
doc.font(cs.font).fontSize(cs.fontSize).fillColor(cs.color);
|
|
485
|
+
doc.font(safeFont(cs.font)).fontSize(cs.fontSize).fillColor(cs.color);
|
|
433
486
|
if (useCellPos) {
|
|
434
487
|
doc.text(text, flowX, flowY, { continued, ...cellExtra });
|
|
435
488
|
}
|
|
@@ -445,10 +498,10 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
445
498
|
return renderImage(imgChild, tok.href);
|
|
446
499
|
}
|
|
447
500
|
if (headingCtx) {
|
|
448
|
-
doc.font(headingCtx.font).fontSize(headingCtx.fontSize).fillColor(theme.linkColor);
|
|
501
|
+
doc.font(safeFont(headingCtx.font)).fontSize(headingCtx.fontSize).fillColor(theme.linkColor);
|
|
449
502
|
}
|
|
450
503
|
else {
|
|
451
|
-
doc.font(theme.body.font).fontSize(theme.body.fontSize).fillColor(theme.linkColor);
|
|
504
|
+
doc.font(safeFont(theme.body.font)).fontSize(theme.body.fontSize).fillColor(theme.linkColor);
|
|
452
505
|
}
|
|
453
506
|
const linkText = tok.text || tok.href;
|
|
454
507
|
renderTextWithEmoji(linkText, { continued, underline: true, link: tok.href });
|
|
@@ -666,7 +719,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
666
719
|
const spaceBelow = style.fontSize * 0.3;
|
|
667
720
|
ensureSpace(spaceAbove + style.fontSize + spaceBelow);
|
|
668
721
|
doc.moveDown(spaceAbove / doc.currentLineHeight());
|
|
669
|
-
doc.font(style.font).fontSize(style.fontSize).fillColor(style.color);
|
|
722
|
+
doc.font(safeFont(style.font)).fontSize(style.fontSize).fillColor(style.color);
|
|
670
723
|
headingCtx = style;
|
|
671
724
|
if (t.tokens && t.tokens.length > 0) {
|
|
672
725
|
await renderInlineTokens(t.tokens, false, style.bold ?? false, style.italic ?? false);
|
|
@@ -712,7 +765,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
712
765
|
x: margins.left,
|
|
713
766
|
y: doc.y,
|
|
714
767
|
width: contentWidth,
|
|
715
|
-
font: cs.font,
|
|
768
|
+
font: safeFont(cs.font),
|
|
716
769
|
fontSize: cs.fontSize,
|
|
717
770
|
lineHeight: 1.5,
|
|
718
771
|
padding: cs.padding,
|
|
@@ -736,7 +789,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
736
789
|
doc.save();
|
|
737
790
|
doc.rect(x, y, contentWidth, blockH).fill(cs.backgroundColor);
|
|
738
791
|
doc.restore();
|
|
739
|
-
doc.font(cs.font).fontSize(cs.fontSize).fillColor(cs.color);
|
|
792
|
+
doc.font(safeFont(cs.font)).fontSize(cs.fontSize).fillColor(cs.color);
|
|
740
793
|
let textY = y + cs.padding;
|
|
741
794
|
for (const line of lines) {
|
|
742
795
|
doc.text(line, x + cs.padding, textY, { width: contentWidth - cs.padding * 2 });
|
|
@@ -762,7 +815,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
762
815
|
if (child.type === 'paragraph') {
|
|
763
816
|
const p = child;
|
|
764
817
|
const font = bq.italic ? italicVariant(theme.body.font) : theme.body.font;
|
|
765
|
-
doc.font(font).fontSize(theme.body.fontSize).fillColor(theme.body.color);
|
|
818
|
+
doc.font(safeFont(font)).fontSize(theme.body.fontSize).fillColor(theme.body.color);
|
|
766
819
|
doc.text('', textX, doc.y, { width: textWidth });
|
|
767
820
|
await renderInlineTokens(p.tokens, false, false, bq.italic);
|
|
768
821
|
doc.moveDown(0.3);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@speajus/markdown-to-pdf",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.15",
|
|
4
4
|
"description": "A new project created with Intent by Augment.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -29,9 +29,11 @@
|
|
|
29
29
|
"README.md"
|
|
30
30
|
],
|
|
31
31
|
"scripts": {
|
|
32
|
+
"docs:dev": "cd docs && $npm_execpath dev",
|
|
32
33
|
"build": "tsc && mkdir -p dist/fonts && cp src/fonts/* dist/fonts/",
|
|
33
34
|
"generate": "tsx samples/generate.ts",
|
|
34
|
-
"
|
|
35
|
+
"readme": "pnpm tsx ./src/cli.ts README.md README.pdf",
|
|
36
|
+
"test": "tsx --test test/*.test.ts && tsx samples/generate.ts && pnpm run readme"
|
|
35
37
|
},
|
|
36
38
|
"keywords": [
|
|
37
39
|
"markdown",
|