@speajus/markdown-to-pdf 1.0.19 → 1.0.21

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.pdf CHANGED
Binary file
package/dist/renderer.js CHANGED
@@ -411,6 +411,21 @@ async function renderMarkdownToPdf(markdown, options) {
411
411
  doc.moveDown(0.5);
412
412
  break;
413
413
  }
414
+ case 'html': {
415
+ const htmlText = (tok.text ?? tok.raw ?? '').trim();
416
+ if (/^<br\s*\/?>$/i.test(htmlText)) {
417
+ doc.moveDown(0.5);
418
+ }
419
+ else {
420
+ // Non-br HTML: render as text (fall through to default behavior)
421
+ const raw = tok.text ?? tok.raw ?? '';
422
+ if (raw) {
423
+ applyBodyFont(insideBold, insideItalic);
424
+ renderText(raw, { continued: cont, underline: false, strike: false });
425
+ }
426
+ }
427
+ break;
428
+ }
414
429
  default: {
415
430
  const raw = tok.text ?? tok.raw ?? '';
416
431
  if (raw) {
@@ -449,6 +464,7 @@ async function renderMarkdownToPdf(markdown, options) {
449
464
  if (linkUrl) {
450
465
  doc.link(imgX, imgY, displayWidth, displayHeight, linkUrl);
451
466
  }
467
+ doc.y = imgY + displayHeight;
452
468
  doc.moveDown(0.5);
453
469
  }
454
470
  catch {
@@ -504,39 +520,101 @@ async function renderMarkdownToPdf(markdown, options) {
504
520
  }
505
521
  doc.y = savedY;
506
522
  }
523
+ /**
524
+ * Extract plain text from cell tokens, converting <br> HTML tokens to \n
525
+ * so that heightOfString can measure the full multiline content.
526
+ */
527
+ function cellPlainText(cell) {
528
+ if (!cell.tokens || cell.tokens.length === 0)
529
+ return cell.text;
530
+ function extract(tokens) {
531
+ let result = '';
532
+ for (const tok of tokens) {
533
+ if (tok.type === 'br') {
534
+ result += '\n';
535
+ }
536
+ else if (tok.type === 'html') {
537
+ const raw = (tok.text ?? tok.raw ?? '').trim();
538
+ if (/^<br\s*\/?>$/i.test(raw)) {
539
+ result += '\n';
540
+ }
541
+ else {
542
+ result += tok.text ?? tok.raw ?? '';
543
+ }
544
+ }
545
+ else if (tok.tokens && tok.tokens.length > 0) {
546
+ result += extract(tok.tokens);
547
+ }
548
+ else {
549
+ result += tok.text ?? tok.raw ?? '';
550
+ }
551
+ }
552
+ return result;
553
+ }
554
+ return extract(cell.tokens);
555
+ }
556
+ /**
557
+ * Measure the height a cell needs given its content and available width.
558
+ */
559
+ function measureCellHeight(cell, cellWidth, bold) {
560
+ const text = cellPlainText(cell);
561
+ const font = bold
562
+ ? safeFont(resolveFont(theme.body.font, true, false))
563
+ : safeFont(theme.body.font);
564
+ doc.font(font).fontSize(theme.body.fontSize);
565
+ return doc.heightOfString(text, { width: cellWidth });
566
+ }
507
567
  async function renderTable(table) {
508
568
  const colCount = table.header.length;
509
569
  if (colCount === 0)
510
570
  return;
511
571
  const cellPad = theme.table.cellPadding;
512
572
  const colWidth = contentWidth / colCount;
513
- const rowH = theme.body.fontSize + cellPad * 2 + 4;
514
- const textInsetY = (rowH - theme.body.fontSize) / 2;
515
- ensureSpace(rowH * 2);
573
+ const minRowH = theme.body.fontSize + cellPad * 2 + 4;
574
+ const textWidth = colWidth - cellPad * 2;
575
+ ensureSpace(minRowH * 2);
516
576
  const startX = margins.left;
517
577
  let y = doc.y;
518
- // Header row
578
+ // ── Measure header row height ──
579
+ let headerH = minRowH;
580
+ let maxHeaderTextHeight = 0;
581
+ for (let c = 0; c < colCount; c++) {
582
+ const h = measureCellHeight(table.header[c], textWidth, true);
583
+ maxHeaderTextHeight = Math.max(maxHeaderTextHeight, h);
584
+ headerH = Math.max(headerH, h + cellPad * 2 + 4);
585
+ }
586
+ const headerTextInsetY = (headerH - maxHeaderTextHeight) / 2;
587
+ // Header row background
519
588
  doc.save();
520
- doc.rect(startX, y, contentWidth, rowH).fill(theme.table.headerBackground);
589
+ doc.rect(startX, y, contentWidth, headerH).fill(theme.table.headerBackground);
521
590
  doc.restore();
522
591
  for (let c = 0; c < colCount; c++) {
523
592
  const cellX = startX + c * colWidth;
524
- await renderCellTokens(table.header[c], cellX + cellPad, y + textInsetY, colWidth - cellPad * 2, table.align[c] || 'left', true);
593
+ await renderCellTokens(table.header[c], cellX + cellPad, y + headerTextInsetY, textWidth, table.align[c] || 'left', true);
525
594
  }
526
595
  // Header border
527
596
  doc.save();
528
597
  doc.strokeColor(theme.table.borderColor).lineWidth(0.5);
529
- doc.rect(startX, y, contentWidth, rowH).stroke();
598
+ doc.rect(startX, y, contentWidth, headerH).stroke();
530
599
  for (let c = 1; c < colCount; c++) {
531
600
  const cx = startX + c * colWidth;
532
- doc.moveTo(cx, y).lineTo(cx, y + rowH).stroke();
601
+ doc.moveTo(cx, y).lineTo(cx, y + headerH).stroke();
533
602
  }
534
603
  doc.restore();
535
- y += rowH;
604
+ y += headerH;
536
605
  // Body rows
537
606
  const zebraColor = theme.table.zebraColor ?? '#f9f9f9';
538
607
  for (let r = 0; r < table.rows.length; r++) {
539
608
  const row = table.rows[r];
609
+ // ── Measure row height ──
610
+ let rowH = minRowH;
611
+ let maxRowTextHeight = 0;
612
+ for (let c = 0; c < colCount; c++) {
613
+ const h = measureCellHeight(row[c], textWidth, false);
614
+ maxRowTextHeight = Math.max(maxRowTextHeight, h);
615
+ rowH = Math.max(rowH, h + cellPad * 2 + 4);
616
+ }
617
+ const textInsetY = (rowH - maxRowTextHeight) / 2;
540
618
  doc.y = y; // sync doc.y BEFORE ensureSpace check
541
619
  ensureSpace(rowH);
542
620
  y = doc.y; // re-sync AFTER possible page break
@@ -548,7 +626,7 @@ async function renderMarkdownToPdf(markdown, options) {
548
626
  }
549
627
  for (let c = 0; c < colCount; c++) {
550
628
  const cellX = startX + c * colWidth;
551
- await renderCellTokens(row[c], cellX + cellPad, y + textInsetY, colWidth - cellPad * 2, table.align[c] || 'left', false);
629
+ await renderCellTokens(row[c], cellX + cellPad, y + textInsetY, textWidth, table.align[c] || 'left', false);
552
630
  }
553
631
  doc.save();
554
632
  doc.strokeColor(theme.table.borderColor).lineWidth(0.5);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@speajus/markdown-to-pdf",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "description": "A new project created with Intent by Augment.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -26,6 +26,7 @@
26
26
  },
27
27
  "files": [
28
28
  "dist",
29
+ "scripts",
29
30
  "README.md"
30
31
  ],
31
32
  "scripts": {
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Patches fontkit's COLRGlyph class to guard against null baseGlyphRecord.
3
+ *
4
+ * Some emoji fonts have a COLR table but null/missing baseGlyphRecord,
5
+ * causing `TypeError: Cannot read properties of null (reading 'length')`
6
+ * when fontkit's COLRGlyph.layers getter is called.
7
+ *
8
+ * This script patches both the src and dist files of fontkit in node_modules.
9
+ */
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ const nmDir = path.join(__dirname, '..', 'node_modules');
14
+
15
+ function patchFile(filePath, patches) {
16
+ if (!fs.existsSync(filePath)) return false;
17
+ let content = fs.readFileSync(filePath, 'utf8');
18
+ let changed = false;
19
+ for (const [search, replace] of patches) {
20
+ if (content.includes(search) && !content.includes(replace)) {
21
+ // Replace ALL occurrences (some bundled files may have multiple copies)
22
+ content = content.split(search).join(replace);
23
+ changed = true;
24
+ }
25
+ }
26
+ if (changed) {
27
+ fs.writeFileSync(filePath, content, 'utf8');
28
+ console.log(` Patched: ${filePath}`);
29
+ }
30
+ return changed;
31
+ }
32
+
33
+ // Patch 1: Guard against null baseGlyphRecord in COLRGlyph.layers getter
34
+ const layersGetterPatch = [
35
+ 'let high = colr.baseGlyphRecord.length - 1;',
36
+ 'if (!colr || !colr.baseGlyphRecord) { return null; } let high = colr.baseGlyphRecord.length - 1;',
37
+ ];
38
+
39
+ // Patch 2: Don't create COLRGlyph for COLR v1 fonts (no v0 baseGlyphRecord)
40
+ // COLR v1 uses a different paint-based structure that fontkit doesn't support.
41
+ // Without this, COLR v1 glyphs (e.g. OpenMoji) get typed as COLR but can't
42
+ // render, and they also won't fall back to SBIX/CBDT/SVG alternatives.
43
+ const getGlyphPatch = [
44
+ 'this.directory.tables.COLR && this.directory.tables.CPAL)',
45
+ 'this.directory.tables.COLR && this.directory.tables.CPAL && this.COLR && this.COLR.baseGlyphRecord)',
46
+ ];
47
+
48
+ // Patch 3: Fix _getContours crash for composite glyphs in COLR fonts
49
+ // When a composite TTF glyph references a component that is a COLRGlyph,
50
+ // calling _getContours() on it crashes because COLRGlyph doesn't have that method.
51
+ // Use _getBaseGlyph to get the TTF outline instead.
52
+ const getContoursPatch = [
53
+ 'this._font.getGlyph(component.glyphID)._getContours()',
54
+ '((g) => { if (!g || typeof g._getContours !== "function") return []; return g._getContours(); })(this._font._getBaseGlyph(component.glyphID) || this._font.getGlyph(component.glyphID))',
55
+ ];
56
+
57
+ // Patch 4: Fix _decode crash in TTF subsetter for COLR glyphs
58
+ // COLRGlyph doesn't have _decode() method. The subsetter needs to handle
59
+ // COLR glyphs gracefully by treating them as simple (non-compound) glyphs.
60
+ const decodePatch = [
61
+ 'let glyf = glyph._decode();',
62
+ 'let glyf = typeof glyph._decode === "function" ? glyph._decode() : null;',
63
+ ];
64
+
65
+ const allPatches = [layersGetterPatch, getGlyphPatch, getContoursPatch, decodePatch];
66
+
67
+ let patchCount = 0;
68
+
69
+ // 1. Patch fontkit dist/src files directly
70
+ const fontkitPaths = [
71
+ path.join(nmDir, 'fontkit'),
72
+ path.join(nmDir, 'pdfkit', 'node_modules', 'fontkit'),
73
+ ];
74
+
75
+ for (const fontkitDir of fontkitPaths) {
76
+ if (!fs.existsSync(fontkitDir)) continue;
77
+ for (const distFile of ['dist/browser-module.mjs', 'dist/main.cjs']) {
78
+ if (patchFile(path.join(fontkitDir, distFile), allPatches)) patchCount++;
79
+ }
80
+ if (patchFile(path.join(fontkitDir, 'src', 'glyph', 'COLRGlyph.js'), [layersGetterPatch])) patchCount++;
81
+ }
82
+
83
+ // 2. Patch pdfkit standalone bundles (these embed fontkit inline)
84
+ const pdfkitBundles = [
85
+ path.join(nmDir, 'pdfkit', 'js', 'pdfkit.standalone.js'),
86
+ path.join(nmDir, 'pdfkit', 'js', 'pdfkit.js'),
87
+ ];
88
+
89
+ for (const bundle of pdfkitBundles) {
90
+ if (patchFile(bundle, allPatches)) patchCount++;
91
+ }
92
+
93
+ // 3. Clear Vite dep cache so it re-bundles with patched files
94
+ const viteCacheDir = path.join(nmDir, '.vite');
95
+ if (fs.existsSync(viteCacheDir)) {
96
+ fs.rmSync(viteCacheDir, { recursive: true, force: true });
97
+ console.log(' Cleared Vite dep cache (.vite)');
98
+ }
99
+
100
+ if (patchCount > 0) {
101
+ console.log(`fontkit COLR patch: ${patchCount} file(s) patched.`);
102
+ } else {
103
+ console.log('fontkit COLR patch: already applied or fontkit not found.');
104
+ }
105
+