@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 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
- [![Live Demo](./docs/image.png)](https://speajus.github.io/markdown-to-pdf/)
5
+ -[![Live Demo](./docs/image.png)]([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 | 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 |
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
- | [marked](https://github.com/markedjs/marked) | Markdown parsing and tokenization |
168
- | [pdfkit](https://pdfkit.org/) | PDF document generation |
169
- | [@resvg/resvg-js](https://github.com/nicolo-ribaudo/resvg-js) | SVG-to-PNG rasterization for image embedding |
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
- | `npm run build` | Compile TypeScript to `dist/` |
176
- | `npm run generate` | Generate sample PDFs from `samples/*.md` into `output/` |
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.13",
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
- "test": "tsx --test test/*.test.ts && tsx samples/generate.ts && tsx ./src/cli.ts README.md README.pdf"
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",