@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/README.md +22 -15
- package/dist/browser-image-renderer.d.ts +12 -0
- package/dist/browser-image-renderer.js +202 -0
- package/dist/browser.d.ts +6 -0
- package/dist/browser.js +19 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +40 -0
- package/dist/color-emoji.d.ts +43 -0
- package/dist/color-emoji.js +142 -0
- package/dist/defaults.d.ts +3 -0
- package/dist/defaults.js +4 -0
- package/dist/emoji.d.ts +35 -0
- package/dist/emoji.js +137 -0
- package/dist/highlight.prism.d.ts +76 -0
- package/dist/highlight.prism.js +227 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +20 -0
- package/dist/node-color-emoji.d.ts +15 -0
- package/dist/node-color-emoji.js +57 -0
- package/dist/node-image-renderer.d.ts +10 -0
- package/dist/node-image-renderer.js +81 -0
- package/dist/renderer.d.ts +2 -0
- package/dist/renderer.js +644 -0
- package/dist/styles.d.ts +5 -0
- package/dist/styles.js +92 -0
- package/dist/themes/index.d.ts +12 -0
- package/dist/themes/index.js +158 -0
- package/dist/types.d.ts +128 -0
- package/dist/types.js +2 -0
- package/package.json +12 -9
- /package/dist/{src/fonts → fonts}/NotoEmoji-Regular.ttf +0 -0
- /package/dist/{src/fonts → fonts}/NotoSans-Bold.ttf +0 -0
- /package/dist/{src/fonts → fonts}/NotoSans-BoldItalic.ttf +0 -0
- /package/dist/{src/fonts → fonts}/NotoSans-Italic.ttf +0 -0
- /package/dist/{src/fonts → fonts}/NotoSans-Regular.ttf +0 -0
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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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;
|