@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/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',
@@ -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.14",
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": "^0.17.2",
55
+ "pdfkit": "github:jspears/pdfkit#support-color-emoji-google",
54
56
  "prismjs": "^1.30.0"
55
57
  },
56
58
  "devDependencies": {
@@ -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>>;
@@ -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;
@@ -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
- }