@speajus/markdown-to-pdf 1.0.14 → 1.0.16
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/browser.d.ts +2 -3
- package/dist/browser.js +2 -3
- package/dist/fonts/OpenMoji-Color.ttf +0 -0
- package/dist/fonts/Twemoji.Mozilla.ttf +0 -0
- package/dist/highlight.prism.d.ts +2 -0
- package/dist/highlight.prism.js +9 -4
- package/dist/index.d.ts +2 -4
- package/dist/index.js +2 -5
- package/dist/renderer.js +156 -238
- package/dist/styles.d.ts +3 -1
- package/dist/styles.js +15 -1
- package/dist/themes/index.js +12 -0
- package/dist/types.d.ts +23 -16
- package/package.json +4 -2
- package/dist/color-emoji.d.ts +0 -43
- package/dist/color-emoji.js +0 -142
- package/dist/emoji.d.ts +0 -35
- package/dist/emoji.js +0 -137
- package/dist/fonts/NotoEmoji-Regular.ttf +0 -0
- package/dist/node-color-emoji.d.ts +0 -15
- package/dist/node-color-emoji.js +0 -57
package/dist/renderer.js
CHANGED
|
@@ -10,10 +10,15 @@ const marked_1 = require("marked");
|
|
|
10
10
|
const stream_1 = require("stream");
|
|
11
11
|
const defaults_js_1 = require("./defaults.js");
|
|
12
12
|
const highlight_prism_js_1 = require("./highlight.prism.js");
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
/**
|
|
16
|
-
const
|
|
13
|
+
/** Name used to identify the emoji font (for safeFont checks). */
|
|
14
|
+
const EMOJI_FONT_NAME = 'EmojiFont';
|
|
15
|
+
/** Standard PDF fonts built into PDFKit — always available without filesystem access. */
|
|
16
|
+
const STANDARD_PDF_FONTS = new Set([
|
|
17
|
+
'Helvetica', 'Helvetica-Bold', 'Helvetica-Oblique', 'Helvetica-BoldOblique',
|
|
18
|
+
'Courier', 'Courier-Bold', 'Courier-Oblique', 'Courier-BoldOblique',
|
|
19
|
+
'Times-Roman', 'Times-Bold', 'Times-Italic', 'Times-BoldItalic',
|
|
20
|
+
'Symbol', 'ZapfDingbats',
|
|
21
|
+
]);
|
|
17
22
|
async function renderMarkdownToPdf(markdown, options) {
|
|
18
23
|
const theme = options?.theme ?? styles_js_1.defaultTheme;
|
|
19
24
|
const layout = options?.pageLayout ?? styles_js_1.defaultPageLayout;
|
|
@@ -24,49 +29,61 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
24
29
|
}
|
|
25
30
|
const lineNumbers = options?.lineNumbers ?? false;
|
|
26
31
|
const zebraStripes = options?.zebraStripes !== false;
|
|
27
|
-
|
|
32
|
+
// ── Resolve effective emoji font setting ──────────────────────────────
|
|
33
|
+
// PdfOptions.emojiFont (boolean | string | Buffer) overrides theme when
|
|
34
|
+
// explicitly set; otherwise fall back to theme.emojiFont ('twemoji'|'openmoji'|'none').
|
|
35
|
+
const themeEmojiFont = theme.emojiFont ?? 'twemoji';
|
|
36
|
+
const emojiFontOpt = options?.emojiFont !== undefined
|
|
37
|
+
? options.emojiFont
|
|
38
|
+
: themeEmojiFont === 'none'
|
|
39
|
+
? false
|
|
40
|
+
: themeEmojiFont; // 'twemoji' | 'openmoji'
|
|
28
41
|
// Use provided image renderer or create default Node.js renderer
|
|
29
42
|
const imageRenderer = options?.renderImage ?? defaults_js_1.DEFAULTS.renderImage(basePath);
|
|
30
43
|
const { margins } = layout;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// ── Emoji font registration ───────────────────────────────────────────────
|
|
37
|
-
// The font registration block uses dynamic require() for `path` and `fs` so
|
|
38
|
-
// that the renderer module can also be imported in browser environments where
|
|
39
|
-
// those Node.js built-ins are unavailable. If they aren't available the
|
|
40
|
-
// try/catch simply falls through and emoji support is disabled (unless the
|
|
41
|
-
// caller passes a Buffer directly via the `emojiFont` option).
|
|
42
|
-
let emojiEnabled = false;
|
|
44
|
+
// ── Resolve emoji font for pdfkit native color emoji support ────────────
|
|
45
|
+
// The fork at jspears/pdfkit#support-color-emoji-google handles emoji
|
|
46
|
+
// segmentation and rendering (COLR/CPAL, SBIX, CBDT) internally when
|
|
47
|
+
// an `emojiFont` option is passed to the PDFDocument constructor.
|
|
48
|
+
let resolvedEmojiFont;
|
|
43
49
|
if (emojiFontOpt !== false) {
|
|
44
50
|
try {
|
|
45
51
|
if (Buffer.isBuffer(emojiFontOpt)) {
|
|
46
|
-
|
|
47
|
-
doc.registerFont(EMOJI_FONT_NAME, emojiFontOpt);
|
|
48
|
-
emojiEnabled = true;
|
|
52
|
+
resolvedEmojiFont = emojiFontOpt;
|
|
49
53
|
}
|
|
50
54
|
else {
|
|
51
|
-
// Resolve a file path — requires Node.js built-ins.
|
|
52
55
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
53
56
|
const nodePath = require('path');
|
|
54
57
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
55
58
|
const nodeFs = require('fs');
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
let fontPath;
|
|
60
|
+
if (typeof emojiFontOpt === 'string' && emojiFontOpt !== 'twemoji' && emojiFontOpt !== 'openmoji' && emojiFontOpt !== 'none') {
|
|
61
|
+
fontPath = emojiFontOpt; // custom file path
|
|
62
|
+
}
|
|
63
|
+
else if (emojiFontOpt === 'openmoji') {
|
|
64
|
+
fontPath = nodePath.join(__dirname, 'fonts', 'OpenMoji-Color.ttf');
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
fontPath = nodePath.join(__dirname, 'fonts', 'Twemoji.Mozilla.ttf');
|
|
68
|
+
}
|
|
59
69
|
if (nodeFs.existsSync(fontPath)) {
|
|
60
|
-
|
|
61
|
-
emojiEnabled = true;
|
|
70
|
+
resolvedEmojiFont = fontPath;
|
|
62
71
|
}
|
|
63
72
|
}
|
|
64
73
|
}
|
|
65
74
|
catch {
|
|
66
|
-
//
|
|
67
|
-
// fall through without emoji support.
|
|
75
|
+
// Node.js APIs not available (browser) — no emoji font.
|
|
68
76
|
}
|
|
69
77
|
}
|
|
78
|
+
const doc = new pdfkit_1.default({
|
|
79
|
+
size: layout.pageSize,
|
|
80
|
+
margins,
|
|
81
|
+
...(resolvedEmojiFont ? { emojiFont: resolvedEmojiFont } : {}),
|
|
82
|
+
});
|
|
83
|
+
const stream = new stream_1.PassThrough();
|
|
84
|
+
const chunks = [];
|
|
85
|
+
doc.pipe(stream);
|
|
86
|
+
stream.on('data', (chunk) => chunks.push(chunk));
|
|
70
87
|
// ── Custom font registration ─────────────────────────────────────────────
|
|
71
88
|
// Register user-supplied font families so they can be referenced by name
|
|
72
89
|
// in ThemeConfig font fields. Each variant is registered with a suffix:
|
|
@@ -87,15 +104,22 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
87
104
|
}
|
|
88
105
|
}
|
|
89
106
|
}
|
|
90
|
-
// ──
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
107
|
+
// ── Browser font safety ────────────────────────────────────────────────
|
|
108
|
+
// In browser environments, `fs.readFileSync` doesn't exist. When PDFKit
|
|
109
|
+
// encounters an unknown font name it tries to load it from disk via
|
|
110
|
+
// readFileSync, which crashes in the browser. Detect whether we're in a
|
|
111
|
+
// filesystem-capable environment so we can guard calls to `doc.font()`.
|
|
112
|
+
let fsAvailable = false;
|
|
113
|
+
try {
|
|
114
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
115
|
+
const nodeFs = require('fs');
|
|
116
|
+
fsAvailable = typeof nodeFs.readFileSync === 'function';
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Node.js `fs` not available — we're in a browser environment.
|
|
98
120
|
}
|
|
121
|
+
// ── Spacing config ────────────────────────────────────────────────────
|
|
122
|
+
const sp = { ...styles_js_1.defaultSpacing, ...theme.spacing };
|
|
99
123
|
const tokens = marked_1.marked.lexer(markdown);
|
|
100
124
|
const contentWidth = doc.page.width - margins.left - margins.right;
|
|
101
125
|
// ── Table cell context ──────────────────────────────────────────────────
|
|
@@ -122,6 +146,38 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
122
146
|
return customFontNames.has(font.substring(0, dash));
|
|
123
147
|
return false;
|
|
124
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Return a safe font name that will not crash `doc.font()`.
|
|
151
|
+
*
|
|
152
|
+
* In Node.js (`fsAvailable === true`) this is a no-op — PDFKit can load
|
|
153
|
+
* fonts from the filesystem. In browser environments, if the font is
|
|
154
|
+
* neither a standard PDF font nor a registered custom / emoji font, we
|
|
155
|
+
* return a standard fallback and log a warning so the user knows which
|
|
156
|
+
* font was unavailable.
|
|
157
|
+
*/
|
|
158
|
+
function safeFont(name) {
|
|
159
|
+
if (fsAvailable)
|
|
160
|
+
return name;
|
|
161
|
+
if (STANDARD_PDF_FONTS.has(name) || isCustomFont(name) || name === EMOJI_FONT_NAME)
|
|
162
|
+
return name;
|
|
163
|
+
// Determine the closest standard fallback, preserving bold/italic intent
|
|
164
|
+
// by inspecting the variant suffix added by resolveFont().
|
|
165
|
+
let fallback;
|
|
166
|
+
if (name.endsWith('-BoldOblique') || name.endsWith('-BoldItalic')) {
|
|
167
|
+
fallback = 'Helvetica-BoldOblique';
|
|
168
|
+
}
|
|
169
|
+
else if (name.endsWith('-Bold')) {
|
|
170
|
+
fallback = 'Helvetica-Bold';
|
|
171
|
+
}
|
|
172
|
+
else if (name.endsWith('-Oblique') || name.endsWith('-Italic')) {
|
|
173
|
+
fallback = 'Helvetica-Oblique';
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
fallback = 'Helvetica';
|
|
177
|
+
}
|
|
178
|
+
console.warn(`[markdown-to-pdf] Font "${name}" is not available; falling back to "${fallback}"`);
|
|
179
|
+
return fallback;
|
|
180
|
+
}
|
|
125
181
|
/** Return the base (registered) name of a custom font, stripping any variant suffix. */
|
|
126
182
|
function customFontBase(font) {
|
|
127
183
|
if (customFontNames.has(font))
|
|
@@ -190,34 +246,30 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
190
246
|
let font = headingCtx.font;
|
|
191
247
|
if (bold || italic)
|
|
192
248
|
font = resolveFont(font, bold, italic);
|
|
193
|
-
doc.font(font).fontSize(headingCtx.fontSize).fillColor(headingCtx.color);
|
|
249
|
+
doc.font(safeFont(font)).fontSize(headingCtx.fontSize).fillColor(headingCtx.color);
|
|
194
250
|
return;
|
|
195
251
|
}
|
|
196
252
|
const font = resolveFont(theme.body.font, bold, italic);
|
|
197
|
-
doc.font(font).fontSize(theme.body.fontSize).fillColor(theme.body.color);
|
|
253
|
+
doc.font(safeFont(font)).fontSize(theme.body.fontSize).fillColor(theme.body.color);
|
|
198
254
|
}
|
|
199
255
|
function resetBodyFont() {
|
|
200
256
|
if (headingCtx) {
|
|
201
|
-
doc.font(headingCtx.font).fontSize(headingCtx.fontSize).fillColor(headingCtx.color);
|
|
257
|
+
doc.font(safeFont(headingCtx.font)).fontSize(headingCtx.fontSize).fillColor(headingCtx.color);
|
|
202
258
|
return;
|
|
203
259
|
}
|
|
204
|
-
doc.font(theme.body.font).fontSize(theme.body.fontSize).fillColor(theme.body.color);
|
|
260
|
+
doc.font(safeFont(theme.body.font)).fontSize(theme.body.fontSize).fillColor(theme.body.color);
|
|
205
261
|
}
|
|
206
262
|
/**
|
|
207
|
-
* Render
|
|
263
|
+
* Render text, delegating to `doc.text()`.
|
|
208
264
|
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
265
|
+
* pdfkit's native color emoji support (via the `emojiFont` constructor
|
|
266
|
+
* option) handles emoji segmentation and rendering internally, so this
|
|
267
|
+
* function only normalises call signatures and handles table cell context.
|
|
212
268
|
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
* glyphs, with manual (x, y) positioning to keep them in the text flow.
|
|
216
|
-
*
|
|
217
|
-
* Supports both `renderTextWithEmoji(text, opts?)` and the positioned
|
|
218
|
-
* form `renderTextWithEmoji(text, x, y, opts?)` used by table cells.
|
|
269
|
+
* Supports both `renderText(text, opts?)` and the positioned form
|
|
270
|
+
* `renderText(text, x, y, opts?)` used by table cells.
|
|
219
271
|
*/
|
|
220
|
-
function
|
|
272
|
+
function renderText(text, xOrOpts, yOrUndefined, posOpts) {
|
|
221
273
|
// Normalise the two call signatures into a single (opts, firstX, firstY).
|
|
222
274
|
let opts;
|
|
223
275
|
let firstX;
|
|
@@ -237,169 +289,18 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
237
289
|
opts = { ...opts, width: cellCtx.width, align: cellCtx.align };
|
|
238
290
|
cellCtx.used = true;
|
|
239
291
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if ((!emojiEnabled && !colorEmojiEnabled) || !hasEmoji) {
|
|
243
|
-
if (firstX !== undefined) {
|
|
244
|
-
doc.text(text, firstX, firstY, opts);
|
|
245
|
-
}
|
|
246
|
-
else {
|
|
247
|
-
doc.text(text, opts);
|
|
248
|
-
}
|
|
249
|
-
return;
|
|
292
|
+
if (firstX !== undefined) {
|
|
293
|
+
doc.text(text, firstX, firstY, opts);
|
|
250
294
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const prevSize = doc._fontSize ?? theme.body.fontSize;
|
|
254
|
-
const segments = (0, emoji_js_1.splitEmojiSegments)(text);
|
|
255
|
-
// ── Color emoji path: render emoji as inline PNG images ──────────────
|
|
256
|
-
// Strategy: two-pass rendering.
|
|
257
|
-
// Pass 1 – Render all text through doc.text() with `continued`,
|
|
258
|
-
// exactly like the monochrome path. For emoji segments,
|
|
259
|
-
// render a space-placeholder to reserve width. Read the
|
|
260
|
-
// real X position from PDFKit's internal wrapper state
|
|
261
|
-
// (_wrapper.startX + _wrapper.continuedX) — doc.x stays
|
|
262
|
-
// at the left margin after continued:true so it is NOT
|
|
263
|
-
// usable for positioning.
|
|
264
|
-
// Pass 2 – Overlay emoji PNG images at the recorded positions.
|
|
265
|
-
if (colorEmojiEnabled && emojiImageCache && emojiImageCache.size > 0) {
|
|
266
|
-
const emojiRe = (0, emoji_js_1.getEmojiRegex)();
|
|
267
|
-
const emojiSize = prevSize; // match font size
|
|
268
|
-
const emojiPlacements = [];
|
|
269
|
-
// Read the actual text-flow X from PDFKit's internal LineWrapper.
|
|
270
|
-
// After doc.text(…, {continued:true}), _wrapper.continuedX tracks
|
|
271
|
-
// cumulative rendered width; startX is the original doc.x at
|
|
272
|
-
// wrapper creation time.
|
|
273
|
-
const getFlowX = () => {
|
|
274
|
-
const w = doc._wrapper;
|
|
275
|
-
if (w)
|
|
276
|
-
return w.startX + w.continuedX;
|
|
277
|
-
return firstX ?? doc.x;
|
|
278
|
-
};
|
|
279
|
-
// Build a space-placeholder string whose width ≈ emojiSize.
|
|
280
|
-
doc.font(prevFont).fontSize(prevSize);
|
|
281
|
-
const spaceW = doc.widthOfString(' ');
|
|
282
|
-
const spacesPerEmoji = Math.max(1, Math.round(emojiSize / spaceW));
|
|
283
|
-
const placeholder = ' '.repeat(spacesPerEmoji);
|
|
284
|
-
const placeholderW = doc.widthOfString(placeholder);
|
|
285
|
-
// Vertical alignment: centre emoji within the current line height.
|
|
286
|
-
const lineH = doc.currentLineHeight(true);
|
|
287
|
-
const yOffset = Math.max(0, (lineH - emojiSize) / 2);
|
|
288
|
-
// Only use explicit (firstX, firstY) coordinates on the very first
|
|
289
|
-
// doc.text() call, and only when no wrapper already exists (i.e.
|
|
290
|
-
// we are not continuing from a prior renderTextWithEmoji call).
|
|
291
|
-
let usedExplicitCoords = false;
|
|
292
|
-
for (let i = 0; i < segments.length; i++) {
|
|
293
|
-
const seg = segments[i];
|
|
294
|
-
const isLast = i === segments.length - 1;
|
|
295
|
-
const cont = isLast ? !!opts.continued : true;
|
|
296
|
-
if (seg.isEmoji) {
|
|
297
|
-
emojiRe.lastIndex = 0;
|
|
298
|
-
let em;
|
|
299
|
-
while ((em = emojiRe.exec(seg.text)) !== null) {
|
|
300
|
-
const png = emojiImageCache.get(em[0]);
|
|
301
|
-
const isLastEmoji = isLast && emojiRe.lastIndex >= seg.text.length;
|
|
302
|
-
const eCont = isLastEmoji ? !!opts.continued : true;
|
|
303
|
-
if (png) {
|
|
304
|
-
// Position BEFORE rendering the placeholder.
|
|
305
|
-
const emojiX = getFlowX();
|
|
306
|
-
// For positioned calls (table cells), doc.y may be stale from
|
|
307
|
-
// a previous cell — use the explicit firstY instead.
|
|
308
|
-
const emojiY = (!usedExplicitCoords && firstY !== undefined) ? firstY : doc.y;
|
|
309
|
-
doc.font(prevFont).fontSize(prevSize);
|
|
310
|
-
if (!usedExplicitCoords && firstX !== undefined) {
|
|
311
|
-
doc.text(placeholder, firstX, firstY, { ...opts, continued: eCont });
|
|
312
|
-
usedExplicitCoords = true;
|
|
313
|
-
}
|
|
314
|
-
else {
|
|
315
|
-
doc.text(placeholder, { ...opts, continued: eCont });
|
|
316
|
-
}
|
|
317
|
-
// Compute alignment-aware X position for the emoji image.
|
|
318
|
-
let placedX = emojiX + (placeholderW - emojiSize) / 2;
|
|
319
|
-
if (firstX !== undefined && typeof opts.width === 'number' && !doc._wrapper) {
|
|
320
|
-
// Table cell with alignment — PDFKit already rendered the
|
|
321
|
-
// placeholder at the aligned position; match it.
|
|
322
|
-
const cellW = opts.width;
|
|
323
|
-
const a = opts.align || 'left';
|
|
324
|
-
if (a === 'center')
|
|
325
|
-
placedX = firstX + (cellW - emojiSize) / 2;
|
|
326
|
-
else if (a === 'right')
|
|
327
|
-
placedX = firstX + cellW - emojiSize;
|
|
328
|
-
}
|
|
329
|
-
emojiPlacements.push({
|
|
330
|
-
png,
|
|
331
|
-
x: placedX,
|
|
332
|
-
y: emojiY + yOffset,
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
else {
|
|
336
|
-
// Fallback: monochrome glyph.
|
|
337
|
-
if (emojiEnabled)
|
|
338
|
-
doc.font(EMOJI_FONT_NAME).fontSize(prevSize);
|
|
339
|
-
if (!usedExplicitCoords && firstX !== undefined) {
|
|
340
|
-
doc.text(em[0], firstX, firstY, { ...opts, continued: isLastEmoji ? !!opts.continued : true });
|
|
341
|
-
usedExplicitCoords = true;
|
|
342
|
-
}
|
|
343
|
-
else {
|
|
344
|
-
doc.text(em[0], { ...opts, continued: isLastEmoji ? !!opts.continued : true });
|
|
345
|
-
}
|
|
346
|
-
doc.font(prevFont).fontSize(prevSize);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
else {
|
|
351
|
-
// ── Text segment ──
|
|
352
|
-
doc.font(prevFont).fontSize(prevSize);
|
|
353
|
-
if (!usedExplicitCoords && firstX !== undefined) {
|
|
354
|
-
doc.text(seg.text, firstX, firstY, { ...opts, continued: cont });
|
|
355
|
-
usedExplicitCoords = true;
|
|
356
|
-
}
|
|
357
|
-
else {
|
|
358
|
-
doc.text(seg.text, { ...opts, continued: cont });
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
// ── Pass 2: overlay emoji images at recorded positions ──
|
|
363
|
-
const savedY = doc.y;
|
|
364
|
-
const savedX = doc.x;
|
|
365
|
-
for (const ep of emojiPlacements) {
|
|
366
|
-
doc.image(ep.png, ep.x, ep.y, {
|
|
367
|
-
width: emojiSize,
|
|
368
|
-
height: emojiSize,
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
|
-
doc.y = savedY;
|
|
372
|
-
doc.x = savedX;
|
|
373
|
-
doc.font(prevFont).fontSize(prevSize);
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
// ── Monochrome font path (original behaviour) ────────────────────────
|
|
377
|
-
for (let i = 0; i < segments.length; i++) {
|
|
378
|
-
const seg = segments[i];
|
|
379
|
-
const isLast = i === segments.length - 1;
|
|
380
|
-
const cont = isLast ? !!opts.continued : true;
|
|
381
|
-
if (seg.isEmoji) {
|
|
382
|
-
doc.font(EMOJI_FONT_NAME).fontSize(prevSize);
|
|
383
|
-
}
|
|
384
|
-
else {
|
|
385
|
-
doc.font(prevFont).fontSize(prevSize);
|
|
386
|
-
}
|
|
387
|
-
// Only pass explicit x,y for the very first segment.
|
|
388
|
-
if (i === 0 && firstX !== undefined) {
|
|
389
|
-
doc.text(seg.text, firstX, firstY, { ...opts, continued: cont });
|
|
390
|
-
}
|
|
391
|
-
else {
|
|
392
|
-
doc.text(seg.text, { ...opts, continued: cont });
|
|
393
|
-
}
|
|
295
|
+
else {
|
|
296
|
+
doc.text(text, opts);
|
|
394
297
|
}
|
|
395
|
-
// Restore the original font so subsequent calls aren't surprised.
|
|
396
|
-
doc.font(prevFont).fontSize(prevSize);
|
|
397
298
|
}
|
|
398
299
|
function renderCodespan(text, continued) {
|
|
399
300
|
const cs = theme.code.inline;
|
|
400
301
|
const hPad = 2; // horizontal padding each side
|
|
401
302
|
const vPad = 1; // vertical padding each side
|
|
402
|
-
doc.font(cs.font).fontSize(cs.fontSize);
|
|
303
|
+
doc.font(safeFont(cs.font)).fontSize(cs.fontSize);
|
|
403
304
|
const textW = doc.widthOfString(text);
|
|
404
305
|
const textH = doc.currentLineHeight();
|
|
405
306
|
// Determine flow position. If this is the first output in a table cell,
|
|
@@ -429,7 +330,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
429
330
|
.fill(cs.backgroundColor);
|
|
430
331
|
doc.restore();
|
|
431
332
|
// Render inline — use positioned form for cell context, flow form otherwise
|
|
432
|
-
doc.font(cs.font).fontSize(cs.fontSize).fillColor(cs.color);
|
|
333
|
+
doc.font(safeFont(cs.font)).fontSize(cs.fontSize).fillColor(cs.color);
|
|
433
334
|
if (useCellPos) {
|
|
434
335
|
doc.text(text, flowX, flowY, { continued, ...cellExtra });
|
|
435
336
|
}
|
|
@@ -445,13 +346,13 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
445
346
|
return renderImage(imgChild, tok.href);
|
|
446
347
|
}
|
|
447
348
|
if (headingCtx) {
|
|
448
|
-
doc.font(headingCtx.font).fontSize(headingCtx.fontSize).fillColor(theme.linkColor);
|
|
349
|
+
doc.font(safeFont(headingCtx.font)).fontSize(headingCtx.fontSize).fillColor(theme.linkColor);
|
|
449
350
|
}
|
|
450
351
|
else {
|
|
451
|
-
doc.font(theme.body.font).fontSize(theme.body.fontSize).fillColor(theme.linkColor);
|
|
352
|
+
doc.font(safeFont(theme.body.font)).fontSize(theme.body.fontSize).fillColor(theme.linkColor);
|
|
452
353
|
}
|
|
453
354
|
const linkText = tok.text || tok.href;
|
|
454
|
-
|
|
355
|
+
renderText(linkText, { continued, underline: true, link: tok.href });
|
|
455
356
|
doc.fillColor(headingCtx ? headingCtx.color : theme.body.color);
|
|
456
357
|
}
|
|
457
358
|
async function renderInlineTokens(inlineTokens, continued, insideBold = false, insideItalic = false) {
|
|
@@ -467,7 +368,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
467
368
|
}
|
|
468
369
|
else {
|
|
469
370
|
applyBodyFont(insideBold, insideItalic);
|
|
470
|
-
|
|
371
|
+
renderText(t.text, { continued: cont, underline: false, strike: false });
|
|
471
372
|
}
|
|
472
373
|
break;
|
|
473
374
|
}
|
|
@@ -495,12 +396,12 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
495
396
|
}
|
|
496
397
|
case 'del': {
|
|
497
398
|
applyBodyFont(insideBold, insideItalic);
|
|
498
|
-
|
|
399
|
+
renderText(tok.text, { continued: cont, strike: true, underline: false });
|
|
499
400
|
break;
|
|
500
401
|
}
|
|
501
402
|
case 'escape': {
|
|
502
403
|
applyBodyFont(insideBold, insideItalic);
|
|
503
|
-
|
|
404
|
+
renderText(tok.text, { continued: cont, underline: false, strike: false });
|
|
504
405
|
break;
|
|
505
406
|
}
|
|
506
407
|
case 'br': {
|
|
@@ -511,7 +412,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
511
412
|
const raw = tok.text ?? tok.raw ?? '';
|
|
512
413
|
if (raw) {
|
|
513
414
|
applyBodyFont(insideBold, insideItalic);
|
|
514
|
-
|
|
415
|
+
renderText(raw, { continued: cont, underline: false, strike: false });
|
|
515
416
|
}
|
|
516
417
|
break;
|
|
517
418
|
}
|
|
@@ -535,9 +436,12 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
535
436
|
displayWidth = img.width * (displayHeight / img.height);
|
|
536
437
|
}
|
|
537
438
|
ensureSpace(displayHeight + 10);
|
|
538
|
-
|
|
439
|
+
// Compute horizontal position based on imageAlign
|
|
440
|
+
const imgX = theme.imageAlign === 'center'
|
|
441
|
+
? margins.left + (contentWidth - displayWidth) / 2
|
|
442
|
+
: doc.x;
|
|
539
443
|
const imgY = doc.y;
|
|
540
|
-
doc.image(imgBuffer, { width: displayWidth, height: displayHeight });
|
|
444
|
+
doc.image(imgBuffer, imgX, imgY, { width: displayWidth, height: displayHeight });
|
|
541
445
|
// If the image is wrapped in a link, overlay a clickable annotation
|
|
542
446
|
if (linkUrl) {
|
|
543
447
|
doc.link(imgX, imgY, displayWidth, displayHeight, linkUrl);
|
|
@@ -552,13 +456,13 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
552
456
|
}
|
|
553
457
|
}
|
|
554
458
|
async function renderList(list, depth) {
|
|
555
|
-
const indent = margins.left + depth *
|
|
459
|
+
const indent = margins.left + depth * sp.listIndent;
|
|
556
460
|
for (let idx = 0; idx < list.items.length; idx++) {
|
|
557
461
|
const item = list.items[idx];
|
|
558
462
|
ensureSpace(theme.body.fontSize * 2);
|
|
559
463
|
resetBodyFont();
|
|
560
464
|
const bullet = list.ordered ? `${list.start + idx}.` : '•';
|
|
561
|
-
doc.text(bullet, indent, doc.y, { continued: true, width: contentWidth - depth *
|
|
465
|
+
doc.text(bullet, indent, doc.y, { continued: true, width: contentWidth - depth * sp.listIndent });
|
|
562
466
|
doc.text(' ', { continued: true });
|
|
563
467
|
// Render item inline tokens
|
|
564
468
|
const itemTokens = item.tokens;
|
|
@@ -569,7 +473,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
569
473
|
await renderInlineTokens(t.tokens, false);
|
|
570
474
|
}
|
|
571
475
|
else {
|
|
572
|
-
|
|
476
|
+
renderText(t.text);
|
|
573
477
|
}
|
|
574
478
|
}
|
|
575
479
|
else if (child.type === 'paragraph') {
|
|
@@ -579,7 +483,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
579
483
|
await renderList(child, depth + 1);
|
|
580
484
|
}
|
|
581
485
|
}
|
|
582
|
-
doc.moveDown(
|
|
486
|
+
doc.moveDown(sp.listItemSpacing);
|
|
583
487
|
}
|
|
584
488
|
}
|
|
585
489
|
async function renderCellTokens(cell, x, y, width, align, bold) {
|
|
@@ -593,7 +497,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
593
497
|
else {
|
|
594
498
|
// Fallback: plain text (no inline tokens)
|
|
595
499
|
applyBodyFont(bold, false);
|
|
596
|
-
|
|
500
|
+
renderText(cell.text, x, y, { width, align });
|
|
597
501
|
}
|
|
598
502
|
doc.y = savedY;
|
|
599
503
|
}
|
|
@@ -662,17 +566,17 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
662
566
|
const t = token;
|
|
663
567
|
const key = `h${t.depth}`;
|
|
664
568
|
const style = theme.headings[key];
|
|
665
|
-
const spaceAbove = style.fontSize *
|
|
666
|
-
const spaceBelow = style.fontSize *
|
|
569
|
+
const spaceAbove = style.fontSize * sp.headingSpaceAbove;
|
|
570
|
+
const spaceBelow = style.fontSize * sp.headingSpaceBelow;
|
|
667
571
|
ensureSpace(spaceAbove + style.fontSize + spaceBelow);
|
|
668
572
|
doc.moveDown(spaceAbove / doc.currentLineHeight());
|
|
669
|
-
doc.font(style.font).fontSize(style.fontSize).fillColor(style.color);
|
|
573
|
+
doc.font(safeFont(style.font)).fontSize(style.fontSize).fillColor(style.color);
|
|
670
574
|
headingCtx = style;
|
|
671
575
|
if (t.tokens && t.tokens.length > 0) {
|
|
672
576
|
await renderInlineTokens(t.tokens, false, style.bold ?? false, style.italic ?? false);
|
|
673
577
|
}
|
|
674
578
|
else {
|
|
675
|
-
|
|
579
|
+
renderText(t.text);
|
|
676
580
|
}
|
|
677
581
|
headingCtx = null;
|
|
678
582
|
// Draw an underline beneath h1 and h2
|
|
@@ -695,7 +599,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
695
599
|
ensureSpace(theme.body.fontSize * 2);
|
|
696
600
|
resetBodyFont();
|
|
697
601
|
await renderInlineTokens(t.tokens, false);
|
|
698
|
-
doc.moveDown(
|
|
602
|
+
doc.moveDown(sp.paragraphSpacing);
|
|
699
603
|
break;
|
|
700
604
|
}
|
|
701
605
|
case 'code': {
|
|
@@ -712,17 +616,18 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
712
616
|
x: margins.left,
|
|
713
617
|
y: doc.y,
|
|
714
618
|
width: contentWidth,
|
|
715
|
-
font: cs.font,
|
|
619
|
+
font: safeFont(cs.font),
|
|
716
620
|
fontSize: cs.fontSize,
|
|
717
621
|
lineHeight: 1.5,
|
|
718
622
|
padding: cs.padding,
|
|
719
623
|
lineNumbers,
|
|
720
624
|
drawBackground: true,
|
|
721
625
|
theme: theme.syntaxHighlight,
|
|
626
|
+
borderRadius: cs.borderRadius,
|
|
722
627
|
});
|
|
723
628
|
doc.x = margins.left;
|
|
724
629
|
doc.y = newY;
|
|
725
|
-
doc.moveDown(
|
|
630
|
+
doc.moveDown(sp.codeBlockSpacing);
|
|
726
631
|
resetBodyFont();
|
|
727
632
|
}
|
|
728
633
|
else {
|
|
@@ -733,10 +638,16 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
733
638
|
ensureSpace(blockH + 10);
|
|
734
639
|
const x = margins.left;
|
|
735
640
|
const y = doc.y;
|
|
641
|
+
const cbRadius = cs.borderRadius ?? 0;
|
|
736
642
|
doc.save();
|
|
737
|
-
|
|
643
|
+
if (cbRadius > 0) {
|
|
644
|
+
doc.roundedRect(x, y, contentWidth, blockH, cbRadius).fill(cs.backgroundColor);
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
doc.rect(x, y, contentWidth, blockH).fill(cs.backgroundColor);
|
|
648
|
+
}
|
|
738
649
|
doc.restore();
|
|
739
|
-
doc.font(cs.font).fontSize(cs.fontSize).fillColor(cs.color);
|
|
650
|
+
doc.font(safeFont(cs.font)).fontSize(cs.fontSize).fillColor(cs.color);
|
|
740
651
|
let textY = y + cs.padding;
|
|
741
652
|
for (const line of lines) {
|
|
742
653
|
doc.text(line, x + cs.padding, textY, { width: contentWidth - cs.padding * 2 });
|
|
@@ -744,7 +655,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
744
655
|
}
|
|
745
656
|
doc.x = margins.left;
|
|
746
657
|
doc.y = y + blockH;
|
|
747
|
-
doc.moveDown(
|
|
658
|
+
doc.moveDown(sp.codeBlockSpacing);
|
|
748
659
|
resetBodyFont();
|
|
749
660
|
}
|
|
750
661
|
break;
|
|
@@ -753,7 +664,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
753
664
|
const t = token;
|
|
754
665
|
const bq = theme.blockquote;
|
|
755
666
|
ensureSpace(30);
|
|
756
|
-
const bqPadding =
|
|
667
|
+
const bqPadding = bq.padding ?? 6;
|
|
757
668
|
const startY = doc.y;
|
|
758
669
|
doc.y += bqPadding; // add top padding before text
|
|
759
670
|
const textX = margins.left + bq.borderWidth + bq.indent;
|
|
@@ -762,10 +673,10 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
762
673
|
if (child.type === 'paragraph') {
|
|
763
674
|
const p = child;
|
|
764
675
|
const font = bq.italic ? italicVariant(theme.body.font) : theme.body.font;
|
|
765
|
-
doc.font(font).fontSize(theme.body.fontSize).fillColor(theme.body.color);
|
|
676
|
+
doc.font(safeFont(font)).fontSize(theme.body.fontSize).fillColor(theme.body.color);
|
|
766
677
|
doc.text('', textX, doc.y, { width: textWidth });
|
|
767
678
|
await renderInlineTokens(p.tokens, false, false, bq.italic);
|
|
768
|
-
doc.moveDown(
|
|
679
|
+
doc.moveDown(sp.blockquoteSpacing);
|
|
769
680
|
}
|
|
770
681
|
else {
|
|
771
682
|
await renderToken(child);
|
|
@@ -773,11 +684,18 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
773
684
|
}
|
|
774
685
|
doc.y += bqPadding; // add bottom padding after text
|
|
775
686
|
const endY = doc.y;
|
|
687
|
+
// Draw optional background fill behind blockquote area
|
|
688
|
+
if (bq.backgroundColor) {
|
|
689
|
+
doc.save();
|
|
690
|
+
doc.rect(margins.left + bq.borderWidth, startY, contentWidth - bq.borderWidth, endY - startY).fill(bq.backgroundColor);
|
|
691
|
+
doc.restore();
|
|
692
|
+
}
|
|
693
|
+
// Draw left border
|
|
776
694
|
doc.save();
|
|
777
695
|
doc.rect(margins.left, startY, bq.borderWidth, endY - startY).fill(bq.borderColor);
|
|
778
696
|
doc.restore();
|
|
779
697
|
doc.x = margins.left;
|
|
780
|
-
doc.moveDown(
|
|
698
|
+
doc.moveDown(sp.blockquoteSpacing);
|
|
781
699
|
resetBodyFont();
|
|
782
700
|
break;
|
|
783
701
|
}
|
|
@@ -788,7 +706,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
788
706
|
}
|
|
789
707
|
case 'hr': {
|
|
790
708
|
ensureSpace(20);
|
|
791
|
-
doc.moveDown(
|
|
709
|
+
doc.moveDown(sp.hrSpacing);
|
|
792
710
|
const y = doc.y;
|
|
793
711
|
doc.save();
|
|
794
712
|
doc.strokeColor(theme.horizontalRuleColor).lineWidth(1)
|
|
@@ -797,7 +715,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
797
715
|
.stroke();
|
|
798
716
|
doc.restore();
|
|
799
717
|
doc.y = y;
|
|
800
|
-
doc.moveDown(
|
|
718
|
+
doc.moveDown(sp.hrSpacing);
|
|
801
719
|
resetBodyFont();
|
|
802
720
|
break;
|
|
803
721
|
}
|
package/dist/styles.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type { ThemeConfig, PageLayout, SyntaxHighlightTheme } from './types.js';
|
|
1
|
+
import type { ThemeConfig, PageLayout, SpacingConfig, SyntaxHighlightTheme } from './types.js';
|
|
2
|
+
/** Default spacing values — used as fallbacks when theme.spacing is partial or absent. */
|
|
3
|
+
export declare const defaultSpacing: Required<SpacingConfig>;
|
|
2
4
|
/** Prism.js default light theme — used as the default (print-friendly). */
|
|
3
5
|
export declare const defaultSyntaxHighlightTheme: SyntaxHighlightTheme;
|
|
4
6
|
export declare const defaultTheme: ThemeConfig;
|