Render emoji shortcodes as icons in chat (#345) (#629)

Chat models often emit GitHub/Slack-style :shortcode: text (e.g. 😊,
🎤) instead of the actual emoji. The renderer only converted real
Unicode emoji to the monochrome line icons, so shortcodes rendered as literal
text.

Add a pure, browser-free shortcode->Unicode map (emojiShortcodes.js) and run it
inside svgifyEmoji ahead of the existing Unicode->SVG pass, skipping <code>/<pre>
so code stays literal. Covers ~430 common shortcodes plus common aliases
(+1/thumbsup, etc.).

Keep the conversion from touching anything it shouldn't:
* Scope it to chat. mdToHtml/svgifyEmoji take a { shortcodes } option (default
  on); document and email body rendering (compose, export, preview) pass it as
  false so author-typed :shortcode: text stays literal. The Unicode->SVG pass
  still runs there exactly as before.
* Only convert a :shortcode: that stands on its own. A word-boundary guard
  leaves embedded colon runs alone, so "1:100:2", "10:30:45", "16:9" and
  host:fire:port are never rewritten.

Tests: extend the node-driven unit test with the boundary/false-positive cases,
and fix the markdown-rendering test loader to resolve the new emojiShortcodes
import.
This commit is contained in:
Zeus-Deus
2026-06-05 02:28:42 +02:00
committed by GitHub
parent f9c81f3c8d
commit 85334e8f3d
6 changed files with 604 additions and 9 deletions
+23 -5
View File
@@ -6,6 +6,7 @@
import uiModule from './ui.js';
import { splitTableRow } from './markdown/tableRow.js';
import { replaceEmojiShortcodes, hasEmojiShortcode } from './emojiShortcodes.js';
var escapeHtml = uiModule.esc;
@@ -366,8 +367,19 @@ function _useSvgEmoji() {
return typeof document === 'undefined' || !document.body?.classList.contains('text-emojis');
}
export function svgifyEmoji(html) {
if (!_useSvgEmoji() || !html || !_EMOJI_RE.test(html)) return html;
// `opts.shortcodes` (default true) controls the issue-#345 `:name:` → emoji
// expansion. Chat passes it through as true; document/email body renderers pass
// false so author-typed `:shortcode:` text stays literal (see mdToHtml callers).
// The Unicode-emoji → monochrome-SVG pass always runs regardless, so a real 😀
// in a document still renders as the themed line icon as it always has.
export function svgifyEmoji(html, opts) {
if (!_useSvgEmoji() || !html) return html;
const allowShortcodes = !opts || opts.shortcodes !== false;
// Two reasons to walk the HTML: real Unicode emoji to turn into SVG icons,
// or `:shortcode:` text the model emitted instead of an emoji (issue #345).
const hasUnicode = _EMOJI_RE.test(html);
const hasShortcode = allowShortcodes && hasEmojiShortcode(html);
if (!hasUnicode && !hasShortcode) return html;
const parts = html.split(/(<[^>]*>)/); // odd indices = tags
let codeDepth = 0;
for (let i = 0; i < parts.length; i++) {
@@ -377,7 +389,13 @@ export function svgifyEmoji(html) {
else if (/^<\/(pre|code)\s*>/.test(t)) codeDepth = Math.max(0, codeDepth - 1);
continue;
}
if (codeDepth === 0 && _EMOJI_RE.test(parts[i])) parts[i] = _svgifyText(parts[i]);
if (codeDepth !== 0) continue;
let seg = parts[i];
// Expand shortcodes to Unicode first, then both they and any pre-existing
// Unicode emoji get rendered as the same monochrome line icons below.
if (hasShortcode) seg = replaceEmojiShortcodes(seg);
if (_EMOJI_RE.test(seg)) seg = _svgifyText(seg);
parts[i] = seg;
}
return parts.join('');
}
@@ -421,7 +439,7 @@ export function processWithThinking(text) {
/**
* Convert markdown to HTML
*/
export function mdToHtml(src) {
export function mdToHtml(src, opts) {
const allowedHtmlBlocks = [];
const codeBlocks = [];
const mermaidBlocks = [];
@@ -678,7 +696,7 @@ export function mdToHtml(src) {
s = s.replace(`___CODE_BLOCK_${index}___`, block);
});
return _useSvgEmoji() ? svgifyEmoji(s) : s;
return _useSvgEmoji() ? svgifyEmoji(s, opts) : s;
}
/**