@speajus/markdown-to-pdf 1.0.5 → 1.0.6
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/renderer.js +101 -27
- package/package.json +1 -1
package/dist/renderer.js
CHANGED
|
@@ -76,12 +76,38 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
76
76
|
}
|
|
77
77
|
const tokens = marked_1.marked.lexer(markdown);
|
|
78
78
|
const contentWidth = doc.page.width - margins.left - margins.right;
|
|
79
|
+
// ── Table cell context ──────────────────────────────────────────────────
|
|
80
|
+
// When rendering inline tokens inside a table cell, the first text output
|
|
81
|
+
// must be positioned at the cell's (x, y) with the cell's width/align.
|
|
82
|
+
// Subsequent text outputs in the same cell use PDFKit's continued-flow.
|
|
83
|
+
let cellCtx = null;
|
|
84
|
+
// ── Heading context ───────────────────────────────────────────────────
|
|
85
|
+
// When rendering inline tokens inside a heading, font helpers use the
|
|
86
|
+
// heading's style (font, fontSize, color) instead of the body style.
|
|
87
|
+
let headingCtx = null;
|
|
79
88
|
function ensureSpace(needed) {
|
|
80
89
|
if (doc.y + needed > doc.page.height - margins.bottom) {
|
|
81
90
|
doc.addPage();
|
|
82
91
|
}
|
|
83
92
|
}
|
|
93
|
+
/** Derive the italic variant of a PDFKit built-in font name. */
|
|
94
|
+
function italicVariant(font) {
|
|
95
|
+
if (font.startsWith('Times'))
|
|
96
|
+
return font.replace(/-Bold$/, '-BoldItalic').replace(/^Times-Roman$/, 'Times-Italic');
|
|
97
|
+
// Helvetica / Courier families use "Oblique"
|
|
98
|
+
if (font.endsWith('-Bold'))
|
|
99
|
+
return font + 'Oblique';
|
|
100
|
+
return font + '-Oblique';
|
|
101
|
+
}
|
|
84
102
|
function applyBodyFont(bold, italic) {
|
|
103
|
+
if (headingCtx) {
|
|
104
|
+
// Inside a heading — use heading style as the base.
|
|
105
|
+
let font = headingCtx.font;
|
|
106
|
+
if (italic)
|
|
107
|
+
font = italicVariant(font);
|
|
108
|
+
doc.font(font).fontSize(headingCtx.fontSize).fillColor(headingCtx.color);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
85
111
|
let font = theme.body.font;
|
|
86
112
|
if (bold && italic)
|
|
87
113
|
font = 'Helvetica-BoldOblique';
|
|
@@ -92,6 +118,10 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
92
118
|
doc.font(font).fontSize(theme.body.fontSize).fillColor(theme.body.color);
|
|
93
119
|
}
|
|
94
120
|
function resetBodyFont() {
|
|
121
|
+
if (headingCtx) {
|
|
122
|
+
doc.font(headingCtx.font).fontSize(headingCtx.fontSize).fillColor(headingCtx.color);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
95
125
|
doc.font(theme.body.font).fontSize(theme.body.fontSize).fillColor(theme.body.color);
|
|
96
126
|
}
|
|
97
127
|
/**
|
|
@@ -121,6 +151,13 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
121
151
|
else {
|
|
122
152
|
opts = xOrOpts ?? {};
|
|
123
153
|
}
|
|
154
|
+
// If inside a table cell and this is the first text output, apply cell positioning.
|
|
155
|
+
if (cellCtx && !cellCtx.used && firstX === undefined) {
|
|
156
|
+
firstX = cellCtx.x;
|
|
157
|
+
firstY = cellCtx.y;
|
|
158
|
+
opts = { ...opts, width: cellCtx.width, align: cellCtx.align };
|
|
159
|
+
cellCtx.used = true;
|
|
160
|
+
}
|
|
124
161
|
const hasEmoji = (0, emoji_js_1.containsEmoji)(text);
|
|
125
162
|
// ── Fast path: no emoji handling needed ──────────────────────────────
|
|
126
163
|
if ((!emojiEnabled && !colorEmojiEnabled) || !hasEmoji) {
|
|
@@ -286,27 +323,52 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
286
323
|
doc.font(cs.font).fontSize(cs.fontSize);
|
|
287
324
|
const textW = doc.widthOfString(text);
|
|
288
325
|
const textH = doc.currentLineHeight();
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
326
|
+
// Determine flow position. If this is the first output in a table cell,
|
|
327
|
+
// use the cell context coordinates; otherwise read from the LineWrapper.
|
|
328
|
+
let flowX;
|
|
329
|
+
let flowY;
|
|
330
|
+
let useCellPos = false;
|
|
331
|
+
let cellExtra = {};
|
|
332
|
+
if (cellCtx && !cellCtx.used) {
|
|
333
|
+
flowX = cellCtx.x;
|
|
334
|
+
flowY = cellCtx.y;
|
|
335
|
+
useCellPos = true;
|
|
336
|
+
cellExtra = { width: cellCtx.width, align: cellCtx.align };
|
|
337
|
+
cellCtx.used = true;
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
// Read the real flow X position from PDFKit's internal LineWrapper.
|
|
341
|
+
// After continued:true, doc.x stays at the left margin — the actual
|
|
342
|
+
// cursor position is _wrapper.startX + _wrapper.continuedX.
|
|
343
|
+
const w = doc._wrapper;
|
|
344
|
+
flowX = w ? (w.startX + w.continuedX) : doc.x;
|
|
345
|
+
flowY = doc.y;
|
|
346
|
+
}
|
|
295
347
|
// Draw background at the current flow position (behind the text)
|
|
296
348
|
doc.save();
|
|
297
349
|
doc.roundedRect(flowX - hPad, flowY, textW + hPad * 2, textH + vPad * 2, 2)
|
|
298
350
|
.fill(cs.backgroundColor);
|
|
299
351
|
doc.restore();
|
|
300
|
-
// Render inline —
|
|
352
|
+
// Render inline — use positioned form for cell context, flow form otherwise
|
|
301
353
|
doc.font(cs.font).fontSize(cs.fontSize).fillColor(cs.color);
|
|
302
|
-
|
|
354
|
+
if (useCellPos) {
|
|
355
|
+
doc.text(text, flowX, flowY, { continued, ...cellExtra });
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
doc.text(text, { continued });
|
|
359
|
+
}
|
|
303
360
|
resetBodyFont();
|
|
304
361
|
}
|
|
305
362
|
function renderLink(tok, continued) {
|
|
306
|
-
|
|
363
|
+
if (headingCtx) {
|
|
364
|
+
doc.font(headingCtx.font).fontSize(headingCtx.fontSize).fillColor(theme.linkColor);
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
doc.font(theme.body.font).fontSize(theme.body.fontSize).fillColor(theme.linkColor);
|
|
368
|
+
}
|
|
307
369
|
const linkText = tok.text || tok.href;
|
|
308
370
|
renderTextWithEmoji(linkText, { continued, underline: true, link: tok.href });
|
|
309
|
-
doc.fillColor(theme.body.color);
|
|
371
|
+
doc.fillColor(headingCtx ? headingCtx.color : theme.body.color);
|
|
310
372
|
}
|
|
311
373
|
async function renderInlineTokens(inlineTokens, continued, insideBold = false, insideItalic = false) {
|
|
312
374
|
for (let i = 0; i < inlineTokens.length; i++) {
|
|
@@ -321,7 +383,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
321
383
|
}
|
|
322
384
|
else {
|
|
323
385
|
applyBodyFont(insideBold, insideItalic);
|
|
324
|
-
renderTextWithEmoji(t.text, { continued: cont });
|
|
386
|
+
renderTextWithEmoji(t.text, { continued: cont, underline: false, strike: false });
|
|
325
387
|
}
|
|
326
388
|
break;
|
|
327
389
|
}
|
|
@@ -349,12 +411,12 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
349
411
|
}
|
|
350
412
|
case 'del': {
|
|
351
413
|
applyBodyFont(insideBold, insideItalic);
|
|
352
|
-
renderTextWithEmoji(tok.text, { continued: cont, strike: true });
|
|
414
|
+
renderTextWithEmoji(tok.text, { continued: cont, strike: true, underline: false });
|
|
353
415
|
break;
|
|
354
416
|
}
|
|
355
417
|
case 'escape': {
|
|
356
418
|
applyBodyFont(insideBold, insideItalic);
|
|
357
|
-
renderTextWithEmoji(tok.text, { continued: cont });
|
|
419
|
+
renderTextWithEmoji(tok.text, { continued: cont, underline: false, strike: false });
|
|
358
420
|
break;
|
|
359
421
|
}
|
|
360
422
|
case 'br': {
|
|
@@ -365,7 +427,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
365
427
|
const raw = tok.text ?? tok.raw ?? '';
|
|
366
428
|
if (raw) {
|
|
367
429
|
applyBodyFont(insideBold, insideItalic);
|
|
368
|
-
renderTextWithEmoji(raw, { continued: cont });
|
|
430
|
+
renderTextWithEmoji(raw, { continued: cont, underline: false, strike: false });
|
|
369
431
|
}
|
|
370
432
|
break;
|
|
371
433
|
}
|
|
@@ -430,6 +492,21 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
430
492
|
doc.moveDown(0.2);
|
|
431
493
|
}
|
|
432
494
|
}
|
|
495
|
+
async function renderCellTokens(cell, x, y, width, align, bold) {
|
|
496
|
+
const savedY = doc.y;
|
|
497
|
+
if (cell.tokens && cell.tokens.length > 0) {
|
|
498
|
+
cellCtx = { x, y, width, align, used: false };
|
|
499
|
+
applyBodyFont(bold, false);
|
|
500
|
+
await renderInlineTokens(cell.tokens, false, bold, false);
|
|
501
|
+
cellCtx = null;
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
// Fallback: plain text (no inline tokens)
|
|
505
|
+
applyBodyFont(bold, false);
|
|
506
|
+
renderTextWithEmoji(cell.text, x, y, { width, align });
|
|
507
|
+
}
|
|
508
|
+
doc.y = savedY;
|
|
509
|
+
}
|
|
433
510
|
async function renderTable(table) {
|
|
434
511
|
const colCount = table.header.length;
|
|
435
512
|
if (colCount === 0)
|
|
@@ -445,14 +522,9 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
445
522
|
doc.save();
|
|
446
523
|
doc.rect(startX, y, contentWidth, rowH).fill(theme.table.headerBackground);
|
|
447
524
|
doc.restore();
|
|
448
|
-
doc.font('Helvetica-Bold').fontSize(theme.body.fontSize).fillColor(theme.body.color);
|
|
449
525
|
for (let c = 0; c < colCount; c++) {
|
|
450
526
|
const cellX = startX + c * colWidth;
|
|
451
|
-
|
|
452
|
-
width: colWidth - cellPad * 2,
|
|
453
|
-
height: rowH,
|
|
454
|
-
align: table.align[c] || 'left',
|
|
455
|
-
});
|
|
527
|
+
await renderCellTokens(table.header[c], cellX + cellPad, y + textInsetY, colWidth - cellPad * 2, table.align[c] || 'left', true);
|
|
456
528
|
}
|
|
457
529
|
// Header border
|
|
458
530
|
doc.save();
|
|
@@ -465,16 +537,11 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
465
537
|
doc.restore();
|
|
466
538
|
y += rowH;
|
|
467
539
|
// Body rows
|
|
468
|
-
resetBodyFont();
|
|
469
540
|
for (const row of table.rows) {
|
|
470
541
|
ensureSpace(rowH);
|
|
471
542
|
for (let c = 0; c < colCount; c++) {
|
|
472
543
|
const cellX = startX + c * colWidth;
|
|
473
|
-
|
|
474
|
-
width: colWidth - cellPad * 2,
|
|
475
|
-
height: rowH,
|
|
476
|
-
align: table.align[c] || 'left',
|
|
477
|
-
});
|
|
544
|
+
await renderCellTokens(row[c], cellX + cellPad, y + textInsetY, colWidth - cellPad * 2, table.align[c] || 'left', false);
|
|
478
545
|
}
|
|
479
546
|
doc.save();
|
|
480
547
|
doc.strokeColor(theme.table.borderColor).lineWidth(0.5);
|
|
@@ -502,7 +569,14 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
502
569
|
ensureSpace(spaceAbove + style.fontSize + spaceBelow);
|
|
503
570
|
doc.moveDown(spaceAbove / doc.currentLineHeight());
|
|
504
571
|
doc.font(style.font).fontSize(style.fontSize).fillColor(style.color);
|
|
505
|
-
|
|
572
|
+
headingCtx = style;
|
|
573
|
+
if (t.tokens && t.tokens.length > 0) {
|
|
574
|
+
await renderInlineTokens(t.tokens, false, style.bold ?? false, style.italic ?? false);
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
renderTextWithEmoji(t.text);
|
|
578
|
+
}
|
|
579
|
+
headingCtx = null;
|
|
506
580
|
doc.moveDown(spaceBelow / doc.currentLineHeight());
|
|
507
581
|
resetBodyFont();
|
|
508
582
|
break;
|