Skip to content

fix: render emojis and HTML entities in MCP-created screens - #38

Merged
trmquang93 merged 1 commit into
mainfrom
fix/mcp-emoji-and-html-entity-rendering
Apr 17, 2026
Merged

fix: render emojis and HTML entities in MCP-created screens#38
trmquang93 merged 1 commit into
mainfrom
fix/mcp-emoji-and-html-entity-rendering

Conversation

@trmquang93

Copy link
Copy Markdown
Collaborator

Summary

Fixes two rendering bugs that made MCP-created screens look broken whenever an agent included emojis or HTML entities in the source HTML.

Problem 1 — Emojis render as tofu boxes

Satori ships with no emoji glyphs. It only renders characters present in the fonts passed to it, and satori-renderer.js loads only Inter Regular/Bold. Any emoji an agent wrote (screen titles, status icons, heart/check indicators) came out as missing-glyph boxes in the resulting PNG.

Problem 2 — HTML numeric/named entities render as literal text

satori-html doesn't decode HTML entities. Agents commonly write ● for a black circle, “/” for curly quotes, … for ellipsis — all were landing in rendered PNGs as literal ● text.

Changes

1. Twemoji integration via Satori's loadAdditionalAsset callback

New mcp-server/src/renderer/emoji-loader.js:

  • Resolves emoji graphemes to Twemoji SVGs at https://cdn.jsdelivr.net/gh/jdecked/twemoji@15.1.0/assets/svg/<codepoint>.svg (version-pinned to the maintained fork, never @latest).
  • Three-layer cache: in-flight promise dedupe → in-memory resolved Map → on-disk at ~/.cache/drawd-mcp/emoji/<code>.svg. A batch render with 50 identical checkmarks triggers exactly one fetch; subsequent process restarts hit the disk cache.
  • 3 s AbortSignal.timeout on fetch; transparent 1×1 SVG fallback on error so renders never hang or crash.
  • Handles keycaps (1️⃣), ZWJ families (👨‍👩‍👧‍👦), regional flags (🇯🇵), skin-tone modifiers (👋🏻). The canonical Twemoji toCodePoint rule ("keep FE0F iff ZWJ present") doesn't match jdecked's asset filenames consistently — rainbow flag keeps FE0F but eye-in-speech drops it — so getEmojiCode returns [primary, fallback] and loadEmojiSvg retries with FE0F fully stripped on 404.

2. HTML entity pre-decoding

New decodeSafeEntities() helper in satori-renderer.js, called before satori-html parses the markup:

  • Decodes all numeric entities (&#ddd; / &#xhhh;) — always safe, output is a single non-meta character.
  • Decodes a whitelist of safe named entities whose decoded form contains no HTML metachar: &nbsp;, &hellip;, &mdash;, &bull;, &copy;, &ldquo;/&rdquo;, &larr;/&rarr;, &check;/&cross;, etc.
  • Deliberately leaves &amp;, &lt;, &gt;, &quot;, &apos; untouched — decoding those before HTML parsing would break the parser.

3. Version bump

mcp-serverv1.2.2.

Test plan

  • 9/9 getEmojiCode codepoint encodings match real Twemoji filenames (heart, keycap, flag JP, skin-tone, ZWJ family, man technologist, rainbow flag, eye-in-speech).
  • Cold render 1.9 s → warm 0.3 s (disk cache speedup verified).
  • Offline replay renders emojis from disk cache only.
  • All 648 existing vitest tests pass.
  • ESLint clean.
  • npm run build succeeds; dist/index.js 152 KB → 154 KB (+2 KB for both fixes combined).
  • End-to-end: real user .drawd file re-rendered through the patched pipeline — status bar shows ●●●● instead of literal &#9679;, "xyzzy" with real curly quotes instead of &ldquo;xyzzy&rdquo;, full-color Twemoji glyphs throughout.

Out of scope

  • Noto Color Emoji bundled font — Satori's COLR/CPAL support is partial; flags and ZWJ families render as tofu. Rejected.
  • Bundling local Twemoji SVG set (~10 MB, 3600 files) — legitimate future offline-first option but bloats the npm package. The disk cache covers the practical case.
  • Decoding &amp;/&lt;/&gt;/&quot;/&apos; — would require post-parse VDOM walk; current whitelist covers the typography symbols agents actually use.

MCP-created screens rendered HTML through Satori, which ships no emoji
font and has no built-in entity decoder. Agent-authored HTML that used
emojis or numeric entities (e.g. &#9679; for status-bar dots) rendered
as tofu boxes or literal text.

Two fixes:

1. Wire Satori's `loadAdditionalAsset` callback to resolve emoji
   graphemes to Twemoji SVGs fetched from jsDelivr (version-pinned to
   jdecked/twemoji@15.1.0). New `emoji-loader.js` adds a three-layer
   cache (in-flight promise dedupe, in-memory resolved Map, on-disk
   ~/.cache/drawd-mcp/emoji/<code>.svg), a 3 s AbortSignal timeout, and
   a transparent-SVG fallback so renders never hang or crash. Handles
   keycaps, ZWJ families, regional flags, skin-tone modifiers, and the
   canonical/fallback filename split (rainbow flag keeps FE0F while
   eye-in-speech drops it — retry covers both).

2. Preprocess HTML with `decodeSafeEntities` before satori-html parses
   it: decode all numeric entities (&#ddd; / &#xhhh;) and a whitelist
   of safe named entities whose decoded form contains no HTML
   metachar (&nbsp;, &hellip;, &bull;, &ldquo;/&rdquo;, arrows, etc.).
   Leaves &amp;/&lt;/&gt;/&quot;/&apos; untouched to avoid breaking
   the HTML parser.

Bumps mcp-server to v1.2.2.
@trmquang93
trmquang93 merged commit 708736d into main Apr 17, 2026
1 check passed
@github-actions
github-actions Bot deleted the fix/mcp-emoji-and-html-entity-rendering branch April 17, 2026 01:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant