@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 +0 -0
- package/dist/renderer.js +88 -10
- package/package.json +2 -1
- package/scripts/patch-fontkit-colr.js +105 -0
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
|
|
514
|
-
const
|
|
515
|
-
ensureSpace(
|
|
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
|
-
//
|
|
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,
|
|
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 +
|
|
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,
|
|
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 +
|
|
601
|
+
doc.moveTo(cx, y).lineTo(cx, y + headerH).stroke();
|
|
533
602
|
}
|
|
534
603
|
doc.restore();
|
|
535
|
-
y +=
|
|
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,
|
|
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.
|
|
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
|
+
|