@speajus/markdown-to-pdf 1.0.14 → 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 +156 -238
- 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/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
|
-
}
|