@speajus/markdown-to-pdf 1.0.18 → 1.0.20
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.md +55 -5
- package/README.pdf +0 -0
- package/dist/browser-image-renderer.js +1 -0
- package/dist/browser.d.ts +1 -1
- package/dist/defaults.d.ts +5 -0
- package/dist/index.d.ts +1 -1
- package/dist/mermaid-renderer.d.ts +13 -0
- package/dist/mermaid-renderer.js +50 -0
- package/dist/node-image-renderer.js +5 -4
- package/dist/renderer.js +129 -10
- package/dist/styles.js +1 -0
- package/dist/themes/index.js +31 -0
- package/dist/types.d.ts +36 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ A lightweight TypeScript library that converts Markdown files into styled PDF do
|
|
|
14
14
|
- **Tables** with header row highlighting and cell borders
|
|
15
15
|
- **Links** rendered as clickable PDF hyperlinks
|
|
16
16
|
- **Images** — local (PNG, JPEG) and remote (HTTP/HTTPS) with automatic SVG-to-PNG conversion via [@resvg/resvg-js](https://github.com/nicolo-ribaudo/resvg-js)
|
|
17
|
+
- **Mermaid diagrams** — render `mermaid` fenced code blocks as diagrams (flowchart, sequence, class, state, ER, gantt, pie, mindmap) via [@speajus/mermaid-to-svg](https://github.com/speajus/mermaid-to-svg)
|
|
17
18
|
- **Horizontal rules**
|
|
18
19
|
- **Automatic page breaks** when content exceeds the current page
|
|
19
20
|
- **Fully themeable** — customize fonts, colors, spacing, page size, and margins
|
|
@@ -92,6 +93,50 @@ interface PdfOptions {
|
|
|
92
93
|
}
|
|
93
94
|
```
|
|
94
95
|
|
|
96
|
+
### Mermaid Diagrams
|
|
97
|
+
|
|
98
|
+
Fenced code blocks with the language `mermaid` are automatically rendered as diagrams:
|
|
99
|
+
|
|
100
|
+
````markdown
|
|
101
|
+
```mermaid
|
|
102
|
+
flowchart TD
|
|
103
|
+
A[Start] --> B{Decision}
|
|
104
|
+
B -->|Yes| C[Process A]
|
|
105
|
+
B -->|No| D[Process B]
|
|
106
|
+
C --> E[End]
|
|
107
|
+
D --> E
|
|
108
|
+
```
|
|
109
|
+
````
|
|
110
|
+
|
|
111
|
+
Mermaid theme colors are integrated with the PDF theme. Each built-in theme includes matching mermaid colors. You can also customize them:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
await generatePdf(markdown, {
|
|
115
|
+
theme: {
|
|
116
|
+
...defaultTheme,
|
|
117
|
+
// Use a built-in mermaid theme
|
|
118
|
+
mermaid: 'forest', // 'default' | 'dark' | 'forest' | 'neutral'
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Or provide custom mermaid colors
|
|
123
|
+
await generatePdf(markdown, {
|
|
124
|
+
theme: {
|
|
125
|
+
...defaultTheme,
|
|
126
|
+
mermaid: {
|
|
127
|
+
background: '#ffffff',
|
|
128
|
+
primaryColor: '#4a90d9',
|
|
129
|
+
secondaryColor: '#b8d4f0',
|
|
130
|
+
lineColor: '#333333',
|
|
131
|
+
primaryTextColor: '#1a1a1a',
|
|
132
|
+
borderColor: '#4a90d9',
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Supported diagram types: flowchart, sequence, class, state, ER, gantt, pie, and mindmap.
|
|
139
|
+
|
|
95
140
|
### Page Layout
|
|
96
141
|
|
|
97
142
|
```typescript
|
|
@@ -138,19 +183,23 @@ The full `ThemeConfig` interface exposes styles for:
|
|
|
138
183
|
| table | Header background, border color, cell padding, and zebra color |
|
|
139
184
|
| linkColor | Color for hyperlink text |
|
|
140
185
|
| horizontalRuleColor | Color for --- dividers |
|
|
186
|
+
| mermaid | Diagram colors: primary, secondary, text, line, border, background |
|
|
141
187
|
|
|
142
188
|
## Project Structure
|
|
143
189
|
|
|
144
190
|
```
|
|
145
191
|
├── src/
|
|
146
|
-
│ ├── index.ts
|
|
147
|
-
│ ├── cli.ts
|
|
148
|
-
│ ├── renderer.ts
|
|
149
|
-
│ ├──
|
|
150
|
-
│
|
|
192
|
+
│ ├── index.ts # Public API — generatePdf, renderMarkdownToPdf, exports
|
|
193
|
+
│ ├── cli.ts # Command-line entry point
|
|
194
|
+
│ ├── renderer.ts # Markdown-to-PDF rendering engine
|
|
195
|
+
│ ├── mermaid-renderer.ts # Mermaid diagram → PNG rendering
|
|
196
|
+
│ ├── styles.ts # Default theme and page layout
|
|
197
|
+
│ ├── themes/index.ts # Built-in theme variants
|
|
198
|
+
│ └── types.ts # TypeScript interfaces for options and theming
|
|
151
199
|
├── samples/
|
|
152
200
|
│ ├── generate.ts # Script to batch-generate sample PDFs
|
|
153
201
|
│ ├── sample.md # Full-featured sample document
|
|
202
|
+
│ ├── mermaid.md # Mermaid diagram examples (all diagram types)
|
|
154
203
|
│ ├── image.md # Image rendering tests (local, remote, SVG, broken)
|
|
155
204
|
│ ├── logo.svg # Sample SVG image
|
|
156
205
|
│ ├── logo.png # Sample PNG image
|
|
@@ -167,6 +216,7 @@ The full `ThemeConfig` interface exposes styles for:
|
|
|
167
216
|
| marked | Markdown parsing and tokenization |
|
|
168
217
|
| pdfkit | PDF document generation |
|
|
169
218
|
| @resvg/resvg-js | SVG-to-PNG rasterization for image embedding |
|
|
219
|
+
| @speajus/mermaid-to-svg | Mermaid diagram rendering (parse → layout → SVG) |
|
|
170
220
|
|
|
171
221
|
## Scripts
|
|
172
222
|
|
package/README.pdf
CHANGED
|
Binary file
|
|
@@ -96,6 +96,7 @@ function renderSvgToPng(svgBuffer) {
|
|
|
96
96
|
img.src = url;
|
|
97
97
|
});
|
|
98
98
|
}
|
|
99
|
+
defaults_js_1.DEFAULTS.renderSvg = (svg) => renderSvgToPng(Buffer.from(svg));
|
|
99
100
|
function isSvg(buf) {
|
|
100
101
|
const head = buf.subarray(0, 256).toString('utf-8').trimStart();
|
|
101
102
|
return head.startsWith('<svg') || head.startsWith('<?xml');
|
package/dist/browser.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Browser-specific entry point that excludes Node.js dependencies
|
|
3
3
|
*/
|
|
4
4
|
export { createBrowserImageRenderer } from "./browser-image-renderer.js";
|
|
5
|
-
export type { TextStyle, PageLayout, CodeStyle, CodeBlockStyle, BlockquoteStyle, TableStyles, TokenColors, SyntaxHighlightTheme, SpacingConfig, ThemeConfig, PdfOptions, CustomFontDefinition, } from './types.js';
|
|
5
|
+
export type { TextStyle, PageLayout, CodeStyle, CodeBlockStyle, BlockquoteStyle, TableStyles, TokenColors, SyntaxHighlightTheme, SpacingConfig, ThemeConfig, PdfOptions, CustomFontDefinition, MermaidThemeConfig, } from './types.js';
|
|
6
6
|
export { defaultTheme, defaultPageLayout, defaultSyntaxHighlightTheme, defaultSpacing } from './styles.js';
|
|
7
7
|
export { themes, modernTheme, academicTheme, minimalTheme, oceanTheme } from './themes/index.js';
|
|
8
8
|
export { renderMarkdownToPdf } from "./renderer.js";
|
package/dist/defaults.d.ts
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { TextStyle, PageLayout, CodeStyle, CodeBlockStyle, BlockquoteStyle, TableStyles, TokenColors, SyntaxHighlightTheme, SpacingConfig, ThemeConfig, PdfOptions, CustomFontDefinition, } from './types.js';
|
|
1
|
+
export type { TextStyle, PageLayout, CodeStyle, CodeBlockStyle, BlockquoteStyle, TableStyles, TokenColors, SyntaxHighlightTheme, SpacingConfig, ThemeConfig, PdfOptions, CustomFontDefinition, MermaidThemeConfig, } from './types.js';
|
|
2
2
|
export { defaultTheme, defaultPageLayout, defaultSyntaxHighlightTheme, defaultSpacing } from './styles.js';
|
|
3
3
|
export { renderMarkdownToPdf as generatePdf, renderMarkdownToPdf, } from "./renderer.js";
|
|
4
4
|
export { createBrowserImageRenderer } from './browser-image-renderer.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { MermaidThemeConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Render a mermaid diagram string to a PNG buffer.
|
|
4
|
+
*
|
|
5
|
+
* Uses `@speajus/mermaid-to-svg` to produce SVG, then `@resvg/resvg-js` to
|
|
6
|
+
* rasterise to PNG so PDFKit can embed it.
|
|
7
|
+
*/
|
|
8
|
+
export declare function renderMermaidToPng(mermaidCode: string, themeConfig?: MermaidThemeConfig | 'default' | 'dark' | 'forest' | 'neutral'): Promise<Buffer>;
|
|
9
|
+
/**
|
|
10
|
+
* Call this once when you are done rendering all mermaid diagrams to
|
|
11
|
+
* release the jsdom window that `@speajus/mermaid-to-svg` creates internally.
|
|
12
|
+
*/
|
|
13
|
+
export declare function cleanupMermaid(): Promise<void>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderMermaidToPng = renderMermaidToPng;
|
|
4
|
+
exports.cleanupMermaid = cleanupMermaid;
|
|
5
|
+
const defaults_js_1 = require("./defaults.js");
|
|
6
|
+
let mermaidModule = null;
|
|
7
|
+
async function loadMermaid() {
|
|
8
|
+
if (!mermaidModule) {
|
|
9
|
+
mermaidModule = await import('@speajus/mermaid-to-svg');
|
|
10
|
+
}
|
|
11
|
+
return mermaidModule;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Resolve a mermaid theme config (string name or partial object) into
|
|
15
|
+
* a `Theme` object from `@speajus/mermaid-to-svg`.
|
|
16
|
+
*/
|
|
17
|
+
function resolveTheme(mermaidMod, config) {
|
|
18
|
+
if (!config || config === 'default')
|
|
19
|
+
return mermaidMod.defaultTheme;
|
|
20
|
+
if (config === 'dark')
|
|
21
|
+
return mermaidMod.darkTheme;
|
|
22
|
+
if (config === 'forest')
|
|
23
|
+
return mermaidMod.forestTheme;
|
|
24
|
+
if (config === 'neutral')
|
|
25
|
+
return mermaidMod.neutralTheme;
|
|
26
|
+
// Custom partial config — merge onto default
|
|
27
|
+
return mermaidMod.createTheme(config);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Render a mermaid diagram string to a PNG buffer.
|
|
31
|
+
*
|
|
32
|
+
* Uses `@speajus/mermaid-to-svg` to produce SVG, then `@resvg/resvg-js` to
|
|
33
|
+
* rasterise to PNG so PDFKit can embed it.
|
|
34
|
+
*/
|
|
35
|
+
async function renderMermaidToPng(mermaidCode, themeConfig) {
|
|
36
|
+
const mod = await loadMermaid();
|
|
37
|
+
const theme = resolveTheme(mod, themeConfig);
|
|
38
|
+
const result = await mod.renderMermaid(mermaidCode, { theme });
|
|
39
|
+
// Convert SVG → PNG via resvg
|
|
40
|
+
return defaults_js_1.DEFAULTS.renderSvg(result.svg);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Call this once when you are done rendering all mermaid diagrams to
|
|
44
|
+
* release the jsdom window that `@speajus/mermaid-to-svg` creates internally.
|
|
45
|
+
*/
|
|
46
|
+
async function cleanupMermaid() {
|
|
47
|
+
if (mermaidModule) {
|
|
48
|
+
mermaidModule.cleanup();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -15,8 +15,8 @@ function isSvg(buf) {
|
|
|
15
15
|
const head = buf.subarray(0, 256).toString('utf-8').trimStart();
|
|
16
16
|
return head.startsWith('<svg') || head.startsWith('<?xml');
|
|
17
17
|
}
|
|
18
|
-
function convertSvgToPng(svgData) {
|
|
19
|
-
const resvg = new resvg_js_1.Resvg(svgData, { font: { loadSystemFonts: true } });
|
|
18
|
+
function convertSvgToPng(svgData, options) {
|
|
19
|
+
const resvg = new resvg_js_1.Resvg(svgData, { font: { loadSystemFonts: true }, ...options });
|
|
20
20
|
const rendered = resvg.render();
|
|
21
21
|
return Buffer.from(rendered.asPng());
|
|
22
22
|
}
|
|
@@ -60,7 +60,7 @@ function fetchImageBuffer(url, redirectCount = 0) {
|
|
|
60
60
|
* @returns A function that takes an image URL/path and returns a Buffer
|
|
61
61
|
*/
|
|
62
62
|
function createNodeImageRenderer(basePath = process.cwd()) {
|
|
63
|
-
return async (imageUrl) => {
|
|
63
|
+
return async (imageUrl, options) => {
|
|
64
64
|
let imgBuffer;
|
|
65
65
|
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
|
|
66
66
|
// Remote image - fetch via HTTP/HTTPS
|
|
@@ -73,9 +73,10 @@ function createNodeImageRenderer(basePath = process.cwd()) {
|
|
|
73
73
|
}
|
|
74
74
|
// Convert SVG to PNG since pdfkit doesn't support SVG natively
|
|
75
75
|
if (isSvg(imgBuffer)) {
|
|
76
|
-
imgBuffer = convertSvgToPng(imgBuffer);
|
|
76
|
+
imgBuffer = convertSvgToPng(imgBuffer, options);
|
|
77
77
|
}
|
|
78
78
|
return imgBuffer;
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
81
|
defaults_1.DEFAULTS.renderImage = createNodeImageRenderer;
|
|
82
|
+
defaults_1.DEFAULTS.renderSvg = async (svg) => convertSvgToPng(Buffer.from(svg));
|
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);
|
|
@@ -610,6 +688,39 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
610
688
|
case 'code': {
|
|
611
689
|
const t = token;
|
|
612
690
|
const cs = theme.code.block;
|
|
691
|
+
// ── Mermaid diagrams ──────────────────────────────────────────────
|
|
692
|
+
if (t.lang === 'mermaid') {
|
|
693
|
+
try {
|
|
694
|
+
const mermaidTheme = theme.mermaid;
|
|
695
|
+
const { renderMermaidToPng } = await import('./mermaid-renderer.js');
|
|
696
|
+
const pngBuf = await renderMermaidToPng(t.text, mermaidTheme);
|
|
697
|
+
const img = doc.openImage(pngBuf);
|
|
698
|
+
const maxHeight = doc.page.height - margins.top - margins.bottom;
|
|
699
|
+
let displayWidth = Math.min(img.width, contentWidth);
|
|
700
|
+
let displayHeight = img.height * (displayWidth / img.width);
|
|
701
|
+
if (displayHeight > maxHeight) {
|
|
702
|
+
displayHeight = maxHeight;
|
|
703
|
+
displayWidth = img.width * (displayHeight / img.height);
|
|
704
|
+
}
|
|
705
|
+
ensureSpace(displayHeight + 10);
|
|
706
|
+
const imgX = theme.imageAlign === 'center'
|
|
707
|
+
? margins.left + (contentWidth - displayWidth) / 2
|
|
708
|
+
: doc.x;
|
|
709
|
+
const imgY = doc.y;
|
|
710
|
+
doc.image(pngBuf, imgX, imgY, { width: displayWidth, height: displayHeight });
|
|
711
|
+
// Advance past the image — doc.image() does not move doc.y
|
|
712
|
+
doc.y = imgY + displayHeight;
|
|
713
|
+
doc.moveDown(0.5);
|
|
714
|
+
}
|
|
715
|
+
catch (err) {
|
|
716
|
+
// Fallback: render as plain code block on error
|
|
717
|
+
ensureSpace(20);
|
|
718
|
+
resetBodyFont();
|
|
719
|
+
doc.text(`[Mermaid diagram error: ${err.message}]`);
|
|
720
|
+
doc.moveDown(0.3);
|
|
721
|
+
}
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
613
724
|
// Use syntax highlighting when a language is specified and highlighting is enabled
|
|
614
725
|
if (syntaxHighlight && t.lang) {
|
|
615
726
|
const lines = t.text.split('\n');
|
|
@@ -743,6 +854,14 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
743
854
|
for (const token of tokens) {
|
|
744
855
|
await renderToken(token);
|
|
745
856
|
}
|
|
857
|
+
// Clean up mermaid jsdom window if it was used
|
|
858
|
+
try {
|
|
859
|
+
const { cleanupMermaid } = await import('./mermaid-renderer.js');
|
|
860
|
+
await cleanupMermaid();
|
|
861
|
+
}
|
|
862
|
+
catch {
|
|
863
|
+
// mermaid-renderer may not be available in browser builds
|
|
864
|
+
}
|
|
746
865
|
doc.end();
|
|
747
866
|
return new Promise((resolve) => {
|
|
748
867
|
stream.on('end', () => resolve(Buffer.concat(chunks)));
|
package/dist/styles.js
CHANGED
package/dist/themes/index.js
CHANGED
|
@@ -42,6 +42,16 @@ exports.modernTheme = {
|
|
|
42
42
|
spacing: { ...styles_js_1.defaultSpacing },
|
|
43
43
|
imageAlign: 'left',
|
|
44
44
|
emojiFont: 'twemoji',
|
|
45
|
+
mermaid: {
|
|
46
|
+
background: '#ffffff',
|
|
47
|
+
primaryColor: '#0d7377',
|
|
48
|
+
secondaryColor: '#b2dfdb',
|
|
49
|
+
tertiaryColor: '#e0f2f1',
|
|
50
|
+
primaryTextColor: '#2d3436',
|
|
51
|
+
secondaryTextColor: '#636e72',
|
|
52
|
+
lineColor: '#0984e3',
|
|
53
|
+
borderColor: '#14919b',
|
|
54
|
+
},
|
|
45
55
|
};
|
|
46
56
|
/** Academic — serif fonts, formal look inspired by LaTeX */
|
|
47
57
|
exports.academicTheme = {
|
|
@@ -81,6 +91,16 @@ exports.academicTheme = {
|
|
|
81
91
|
spacing: { ...styles_js_1.defaultSpacing },
|
|
82
92
|
imageAlign: 'left',
|
|
83
93
|
emojiFont: 'twemoji',
|
|
94
|
+
mermaid: {
|
|
95
|
+
background: '#ffffff',
|
|
96
|
+
primaryColor: '#eaecee',
|
|
97
|
+
secondaryColor: '#d5dbdb',
|
|
98
|
+
tertiaryColor: '#f2f3f4',
|
|
99
|
+
primaryTextColor: '#1a1a2e',
|
|
100
|
+
secondaryTextColor: '#7f8c8d',
|
|
101
|
+
lineColor: '#2e4057',
|
|
102
|
+
borderColor: '#6c3483',
|
|
103
|
+
},
|
|
84
104
|
};
|
|
85
105
|
/** Minimal — lots of whitespace, muted greys */
|
|
86
106
|
exports.minimalTheme = {
|
|
@@ -120,6 +140,7 @@ exports.minimalTheme = {
|
|
|
120
140
|
spacing: { ...styles_js_1.defaultSpacing },
|
|
121
141
|
imageAlign: 'left',
|
|
122
142
|
emojiFont: 'twemoji',
|
|
143
|
+
mermaid: 'neutral',
|
|
123
144
|
};
|
|
124
145
|
/** Ocean — deep blue palette */
|
|
125
146
|
exports.oceanTheme = {
|
|
@@ -159,6 +180,16 @@ exports.oceanTheme = {
|
|
|
159
180
|
spacing: { ...styles_js_1.defaultSpacing },
|
|
160
181
|
imageAlign: 'left',
|
|
161
182
|
emojiFont: 'twemoji',
|
|
183
|
+
mermaid: {
|
|
184
|
+
background: '#ffffff',
|
|
185
|
+
primaryColor: '#d4e6f1',
|
|
186
|
+
secondaryColor: '#aed6f1',
|
|
187
|
+
tertiaryColor: '#eaf2f8',
|
|
188
|
+
primaryTextColor: '#1b2631',
|
|
189
|
+
secondaryTextColor: '#5d6d7e',
|
|
190
|
+
lineColor: '#2471a3',
|
|
191
|
+
borderColor: '#2980b9',
|
|
192
|
+
},
|
|
162
193
|
};
|
|
163
194
|
/** Record of all built-in themes keyed by display name */
|
|
164
195
|
exports.themes = {
|
package/dist/types.d.ts
CHANGED
|
@@ -96,6 +96,42 @@ export interface ThemeConfig {
|
|
|
96
96
|
* - `'none'` — disable emoji font; emoji render with the body font.
|
|
97
97
|
*/
|
|
98
98
|
emojiFont?: 'twemoji' | 'openmoji' | 'noto' | 'none';
|
|
99
|
+
/**
|
|
100
|
+
* Mermaid diagram theme configuration.
|
|
101
|
+
*
|
|
102
|
+
* Controls how mermaid fenced code blocks are rendered as diagrams in the PDF.
|
|
103
|
+
* - `'default'` | `'dark'` | `'forest'` | `'neutral'` — use a built-in mermaid theme.
|
|
104
|
+
* - A `MermaidThemeConfig` object — provide custom colors for the diagram.
|
|
105
|
+
* - `undefined` — uses `'default'`.
|
|
106
|
+
*/
|
|
107
|
+
mermaid?: MermaidThemeConfig | 'default' | 'dark' | 'forest' | 'neutral';
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Custom mermaid diagram theme colors.
|
|
111
|
+
*
|
|
112
|
+
* Maps to the `Theme` interface from `@speajus/mermaid-to-svg`.
|
|
113
|
+
*/
|
|
114
|
+
export interface MermaidThemeConfig {
|
|
115
|
+
/** Diagram background color */
|
|
116
|
+
background?: string;
|
|
117
|
+
/** Primary node fill color */
|
|
118
|
+
primaryColor?: string;
|
|
119
|
+
/** Secondary node fill color */
|
|
120
|
+
secondaryColor?: string;
|
|
121
|
+
/** Tertiary node fill color */
|
|
122
|
+
tertiaryColor?: string;
|
|
123
|
+
/** Primary text color */
|
|
124
|
+
primaryTextColor?: string;
|
|
125
|
+
/** Secondary text color */
|
|
126
|
+
secondaryTextColor?: string;
|
|
127
|
+
/** Line / arrow color */
|
|
128
|
+
lineColor?: string;
|
|
129
|
+
/** Node border color */
|
|
130
|
+
borderColor?: string;
|
|
131
|
+
/** Font family for diagram labels */
|
|
132
|
+
fontFamily?: string;
|
|
133
|
+
/** Base font size */
|
|
134
|
+
fontSize?: number;
|
|
99
135
|
}
|
|
100
136
|
/**
|
|
101
137
|
* A custom font definition providing font data for registration with PDFKit.
|
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.20",
|
|
4
4
|
"description": "A new project created with Intent by Augment.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"type": "commonjs",
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@resvg/resvg-js": "^2.6.2",
|
|
54
|
+
"@speajus/mermaid-to-svg": "^0.1.5",
|
|
54
55
|
"marked": "^17.0.2",
|
|
55
56
|
"pdfkit": "github:jspears/pdfkit#support-color-emoji-google",
|
|
56
57
|
"prismjs": "^1.30.0"
|