@speajus/markdown-to-pdf 1.0.14 → 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.
Files changed (2) hide show
  1. package/dist/renderer.js +66 -13
  2. package/package.json +1 -1
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.14",
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",