From 5abe2a2a046364d611c2da9246118000e2e42f26 Mon Sep 17 00:00:00 2001 From: Tyom Semonov Date: Fri, 20 Feb 2026 00:36:55 +0000 Subject: [PATCH 1/3] refactor(quick-7): extract shared utilities and emoji map into dedicated modules - Add utils.ts with escapeHtml and escapeMrkdwn shared helpers - Add emoji.ts with EMOJI_MAP constant and renderEmoji helper --- packages/mrkdwn/src/emoji.ts | 58 ++++++++++++++++++++++++++++++++++++ packages/mrkdwn/src/utils.ts | 13 ++++++++ 2 files changed, 71 insertions(+) create mode 100644 packages/mrkdwn/src/emoji.ts create mode 100644 packages/mrkdwn/src/utils.ts diff --git a/packages/mrkdwn/src/emoji.ts b/packages/mrkdwn/src/emoji.ts new file mode 100644 index 0000000..97ace7d --- /dev/null +++ b/packages/mrkdwn/src/emoji.ts @@ -0,0 +1,58 @@ +export const EMOJI_MAP: Record = { + '+1': '\u{1F44D}', + '-1': '\u{1F44E}', + thumbsup: '\u{1F44D}', + thumbsdown: '\u{1F44E}', + heart: '\u2764\uFE0F', + smile: '\u{1F604}', + laughing: '\u{1F606}', + blush: '\u{1F60A}', + grinning: '\u{1F600}', + wink: '\u{1F609}', + joy: '\u{1F602}', + sob: '\u{1F62D}', + cry: '\u{1F622}', + thinking_face: '\u{1F914}', + white_check_mark: '\u2705', + heavy_check_mark: '\u2714\uFE0F', + x: '\u274C', + warning: '\u26A0\uFE0F', + fire: '\u{1F525}', + rocket: '\u{1F680}', + tada: '\u{1F389}', + party_popper: '\u{1F389}', + eyes: '\u{1F440}', + wave: '\u{1F44B}', + pray: '\u{1F64F}', + clap: '\u{1F44F}', + muscle: '\u{1F4AA}', + star: '\u2B50', + sparkles: '\u2728', + bulb: '\u{1F4A1}', + memo: '\u{1F4DD}', + point_right: '\u{1F449}', + point_left: '\u{1F448}', + raised_hands: '\u{1F64C}', + ok_hand: '\u{1F44C}', + 100: '\u{1F4AF}', + rotating_light: '\u{1F6A8}', + zap: '\u26A1', + boom: '\u{1F4A5}', + bug: '\u{1F41B}', + gear: '\u2699\uFE0F', + lock: '\u{1F512}', + key: '\u{1F511}', + calendar: '\u{1F4C5}', + link: '\u{1F517}', + speech_balloon: '\u{1F4AC}', +} + +/** + * Render an emoji shortcode to an HTML span with tooltip. + * Returns null if the emoji name is not in EMOJI_MAP. + */ +export function renderEmoji(name: string): string | null { + const emoji = EMOJI_MAP[name] + if (!emoji) return null + return `${emoji}${emoji}:${name}:` +} diff --git a/packages/mrkdwn/src/utils.ts b/packages/mrkdwn/src/utils.ts new file mode 100644 index 0000000..b1f1948 --- /dev/null +++ b/packages/mrkdwn/src/utils.ts @@ -0,0 +1,13 @@ +/** Escape HTML entities in text content */ +export function escapeHtml(text: string): string { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') +} + +/** Escape characters that have special meaning in Slack mrkdwn */ +export function escapeMrkdwn(text: string): string { + return text.replace(/&/g, '&').replace(//g, '>') +} From 3871428dc0508988a803485f6ac71268f654bb8f Mon Sep 17 00:00:00 2001 From: Tyom Semonov Date: Fri, 20 Feb 2026 00:38:16 +0000 Subject: [PATCH 2/3] refactor(quick-7): wire converters to use extracted emoji and utils modules - Remove EMOJI_MAP and escapeHtml from mrkdwn-to-html.ts, import from shared modules - Remove escapeMrkdwn from markdown-to-mrkdwn.ts, import from utils.ts - Use renderEmoji helper for emoji replacement in formatInline - Re-export EMOJI_MAP from index.ts for external extensibility --- packages/mrkdwn/src/index.ts | 1 + packages/mrkdwn/src/markdown-to-mrkdwn.ts | 5 +- packages/mrkdwn/src/mrkdwn-to-html.ts | 65 ++--------------------- 3 files changed, 7 insertions(+), 64 deletions(-) diff --git a/packages/mrkdwn/src/index.ts b/packages/mrkdwn/src/index.ts index 0d0de64..5013769 100644 --- a/packages/mrkdwn/src/index.ts +++ b/packages/mrkdwn/src/index.ts @@ -1,2 +1,3 @@ export { mrkdwnToHtml } from './mrkdwn-to-html' export { markdownToMrkdwn } from './markdown-to-mrkdwn' +export { EMOJI_MAP } from './emoji' diff --git a/packages/mrkdwn/src/markdown-to-mrkdwn.ts b/packages/mrkdwn/src/markdown-to-mrkdwn.ts index e8590e1..664c837 100644 --- a/packages/mrkdwn/src/markdown-to-mrkdwn.ts +++ b/packages/mrkdwn/src/markdown-to-mrkdwn.ts @@ -7,10 +7,7 @@ import { Lexer, type Token, type Tokens } from 'marked' -/** Escape characters that have special meaning in Slack mrkdwn */ -function escapeMrkdwn(text: string): string { - return text.replace(/&/g, '&').replace(//g, '>') -} +import { escapeMrkdwn } from './utils' /** Render a single inline token to mrkdwn */ function renderInlineToken(token: Token): string { diff --git a/packages/mrkdwn/src/mrkdwn-to-html.ts b/packages/mrkdwn/src/mrkdwn-to-html.ts index c05a57b..43d2bf0 100644 --- a/packages/mrkdwn/src/mrkdwn-to-html.ts +++ b/packages/mrkdwn/src/mrkdwn-to-html.ts @@ -4,65 +4,12 @@ * Custom parser that handles Slack's mrkdwn syntax including bold, italic, * strikethrough, code, links, mentions, blockquotes, lists, and emoji. * Processes block-level elements line-by-line, then applies inline formatting. + * + * Depends on ./emoji (EMOJI_MAP, renderEmoji) and ./utils (escapeHtml). */ -const EMOJI_MAP: Record = { - '+1': '\u{1F44D}', - '-1': '\u{1F44E}', - thumbsup: '\u{1F44D}', - thumbsdown: '\u{1F44E}', - heart: '\u2764\uFE0F', - smile: '\u{1F604}', - laughing: '\u{1F606}', - blush: '\u{1F60A}', - grinning: '\u{1F600}', - wink: '\u{1F609}', - joy: '\u{1F602}', - sob: '\u{1F62D}', - cry: '\u{1F622}', - thinking_face: '\u{1F914}', - white_check_mark: '\u2705', - heavy_check_mark: '\u2714\uFE0F', - x: '\u274C', - warning: '\u26A0\uFE0F', - fire: '\u{1F525}', - rocket: '\u{1F680}', - tada: '\u{1F389}', - party_popper: '\u{1F389}', - eyes: '\u{1F440}', - wave: '\u{1F44B}', - pray: '\u{1F64F}', - clap: '\u{1F44F}', - muscle: '\u{1F4AA}', - star: '\u2B50', - sparkles: '\u2728', - bulb: '\u{1F4A1}', - memo: '\u{1F4DD}', - point_right: '\u{1F449}', - point_left: '\u{1F448}', - raised_hands: '\u{1F64C}', - ok_hand: '\u{1F44C}', - 100: '\u{1F4AF}', - rotating_light: '\u{1F6A8}', - zap: '\u26A1', - boom: '\u{1F4A5}', - bug: '\u{1F41B}', - gear: '\u2699\uFE0F', - lock: '\u{1F512}', - key: '\u{1F511}', - calendar: '\u{1F4C5}', - link: '\u{1F517}', - speech_balloon: '\u{1F4AC}', -} - -/** Escape HTML entities in text content */ -function escapeHtml(text: string): string { - return text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') -} +import { renderEmoji } from './emoji' +import { escapeHtml } from './utils' /** Apply inline mrkdwn formatting (bold, italic, strike, links, mentions, emoji) */ function formatInline(text: string): string { @@ -125,9 +72,7 @@ function formatInline(text: string): string { // Emoji: :name: text = text.replace(/:([a-z0-9_+-]+):/g, (_match, name: string) => { - const emoji = EMOJI_MAP[name] - if (!emoji) return `:${name}:` - return `${emoji}${emoji}:${name}:` + return renderEmoji(name) ?? `:${name}:` }) // Restore inline code placeholders From c8ec6d93f0ffb43ca4af294534aefd8b44847bce Mon Sep 17 00:00:00 2001 From: Tyom Semonov Date: Fri, 20 Feb 2026 01:22:33 +0000 Subject: [PATCH 3/3] refactor: replace hand-maintained EMOJI_MAP with gemoji package Use gemoji (1,913 shortcodes) instead of our 47-entry static map. Add 5 Slack-specific aliases for shortcode mismatches. --- apps/ui/src/components/Message.svelte | 11 +---- bun.lock | 5 +- packages/mrkdwn/package.json | 1 + packages/mrkdwn/src/emoji.ts | 70 ++++++++------------------- packages/mrkdwn/src/index.ts | 3 +- 5 files changed, 30 insertions(+), 60 deletions(-) diff --git a/apps/ui/src/components/Message.svelte b/apps/ui/src/components/Message.svelte index 6e2caf2..9d5b17e 100644 --- a/apps/ui/src/components/Message.svelte +++ b/apps/ui/src/components/Message.svelte @@ -27,6 +27,7 @@ updateFileExpanded, sendMessageBlockAction, } from '../lib/dispatcher.svelte' + import { resolveEmoji } from '@botarium/mrkdwn' import BlockKitRenderer from './blockkit/BlockKitRenderer.svelte' import { renderMrkdwn } from './blockkit/context' import { @@ -53,14 +54,6 @@ ) => void } - const EMOJI_MAP: Record = { - thinking_face: '🤔', - white_check_mark: '✅', - clock1: '🕐', - clock2: '🕑', - clock3: '🕒', - } - let { message, replyCount = 0, @@ -344,7 +337,7 @@ } function getEmoji(name: string): string { - return EMOJI_MAP[name] || `:${name}:` + return resolveEmoji(name) ?? `:${name}:` } diff --git a/bun.lock b/bun.lock index 5cf4b16..3bd386e 100644 --- a/bun.lock +++ b/bun.lock @@ -44,7 +44,7 @@ "zod": "^4.3.5", }, "devDependencies": { - "@types/bun": "latest", + "@types/bun": "^1.3.6", "pino-pretty": "^13.1.3", "typescript": "^5", }, @@ -106,6 +106,7 @@ "name": "@botarium/mrkdwn", "version": "0.1.0", "dependencies": { + "gemoji": "^8.1.0", "marked": "^15.0.0", }, }, @@ -800,6 +801,8 @@ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "gemoji": ["gemoji@8.1.0", "", {}, "sha512-HA4Gx59dw2+tn+UAa7XEV4ufUKI4fH1KgcbenVA9YKSj1QJTT0xh5Mwv5HMFNN3l2OtUe3ZIfuRwSyZS5pLIWw=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], diff --git a/packages/mrkdwn/package.json b/packages/mrkdwn/package.json index 8abe4f6..9b84478 100644 --- a/packages/mrkdwn/package.json +++ b/packages/mrkdwn/package.json @@ -17,6 +17,7 @@ "test": "bun test" }, "dependencies": { + "gemoji": "^8.1.0", "marked": "^15.0.0" } } diff --git a/packages/mrkdwn/src/emoji.ts b/packages/mrkdwn/src/emoji.ts index 97ace7d..8229460 100644 --- a/packages/mrkdwn/src/emoji.ts +++ b/packages/mrkdwn/src/emoji.ts @@ -1,58 +1,30 @@ -export const EMOJI_MAP: Record = { - '+1': '\u{1F44D}', - '-1': '\u{1F44E}', - thumbsup: '\u{1F44D}', - thumbsdown: '\u{1F44E}', - heart: '\u2764\uFE0F', - smile: '\u{1F604}', - laughing: '\u{1F606}', - blush: '\u{1F60A}', - grinning: '\u{1F600}', - wink: '\u{1F609}', - joy: '\u{1F602}', - sob: '\u{1F62D}', - cry: '\u{1F622}', - thinking_face: '\u{1F914}', - white_check_mark: '\u2705', - heavy_check_mark: '\u2714\uFE0F', - x: '\u274C', - warning: '\u26A0\uFE0F', - fire: '\u{1F525}', - rocket: '\u{1F680}', - tada: '\u{1F389}', - party_popper: '\u{1F389}', - eyes: '\u{1F440}', - wave: '\u{1F44B}', - pray: '\u{1F64F}', - clap: '\u{1F44F}', - muscle: '\u{1F4AA}', - star: '\u2B50', - sparkles: '\u2728', - bulb: '\u{1F4A1}', - memo: '\u{1F4DD}', - point_right: '\u{1F449}', - point_left: '\u{1F448}', - raised_hands: '\u{1F64C}', - ok_hand: '\u{1F44C}', - 100: '\u{1F4AF}', - rotating_light: '\u{1F6A8}', - zap: '\u26A1', - boom: '\u{1F4A5}', - bug: '\u{1F41B}', - gear: '\u2699\uFE0F', - lock: '\u{1F512}', - key: '\u{1F511}', - calendar: '\u{1F4C5}', - link: '\u{1F517}', - speech_balloon: '\u{1F4AC}', +import { nameToEmoji } from 'gemoji' + +/** Slack shortcodes that differ from gemoji's naming */ +const SLACK_ALIASES: Record = { + thinking_face: 'thinking', + party_popper: 'tada', + person_with_pouting_face: 'pouting_face', + person_frowning: 'frowning_person', + person_with_blond_hair: 'blond_haired_person', +} + +// Slack custom image emoji with no unicode equivalent (will render as :name: text): +// bowtie, simple_smile, neckbeard, feelsgood, finnadie, goberserk, godmode, +// hurtrealbad, rage1, rage2, rage3, rage4, suspect, trollface, octocat, +// squirrel, shipit + +/** Resolve a shortcode name to a unicode emoji character. */ +export function resolveEmoji(name: string): string | undefined { + return nameToEmoji[name] ?? nameToEmoji[SLACK_ALIASES[name] ?? ''] } /** * Render an emoji shortcode to an HTML span with tooltip. - * Returns null if the emoji name is not in EMOJI_MAP. + * Returns null if the emoji name is not recognized. */ export function renderEmoji(name: string): string | null { - const emoji = EMOJI_MAP[name] + const emoji = resolveEmoji(name) if (!emoji) return null return `${emoji}${emoji}:${name}:` } diff --git a/packages/mrkdwn/src/index.ts b/packages/mrkdwn/src/index.ts index 5013769..b2b9ba0 100644 --- a/packages/mrkdwn/src/index.ts +++ b/packages/mrkdwn/src/index.ts @@ -1,3 +1,4 @@ export { mrkdwnToHtml } from './mrkdwn-to-html' export { markdownToMrkdwn } from './markdown-to-mrkdwn' -export { EMOJI_MAP } from './emoji' +export { resolveEmoji } from './emoji' +export { nameToEmoji } from 'gemoji'