@speajus/markdown-to-pdf 1.0.15 → 1.0.16
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/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 +92 -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 +23 -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
|
|
@@ -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,61 @@ 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'
|
|
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 !== '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 {
|
|
67
|
+
fontPath = nodePath.join(__dirname, 'fonts', 'Twemoji.Mozilla.ttf');
|
|
68
|
+
}
|
|
66
69
|
if (nodeFs.existsSync(fontPath)) {
|
|
67
|
-
|
|
68
|
-
emojiEnabled = true;
|
|
70
|
+
resolvedEmojiFont = fontPath;
|
|
69
71
|
}
|
|
70
72
|
}
|
|
71
73
|
}
|
|
72
74
|
catch {
|
|
73
|
-
//
|
|
74
|
-
// fall through without emoji support.
|
|
75
|
+
// Node.js APIs not available (browser) — no emoji font.
|
|
75
76
|
}
|
|
76
77
|
}
|
|
78
|
+
const doc = new pdfkit_1.default({
|
|
79
|
+
size: layout.pageSize,
|
|
80
|
+
margins,
|
|
81
|
+
...(resolvedEmojiFont ? { emojiFont: resolvedEmojiFont } : {}),
|
|
82
|
+
});
|
|
83
|
+
const stream = new stream_1.PassThrough();
|
|
84
|
+
const chunks = [];
|
|
85
|
+
doc.pipe(stream);
|
|
86
|
+
stream.on('data', (chunk) => chunks.push(chunk));
|
|
77
87
|
// ── Custom font registration ─────────────────────────────────────────────
|
|
78
88
|
// Register user-supplied font families so they can be referenced by name
|
|
79
89
|
// in ThemeConfig font fields. Each variant is registered with a suffix:
|
|
@@ -108,15 +118,8 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
108
118
|
catch {
|
|
109
119
|
// Node.js `fs` not available — we're in a browser environment.
|
|
110
120
|
}
|
|
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
|
-
}
|
|
121
|
+
// ── Spacing config ────────────────────────────────────────────────────
|
|
122
|
+
const sp = { ...styles_js_1.defaultSpacing, ...theme.spacing };
|
|
120
123
|
const tokens = marked_1.marked.lexer(markdown);
|
|
121
124
|
const contentWidth = doc.page.width - margins.left - margins.right;
|
|
122
125
|
// ── Table cell context ──────────────────────────────────────────────────
|
|
@@ -257,20 +260,16 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
257
260
|
doc.font(safeFont(theme.body.font)).fontSize(theme.body.fontSize).fillColor(theme.body.color);
|
|
258
261
|
}
|
|
259
262
|
/**
|
|
260
|
-
* Render
|
|
261
|
-
*
|
|
262
|
-
* Preserves the caller's current font / fontSize / fillColor for the
|
|
263
|
-
* non-emoji portions. The `continued` flag behaves exactly like the
|
|
264
|
-
* native PDFKit option — pass `true` to keep the line open.
|
|
263
|
+
* Render text, delegating to `doc.text()`.
|
|
265
264
|
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
265
|
+
* pdfkit's native color emoji support (via the `emojiFont` constructor
|
|
266
|
+
* option) handles emoji segmentation and rendering internally, so this
|
|
267
|
+
* function only normalises call signatures and handles table cell context.
|
|
269
268
|
*
|
|
270
|
-
* Supports both `
|
|
271
|
-
*
|
|
269
|
+
* Supports both `renderText(text, opts?)` and the positioned form
|
|
270
|
+
* `renderText(text, x, y, opts?)` used by table cells.
|
|
272
271
|
*/
|
|
273
|
-
function
|
|
272
|
+
function renderText(text, xOrOpts, yOrUndefined, posOpts) {
|
|
274
273
|
// Normalise the two call signatures into a single (opts, firstX, firstY).
|
|
275
274
|
let opts;
|
|
276
275
|
let firstX;
|
|
@@ -290,163 +289,12 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
290
289
|
opts = { ...opts, width: cellCtx.width, align: cellCtx.align };
|
|
291
290
|
cellCtx.used = true;
|
|
292
291
|
}
|
|
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;
|
|
303
|
-
}
|
|
304
|
-
// Remember the caller's font state so we can restore after emoji runs.
|
|
305
|
-
const prevFont = doc._font?.name ?? safeFont(theme.body.font);
|
|
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;
|
|
292
|
+
if (firstX !== undefined) {
|
|
293
|
+
doc.text(text, firstX, firstY, opts);
|
|
428
294
|
}
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
}
|
|
295
|
+
else {
|
|
296
|
+
doc.text(text, opts);
|
|
447
297
|
}
|
|
448
|
-
// Restore the original font so subsequent calls aren't surprised.
|
|
449
|
-
doc.font(prevFont).fontSize(prevSize);
|
|
450
298
|
}
|
|
451
299
|
function renderCodespan(text, continued) {
|
|
452
300
|
const cs = theme.code.inline;
|
|
@@ -504,7 +352,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
504
352
|
doc.font(safeFont(theme.body.font)).fontSize(theme.body.fontSize).fillColor(theme.linkColor);
|
|
505
353
|
}
|
|
506
354
|
const linkText = tok.text || tok.href;
|
|
507
|
-
|
|
355
|
+
renderText(linkText, { continued, underline: true, link: tok.href });
|
|
508
356
|
doc.fillColor(headingCtx ? headingCtx.color : theme.body.color);
|
|
509
357
|
}
|
|
510
358
|
async function renderInlineTokens(inlineTokens, continued, insideBold = false, insideItalic = false) {
|
|
@@ -520,7 +368,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
520
368
|
}
|
|
521
369
|
else {
|
|
522
370
|
applyBodyFont(insideBold, insideItalic);
|
|
523
|
-
|
|
371
|
+
renderText(t.text, { continued: cont, underline: false, strike: false });
|
|
524
372
|
}
|
|
525
373
|
break;
|
|
526
374
|
}
|
|
@@ -548,12 +396,12 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
548
396
|
}
|
|
549
397
|
case 'del': {
|
|
550
398
|
applyBodyFont(insideBold, insideItalic);
|
|
551
|
-
|
|
399
|
+
renderText(tok.text, { continued: cont, strike: true, underline: false });
|
|
552
400
|
break;
|
|
553
401
|
}
|
|
554
402
|
case 'escape': {
|
|
555
403
|
applyBodyFont(insideBold, insideItalic);
|
|
556
|
-
|
|
404
|
+
renderText(tok.text, { continued: cont, underline: false, strike: false });
|
|
557
405
|
break;
|
|
558
406
|
}
|
|
559
407
|
case 'br': {
|
|
@@ -564,7 +412,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
564
412
|
const raw = tok.text ?? tok.raw ?? '';
|
|
565
413
|
if (raw) {
|
|
566
414
|
applyBodyFont(insideBold, insideItalic);
|
|
567
|
-
|
|
415
|
+
renderText(raw, { continued: cont, underline: false, strike: false });
|
|
568
416
|
}
|
|
569
417
|
break;
|
|
570
418
|
}
|
|
@@ -588,9 +436,12 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
588
436
|
displayWidth = img.width * (displayHeight / img.height);
|
|
589
437
|
}
|
|
590
438
|
ensureSpace(displayHeight + 10);
|
|
591
|
-
|
|
439
|
+
// Compute horizontal position based on imageAlign
|
|
440
|
+
const imgX = theme.imageAlign === 'center'
|
|
441
|
+
? margins.left + (contentWidth - displayWidth) / 2
|
|
442
|
+
: doc.x;
|
|
592
443
|
const imgY = doc.y;
|
|
593
|
-
doc.image(imgBuffer, { width: displayWidth, height: displayHeight });
|
|
444
|
+
doc.image(imgBuffer, imgX, imgY, { width: displayWidth, height: displayHeight });
|
|
594
445
|
// If the image is wrapped in a link, overlay a clickable annotation
|
|
595
446
|
if (linkUrl) {
|
|
596
447
|
doc.link(imgX, imgY, displayWidth, displayHeight, linkUrl);
|
|
@@ -605,13 +456,13 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
605
456
|
}
|
|
606
457
|
}
|
|
607
458
|
async function renderList(list, depth) {
|
|
608
|
-
const indent = margins.left + depth *
|
|
459
|
+
const indent = margins.left + depth * sp.listIndent;
|
|
609
460
|
for (let idx = 0; idx < list.items.length; idx++) {
|
|
610
461
|
const item = list.items[idx];
|
|
611
462
|
ensureSpace(theme.body.fontSize * 2);
|
|
612
463
|
resetBodyFont();
|
|
613
464
|
const bullet = list.ordered ? `${list.start + idx}.` : '•';
|
|
614
|
-
doc.text(bullet, indent, doc.y, { continued: true, width: contentWidth - depth *
|
|
465
|
+
doc.text(bullet, indent, doc.y, { continued: true, width: contentWidth - depth * sp.listIndent });
|
|
615
466
|
doc.text(' ', { continued: true });
|
|
616
467
|
// Render item inline tokens
|
|
617
468
|
const itemTokens = item.tokens;
|
|
@@ -622,7 +473,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
622
473
|
await renderInlineTokens(t.tokens, false);
|
|
623
474
|
}
|
|
624
475
|
else {
|
|
625
|
-
|
|
476
|
+
renderText(t.text);
|
|
626
477
|
}
|
|
627
478
|
}
|
|
628
479
|
else if (child.type === 'paragraph') {
|
|
@@ -632,7 +483,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
632
483
|
await renderList(child, depth + 1);
|
|
633
484
|
}
|
|
634
485
|
}
|
|
635
|
-
doc.moveDown(
|
|
486
|
+
doc.moveDown(sp.listItemSpacing);
|
|
636
487
|
}
|
|
637
488
|
}
|
|
638
489
|
async function renderCellTokens(cell, x, y, width, align, bold) {
|
|
@@ -646,7 +497,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
646
497
|
else {
|
|
647
498
|
// Fallback: plain text (no inline tokens)
|
|
648
499
|
applyBodyFont(bold, false);
|
|
649
|
-
|
|
500
|
+
renderText(cell.text, x, y, { width, align });
|
|
650
501
|
}
|
|
651
502
|
doc.y = savedY;
|
|
652
503
|
}
|
|
@@ -715,8 +566,8 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
715
566
|
const t = token;
|
|
716
567
|
const key = `h${t.depth}`;
|
|
717
568
|
const style = theme.headings[key];
|
|
718
|
-
const spaceAbove = style.fontSize *
|
|
719
|
-
const spaceBelow = style.fontSize *
|
|
569
|
+
const spaceAbove = style.fontSize * sp.headingSpaceAbove;
|
|
570
|
+
const spaceBelow = style.fontSize * sp.headingSpaceBelow;
|
|
720
571
|
ensureSpace(spaceAbove + style.fontSize + spaceBelow);
|
|
721
572
|
doc.moveDown(spaceAbove / doc.currentLineHeight());
|
|
722
573
|
doc.font(safeFont(style.font)).fontSize(style.fontSize).fillColor(style.color);
|
|
@@ -725,7 +576,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
725
576
|
await renderInlineTokens(t.tokens, false, style.bold ?? false, style.italic ?? false);
|
|
726
577
|
}
|
|
727
578
|
else {
|
|
728
|
-
|
|
579
|
+
renderText(t.text);
|
|
729
580
|
}
|
|
730
581
|
headingCtx = null;
|
|
731
582
|
// Draw an underline beneath h1 and h2
|
|
@@ -748,7 +599,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
748
599
|
ensureSpace(theme.body.fontSize * 2);
|
|
749
600
|
resetBodyFont();
|
|
750
601
|
await renderInlineTokens(t.tokens, false);
|
|
751
|
-
doc.moveDown(
|
|
602
|
+
doc.moveDown(sp.paragraphSpacing);
|
|
752
603
|
break;
|
|
753
604
|
}
|
|
754
605
|
case 'code': {
|
|
@@ -772,10 +623,11 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
772
623
|
lineNumbers,
|
|
773
624
|
drawBackground: true,
|
|
774
625
|
theme: theme.syntaxHighlight,
|
|
626
|
+
borderRadius: cs.borderRadius,
|
|
775
627
|
});
|
|
776
628
|
doc.x = margins.left;
|
|
777
629
|
doc.y = newY;
|
|
778
|
-
doc.moveDown(
|
|
630
|
+
doc.moveDown(sp.codeBlockSpacing);
|
|
779
631
|
resetBodyFont();
|
|
780
632
|
}
|
|
781
633
|
else {
|
|
@@ -786,8 +638,14 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
786
638
|
ensureSpace(blockH + 10);
|
|
787
639
|
const x = margins.left;
|
|
788
640
|
const y = doc.y;
|
|
641
|
+
const cbRadius = cs.borderRadius ?? 0;
|
|
789
642
|
doc.save();
|
|
790
|
-
|
|
643
|
+
if (cbRadius > 0) {
|
|
644
|
+
doc.roundedRect(x, y, contentWidth, blockH, cbRadius).fill(cs.backgroundColor);
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
doc.rect(x, y, contentWidth, blockH).fill(cs.backgroundColor);
|
|
648
|
+
}
|
|
791
649
|
doc.restore();
|
|
792
650
|
doc.font(safeFont(cs.font)).fontSize(cs.fontSize).fillColor(cs.color);
|
|
793
651
|
let textY = y + cs.padding;
|
|
@@ -797,7 +655,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
797
655
|
}
|
|
798
656
|
doc.x = margins.left;
|
|
799
657
|
doc.y = y + blockH;
|
|
800
|
-
doc.moveDown(
|
|
658
|
+
doc.moveDown(sp.codeBlockSpacing);
|
|
801
659
|
resetBodyFont();
|
|
802
660
|
}
|
|
803
661
|
break;
|
|
@@ -806,7 +664,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
806
664
|
const t = token;
|
|
807
665
|
const bq = theme.blockquote;
|
|
808
666
|
ensureSpace(30);
|
|
809
|
-
const bqPadding =
|
|
667
|
+
const bqPadding = bq.padding ?? 6;
|
|
810
668
|
const startY = doc.y;
|
|
811
669
|
doc.y += bqPadding; // add top padding before text
|
|
812
670
|
const textX = margins.left + bq.borderWidth + bq.indent;
|
|
@@ -818,7 +676,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
818
676
|
doc.font(safeFont(font)).fontSize(theme.body.fontSize).fillColor(theme.body.color);
|
|
819
677
|
doc.text('', textX, doc.y, { width: textWidth });
|
|
820
678
|
await renderInlineTokens(p.tokens, false, false, bq.italic);
|
|
821
|
-
doc.moveDown(
|
|
679
|
+
doc.moveDown(sp.blockquoteSpacing);
|
|
822
680
|
}
|
|
823
681
|
else {
|
|
824
682
|
await renderToken(child);
|
|
@@ -826,11 +684,18 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
826
684
|
}
|
|
827
685
|
doc.y += bqPadding; // add bottom padding after text
|
|
828
686
|
const endY = doc.y;
|
|
687
|
+
// Draw optional background fill behind blockquote area
|
|
688
|
+
if (bq.backgroundColor) {
|
|
689
|
+
doc.save();
|
|
690
|
+
doc.rect(margins.left + bq.borderWidth, startY, contentWidth - bq.borderWidth, endY - startY).fill(bq.backgroundColor);
|
|
691
|
+
doc.restore();
|
|
692
|
+
}
|
|
693
|
+
// Draw left border
|
|
829
694
|
doc.save();
|
|
830
695
|
doc.rect(margins.left, startY, bq.borderWidth, endY - startY).fill(bq.borderColor);
|
|
831
696
|
doc.restore();
|
|
832
697
|
doc.x = margins.left;
|
|
833
|
-
doc.moveDown(
|
|
698
|
+
doc.moveDown(sp.blockquoteSpacing);
|
|
834
699
|
resetBodyFont();
|
|
835
700
|
break;
|
|
836
701
|
}
|
|
@@ -841,7 +706,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
841
706
|
}
|
|
842
707
|
case 'hr': {
|
|
843
708
|
ensureSpace(20);
|
|
844
|
-
doc.moveDown(
|
|
709
|
+
doc.moveDown(sp.hrSpacing);
|
|
845
710
|
const y = doc.y;
|
|
846
711
|
doc.save();
|
|
847
712
|
doc.strokeColor(theme.horizontalRuleColor).lineWidth(1)
|
|
@@ -850,7 +715,7 @@ async function renderMarkdownToPdf(markdown, options) {
|
|
|
850
715
|
.stroke();
|
|
851
716
|
doc.restore();
|
|
852
717
|
doc.y = y;
|
|
853
|
-
doc.moveDown(
|
|
718
|
+
doc.moveDown(sp.hrSpacing);
|
|
854
719
|
resetBodyFont();
|
|
855
720
|
break;
|
|
856
721
|
}
|
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,16 @@ 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
|
+
* - `'none'` — disable emoji font; emoji render with the body font.
|
|
96
|
+
*/
|
|
97
|
+
emojiFont?: 'twemoji' | 'openmoji' | 'none';
|
|
75
98
|
}
|
|
76
99
|
/**
|
|
77
100
|
* A custom font definition providing font data for registration with PDFKit.
|
|
@@ -92,8 +115,6 @@ export interface CustomFontDefinition {
|
|
|
92
115
|
/** Bold-italic variant (falls back to bold or regular). */
|
|
93
116
|
boldItalic?: Buffer;
|
|
94
117
|
}
|
|
95
|
-
/** Converts a single emoji string to a PNG `Buffer`. */
|
|
96
|
-
export type ColorEmojiRenderer = (emoji: string) => Promise<Buffer>;
|
|
97
118
|
export interface PdfOptions {
|
|
98
119
|
theme?: ThemeConfig;
|
|
99
120
|
pageLayout?: PageLayout;
|
|
@@ -151,20 +172,6 @@ export interface PdfOptions {
|
|
|
151
172
|
* @default true
|
|
152
173
|
*/
|
|
153
174
|
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
175
|
/**
|
|
169
176
|
* Custom font definitions to register with PDFKit.
|
|
170
177
|
*
|
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.16",
|
|
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",
|
|
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
|
-
}
|