@speajus/markdown-to-pdf 1.0.15 → 1.0.17
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/browser.d.ts +2 -3
- package/dist/browser.js +2 -3
- package/dist/fonts/NotoColorEmoji.ttf +0 -0
- package/dist/fonts/OpenMoji-Color.ttf +0 -0
- package/dist/fonts/Twemoji.Mozilla.ttf +0 -0
- package/dist/highlight.prism.d.ts +2 -0
- package/dist/highlight.prism.js +9 -4
- package/dist/index.d.ts +2 -4
- package/dist/index.js +2 -5
- package/dist/renderer.js +95 -227
- package/dist/styles.d.ts +3 -1
- package/dist/styles.js +15 -1
- package/dist/themes/index.js +12 -0
- package/dist/types.d.ts +24 -16
- package/package.json +4 -2
- package/dist/color-emoji.d.ts +0 -43
- package/dist/color-emoji.js +0 -142
- package/dist/emoji.d.ts +0 -35
- package/dist/emoji.js +0 -137
- package/dist/fonts/NotoEmoji-Regular.ttf +0 -0
- package/dist/node-color-emoji.d.ts +0 -15
- package/dist/node-color-emoji.js +0 -57
package/dist/browser.d.ts
CHANGED
|
@@ -2,8 +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 {
|
|
6
|
-
export
|
|
7
|
-
export { defaultTheme, defaultPageLayout, defaultSyntaxHighlightTheme } from './styles.js';
|
|
5
|
+
export type { TextStyle, PageLayout, CodeStyle, CodeBlockStyle, BlockquoteStyle, TableStyles, TokenColors, SyntaxHighlightTheme, SpacingConfig, ThemeConfig, PdfOptions, CustomFontDefinition, } from './types.js';
|
|
6
|
+
export { defaultTheme, defaultPageLayout, defaultSyntaxHighlightTheme, defaultSpacing } from './styles.js';
|
|
8
7
|
export { themes, modernTheme, academicTheme, minimalTheme, oceanTheme } from './themes/index.js';
|
|
9
8
|
export { renderMarkdownToPdf } from "./renderer.js";
|
package/dist/browser.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.renderMarkdownToPdf = exports.oceanTheme = exports.minimalTheme = exports.academicTheme = exports.modernTheme = exports.themes = exports.
|
|
3
|
+
exports.renderMarkdownToPdf = exports.oceanTheme = exports.minimalTheme = exports.academicTheme = exports.modernTheme = exports.themes = exports.defaultSpacing = exports.defaultSyntaxHighlightTheme = exports.defaultPageLayout = exports.defaultTheme = exports.createBrowserImageRenderer = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Browser-specific entry point that excludes Node.js dependencies
|
|
6
6
|
*/
|
|
7
7
|
var browser_image_renderer_js_1 = require("./browser-image-renderer.js");
|
|
8
8
|
Object.defineProperty(exports, "createBrowserImageRenderer", { enumerable: true, get: function () { return browser_image_renderer_js_1.createBrowserImageRenderer; } });
|
|
9
|
-
var color_emoji_js_1 = require("./color-emoji.js");
|
|
10
|
-
Object.defineProperty(exports, "createBrowserColorEmojiRenderer", { enumerable: true, get: function () { return color_emoji_js_1.createBrowserColorEmojiRenderer; } });
|
|
11
9
|
var styles_js_1 = require("./styles.js");
|
|
12
10
|
Object.defineProperty(exports, "defaultTheme", { enumerable: true, get: function () { return styles_js_1.defaultTheme; } });
|
|
13
11
|
Object.defineProperty(exports, "defaultPageLayout", { enumerable: true, get: function () { return styles_js_1.defaultPageLayout; } });
|
|
14
12
|
Object.defineProperty(exports, "defaultSyntaxHighlightTheme", { enumerable: true, get: function () { return styles_js_1.defaultSyntaxHighlightTheme; } });
|
|
13
|
+
Object.defineProperty(exports, "defaultSpacing", { enumerable: true, get: function () { return styles_js_1.defaultSpacing; } });
|
|
15
14
|
var index_js_1 = require("./themes/index.js");
|
|
16
15
|
Object.defineProperty(exports, "themes", { enumerable: true, get: function () { return index_js_1.themes; } });
|
|
17
16
|
Object.defineProperty(exports, "modernTheme", { enumerable: true, get: function () { return index_js_1.modernTheme; } });
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -57,6 +57,8 @@ export interface RenderCodeOptions {
|
|
|
57
57
|
lineNumbers?: boolean;
|
|
58
58
|
drawBackground?: boolean;
|
|
59
59
|
theme?: Partial<SyntaxHighlightTheme>;
|
|
60
|
+
/** Corner radius for the code block background. @default 0 */
|
|
61
|
+
borderRadius?: number;
|
|
60
62
|
}
|
|
61
63
|
export declare function colorFor(tokenType: string | null, theme: SyntaxHighlightTheme): string;
|
|
62
64
|
/**
|
package/dist/highlight.prism.js
CHANGED
|
@@ -172,12 +172,17 @@ function renderCode(doc, code, opts) {
|
|
|
172
172
|
: 0;
|
|
173
173
|
const codeX = x + padding + gutterWidth;
|
|
174
174
|
const blockHeight = lines.length * lineH + padding * 2;
|
|
175
|
+
const borderRadius = opts.borderRadius ?? 0;
|
|
175
176
|
// --- Background ---
|
|
176
177
|
if (drawBackground) {
|
|
177
|
-
doc
|
|
178
|
-
|
|
179
|
-
.
|
|
180
|
-
|
|
178
|
+
doc.save();
|
|
179
|
+
if (borderRadius > 0) {
|
|
180
|
+
doc.roundedRect(x, y, blockWidth, blockHeight, borderRadius);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
doc.rect(x, y, blockWidth, blockHeight);
|
|
184
|
+
}
|
|
185
|
+
doc.fill(theme.background);
|
|
181
186
|
doc.restore();
|
|
182
187
|
}
|
|
183
188
|
// --- Set font once ---
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
export type { TextStyle, PageLayout, CodeStyle, CodeBlockStyle, BlockquoteStyle, TableStyles, TokenColors, SyntaxHighlightTheme, ThemeConfig, PdfOptions,
|
|
2
|
-
export { defaultTheme, defaultPageLayout, defaultSyntaxHighlightTheme } from './styles.js';
|
|
1
|
+
export type { TextStyle, PageLayout, CodeStyle, CodeBlockStyle, BlockquoteStyle, TableStyles, TokenColors, SyntaxHighlightTheme, SpacingConfig, ThemeConfig, PdfOptions, CustomFontDefinition, } from './types.js';
|
|
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';
|
|
5
5
|
export { createNodeImageRenderer } from './node-image-renderer.js';
|
|
6
|
-
export { createBrowserColorEmojiRenderer } from './color-emoji.js';
|
|
7
|
-
export { createNodeColorEmojiRenderer } from './node-color-emoji.js';
|
|
8
6
|
export { loadHighlightLanguages } from './highlight.prism.js';
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.loadHighlightLanguages = exports.
|
|
3
|
+
exports.loadHighlightLanguages = exports.createNodeImageRenderer = exports.createBrowserImageRenderer = exports.renderMarkdownToPdf = exports.generatePdf = exports.defaultSpacing = exports.defaultSyntaxHighlightTheme = exports.defaultPageLayout = exports.defaultTheme = void 0;
|
|
4
4
|
var styles_js_1 = require("./styles.js");
|
|
5
5
|
Object.defineProperty(exports, "defaultTheme", { enumerable: true, get: function () { return styles_js_1.defaultTheme; } });
|
|
6
6
|
Object.defineProperty(exports, "defaultPageLayout", { enumerable: true, get: function () { return styles_js_1.defaultPageLayout; } });
|
|
7
7
|
Object.defineProperty(exports, "defaultSyntaxHighlightTheme", { enumerable: true, get: function () { return styles_js_1.defaultSyntaxHighlightTheme; } });
|
|
8
|
+
Object.defineProperty(exports, "defaultSpacing", { enumerable: true, get: function () { return styles_js_1.defaultSpacing; } });
|
|
8
9
|
var renderer_js_1 = require("./renderer.js");
|
|
9
10
|
Object.defineProperty(exports, "generatePdf", { enumerable: true, get: function () { return renderer_js_1.renderMarkdownToPdf; } });
|
|
10
11
|
Object.defineProperty(exports, "renderMarkdownToPdf", { enumerable: true, get: function () { return renderer_js_1.renderMarkdownToPdf; } });
|
|
@@ -12,9 +13,5 @@ var browser_image_renderer_js_1 = require("./browser-image-renderer.js");
|
|
|
12
13
|
Object.defineProperty(exports, "createBrowserImageRenderer", { enumerable: true, get: function () { return browser_image_renderer_js_1.createBrowserImageRenderer; } });
|
|
13
14
|
var node_image_renderer_js_1 = require("./node-image-renderer.js");
|
|
14
15
|
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
16
|
var highlight_prism_js_1 = require("./highlight.prism.js");
|
|
20
17
|
Object.defineProperty(exports, "loadHighlightLanguages", { enumerable: true, get: function () { return highlight_prism_js_1.loadHighlightLanguages; } });
|
package/dist/renderer.js
CHANGED
|
@@ -10,10 +10,8 @@ const marked_1 = require("marked");
|
|
|
10
10
|
const stream_1 = require("stream");
|
|
11
11
|
const defaults_js_1 = require("./defaults.js");
|
|
12
12
|
const highlight_prism_js_1 = require("./highlight.prism.js");
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
/** Name used to register the emoji font with PDFKit */
|
|
16
|
-
const EMOJI_FONT_NAME = 'NotoEmoji';
|
|
13
|
+
/** Name used to identify the emoji font (for safeFont checks). */
|
|
14
|
+
const EMOJI_FONT_NAME = 'EmojiFont';
|
|
17
15
|
/** Standard PDF fonts built into PDFKit — always available without filesystem access. */
|
|
18
16
|
const STANDARD_PDF_FONTS = new Set([
|
|
19
17
|
'Helvetica', 'Helvetica-Bold', 'Helvetica-Oblique', 'Helvetica-BoldOblique',
|
|
@@ -31,49 +29,64 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
31
29
|
}
|
|
32
30
|
const lineNumbers = options?.lineNumbers ?? false;
|
|
33
31
|
const zebraStripes = options?.zebraStripes !== false;
|
|
34
|
-
|
|
32
|
+
// ── Resolve effective emoji font setting ──────────────────────────────
|
|
33
|
+
// PdfOptions.emojiFont (boolean | string | Buffer) overrides theme when
|
|
34
|
+
// explicitly set; otherwise fall back to theme.emojiFont ('twemoji'|'openmoji'|'none').
|
|
35
|
+
const themeEmojiFont = theme.emojiFont ?? 'twemoji';
|
|
36
|
+
const emojiFontOpt = options?.emojiFont !== undefined
|
|
37
|
+
? options.emojiFont
|
|
38
|
+
: themeEmojiFont === 'none'
|
|
39
|
+
? false
|
|
40
|
+
: themeEmojiFont; // 'twemoji' | 'openmoji' | 'noto'
|
|
35
41
|
// Use provided image renderer or create default Node.js renderer
|
|
36
42
|
const imageRenderer = options?.renderImage ?? defaults_js_1.DEFAULTS.renderImage(basePath);
|
|
37
43
|
const { margins } = layout;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// ── Emoji font registration ───────────────────────────────────────────────
|
|
44
|
-
// The font registration block uses dynamic require() for `path` and `fs` so
|
|
45
|
-
// that the renderer module can also be imported in browser environments where
|
|
46
|
-
// those Node.js built-ins are unavailable. If they aren't available the
|
|
47
|
-
// try/catch simply falls through and emoji support is disabled (unless the
|
|
48
|
-
// caller passes a Buffer directly via the `emojiFont` option).
|
|
49
|
-
let emojiEnabled = false;
|
|
44
|
+
// ── Resolve emoji font for pdfkit native color emoji support ────────────
|
|
45
|
+
// The fork at jspears/pdfkit#support-color-emoji-google handles emoji
|
|
46
|
+
// segmentation and rendering (COLR/CPAL, SBIX, CBDT) internally when
|
|
47
|
+
// an `emojiFont` option is passed to the PDFDocument constructor.
|
|
48
|
+
let resolvedEmojiFont;
|
|
50
49
|
if (emojiFontOpt !== false) {
|
|
51
50
|
try {
|
|
52
51
|
if (Buffer.isBuffer(emojiFontOpt)) {
|
|
53
|
-
|
|
54
|
-
doc.registerFont(EMOJI_FONT_NAME, emojiFontOpt);
|
|
55
|
-
emojiEnabled = true;
|
|
52
|
+
resolvedEmojiFont = emojiFontOpt;
|
|
56
53
|
}
|
|
57
54
|
else {
|
|
58
|
-
// Resolve a file path — requires Node.js built-ins.
|
|
59
55
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
60
56
|
const nodePath = require('path');
|
|
61
57
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
62
58
|
const nodeFs = require('fs');
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
let fontPath;
|
|
60
|
+
if (typeof emojiFontOpt === 'string' && emojiFontOpt !== 'twemoji' && emojiFontOpt !== 'openmoji' && emojiFontOpt !== 'noto' && emojiFontOpt !== 'none') {
|
|
61
|
+
fontPath = emojiFontOpt; // custom file path
|
|
62
|
+
}
|
|
63
|
+
else if (emojiFontOpt === 'openmoji') {
|
|
64
|
+
fontPath = nodePath.join(__dirname, 'fonts', 'OpenMoji-Color.ttf');
|
|
65
|
+
}
|
|
66
|
+
else if (emojiFontOpt === 'noto') {
|
|
67
|
+
fontPath = nodePath.join(__dirname, 'fonts', 'NotoColorEmoji.ttf');
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
fontPath = nodePath.join(__dirname, 'fonts', 'Twemoji.Mozilla.ttf');
|
|
71
|
+
}
|
|
66
72
|
if (nodeFs.existsSync(fontPath)) {
|
|
67
|
-
|
|
68
|
-
emojiEnabled = true;
|
|
73
|
+
resolvedEmojiFont = fontPath;
|
|
69
74
|
}
|
|
70
75
|
}
|
|
71
76
|
}
|
|
72
77
|
catch {
|
|
73
|
-
//
|
|
74
|
-
// fall through without emoji support.
|
|
78
|
+
// Node.js APIs not available (browser) — no emoji font.
|
|
75
79
|
}
|
|
76
80
|
}
|
|
81
|
+
const doc = new pdfkit_1.default({
|
|
82
|
+
size: layout.pageSize,
|
|
83
|
+
margins,
|
|
84
|
+
...(resolvedEmojiFont ? { emojiFont: resolvedEmojiFont } : {}),
|
|
85
|
+
});
|
|
86
|
+
const stream = new stream_1.PassThrough();
|
|
87
|
+
const chunks = [];
|
|
88
|
+
doc.pipe(stream);
|
|
89
|
+
stream.on('data', (chunk) => chunks.push(chunk));
|
|
77
90
|
// ── Custom font registration ─────────────────────────────────────────────
|
|
78
91
|
// Register user-supplied font families so they can be referenced by name
|
|
79
92
|
// in ThemeConfig font fields. Each variant is registered with a suffix:
|
|
@@ -108,15 +121,8 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
108
121
|
catch {
|
|
109
122
|
// Node.js `fs` not available — we're in a browser environment.
|
|
110
123
|
}
|
|
111
|
-
// ──
|
|
112
|
-
|
|
113
|
-
// every unique emoji and convert them all to PNG buffers up-front. This
|
|
114
|
-
// lets `renderTextWithEmoji` remain synchronous during page rendering.
|
|
115
|
-
let emojiImageCache;
|
|
116
|
-
const colorEmojiEnabled = !!options?.colorEmoji;
|
|
117
|
-
if (options?.colorEmoji) {
|
|
118
|
-
emojiImageCache = await (0, color_emoji_js_1.preRenderEmoji)(markdown, options.colorEmoji, (0, emoji_js_1.getEmojiRegex)());
|
|
119
|
-
}
|
|
124
|
+
// ── Spacing config ────────────────────────────────────────────────────
|
|
125
|
+
const sp = { ...styles_js_1.defaultSpacing, ...theme.spacing };
|
|
120
126
|
const tokens = marked_1.marked.lexer(markdown);
|
|
121
127
|
const contentWidth = doc.page.width - margins.left - margins.right;
|
|
122
128
|
// ── Table cell context ──────────────────────────────────────────────────
|
|
@@ -257,20 +263,16 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
257
263
|
doc.font(safeFont(theme.body.font)).fontSize(theme.body.fontSize).fillColor(theme.body.color);
|
|
258
264
|
}
|
|
259
265
|
/**
|
|
260
|
-
* Render
|
|
266
|
+
* Render text, delegating to `doc.text()`.
|
|
261
267
|
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
268
|
+
* pdfkit's native color emoji support (via the `emojiFont` constructor
|
|
269
|
+
* option) handles emoji segmentation and rendering internally, so this
|
|
270
|
+
* function only normalises call signatures and handles table cell context.
|
|
265
271
|
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
* glyphs, with manual (x, y) positioning to keep them in the text flow.
|
|
269
|
-
*
|
|
270
|
-
* Supports both `renderTextWithEmoji(text, opts?)` and the positioned
|
|
271
|
-
* form `renderTextWithEmoji(text, x, y, opts?)` used by table cells.
|
|
272
|
+
* Supports both `renderText(text, opts?)` and the positioned form
|
|
273
|
+
* `renderText(text, x, y, opts?)` used by table cells.
|
|
272
274
|
*/
|
|
273
|
-
function
|
|
275
|
+
function renderText(text, xOrOpts, yOrUndefined, posOpts) {
|
|
274
276
|
// Normalise the two call signatures into a single (opts, firstX, firstY).
|
|
275
277
|
let opts;
|
|
276
278
|
let firstX;
|
|
@@ -290,163 +292,12 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
290
292
|
opts = { ...opts, width: cellCtx.width, align: cellCtx.align };
|
|
291
293
|
cellCtx.used = true;
|
|
292
294
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if ((!emojiEnabled && !colorEmojiEnabled) || !hasEmoji) {
|
|
296
|
-
if (firstX !== undefined) {
|
|
297
|
-
doc.text(text, firstX, firstY, opts);
|
|
298
|
-
}
|
|
299
|
-
else {
|
|
300
|
-
doc.text(text, opts);
|
|
301
|
-
}
|
|
302
|
-
return;
|
|
295
|
+
if (firstX !== undefined) {
|
|
296
|
+
doc.text(text, firstX, firstY, opts);
|
|
303
297
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const prevSize = doc._fontSize ?? theme.body.fontSize;
|
|
307
|
-
const segments = (0, emoji_js_1.splitEmojiSegments)(text);
|
|
308
|
-
// ── Color emoji path: render emoji as inline PNG images ──────────────
|
|
309
|
-
// Strategy: two-pass rendering.
|
|
310
|
-
// Pass 1 – Render all text through doc.text() with `continued`,
|
|
311
|
-
// exactly like the monochrome path. For emoji segments,
|
|
312
|
-
// render a space-placeholder to reserve width. Read the
|
|
313
|
-
// real X position from PDFKit's internal wrapper state
|
|
314
|
-
// (_wrapper.startX + _wrapper.continuedX) — doc.x stays
|
|
315
|
-
// at the left margin after continued:true so it is NOT
|
|
316
|
-
// usable for positioning.
|
|
317
|
-
// Pass 2 – Overlay emoji PNG images at the recorded positions.
|
|
318
|
-
if (colorEmojiEnabled && emojiImageCache && emojiImageCache.size > 0) {
|
|
319
|
-
const emojiRe = (0, emoji_js_1.getEmojiRegex)();
|
|
320
|
-
const emojiSize = prevSize; // match font size
|
|
321
|
-
const emojiPlacements = [];
|
|
322
|
-
// Read the actual text-flow X from PDFKit's internal LineWrapper.
|
|
323
|
-
// After doc.text(…, {continued:true}), _wrapper.continuedX tracks
|
|
324
|
-
// cumulative rendered width; startX is the original doc.x at
|
|
325
|
-
// wrapper creation time.
|
|
326
|
-
const getFlowX = () => {
|
|
327
|
-
const w = doc._wrapper;
|
|
328
|
-
if (w)
|
|
329
|
-
return w.startX + w.continuedX;
|
|
330
|
-
return firstX ?? doc.x;
|
|
331
|
-
};
|
|
332
|
-
// Build a space-placeholder string whose width ≈ emojiSize.
|
|
333
|
-
doc.font(prevFont).fontSize(prevSize);
|
|
334
|
-
const spaceW = doc.widthOfString(' ');
|
|
335
|
-
const spacesPerEmoji = Math.max(1, Math.round(emojiSize / spaceW));
|
|
336
|
-
const placeholder = ' '.repeat(spacesPerEmoji);
|
|
337
|
-
const placeholderW = doc.widthOfString(placeholder);
|
|
338
|
-
// Vertical alignment: centre emoji within the current line height.
|
|
339
|
-
const lineH = doc.currentLineHeight(true);
|
|
340
|
-
const yOffset = Math.max(0, (lineH - emojiSize) / 2);
|
|
341
|
-
// Only use explicit (firstX, firstY) coordinates on the very first
|
|
342
|
-
// doc.text() call, and only when no wrapper already exists (i.e.
|
|
343
|
-
// we are not continuing from a prior renderTextWithEmoji call).
|
|
344
|
-
let usedExplicitCoords = false;
|
|
345
|
-
for (let i = 0; i < segments.length; i++) {
|
|
346
|
-
const seg = segments[i];
|
|
347
|
-
const isLast = i === segments.length - 1;
|
|
348
|
-
const cont = isLast ? !!opts.continued : true;
|
|
349
|
-
if (seg.isEmoji) {
|
|
350
|
-
emojiRe.lastIndex = 0;
|
|
351
|
-
let em;
|
|
352
|
-
while ((em = emojiRe.exec(seg.text)) !== null) {
|
|
353
|
-
const png = emojiImageCache.get(em[0]);
|
|
354
|
-
const isLastEmoji = isLast && emojiRe.lastIndex >= seg.text.length;
|
|
355
|
-
const eCont = isLastEmoji ? !!opts.continued : true;
|
|
356
|
-
if (png) {
|
|
357
|
-
// Position BEFORE rendering the placeholder.
|
|
358
|
-
const emojiX = getFlowX();
|
|
359
|
-
// For positioned calls (table cells), doc.y may be stale from
|
|
360
|
-
// a previous cell — use the explicit firstY instead.
|
|
361
|
-
const emojiY = (!usedExplicitCoords && firstY !== undefined) ? firstY : doc.y;
|
|
362
|
-
doc.font(prevFont).fontSize(prevSize);
|
|
363
|
-
if (!usedExplicitCoords && firstX !== undefined) {
|
|
364
|
-
doc.text(placeholder, firstX, firstY, { ...opts, continued: eCont });
|
|
365
|
-
usedExplicitCoords = true;
|
|
366
|
-
}
|
|
367
|
-
else {
|
|
368
|
-
doc.text(placeholder, { ...opts, continued: eCont });
|
|
369
|
-
}
|
|
370
|
-
// Compute alignment-aware X position for the emoji image.
|
|
371
|
-
let placedX = emojiX + (placeholderW - emojiSize) / 2;
|
|
372
|
-
if (firstX !== undefined && typeof opts.width === 'number' && !doc._wrapper) {
|
|
373
|
-
// Table cell with alignment — PDFKit already rendered the
|
|
374
|
-
// placeholder at the aligned position; match it.
|
|
375
|
-
const cellW = opts.width;
|
|
376
|
-
const a = opts.align || 'left';
|
|
377
|
-
if (a === 'center')
|
|
378
|
-
placedX = firstX + (cellW - emojiSize) / 2;
|
|
379
|
-
else if (a === 'right')
|
|
380
|
-
placedX = firstX + cellW - emojiSize;
|
|
381
|
-
}
|
|
382
|
-
emojiPlacements.push({
|
|
383
|
-
png,
|
|
384
|
-
x: placedX,
|
|
385
|
-
y: emojiY + yOffset,
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
else {
|
|
389
|
-
// Fallback: monochrome glyph.
|
|
390
|
-
if (emojiEnabled)
|
|
391
|
-
doc.font(EMOJI_FONT_NAME).fontSize(prevSize);
|
|
392
|
-
if (!usedExplicitCoords && firstX !== undefined) {
|
|
393
|
-
doc.text(em[0], firstX, firstY, { ...opts, continued: isLastEmoji ? !!opts.continued : true });
|
|
394
|
-
usedExplicitCoords = true;
|
|
395
|
-
}
|
|
396
|
-
else {
|
|
397
|
-
doc.text(em[0], { ...opts, continued: isLastEmoji ? !!opts.continued : true });
|
|
398
|
-
}
|
|
399
|
-
doc.font(prevFont).fontSize(prevSize);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
else {
|
|
404
|
-
// ── Text segment ──
|
|
405
|
-
doc.font(prevFont).fontSize(prevSize);
|
|
406
|
-
if (!usedExplicitCoords && firstX !== undefined) {
|
|
407
|
-
doc.text(seg.text, firstX, firstY, { ...opts, continued: cont });
|
|
408
|
-
usedExplicitCoords = true;
|
|
409
|
-
}
|
|
410
|
-
else {
|
|
411
|
-
doc.text(seg.text, { ...opts, continued: cont });
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
// ── Pass 2: overlay emoji images at recorded positions ──
|
|
416
|
-
const savedY = doc.y;
|
|
417
|
-
const savedX = doc.x;
|
|
418
|
-
for (const ep of emojiPlacements) {
|
|
419
|
-
doc.image(ep.png, ep.x, ep.y, {
|
|
420
|
-
width: emojiSize,
|
|
421
|
-
height: emojiSize,
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
doc.y = savedY;
|
|
425
|
-
doc.x = savedX;
|
|
426
|
-
doc.font(prevFont).fontSize(prevSize);
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
// ── Monochrome font path (original behaviour) ────────────────────────
|
|
430
|
-
for (let i = 0; i < segments.length; i++) {
|
|
431
|
-
const seg = segments[i];
|
|
432
|
-
const isLast = i === segments.length - 1;
|
|
433
|
-
const cont = isLast ? !!opts.continued : true;
|
|
434
|
-
if (seg.isEmoji) {
|
|
435
|
-
doc.font(EMOJI_FONT_NAME).fontSize(prevSize);
|
|
436
|
-
}
|
|
437
|
-
else {
|
|
438
|
-
doc.font(prevFont).fontSize(prevSize);
|
|
439
|
-
}
|
|
440
|
-
// Only pass explicit x,y for the very first segment.
|
|
441
|
-
if (i === 0 && firstX !== undefined) {
|
|
442
|
-
doc.text(seg.text, firstX, firstY, { ...opts, continued: cont });
|
|
443
|
-
}
|
|
444
|
-
else {
|
|
445
|
-
doc.text(seg.text, { ...opts, continued: cont });
|
|
446
|
-
}
|
|
298
|
+
else {
|
|
299
|
+
doc.text(text, opts);
|
|
447
300
|
}
|
|
448
|
-
// Restore the original font so subsequent calls aren't surprised.
|
|
449
|
-
doc.font(prevFont).fontSize(prevSize);
|
|
450
301
|
}
|
|
451
302
|
function renderCodespan(text, continued) {
|
|
452
303
|
const cs = theme.code.inline;
|
|
@@ -504,7 +355,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
504
355
|
doc.font(safeFont(theme.body.font)).fontSize(theme.body.fontSize).fillColor(theme.linkColor);
|
|
505
356
|
}
|
|
506
357
|
const linkText = tok.text || tok.href;
|
|
507
|
-
|
|
358
|
+
renderText(linkText, { continued, underline: true, link: tok.href });
|
|
508
359
|
doc.fillColor(headingCtx ? headingCtx.color : theme.body.color);
|
|
509
360
|
}
|
|
510
361
|
async function renderInlineTokens(inlineTokens, continued, insideBold = false, insideItalic = false) {
|
|
@@ -520,7 +371,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
520
371
|
}
|
|
521
372
|
else {
|
|
522
373
|
applyBodyFont(insideBold, insideItalic);
|
|
523
|
-
|
|
374
|
+
renderText(t.text, { continued: cont, underline: false, strike: false });
|
|
524
375
|
}
|
|
525
376
|
break;
|
|
526
377
|
}
|
|
@@ -548,12 +399,12 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
548
399
|
}
|
|
549
400
|
case 'del': {
|
|
550
401
|
applyBodyFont(insideBold, insideItalic);
|
|
551
|
-
|
|
402
|
+
renderText(tok.text, { continued: cont, strike: true, underline: false });
|
|
552
403
|
break;
|
|
553
404
|
}
|
|
554
405
|
case 'escape': {
|
|
555
406
|
applyBodyFont(insideBold, insideItalic);
|
|
556
|
-
|
|
407
|
+
renderText(tok.text, { continued: cont, underline: false, strike: false });
|
|
557
408
|
break;
|
|
558
409
|
}
|
|
559
410
|
case 'br': {
|
|
@@ -564,7 +415,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
564
415
|
const raw = tok.text ?? tok.raw ?? '';
|
|
565
416
|
if (raw) {
|
|
566
417
|
applyBodyFont(insideBold, insideItalic);
|
|
567
|
-
|
|
418
|
+
renderText(raw, { continued: cont, underline: false, strike: false });
|
|
568
419
|
}
|
|
569
420
|
break;
|
|
570
421
|
}
|
|
@@ -588,9 +439,12 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
588
439
|
displayWidth = img.width * (displayHeight / img.height);
|
|
589
440
|
}
|
|
590
441
|
ensureSpace(displayHeight + 10);
|
|
591
|
-
|
|
442
|
+
// Compute horizontal position based on imageAlign
|
|
443
|
+
const imgX = theme.imageAlign === 'center'
|
|
444
|
+
? margins.left + (contentWidth - displayWidth) / 2
|
|
445
|
+
: doc.x;
|
|
592
446
|
const imgY = doc.y;
|
|
593
|
-
doc.image(imgBuffer, { width: displayWidth, height: displayHeight });
|
|
447
|
+
doc.image(imgBuffer, imgX, imgY, { width: displayWidth, height: displayHeight });
|
|
594
448
|
// If the image is wrapped in a link, overlay a clickable annotation
|
|
595
449
|
if (linkUrl) {
|
|
596
450
|
doc.link(imgX, imgY, displayWidth, displayHeight, linkUrl);
|
|
@@ -605,13 +459,13 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
605
459
|
}
|
|
606
460
|
}
|
|
607
461
|
async function renderList(list, depth) {
|
|
608
|
-
const indent = margins.left + depth *
|
|
462
|
+
const indent = margins.left + depth * sp.listIndent;
|
|
609
463
|
for (let idx = 0; idx < list.items.length; idx++) {
|
|
610
464
|
const item = list.items[idx];
|
|
611
465
|
ensureSpace(theme.body.fontSize * 2);
|
|
612
466
|
resetBodyFont();
|
|
613
467
|
const bullet = list.ordered ? `${list.start + idx}.` : '•';
|
|
614
|
-
doc.text(bullet, indent, doc.y, { continued: true, width: contentWidth - depth *
|
|
468
|
+
doc.text(bullet, indent, doc.y, { continued: true, width: contentWidth - depth * sp.listIndent });
|
|
615
469
|
doc.text(' ', { continued: true });
|
|
616
470
|
// Render item inline tokens
|
|
617
471
|
const itemTokens = item.tokens;
|
|
@@ -622,7 +476,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
622
476
|
await renderInlineTokens(t.tokens, false);
|
|
623
477
|
}
|
|
624
478
|
else {
|
|
625
|
-
|
|
479
|
+
renderText(t.text);
|
|
626
480
|
}
|
|
627
481
|
}
|
|
628
482
|
else if (child.type === 'paragraph') {
|
|
@@ -632,7 +486,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
632
486
|
await renderList(child, depth + 1);
|
|
633
487
|
}
|
|
634
488
|
}
|
|
635
|
-
doc.moveDown(
|
|
489
|
+
doc.moveDown(sp.listItemSpacing);
|
|
636
490
|
}
|
|
637
491
|
}
|
|
638
492
|
async function renderCellTokens(cell, x, y, width, align, bold) {
|
|
@@ -646,7 +500,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
646
500
|
else {
|
|
647
501
|
// Fallback: plain text (no inline tokens)
|
|
648
502
|
applyBodyFont(bold, false);
|
|
649
|
-
|
|
503
|
+
renderText(cell.text, x, y, { width, align });
|
|
650
504
|
}
|
|
651
505
|
doc.y = savedY;
|
|
652
506
|
}
|
|
@@ -715,8 +569,8 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
715
569
|
const t = token;
|
|
716
570
|
const key = `h${t.depth}`;
|
|
717
571
|
const style = theme.headings[key];
|
|
718
|
-
const spaceAbove = style.fontSize *
|
|
719
|
-
const spaceBelow = style.fontSize *
|
|
572
|
+
const spaceAbove = style.fontSize * sp.headingSpaceAbove;
|
|
573
|
+
const spaceBelow = style.fontSize * sp.headingSpaceBelow;
|
|
720
574
|
ensureSpace(spaceAbove + style.fontSize + spaceBelow);
|
|
721
575
|
doc.moveDown(spaceAbove / doc.currentLineHeight());
|
|
722
576
|
doc.font(safeFont(style.font)).fontSize(style.fontSize).fillColor(style.color);
|
|
@@ -725,7 +579,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
725
579
|
await renderInlineTokens(t.tokens, false, style.bold ?? false, style.italic ?? false);
|
|
726
580
|
}
|
|
727
581
|
else {
|
|
728
|
-
|
|
582
|
+
renderText(t.text);
|
|
729
583
|
}
|
|
730
584
|
headingCtx = null;
|
|
731
585
|
// Draw an underline beneath h1 and h2
|
|
@@ -748,7 +602,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
748
602
|
ensureSpace(theme.body.fontSize * 2);
|
|
749
603
|
resetBodyFont();
|
|
750
604
|
await renderInlineTokens(t.tokens, false);
|
|
751
|
-
doc.moveDown(
|
|
605
|
+
doc.moveDown(sp.paragraphSpacing);
|
|
752
606
|
break;
|
|
753
607
|
}
|
|
754
608
|
case 'code': {
|
|
@@ -772,10 +626,11 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
772
626
|
lineNumbers,
|
|
773
627
|
drawBackground: true,
|
|
774
628
|
theme: theme.syntaxHighlight,
|
|
629
|
+
borderRadius: cs.borderRadius,
|
|
775
630
|
});
|
|
776
631
|
doc.x = margins.left;
|
|
777
632
|
doc.y = newY;
|
|
778
|
-
doc.moveDown(
|
|
633
|
+
doc.moveDown(sp.codeBlockSpacing);
|
|
779
634
|
resetBodyFont();
|
|
780
635
|
}
|
|
781
636
|
else {
|
|
@@ -786,8 +641,14 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
786
641
|
ensureSpace(blockH + 10);
|
|
787
642
|
const x = margins.left;
|
|
788
643
|
const y = doc.y;
|
|
644
|
+
const cbRadius = cs.borderRadius ?? 0;
|
|
789
645
|
doc.save();
|
|
790
|
-
|
|
646
|
+
if (cbRadius > 0) {
|
|
647
|
+
doc.roundedRect(x, y, contentWidth, blockH, cbRadius).fill(cs.backgroundColor);
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
doc.rect(x, y, contentWidth, blockH).fill(cs.backgroundColor);
|
|
651
|
+
}
|
|
791
652
|
doc.restore();
|
|
792
653
|
doc.font(safeFont(cs.font)).fontSize(cs.fontSize).fillColor(cs.color);
|
|
793
654
|
let textY = y + cs.padding;
|
|
@@ -797,7 +658,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
797
658
|
}
|
|
798
659
|
doc.x = margins.left;
|
|
799
660
|
doc.y = y + blockH;
|
|
800
|
-
doc.moveDown(
|
|
661
|
+
doc.moveDown(sp.codeBlockSpacing);
|
|
801
662
|
resetBodyFont();
|
|
802
663
|
}
|
|
803
664
|
break;
|
|
@@ -806,7 +667,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
806
667
|
const t = token;
|
|
807
668
|
const bq = theme.blockquote;
|
|
808
669
|
ensureSpace(30);
|
|
809
|
-
const bqPadding =
|
|
670
|
+
const bqPadding = bq.padding ?? 6;
|
|
810
671
|
const startY = doc.y;
|
|
811
672
|
doc.y += bqPadding; // add top padding before text
|
|
812
673
|
const textX = margins.left + bq.borderWidth + bq.indent;
|
|
@@ -818,7 +679,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
818
679
|
doc.font(safeFont(font)).fontSize(theme.body.fontSize).fillColor(theme.body.color);
|
|
819
680
|
doc.text('', textX, doc.y, { width: textWidth });
|
|
820
681
|
await renderInlineTokens(p.tokens, false, false, bq.italic);
|
|
821
|
-
doc.moveDown(
|
|
682
|
+
doc.moveDown(sp.blockquoteSpacing);
|
|
822
683
|
}
|
|
823
684
|
else {
|
|
824
685
|
await renderToken(child);
|
|
@@ -826,11 +687,18 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
826
687
|
}
|
|
827
688
|
doc.y += bqPadding; // add bottom padding after text
|
|
828
689
|
const endY = doc.y;
|
|
690
|
+
// Draw optional background fill behind blockquote area
|
|
691
|
+
if (bq.backgroundColor) {
|
|
692
|
+
doc.save();
|
|
693
|
+
doc.rect(margins.left + bq.borderWidth, startY, contentWidth - bq.borderWidth, endY - startY).fill(bq.backgroundColor);
|
|
694
|
+
doc.restore();
|
|
695
|
+
}
|
|
696
|
+
// Draw left border
|
|
829
697
|
doc.save();
|
|
830
698
|
doc.rect(margins.left, startY, bq.borderWidth, endY - startY).fill(bq.borderColor);
|
|
831
699
|
doc.restore();
|
|
832
700
|
doc.x = margins.left;
|
|
833
|
-
doc.moveDown(
|
|
701
|
+
doc.moveDown(sp.blockquoteSpacing);
|
|
834
702
|
resetBodyFont();
|
|
835
703
|
break;
|
|
836
704
|
}
|
|
@@ -841,7 +709,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
841
709
|
}
|
|
842
710
|
case 'hr': {
|
|
843
711
|
ensureSpace(20);
|
|
844
|
-
doc.moveDown(
|
|
712
|
+
doc.moveDown(sp.hrSpacing);
|
|
845
713
|
const y = doc.y;
|
|
846
714
|
doc.save();
|
|
847
715
|
doc.strokeColor(theme.horizontalRuleColor).lineWidth(1)
|
|
@@ -850,7 +718,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
850
718
|
.stroke();
|
|
851
719
|
doc.restore();
|
|
852
720
|
doc.y = y;
|
|
853
|
-
doc.moveDown(
|
|
721
|
+
doc.moveDown(sp.hrSpacing);
|
|
854
722
|
resetBodyFont();
|
|
855
723
|
break;
|
|
856
724
|
}
|
package/dist/styles.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type { ThemeConfig, PageLayout, SyntaxHighlightTheme } from './types.js';
|
|
1
|
+
import type { ThemeConfig, PageLayout, SpacingConfig, SyntaxHighlightTheme } from './types.js';
|
|
2
|
+
/** Default spacing values — used as fallbacks when theme.spacing is partial or absent. */
|
|
3
|
+
export declare const defaultSpacing: Required<SpacingConfig>;
|
|
2
4
|
/** Prism.js default light theme — used as the default (print-friendly). */
|
|
3
5
|
export declare const defaultSyntaxHighlightTheme: SyntaxHighlightTheme;
|
|
4
6
|
export declare const defaultTheme: ThemeConfig;
|
package/dist/styles.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.defaultPageLayout = exports.defaultTheme = exports.defaultSyntaxHighlightTheme = void 0;
|
|
3
|
+
exports.defaultPageLayout = exports.defaultTheme = exports.defaultSyntaxHighlightTheme = exports.defaultSpacing = void 0;
|
|
4
|
+
/** Default spacing values — used as fallbacks when theme.spacing is partial or absent. */
|
|
5
|
+
exports.defaultSpacing = {
|
|
6
|
+
headingSpaceAbove: 0.8,
|
|
7
|
+
headingSpaceBelow: 0.3,
|
|
8
|
+
paragraphSpacing: 0.5,
|
|
9
|
+
listItemSpacing: 0.2,
|
|
10
|
+
listIndent: 20,
|
|
11
|
+
blockquoteSpacing: 0.3,
|
|
12
|
+
codeBlockSpacing: 0.5,
|
|
13
|
+
hrSpacing: 0.5,
|
|
14
|
+
};
|
|
4
15
|
/** Prism.js default light theme — used as the default (print-friendly). */
|
|
5
16
|
exports.defaultSyntaxHighlightTheme = {
|
|
6
17
|
background: '#f5f2f0',
|
|
@@ -81,6 +92,9 @@ exports.defaultTheme = {
|
|
|
81
92
|
zebraColor: '#f9f9f9',
|
|
82
93
|
},
|
|
83
94
|
syntaxHighlight: exports.defaultSyntaxHighlightTheme,
|
|
95
|
+
spacing: { ...exports.defaultSpacing },
|
|
96
|
+
imageAlign: 'left',
|
|
97
|
+
emojiFont: 'twemoji',
|
|
84
98
|
};
|
|
85
99
|
exports.defaultPageLayout = {
|
|
86
100
|
pageSize: 'LETTER',
|
package/dist/themes/index.js
CHANGED
|
@@ -39,6 +39,9 @@ exports.modernTheme = {
|
|
|
39
39
|
default: '#2d3436',
|
|
40
40
|
},
|
|
41
41
|
},
|
|
42
|
+
spacing: { ...styles_js_1.defaultSpacing },
|
|
43
|
+
imageAlign: 'left',
|
|
44
|
+
emojiFont: 'twemoji',
|
|
42
45
|
};
|
|
43
46
|
/** Academic — serif fonts, formal look inspired by LaTeX */
|
|
44
47
|
exports.academicTheme = {
|
|
@@ -75,6 +78,9 @@ exports.academicTheme = {
|
|
|
75
78
|
default: '#1a1a2e',
|
|
76
79
|
},
|
|
77
80
|
},
|
|
81
|
+
spacing: { ...styles_js_1.defaultSpacing },
|
|
82
|
+
imageAlign: 'left',
|
|
83
|
+
emojiFont: 'twemoji',
|
|
78
84
|
};
|
|
79
85
|
/** Minimal — lots of whitespace, muted greys */
|
|
80
86
|
exports.minimalTheme = {
|
|
@@ -111,6 +117,9 @@ exports.minimalTheme = {
|
|
|
111
117
|
default: '#333333',
|
|
112
118
|
},
|
|
113
119
|
},
|
|
120
|
+
spacing: { ...styles_js_1.defaultSpacing },
|
|
121
|
+
imageAlign: 'left',
|
|
122
|
+
emojiFont: 'twemoji',
|
|
114
123
|
};
|
|
115
124
|
/** Ocean — deep blue palette */
|
|
116
125
|
exports.oceanTheme = {
|
|
@@ -147,6 +156,9 @@ exports.oceanTheme = {
|
|
|
147
156
|
default: '#1b2631',
|
|
148
157
|
},
|
|
149
158
|
},
|
|
159
|
+
spacing: { ...styles_js_1.defaultSpacing },
|
|
160
|
+
imageAlign: 'left',
|
|
161
|
+
emojiFont: 'twemoji',
|
|
150
162
|
};
|
|
151
163
|
/** Record of all built-in themes keyed by display name */
|
|
152
164
|
exports.themes = {
|
package/dist/types.d.ts
CHANGED
|
@@ -23,12 +23,15 @@ export interface CodeStyle {
|
|
|
23
23
|
}
|
|
24
24
|
export interface CodeBlockStyle extends CodeStyle {
|
|
25
25
|
padding: number;
|
|
26
|
+
borderRadius?: number;
|
|
26
27
|
}
|
|
27
28
|
export interface BlockquoteStyle {
|
|
28
29
|
borderColor: string;
|
|
29
30
|
borderWidth: number;
|
|
30
31
|
italic: boolean;
|
|
31
32
|
indent: number;
|
|
33
|
+
backgroundColor?: string;
|
|
34
|
+
padding?: number;
|
|
32
35
|
}
|
|
33
36
|
export interface TableStyles {
|
|
34
37
|
headerBackground: string;
|
|
@@ -49,6 +52,16 @@ export interface SyntaxHighlightTheme {
|
|
|
49
52
|
lineHighlight: string;
|
|
50
53
|
tokens: TokenColors;
|
|
51
54
|
}
|
|
55
|
+
export interface SpacingConfig {
|
|
56
|
+
headingSpaceAbove?: number;
|
|
57
|
+
headingSpaceBelow?: number;
|
|
58
|
+
paragraphSpacing?: number;
|
|
59
|
+
listItemSpacing?: number;
|
|
60
|
+
listIndent?: number;
|
|
61
|
+
blockquoteSpacing?: number;
|
|
62
|
+
codeBlockSpacing?: number;
|
|
63
|
+
hrSpacing?: number;
|
|
64
|
+
}
|
|
52
65
|
export interface ThemeConfig {
|
|
53
66
|
headings: {
|
|
54
67
|
h1: TextStyle;
|
|
@@ -72,6 +85,17 @@ export interface ThemeConfig {
|
|
|
72
85
|
* When omitted, falls back to a VS Code Dark+ inspired palette.
|
|
73
86
|
*/
|
|
74
87
|
syntaxHighlight?: SyntaxHighlightTheme;
|
|
88
|
+
/** Configurable spacing multipliers and indentation values. */
|
|
89
|
+
spacing?: SpacingConfig;
|
|
90
|
+
/** Image horizontal alignment. */
|
|
91
|
+
imageAlign?: 'left' | 'center';
|
|
92
|
+
/** Emoji font to use for rendering emoji characters.
|
|
93
|
+
* - `'twemoji'` (default) — use the bundled Twemoji.Mozilla.ttf color emoji font.
|
|
94
|
+
* - `'openmoji'` — use the bundled OpenMoji-Color.ttf (COLR) emoji font.
|
|
95
|
+
* - `'noto'` — use the bundled NotoColorEmoji.ttf (CBDT) emoji font.
|
|
96
|
+
* - `'none'` — disable emoji font; emoji render with the body font.
|
|
97
|
+
*/
|
|
98
|
+
emojiFont?: 'twemoji' | 'openmoji' | 'noto' | 'none';
|
|
75
99
|
}
|
|
76
100
|
/**
|
|
77
101
|
* A custom font definition providing font data for registration with PDFKit.
|
|
@@ -92,8 +116,6 @@ export interface CustomFontDefinition {
|
|
|
92
116
|
/** Bold-italic variant (falls back to bold or regular). */
|
|
93
117
|
boldItalic?: Buffer;
|
|
94
118
|
}
|
|
95
|
-
/** Converts a single emoji string to a PNG `Buffer`. */
|
|
96
|
-
export type ColorEmojiRenderer = (emoji: string) => Promise<Buffer>;
|
|
97
119
|
export interface PdfOptions {
|
|
98
120
|
theme?: ThemeConfig;
|
|
99
121
|
pageLayout?: PageLayout;
|
|
@@ -151,20 +173,6 @@ export interface PdfOptions {
|
|
|
151
173
|
* @default true
|
|
152
174
|
*/
|
|
153
175
|
emojiFont?: boolean | string | Buffer;
|
|
154
|
-
/**
|
|
155
|
-
* Color emoji renderer.
|
|
156
|
-
*
|
|
157
|
-
* When provided, emoji characters are rendered as inline color PNG images
|
|
158
|
-
* (sourced from Twemoji SVGs) instead of monochrome font glyphs.
|
|
159
|
-
*
|
|
160
|
-
* Use `createNodeColorEmojiRenderer()` (Node.js) or
|
|
161
|
-
* `createBrowserColorEmojiRenderer()` (browser) to obtain a renderer.
|
|
162
|
-
*
|
|
163
|
-
* Takes priority over `emojiFont` for emoji that are successfully rendered.
|
|
164
|
-
* Emoji that fail to render (e.g. missing from Twemoji) fall back to the
|
|
165
|
-
* monochrome font or the body font.
|
|
166
|
-
*/
|
|
167
|
-
colorEmoji?: ColorEmojiRenderer;
|
|
168
176
|
/**
|
|
169
177
|
* Custom font definitions to register with PDFKit.
|
|
170
178
|
*
|
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.17",
|
|
4
4
|
"description": "A new project created with Intent by Augment.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
"README.md"
|
|
30
30
|
],
|
|
31
31
|
"scripts": {
|
|
32
|
+
"build:pdfkit": "node -e \"const fs=require('fs'),p=require('path'),{execSync:e}=require('child_process'),d=p.join('node_modules','pdfkit','js');if(!fs.existsSync(d)){const t='/tmp/pdfkit-build-'+Date.now();e('git clone --depth 1 --branch support-color-emoji-google https://github.com/jspears/pdfkit.git '+t,{stdio:'inherit'});const pd=p.resolve('node_modules/pdfkit');for(const f of['lib','rollup.config.js','.babelrc','yarn.lock']){const s=p.join(t,f);if(fs.existsSync(s)){e('cp -r '+s+' '+pd,{stdio:'inherit'})}}e('cd '+pd+' && npm install --ignore-scripts && npx rollup -c',{stdio:'inherit'});e('rm -rf '+t,{stdio:'inherit'})}\"",
|
|
33
|
+
"postinstall": "node -e \"if(require('fs').existsSync('src')){require('child_process').execSync('npm run build:pdfkit',{stdio:'inherit'})}\" || true && node scripts/patch-fontkit-colr.js",
|
|
32
34
|
"docs:dev": "cd docs && $npm_execpath dev",
|
|
33
35
|
"build": "tsc && mkdir -p dist/fonts && cp src/fonts/* dist/fonts/",
|
|
34
36
|
"generate": "tsx samples/generate.ts",
|
|
@@ -50,7 +52,7 @@
|
|
|
50
52
|
"dependencies": {
|
|
51
53
|
"@resvg/resvg-js": "^2.6.2",
|
|
52
54
|
"marked": "^17.0.2",
|
|
53
|
-
"pdfkit": "
|
|
55
|
+
"pdfkit": "github:jspears/pdfkit#support-color-emoji-google",
|
|
54
56
|
"prismjs": "^1.30.0"
|
|
55
57
|
},
|
|
56
58
|
"devDependencies": {
|
package/dist/color-emoji.d.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Color emoji rendering via SVG → PNG conversion.
|
|
3
|
-
*
|
|
4
|
-
* Converts emoji characters to color PNG images using Twemoji SVG assets,
|
|
5
|
-
* with factory functions for Node.js and browser environments.
|
|
6
|
-
*/
|
|
7
|
-
/** Pixel size at which emoji PNGs are rasterised (scaled to font size by PDFKit). */
|
|
8
|
-
export declare const EMOJI_RENDER_SIZE = 128;
|
|
9
|
-
/**
|
|
10
|
-
* Convert an emoji string to its Twemoji SVG filename (without extension).
|
|
11
|
-
*
|
|
12
|
-
* Twemoji filenames use lower-case hex codepoints separated by hyphens,
|
|
13
|
-
* with the variation selector U+FE0F omitted.
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* emojiToTwemojiCodepoints('🎉') // '1f389'
|
|
17
|
-
* emojiToTwemojiCodepoints('👨👩👧👦') // '1f468-200d-1f469-200d-1f467-200d-1f466'
|
|
18
|
-
*/
|
|
19
|
-
export declare function emojiToTwemojiCodepoints(emoji: string): string;
|
|
20
|
-
/** Build the full Twemoji CDN URL for a single emoji. */
|
|
21
|
-
export declare function twemojiSvgUrl(emoji: string): string;
|
|
22
|
-
/** Ensure the SVG has explicit pixel dimensions for consistent rasterisation. */
|
|
23
|
-
export declare function sizeSvg(svg: string, size: number): string;
|
|
24
|
-
/** Converts a single emoji string to a PNG `Buffer`. */
|
|
25
|
-
export type ColorEmojiRenderer = (emoji: string) => Promise<Buffer>;
|
|
26
|
-
/**
|
|
27
|
-
* Creates a color-emoji renderer for **browser** environments.
|
|
28
|
-
*
|
|
29
|
-
* Fetches Twemoji SVGs via `fetch()`, rasterises them on a `<canvas>`, and
|
|
30
|
-
* returns a PNG `Buffer`. Results are cached.
|
|
31
|
-
*/
|
|
32
|
-
export declare function createBrowserColorEmojiRenderer(): ColorEmojiRenderer;
|
|
33
|
-
/**
|
|
34
|
-
* Scans the full markdown text for all unique emoji and pre-renders them to
|
|
35
|
-
* PNG `Buffer`s. The returned `Map` allows `renderTextWithEmoji` to stay
|
|
36
|
-
* synchronous during rendering.
|
|
37
|
-
*
|
|
38
|
-
* @param text The raw markdown (or any text) to scan.
|
|
39
|
-
* @param renderer A `ColorEmojiRenderer` (Node.js or browser factory).
|
|
40
|
-
* @param emojiRe The emoji-matching regex (from `emoji.ts`).
|
|
41
|
-
* @returns A `Map` from individual emoji string → PNG Buffer.
|
|
42
|
-
*/
|
|
43
|
-
export declare function preRenderEmoji(text: string, renderer: ColorEmojiRenderer, emojiRe: RegExp): Promise<Map<string, Buffer>>;
|
package/dist/color-emoji.js
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Color emoji rendering via SVG → PNG conversion.
|
|
4
|
-
*
|
|
5
|
-
* Converts emoji characters to color PNG images using Twemoji SVG assets,
|
|
6
|
-
* with factory functions for Node.js and browser environments.
|
|
7
|
-
*/
|
|
8
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.EMOJI_RENDER_SIZE = void 0;
|
|
10
|
-
exports.emojiToTwemojiCodepoints = emojiToTwemojiCodepoints;
|
|
11
|
-
exports.twemojiSvgUrl = twemojiSvgUrl;
|
|
12
|
-
exports.sizeSvg = sizeSvg;
|
|
13
|
-
exports.createBrowserColorEmojiRenderer = createBrowserColorEmojiRenderer;
|
|
14
|
-
exports.preRenderEmoji = preRenderEmoji;
|
|
15
|
-
/**
|
|
16
|
-
* Twemoji SVG CDN base URL.
|
|
17
|
-
* Uses the community-maintained fork (`jdecked/twemoji`) since the original
|
|
18
|
-
* Twitter project was discontinued.
|
|
19
|
-
*/
|
|
20
|
-
const TWEMOJI_BASE = 'https://cdn.jsdelivr.net/gh/jdecked/twemoji@latest/assets/svg/';
|
|
21
|
-
/** Pixel size at which emoji PNGs are rasterised (scaled to font size by PDFKit). */
|
|
22
|
-
exports.EMOJI_RENDER_SIZE = 128;
|
|
23
|
-
// ── Codepoint helpers ──────────────────────────────────────────────────────
|
|
24
|
-
/**
|
|
25
|
-
* Convert an emoji string to its Twemoji SVG filename (without extension).
|
|
26
|
-
*
|
|
27
|
-
* Twemoji filenames use lower-case hex codepoints separated by hyphens,
|
|
28
|
-
* with the variation selector U+FE0F omitted.
|
|
29
|
-
*
|
|
30
|
-
* @example
|
|
31
|
-
* emojiToTwemojiCodepoints('🎉') // '1f389'
|
|
32
|
-
* emojiToTwemojiCodepoints('👨👩👧👦') // '1f468-200d-1f469-200d-1f467-200d-1f466'
|
|
33
|
-
*/
|
|
34
|
-
function emojiToTwemojiCodepoints(emoji) {
|
|
35
|
-
const cps = [];
|
|
36
|
-
for (const ch of emoji) {
|
|
37
|
-
const cp = ch.codePointAt(0);
|
|
38
|
-
if (cp === undefined)
|
|
39
|
-
continue;
|
|
40
|
-
// Twemoji filenames omit VS16 (U+FE0F)
|
|
41
|
-
if (cp === 0xfe0f)
|
|
42
|
-
continue;
|
|
43
|
-
cps.push(cp.toString(16));
|
|
44
|
-
}
|
|
45
|
-
return cps.join('-');
|
|
46
|
-
}
|
|
47
|
-
/** Build the full Twemoji CDN URL for a single emoji. */
|
|
48
|
-
function twemojiSvgUrl(emoji) {
|
|
49
|
-
return `${TWEMOJI_BASE}${emojiToTwemojiCodepoints(emoji)}.svg`;
|
|
50
|
-
}
|
|
51
|
-
// ── SVG sizing ─────────────────────────────────────────────────────────────
|
|
52
|
-
/** Ensure the SVG has explicit pixel dimensions for consistent rasterisation. */
|
|
53
|
-
function sizeSvg(svg, size) {
|
|
54
|
-
// Replace existing width/height attributes …
|
|
55
|
-
let out = svg;
|
|
56
|
-
if (/width="[^"]*"/.test(out)) {
|
|
57
|
-
out = out.replace(/width="[^"]*"/, `width="${size}"`);
|
|
58
|
-
out = out.replace(/height="[^"]*"/, `height="${size}"`);
|
|
59
|
-
return out;
|
|
60
|
-
}
|
|
61
|
-
// … or insert them.
|
|
62
|
-
return out.replace('<svg', `<svg width="${size}" height="${size}"`);
|
|
63
|
-
}
|
|
64
|
-
// ── Browser factory ─────────────────────────────────────────────────────────
|
|
65
|
-
/**
|
|
66
|
-
* Creates a color-emoji renderer for **browser** environments.
|
|
67
|
-
*
|
|
68
|
-
* Fetches Twemoji SVGs via `fetch()`, rasterises them on a `<canvas>`, and
|
|
69
|
-
* returns a PNG `Buffer`. Results are cached.
|
|
70
|
-
*/
|
|
71
|
-
function createBrowserColorEmojiRenderer() {
|
|
72
|
-
const cache = new Map();
|
|
73
|
-
return async (emoji) => {
|
|
74
|
-
const hit = cache.get(emoji);
|
|
75
|
-
if (hit)
|
|
76
|
-
return hit;
|
|
77
|
-
const url = twemojiSvgUrl(emoji);
|
|
78
|
-
const res = await fetch(url);
|
|
79
|
-
if (!res.ok)
|
|
80
|
-
throw new Error(`HTTP ${res.status} fetching ${url}`);
|
|
81
|
-
const svgText = await res.text();
|
|
82
|
-
const sized = sizeSvg(svgText, exports.EMOJI_RENDER_SIZE);
|
|
83
|
-
// Render SVG → Canvas → PNG Blob → Buffer
|
|
84
|
-
const png = await new Promise((resolve, reject) => {
|
|
85
|
-
const img = new Image();
|
|
86
|
-
img.onload = () => {
|
|
87
|
-
const canvas = document.createElement('canvas');
|
|
88
|
-
canvas.width = exports.EMOJI_RENDER_SIZE;
|
|
89
|
-
canvas.height = exports.EMOJI_RENDER_SIZE;
|
|
90
|
-
const ctx = canvas.getContext('2d');
|
|
91
|
-
if (!ctx) {
|
|
92
|
-
reject(new Error('Canvas 2D context unavailable'));
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
ctx.drawImage(img, 0, 0, exports.EMOJI_RENDER_SIZE, exports.EMOJI_RENDER_SIZE);
|
|
96
|
-
canvas.toBlob((blob) => {
|
|
97
|
-
if (!blob) {
|
|
98
|
-
reject(new Error('toBlob failed'));
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
blob.arrayBuffer().then((ab) => resolve(Buffer.from(ab)), reject);
|
|
102
|
-
}, 'image/png');
|
|
103
|
-
};
|
|
104
|
-
img.onerror = () => reject(new Error(`Failed to load SVG as image: ${url}`));
|
|
105
|
-
img.src = `data:image/svg+xml;base64,${btoa(sized)}`;
|
|
106
|
-
});
|
|
107
|
-
cache.set(emoji, png);
|
|
108
|
-
return png;
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
// ── Pre-render helper ───────────────────────────────────────────────────────
|
|
112
|
-
/**
|
|
113
|
-
* Scans the full markdown text for all unique emoji and pre-renders them to
|
|
114
|
-
* PNG `Buffer`s. The returned `Map` allows `renderTextWithEmoji` to stay
|
|
115
|
-
* synchronous during rendering.
|
|
116
|
-
*
|
|
117
|
-
* @param text The raw markdown (or any text) to scan.
|
|
118
|
-
* @param renderer A `ColorEmojiRenderer` (Node.js or browser factory).
|
|
119
|
-
* @param emojiRe The emoji-matching regex (from `emoji.ts`).
|
|
120
|
-
* @returns A `Map` from individual emoji string → PNG Buffer.
|
|
121
|
-
*/
|
|
122
|
-
async function preRenderEmoji(text, renderer, emojiRe) {
|
|
123
|
-
const unique = new Set();
|
|
124
|
-
emojiRe.lastIndex = 0;
|
|
125
|
-
let m;
|
|
126
|
-
while ((m = emojiRe.exec(text)) !== null) {
|
|
127
|
-
unique.add(m[0]);
|
|
128
|
-
}
|
|
129
|
-
const map = new Map();
|
|
130
|
-
// Render all unique emoji in parallel
|
|
131
|
-
await Promise.all([...unique].map(async (emoji) => {
|
|
132
|
-
try {
|
|
133
|
-
const png = await renderer(emoji);
|
|
134
|
-
map.set(emoji, png);
|
|
135
|
-
}
|
|
136
|
-
catch {
|
|
137
|
-
// If a specific emoji fails (missing from Twemoji), skip it —
|
|
138
|
-
// it will fall back to the monochrome font or the body font.
|
|
139
|
-
}
|
|
140
|
-
}));
|
|
141
|
-
return map;
|
|
142
|
-
}
|
package/dist/emoji.d.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Emoji detection and text segmentation utilities.
|
|
3
|
-
*
|
|
4
|
-
* Splits a string into runs of "emoji" vs "non-emoji" characters so the
|
|
5
|
-
* renderer can switch to an emoji font (e.g. Noto Emoji) for glyph coverage.
|
|
6
|
-
*/
|
|
7
|
-
export interface TextSegment {
|
|
8
|
-
text: string;
|
|
9
|
-
isEmoji: boolean;
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Returns `true` when the whole string is emoji (one or more).
|
|
13
|
-
*/
|
|
14
|
-
export declare function isEmoji(text: string): boolean;
|
|
15
|
-
/**
|
|
16
|
-
* Split `text` into contiguous runs of emoji and non-emoji characters.
|
|
17
|
-
*
|
|
18
|
-
* Example:
|
|
19
|
-
* splitEmojiSegments("Hello 🎉🔥 world")
|
|
20
|
-
* => [
|
|
21
|
-
* { text: "Hello ", isEmoji: false },
|
|
22
|
-
* { text: "🎉🔥", isEmoji: true },
|
|
23
|
-
* { text: " world", isEmoji: false },
|
|
24
|
-
* ]
|
|
25
|
-
*/
|
|
26
|
-
export declare function splitEmojiSegments(text: string): TextSegment[];
|
|
27
|
-
/**
|
|
28
|
-
* Quick check: does the string contain at least one emoji?
|
|
29
|
-
*/
|
|
30
|
-
export declare function containsEmoji(text: string): boolean;
|
|
31
|
-
/**
|
|
32
|
-
* Returns the module-level emoji regex. Useful for external callers
|
|
33
|
-
* (e.g. `preRenderEmoji`) that need to scan text with the same pattern.
|
|
34
|
-
*/
|
|
35
|
-
export declare function getEmojiRegex(): RegExp;
|
package/dist/emoji.js
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
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
|
-
}
|
|
Binary file
|
|
@@ -1,15 +0,0 @@
|
|
|
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;
|
package/dist/node-color-emoji.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
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
|
-
}
|