@speajus/markdown-to-pdf 1.0.5 → 1.0.7

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 +101 -27
  2. package/package.json +10 -3
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
- // Read the real flow X position from PDFKit's internal LineWrapper.
290
- // After continued:true, doc.x stays at the left margin the actual
291
- // cursor position is _wrapper.startX + _wrapper.continuedX.
292
- const w = doc._wrapper;
293
- const flowX = w ? (w.startX + w.continuedX) : doc.x;
294
- const flowY = doc.y;
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 — no explicit x,y so PDFKit's flow advances correctly
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
- doc.text(text, { continued });
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
- doc.font(theme.body.font).fontSize(theme.body.fontSize).fillColor(theme.linkColor);
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
- renderTextWithEmoji(table.header[c].text, cellX + cellPad, y + textInsetY, {
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
- renderTextWithEmoji(row[c].text, cellX + cellPad, y + textInsetY, {
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
- renderTextWithEmoji(t.text);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@speajus/markdown-to-pdf",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "A new project created with Intent by Augment.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -9,12 +9,19 @@
9
9
  },
10
10
  "exports": {
11
11
  "node": {
12
+ "types": "./dist/index.d.ts",
12
13
  "require": "./dist/index.js",
13
14
  "import": "./dist/index.js"
14
15
  },
15
16
  "browser": {
17
+ "types": "./dist/browser.d.ts",
16
18
  "require": "./dist/browser.js",
17
19
  "import": "./dist/browser.js"
20
+ },
21
+ "default": {
22
+ "types": "./dist/index.d.ts",
23
+ "require": "./dist/index.js",
24
+ "import": "./dist/index.js"
18
25
  }
19
26
  },
20
27
  "files": [
@@ -23,7 +30,7 @@
23
30
  "scripts": {
24
31
  "build": "tsc && mkdir -p dist/fonts && cp src/fonts/* dist/fonts/",
25
32
  "generate": "tsx samples/generate.ts",
26
- "test": "tsx samples/generate.ts"
33
+ "test": "tsx --test test/*.test.ts"
27
34
  },
28
35
  "keywords": [
29
36
  "markdown",
@@ -34,7 +41,7 @@
34
41
  "license": "ISC",
35
42
  "repository": {
36
43
  "type": "git",
37
- "url": "https://github.com/speajus/markdown-to-pdf"
44
+ "url": "git+https://github.com/speajus/markdown-to-pdf.git"
38
45
  },
39
46
  "type": "commonjs",
40
47
  "dependencies": {