@speajus/markdown-to-pdf 1.0.3 → 1.0.5

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/emoji.js ADDED
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ /**
3
+ * Emoji detection and text segmentation utilities.
4
+ *
5
+ * Splits a string into runs of "emoji" vs "non-emoji" characters so the
6
+ * renderer can switch to an emoji font (e.g. Noto Emoji) for glyph coverage.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.isEmoji = isEmoji;
10
+ exports.splitEmojiSegments = splitEmojiSegments;
11
+ exports.containsEmoji = containsEmoji;
12
+ exports.getEmojiRegex = getEmojiRegex;
13
+ /**
14
+ * Regex that matches a single emoji "unit".
15
+ *
16
+ * Covers:
17
+ * - Emoji presentation sequences (base + U+FE0F)
18
+ * - Keycap sequences (digit + U+FE0F + U+20E3)
19
+ * - Flag sequences (regional indicators)
20
+ * - ZWJ sequences (family, profession, etc.)
21
+ * - Tag sequences (e.g. England flag)
22
+ * - Modifier sequences (skin-tone)
23
+ * - Standalone emoji codepoints in Emoticons, Dingbats, Symbols, etc.
24
+ *
25
+ * We use a wide net via Unicode property escapes where the runtime supports
26
+ * them, falling back to explicit ranges for broad compatibility.
27
+ */
28
+ const EMOJI_RE = buildEmojiRegex();
29
+ function buildEmojiRegex() {
30
+ // Modern runtimes (Node 10+) support Unicode property escapes.
31
+ // We try that first because it is maintained by the Unicode consortium
32
+ // and stays current with new emoji releases.
33
+ try {
34
+ // \p{Emoji_Presentation} — characters rendered as emoji by default
35
+ // \p{Emoji_Modifier_Base} — characters that accept skin-tone modifiers
36
+ // The rest handles ZWJ, keycap, flag, and tag sequences.
37
+ return new RegExp(
38
+ // Regional indicator flag pairs — MUST come first so that
39
+ // individual RI symbols are not consumed by the ZWJ / standalone
40
+ // branches before they can pair up.
41
+ '(?:[\\u{1F1E6}-\\u{1F1FF}]){2}' +
42
+ '|' +
43
+ // Tag sequences (e.g. flag of England)
44
+ '\\u{1F3F4}[\\u{E0060}-\\u{E007E}]+\\u{E007F}' +
45
+ '|' +
46
+ // Keycap sequences: digit/# /* + VS16 + combining enclosing keycap
47
+ '[0-9#*]\\uFE0F?\\u20E3' +
48
+ '|' +
49
+ // ZWJ sequences (family, professions, etc.)
50
+ '(?:' +
51
+ '(?:\\p{Emoji_Presentation}|\\p{Emoji_Modifier_Base})' +
52
+ '(?:\\p{Emoji_Modifier})?' +
53
+ '(?:\\u200D(?:\\p{Emoji_Presentation}|\\p{Emoji_Modifier_Base})(?:\\p{Emoji_Modifier})?)*)' +
54
+ '|' +
55
+ // Standalone emoji with optional VS16 — exclude RI range
56
+ // (U+1F1E6–U+1F1FF) so lone indicators don't match here.
57
+ '[\\u{1F000}-\\u{1F1E5}\\u{1F200}-\\u{1FAFF}\\u{2600}-\\u{27BF}\\u{2300}-\\u{23FF}\\u{2B50}\\u{2B55}\\u{FE00}-\\u{FE0F}\\u{200D}]\\uFE0F?' +
58
+ '', 'gu');
59
+ }
60
+ catch {
61
+ // Fallback: cover the most common emoji ranges without property escapes.
62
+ return new RegExp('[' +
63
+ '\\u{1F600}-\\u{1F64F}' + // Emoticons
64
+ '\\u{1F300}-\\u{1F5FF}' + // Misc Symbols & Pictographs
65
+ '\\u{1F680}-\\u{1F6FF}' + // Transport & Map
66
+ '\\u{1F900}-\\u{1F9FF}' + // Supplemental Symbols
67
+ '\\u{1FA00}-\\u{1FA6F}' + // Chess Symbols
68
+ '\\u{1FA70}-\\u{1FAFF}' + // Symbols Extended-A
69
+ '\\u{2600}-\\u{26FF}' + // Misc Symbols
70
+ '\\u{2700}-\\u{27BF}' + // Dingbats
71
+ '\\u{FE00}-\\u{FE0F}' + // Variation Selectors
72
+ '\\u{200D}' + // ZWJ
73
+ '\\u{1F1E6}-\\u{1F1FF}' + // Regional Indicators
74
+ ']+', 'gu');
75
+ }
76
+ }
77
+ /**
78
+ * Returns `true` when the whole string is emoji (one or more).
79
+ */
80
+ function isEmoji(text) {
81
+ const stripped = text.replace(EMOJI_RE, '');
82
+ return stripped.trim().length === 0 && text.length > 0;
83
+ }
84
+ /**
85
+ * Split `text` into contiguous runs of emoji and non-emoji characters.
86
+ *
87
+ * Example:
88
+ * splitEmojiSegments("Hello 🎉🔥 world")
89
+ * => [
90
+ * { text: "Hello ", isEmoji: false },
91
+ * { text: "🎉🔥", isEmoji: true },
92
+ * { text: " world", isEmoji: false },
93
+ * ]
94
+ */
95
+ function splitEmojiSegments(text) {
96
+ if (!text)
97
+ return [];
98
+ const segments = [];
99
+ let lastIndex = 0;
100
+ // Reset global regex state
101
+ EMOJI_RE.lastIndex = 0;
102
+ let match;
103
+ while ((match = EMOJI_RE.exec(text)) !== null) {
104
+ // Push any non-emoji text before this match
105
+ if (match.index > lastIndex) {
106
+ segments.push({ text: text.slice(lastIndex, match.index), isEmoji: false });
107
+ }
108
+ // Merge consecutive emoji matches into one segment
109
+ const prev = segments[segments.length - 1];
110
+ if (prev && prev.isEmoji) {
111
+ prev.text += match[0];
112
+ }
113
+ else {
114
+ segments.push({ text: match[0], isEmoji: true });
115
+ }
116
+ lastIndex = EMOJI_RE.lastIndex;
117
+ }
118
+ // Trailing non-emoji text
119
+ if (lastIndex < text.length) {
120
+ segments.push({ text: text.slice(lastIndex), isEmoji: false });
121
+ }
122
+ return segments;
123
+ }
124
+ /**
125
+ * Quick check: does the string contain at least one emoji?
126
+ */
127
+ function containsEmoji(text) {
128
+ EMOJI_RE.lastIndex = 0;
129
+ return EMOJI_RE.test(text);
130
+ }
131
+ /**
132
+ * Returns the module-level emoji regex. Useful for external callers
133
+ * (e.g. `preRenderEmoji`) that need to scan text with the same pattern.
134
+ */
135
+ function getEmojiRegex() {
136
+ return EMOJI_RE;
137
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * pdfkit-highlight.ts
3
+ * Syntax highlighting for PDFKit using Prism.js
4
+ *
5
+ * Usage:
6
+ * import { loadHighlightLanguages, renderCode } from './highlight.prism.js';
7
+ * loadHighlightLanguages(['javascript', 'python']); // or omit to load all
8
+ * renderCode(doc, sourceCode, { language: 'javascript', x: 50, y: 100 });
9
+ */
10
+ import 'prismjs/components/prism-typescript.js';
11
+ import 'prismjs/components/prism-python.js';
12
+ import 'prismjs/components/prism-bash.js';
13
+ import 'prismjs/components/prism-json.js';
14
+ import 'prismjs/components/prism-jsx.js';
15
+ import 'prismjs/components/prism-tsx.js';
16
+ import 'prismjs/components/prism-yaml.js';
17
+ import 'prismjs/components/prism-sql.js';
18
+ import 'prismjs/components/prism-go.js';
19
+ import 'prismjs/components/prism-rust.js';
20
+ import 'prismjs/components/prism-java.js';
21
+ import 'prismjs/components/prism-c.js';
22
+ import 'prismjs/components/prism-cpp.js';
23
+ import 'prismjs/components/prism-diff.js';
24
+ import 'prismjs/components/prism-markdown.js';
25
+ import 'prismjs/components/prism-docker.js';
26
+ import type PDFDocument from 'pdfkit';
27
+ import type { SyntaxHighlightTheme } from './types.js';
28
+ /**
29
+ * Load Prism.js language grammars for syntax highlighting.
30
+ *
31
+ * @param languages Optional array of language identifiers to load
32
+ * (e.g. `['javascript', 'python', 'bash']`).
33
+ * When omitted or `undefined`, **all** available Prism.js
34
+ * languages (~300) are loaded.
35
+ *
36
+ * In Node.js, this dynamically loads grammars via `prismjs/components/`.
37
+ * In browser environments (Vite, webpack, etc.) the dynamic loader is not
38
+ * available; a set of common languages is pre-loaded via static imports
39
+ * and unknown languages degrade to unstyled plain text.
40
+ *
41
+ * Safe to call multiple times — subsequent calls are no-ops.
42
+ */
43
+ export declare function loadHighlightLanguages(languages?: string[]): void;
44
+ interface FlatToken {
45
+ type: string | null;
46
+ content: string;
47
+ }
48
+ export interface RenderCodeOptions {
49
+ language?: string;
50
+ x: number;
51
+ y: number;
52
+ width?: number;
53
+ font?: string;
54
+ fontSize?: number;
55
+ lineHeight?: number;
56
+ padding?: number;
57
+ lineNumbers?: boolean;
58
+ drawBackground?: boolean;
59
+ theme?: Partial<SyntaxHighlightTheme>;
60
+ }
61
+ export declare function colorFor(tokenType: string | null, theme: SyntaxHighlightTheme): string;
62
+ /**
63
+ * Split flat tokens at newline boundaries so we get per-line arrays.
64
+ * Each line is an array of { type, content } segments (no newlines).
65
+ */
66
+ export declare function tokenizeToLines(code: string, language: string): FlatToken[][];
67
+ /**
68
+ * Render syntax-highlighted code into a PDFKit document.
69
+ *
70
+ * @param doc PDFKit document instance
71
+ * @param code Source code string
72
+ * @param opts Rendering options
73
+ * @returns The Y position immediately after the rendered block
74
+ */
75
+ export declare function renderCode(doc: InstanceType<typeof PDFDocument>, code: string, opts: RenderCodeOptions): number;
76
+ export {};
@@ -0,0 +1,227 @@
1
+ "use strict";
2
+ /**
3
+ * pdfkit-highlight.ts
4
+ * Syntax highlighting for PDFKit using Prism.js
5
+ *
6
+ * Usage:
7
+ * import { loadHighlightLanguages, renderCode } from './highlight.prism.js';
8
+ * loadHighlightLanguages(['javascript', 'python']); // or omit to load all
9
+ * renderCode(doc, sourceCode, { language: 'javascript', x: 50, y: 100 });
10
+ */
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.loadHighlightLanguages = loadHighlightLanguages;
16
+ exports.colorFor = colorFor;
17
+ exports.tokenizeToLines = tokenizeToLines;
18
+ exports.renderCode = renderCode;
19
+ const prismjs_1 = __importDefault(require("prismjs"));
20
+ // Static imports for commonly-used languages — these are self-registering
21
+ // side-effect imports that work in both Node.js and browser environments.
22
+ // Base Prism already includes: markup, css, clike, javascript.
23
+ require("prismjs/components/prism-typescript.js");
24
+ require("prismjs/components/prism-python.js");
25
+ require("prismjs/components/prism-bash.js");
26
+ require("prismjs/components/prism-json.js");
27
+ require("prismjs/components/prism-jsx.js");
28
+ require("prismjs/components/prism-tsx.js");
29
+ require("prismjs/components/prism-yaml.js");
30
+ require("prismjs/components/prism-sql.js");
31
+ require("prismjs/components/prism-go.js");
32
+ require("prismjs/components/prism-rust.js");
33
+ require("prismjs/components/prism-java.js");
34
+ require("prismjs/components/prism-c.js");
35
+ require("prismjs/components/prism-cpp.js");
36
+ require("prismjs/components/prism-diff.js");
37
+ require("prismjs/components/prism-markdown.js");
38
+ require("prismjs/components/prism-docker.js");
39
+ const styles_js_1 = require("./styles.js");
40
+ // ---------------------------------------------------------------------------
41
+ // Language loading
42
+ // ---------------------------------------------------------------------------
43
+ let languagesLoaded = false;
44
+ /**
45
+ * Load Prism.js language grammars for syntax highlighting.
46
+ *
47
+ * @param languages Optional array of language identifiers to load
48
+ * (e.g. `['javascript', 'python', 'bash']`).
49
+ * When omitted or `undefined`, **all** available Prism.js
50
+ * languages (~300) are loaded.
51
+ *
52
+ * In Node.js, this dynamically loads grammars via `prismjs/components/`.
53
+ * In browser environments (Vite, webpack, etc.) the dynamic loader is not
54
+ * available; a set of common languages is pre-loaded via static imports
55
+ * and unknown languages degrade to unstyled plain text.
56
+ *
57
+ * Safe to call multiple times — subsequent calls are no-ops.
58
+ */
59
+ function loadHighlightLanguages(languages) {
60
+ if (languagesLoaded)
61
+ return;
62
+ try {
63
+ // prismjs/components/index.js uses require.resolve() internally,
64
+ // which is only available in Node.js. In browser bundlers (Vite,
65
+ // webpack) it is transformed to __require.resolve which does not exist.
66
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
67
+ const loadLanguages = require('prismjs/components/index.js');
68
+ loadLanguages.silent = true;
69
+ if (languages && languages.length > 0) {
70
+ loadLanguages(languages);
71
+ }
72
+ else {
73
+ loadLanguages(); // loads every language
74
+ }
75
+ }
76
+ catch {
77
+ // Browser / ESM environment — require.resolve not available.
78
+ // The static imports above cover the most common languages;
79
+ // any unsupported language falls back to unstyled plain text
80
+ // in tokenizeToLines().
81
+ }
82
+ languagesLoaded = true;
83
+ }
84
+ // ---------------------------------------------------------------------------
85
+ // Helpers
86
+ // ---------------------------------------------------------------------------
87
+ function colorFor(tokenType, theme) {
88
+ if (!tokenType)
89
+ return theme.tokens.default;
90
+ const types = tokenType.split(' ');
91
+ for (const t of types) {
92
+ if (theme.tokens[t])
93
+ return theme.tokens[t];
94
+ }
95
+ return theme.tokens.default;
96
+ }
97
+ /**
98
+ * Flatten Prism's nested token tree into a simple
99
+ * [{ type: string|null, content: string }] array.
100
+ */
101
+ function flattenTokens(tokens) {
102
+ const result = [];
103
+ for (const token of tokens) {
104
+ if (typeof token === 'string') {
105
+ result.push({ type: null, content: token });
106
+ }
107
+ else if (token.type) {
108
+ const inner = flattenTokens(Array.isArray(token.content)
109
+ ? token.content
110
+ : [token.content]);
111
+ for (const child of inner) {
112
+ result.push({ type: child.type || token.type, content: child.content });
113
+ }
114
+ }
115
+ }
116
+ return result;
117
+ }
118
+ /**
119
+ * Split flat tokens at newline boundaries so we get per-line arrays.
120
+ * Each line is an array of { type, content } segments (no newlines).
121
+ */
122
+ function tokenizeToLines(code, language) {
123
+ const grammar = prismjs_1.default.languages[language];
124
+ if (!grammar) {
125
+ // Grammar not loaded / unknown language — fall back to plain (unstyled) text
126
+ return code.split('\n').map(line => line.length > 0 ? [{ type: null, content: line }] : []);
127
+ }
128
+ const tokens = prismjs_1.default.tokenize(code, grammar);
129
+ const flat = flattenTokens(tokens);
130
+ const lines = [[]];
131
+ for (const seg of flat) {
132
+ const parts = seg.content.split('\n');
133
+ for (let i = 0; i < parts.length; i++) {
134
+ if (parts[i].length > 0) {
135
+ lines[lines.length - 1].push({ type: seg.type, content: parts[i] });
136
+ }
137
+ if (i < parts.length - 1) {
138
+ lines.push([]);
139
+ }
140
+ }
141
+ }
142
+ return lines;
143
+ }
144
+ // ---------------------------------------------------------------------------
145
+ // Public API
146
+ // ---------------------------------------------------------------------------
147
+ /**
148
+ * Render syntax-highlighted code into a PDFKit document.
149
+ *
150
+ * @param doc PDFKit document instance
151
+ * @param code Source code string
152
+ * @param opts Rendering options
153
+ * @returns The Y position immediately after the rendered block
154
+ */
155
+ function renderCode(doc, code, opts) {
156
+ const { language = 'javascript', x, y, font = 'Courier', fontSize = 10, lineHeight = 1.5, padding = 12, lineNumbers = true, drawBackground = true, } = opts;
157
+ const theme = opts.theme
158
+ ? {
159
+ ...styles_js_1.defaultSyntaxHighlightTheme,
160
+ tokens: { ...styles_js_1.defaultSyntaxHighlightTheme.tokens, ...(opts.theme.tokens || {}) },
161
+ ...opts.theme,
162
+ }
163
+ : styles_js_1.defaultSyntaxHighlightTheme;
164
+ const blockWidth = opts.width || doc.page.width - x - doc.page.margins.right;
165
+ const lineH = fontSize * lineHeight;
166
+ // Normalize tabs → 2 spaces
167
+ const normalized = code.replace(/\t/g, ' ');
168
+ const lines = tokenizeToLines(normalized, language);
169
+ // Gutter width (only if line numbers enabled)
170
+ const gutterWidth = lineNumbers
171
+ ? doc.widthOfString(String(lines.length), { font, size: fontSize }) + 16
172
+ : 0;
173
+ const codeX = x + padding + gutterWidth;
174
+ const blockHeight = lines.length * lineH + padding * 2;
175
+ // --- Background ---
176
+ if (drawBackground) {
177
+ doc
178
+ .save()
179
+ .rect(x, y, blockWidth, blockHeight)
180
+ .fill(theme.background);
181
+ doc.restore();
182
+ }
183
+ // --- Set font once ---
184
+ if (font !== 'Courier' && font !== 'Courier-Bold') {
185
+ // Assume it's a path to an embedded font registered by the caller,
186
+ // or register it here if it looks like a file path
187
+ doc.font(font).fontSize(fontSize);
188
+ }
189
+ else {
190
+ doc.font('Courier').fontSize(fontSize);
191
+ }
192
+ let curY = y + padding;
193
+ for (let i = 0; i < lines.length; i++) {
194
+ const lineTokens = lines[i];
195
+ // Check for page overflow
196
+ if (curY + lineH > doc.page.height - doc.page.margins.bottom) {
197
+ doc.addPage();
198
+ curY = doc.page.margins.top;
199
+ // Optionally redraw background on new page — left to caller
200
+ }
201
+ // --- Line number ---
202
+ if (lineNumbers) {
203
+ doc.fillColor(theme.gutter).text(String(i + 1), x + padding, curY, {
204
+ lineBreak: false,
205
+ width: gutterWidth - 8,
206
+ align: 'right',
207
+ });
208
+ }
209
+ // --- Code tokens ---
210
+ let curX = codeX;
211
+ for (const seg of lineTokens) {
212
+ if (!seg.content)
213
+ continue;
214
+ const color = colorFor(seg.type, theme);
215
+ const segWidth = doc.widthOfString(seg.content);
216
+ doc.fillColor(color).text(seg.content, curX, curY, {
217
+ lineBreak: false,
218
+ continued: false,
219
+ });
220
+ curX += segWidth;
221
+ }
222
+ curY += lineH;
223
+ }
224
+ // Reset fill color to black for subsequent content
225
+ doc.fillColor('black');
226
+ return curY + padding; // return Y after block
227
+ }
@@ -0,0 +1,8 @@
1
+ export type { TextStyle, PageLayout, CodeStyle, CodeBlockStyle, BlockquoteStyle, TableStyles, TokenColors, SyntaxHighlightTheme, ThemeConfig, PdfOptions, ColorEmojiRenderer, } from './types.js';
2
+ export { defaultTheme, defaultPageLayout, defaultSyntaxHighlightTheme } from './styles.js';
3
+ export { renderMarkdownToPdf as generatePdf, renderMarkdownToPdf, } from "./renderer.js";
4
+ export { createBrowserImageRenderer } from './browser-image-renderer.js';
5
+ export { createNodeImageRenderer } from './node-image-renderer.js';
6
+ export { createBrowserColorEmojiRenderer } from './color-emoji.js';
7
+ export { createNodeColorEmojiRenderer } from './node-color-emoji.js';
8
+ export { loadHighlightLanguages } from './highlight.prism.js';
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadHighlightLanguages = exports.createNodeColorEmojiRenderer = exports.createBrowserColorEmojiRenderer = exports.createNodeImageRenderer = exports.createBrowserImageRenderer = exports.renderMarkdownToPdf = exports.generatePdf = exports.defaultSyntaxHighlightTheme = exports.defaultPageLayout = exports.defaultTheme = void 0;
4
+ var styles_js_1 = require("./styles.js");
5
+ Object.defineProperty(exports, "defaultTheme", { enumerable: true, get: function () { return styles_js_1.defaultTheme; } });
6
+ Object.defineProperty(exports, "defaultPageLayout", { enumerable: true, get: function () { return styles_js_1.defaultPageLayout; } });
7
+ Object.defineProperty(exports, "defaultSyntaxHighlightTheme", { enumerable: true, get: function () { return styles_js_1.defaultSyntaxHighlightTheme; } });
8
+ var renderer_js_1 = require("./renderer.js");
9
+ Object.defineProperty(exports, "generatePdf", { enumerable: true, get: function () { return renderer_js_1.renderMarkdownToPdf; } });
10
+ Object.defineProperty(exports, "renderMarkdownToPdf", { enumerable: true, get: function () { return renderer_js_1.renderMarkdownToPdf; } });
11
+ var browser_image_renderer_js_1 = require("./browser-image-renderer.js");
12
+ Object.defineProperty(exports, "createBrowserImageRenderer", { enumerable: true, get: function () { return browser_image_renderer_js_1.createBrowserImageRenderer; } });
13
+ var node_image_renderer_js_1 = require("./node-image-renderer.js");
14
+ Object.defineProperty(exports, "createNodeImageRenderer", { enumerable: true, get: function () { return node_image_renderer_js_1.createNodeImageRenderer; } });
15
+ var color_emoji_js_1 = require("./color-emoji.js");
16
+ Object.defineProperty(exports, "createBrowserColorEmojiRenderer", { enumerable: true, get: function () { return color_emoji_js_1.createBrowserColorEmojiRenderer; } });
17
+ var node_color_emoji_js_1 = require("./node-color-emoji.js");
18
+ Object.defineProperty(exports, "createNodeColorEmojiRenderer", { enumerable: true, get: function () { return node_color_emoji_js_1.createNodeColorEmojiRenderer; } });
19
+ var highlight_prism_js_1 = require("./highlight.prism.js");
20
+ Object.defineProperty(exports, "loadHighlightLanguages", { enumerable: true, get: function () { return highlight_prism_js_1.loadHighlightLanguages; } });
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Node.js color emoji renderer using `@resvg/resvg-js`.
3
+ *
4
+ * Separated from `color-emoji.ts` so that the browser entry point
5
+ * (`src/browser.ts`) can import the shared helpers and browser factory
6
+ * without pulling in the native `@resvg/resvg-js` addon.
7
+ */
8
+ import type { ColorEmojiRenderer } from './color-emoji.js';
9
+ /**
10
+ * Creates a color-emoji renderer for **Node.js** using `@resvg/resvg-js`.
11
+ *
12
+ * Fetches Twemoji SVGs over HTTPS on first use and caches the resulting
13
+ * PNGs for the lifetime of the returned function.
14
+ */
15
+ export declare function createNodeColorEmojiRenderer(): ColorEmojiRenderer;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ /**
3
+ * Node.js color emoji renderer using `@resvg/resvg-js`.
4
+ *
5
+ * Separated from `color-emoji.ts` so that the browser entry point
6
+ * (`src/browser.ts`) can import the shared helpers and browser factory
7
+ * without pulling in the native `@resvg/resvg-js` addon.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.createNodeColorEmojiRenderer = createNodeColorEmojiRenderer;
11
+ const color_emoji_js_1 = require("./color-emoji.js");
12
+ /**
13
+ * Creates a color-emoji renderer for **Node.js** using `@resvg/resvg-js`.
14
+ *
15
+ * Fetches Twemoji SVGs over HTTPS on first use and caches the resulting
16
+ * PNGs for the lifetime of the returned function.
17
+ */
18
+ function createNodeColorEmojiRenderer() {
19
+ const cache = new Map();
20
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
21
+ const { Resvg } = require('@resvg/resvg-js');
22
+ function fetchSvg(url) {
23
+ return new Promise((resolve, reject) => {
24
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
25
+ const mod = url.startsWith('https') ? require('https') : require('http');
26
+ mod.get(url, (res) => {
27
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
28
+ res.resume();
29
+ fetchSvg(res.headers.location).then(resolve, reject);
30
+ return;
31
+ }
32
+ if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
33
+ res.resume();
34
+ reject(new Error(`HTTP ${res.statusCode} fetching ${url}`));
35
+ return;
36
+ }
37
+ const chunks = [];
38
+ res.on('data', (c) => chunks.push(c));
39
+ res.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
40
+ res.on('error', reject);
41
+ }).on('error', reject);
42
+ });
43
+ }
44
+ return async (emoji) => {
45
+ const hit = cache.get(emoji);
46
+ if (hit)
47
+ return hit;
48
+ const svg = await fetchSvg((0, color_emoji_js_1.twemojiSvgUrl)(emoji));
49
+ const sized = (0, color_emoji_js_1.sizeSvg)(svg, color_emoji_js_1.EMOJI_RENDER_SIZE);
50
+ const resvg = new Resvg(Buffer.from(sized), {
51
+ fitTo: { mode: 'width', value: color_emoji_js_1.EMOJI_RENDER_SIZE },
52
+ });
53
+ const png = Buffer.from(resvg.render().asPng());
54
+ cache.set(emoji, png);
55
+ return png;
56
+ };
57
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Creates a Node.js-based image renderer that supports:
3
+ * - Remote images (http/https)
4
+ * - Local file system images
5
+ * - SVG to PNG conversion
6
+ *
7
+ * @param basePath - Base directory for resolving relative image paths
8
+ * @returns A function that takes an image URL/path and returns a Buffer
9
+ */
10
+ export declare function createNodeImageRenderer(basePath?: string): (imageUrl: string) => Promise<Buffer>;
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createNodeImageRenderer = createNodeImageRenderer;
7
+ const resvg_js_1 = require("@resvg/resvg-js");
8
+ const https_1 = __importDefault(require("https"));
9
+ const http_1 = __importDefault(require("http"));
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const defaults_1 = require("./defaults");
13
+ function isSvg(buf) {
14
+ // Check for XML/SVG signature in the first 256 bytes
15
+ const head = buf.subarray(0, 256).toString('utf-8').trimStart();
16
+ return head.startsWith('<svg') || head.startsWith('<?xml');
17
+ }
18
+ function convertSvgToPng(svgData) {
19
+ const resvg = new resvg_js_1.Resvg(svgData, { font: { loadSystemFonts: true } });
20
+ const rendered = resvg.render();
21
+ return Buffer.from(rendered.asPng());
22
+ }
23
+ const FETCH_TIMEOUT_MS = 10000;
24
+ const MAX_REDIRECTS = 5;
25
+ function fetchImageBuffer(url, redirectCount = 0) {
26
+ if (redirectCount > MAX_REDIRECTS) {
27
+ return Promise.reject(new Error(`Too many redirects fetching ${url}`));
28
+ }
29
+ return new Promise((resolve, reject) => {
30
+ const get = url.startsWith('https') ? https_1.default.get : http_1.default.get;
31
+ const req = get(url, (res) => {
32
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
33
+ res.resume(); // drain the response so the socket can be reused / freed
34
+ fetchImageBuffer(res.headers.location, redirectCount + 1).then(resolve, reject);
35
+ return;
36
+ }
37
+ if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
38
+ res.resume();
39
+ reject(new Error(`HTTP ${res.statusCode} fetching ${url}`));
40
+ return;
41
+ }
42
+ const chunks = [];
43
+ res.on('data', (chunk) => chunks.push(chunk));
44
+ res.on('end', () => resolve(Buffer.concat(chunks)));
45
+ res.on('error', reject);
46
+ });
47
+ req.on('error', reject);
48
+ req.setTimeout(FETCH_TIMEOUT_MS, () => {
49
+ req.destroy(new Error(`Timeout fetching ${url} after ${FETCH_TIMEOUT_MS}ms`));
50
+ });
51
+ });
52
+ }
53
+ /**
54
+ * Creates a Node.js-based image renderer that supports:
55
+ * - Remote images (http/https)
56
+ * - Local file system images
57
+ * - SVG to PNG conversion
58
+ *
59
+ * @param basePath - Base directory for resolving relative image paths
60
+ * @returns A function that takes an image URL/path and returns a Buffer
61
+ */
62
+ function createNodeImageRenderer(basePath = process.cwd()) {
63
+ return async (imageUrl) => {
64
+ let imgBuffer;
65
+ if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
66
+ // Remote image - fetch via HTTP/HTTPS
67
+ imgBuffer = await fetchImageBuffer(imageUrl);
68
+ }
69
+ else {
70
+ // Local file path — resolve relative to the basePath
71
+ const imgPath = path_1.default.resolve(basePath, imageUrl);
72
+ imgBuffer = fs_1.default.readFileSync(imgPath);
73
+ }
74
+ // Convert SVG to PNG since pdfkit doesn't support SVG natively
75
+ if (isSvg(imgBuffer)) {
76
+ imgBuffer = convertSvgToPng(imgBuffer);
77
+ }
78
+ return imgBuffer;
79
+ };
80
+ }
81
+ defaults_1.DEFAULTS.renderImage = createNodeImageRenderer;
@@ -0,0 +1,2 @@
1
+ import type { PdfOptions } from './types.js';
2
+ export declare function renderMarkdownToPdf(markdown: string, options?: PdfOptions): Promise<Buffer>;