Emojis and mermaid

This commit is contained in:
Matthew Knight 2026-02-14 23:41:31 -08:00
parent 02af677306
commit da50fe4dde
No known key found for this signature in database
3 changed files with 376 additions and 0 deletions

View File

@ -23,6 +23,339 @@ var (
// Matches @username in raw markdown for extraction
RawMentionRegex = regexp.MustCompile(`(?:^|[\s(])@(\w+)`)
// Matches :shortcode: patterns for emoji replacement
emojiRegex = regexp.MustCompile(`:(\w+):`)
// Matches mermaid code blocks in rendered HTML
mermaidBlockRegex = regexp.MustCompile(`(?s)<pre[^>]*><code[^>]*class="[^"]*language-mermaid[^"]*"[^>]*>(.*?)</code>\s*</pre>`)
// emojiMap maps shortcode names to Unicode emoji characters.
emojiMap = map[string]string{
// Smileys & Emotion
"smile": "😄",
"laughing": "😆",
"blush": "😊",
"smiley": "😃",
"relaxed": "☺️",
"smirk": "😏",
"heart_eyes": "😍",
"kissing_heart": "😘",
"kissing_closed_eyes": "😚",
"flushed": "😳",
"relieved": "😌",
"satisfied": "😆",
"grin": "😁",
"wink": "😉",
"stuck_out_tongue_winking_eye": "😜",
"stuck_out_tongue": "😛",
"sleeping": "😴",
"worried": "😟",
"frowning": "😦",
"anguished": "😧",
"open_mouth": "😮",
"grimacing": "😬",
"confused": "😕",
"hushed": "😯",
"expressionless": "😑",
"unamused": "😒",
"sweat_smile": "😅",
"sweat": "😓",
"disappointed_relieved": "😥",
"weary": "😩",
"pensive": "😔",
"disappointed": "😞",
"confounded": "😖",
"fearful": "😨",
"cold_sweat": "😰",
"persevere": "😣",
"cry": "😢",
"sob": "😭",
"joy": "😂",
"astonished": "😲",
"scream": "😱",
"tired_face": "😫",
"angry": "😠",
"rage": "😡",
"triumph": "😤",
"sleepy": "😪",
"yum": "😋",
"mask": "😷",
"sunglasses": "😎",
"dizzy_face": "😵",
"imp": "👿",
"smiling_imp": "😈",
"neutral_face": "😐",
"no_mouth": "😶",
"innocent": "😇",
"alien": "👽",
"yellow_heart": "💛",
"blue_heart": "💙",
"purple_heart": "💜",
"heart": "❤️",
"green_heart": "💚",
"broken_heart": "💔",
"heartbeat": "💓",
"heartpulse": "💗",
"two_hearts": "💕",
"sparkling_heart": "💖",
"star": "⭐",
"star2": "🌟",
"dizzy": "💫",
"boom": "💥",
"anger": "💢",
"exclamation": "❗",
"question": "❓",
"grey_exclamation": "❕",
"grey_question": "❔",
"zzz": "💤",
"dash": "💨",
"sweat_drops": "💦",
"notes": "🎶",
"musical_note": "🎵",
"fire": "🔥",
"poop": "💩",
"thumbsup": "👍",
"+1": "👍",
"thumbsdown": "👎",
"-1": "👎",
"ok_hand": "👌",
"punch": "👊",
"fist": "✊",
"v": "✌️",
"wave": "👋",
"hand": "✋",
"open_hands": "👐",
"point_up": "☝️",
"point_down": "👇",
"point_left": "👈",
"point_right": "👉",
"raised_hands": "🙌",
"pray": "🙏",
"clap": "👏",
"muscle": "💪",
"eyes": "👀",
"tongue": "👅",
"lips": "👄",
// People
"boy": "👦",
"girl": "👧",
"woman": "👩",
"man": "👨",
"baby": "👶",
"older_man": "👴",
"older_woman": "👵",
"skull": "💀",
"ghost": "👻",
"robot": "🤖",
// Nature
"sunny": "☀️",
"umbrella": "☂️",
"cloud": "☁️",
"snowflake": "❄️",
"snowman": "⛄",
"zap": "⚡",
"cyclone": "🌀",
"foggy": "🌁",
"rainbow": "🌈",
"ocean": "🌊",
"dog": "🐶",
"cat": "🐱",
"mouse": "🐭",
"hamster": "🐹",
"rabbit": "🐰",
"bear": "🐻",
"panda_face": "🐼",
"pig": "🐷",
"frog": "🐸",
"monkey_face": "🐵",
"see_no_evil": "🙈",
"hear_no_evil": "🙉",
"speak_no_evil": "🙊",
"chicken": "🐔",
"penguin": "🐧",
"bird": "🐦",
"fish": "🐟",
"whale": "🐳",
"bug": "🐛",
"honeybee": "🐝",
"beetle": "🐞",
"snail": "🐌",
"octopus": "🐙",
"turtle": "🐢",
"snake": "🐍",
"crab": "🦀",
"unicorn": "🦄",
// Food & Drink
"apple": "🍎",
"green_apple": "🍏",
"pear": "🍐",
"tangerine": "🍊",
"lemon": "🍋",
"banana": "🍌",
"watermelon": "🍉",
"grapes": "🍇",
"strawberry": "🍓",
"peach": "🍑",
"cherries": "🍒",
"pizza": "🍕",
"hamburger": "🍔",
"fries": "🍟",
"hotdog": "🌭",
"taco": "🌮",
"burrito": "🌯",
"egg": "🥚",
"coffee": "☕",
"tea": "🍵",
"beer": "🍺",
"beers": "🍻",
"wine_glass": "🍷",
"cocktail": "🍸",
"cake": "🍰",
"cookie": "🍪",
"chocolate_bar": "🍫",
"candy": "🍬",
"icecream": "🍦",
"doughnut": "🍩",
// Objects
"rocket": "🚀",
"airplane": "✈️",
"car": "🚗",
"taxi": "🚕",
"bus": "🚌",
"ambulance": "🚑",
"fire_engine": "🚒",
"bike": "🚲",
"ship": "🚢",
"phone": "📱",
"computer": "💻",
"keyboard": "⌨️",
"desktop_computer": "🖥️",
"tv": "📺",
"camera": "📷",
"mag": "🔍",
"bulb": "💡",
"flashlight": "🔦",
"wrench": "🔧",
"hammer": "🔨",
"nut_and_bolt": "🔩",
"gear": "⚙️",
"lock": "🔒",
"unlock": "🔓",
"key": "🔑",
"bell": "🔔",
"bookmark": "🔖",
"link": "🔗",
"bomb": "💣",
"gem": "💎",
"knife": "🔪",
"shield": "🛡️",
"trophy": "🏆",
"medal": "🏅",
"crown": "👑",
"moneybag": "💰",
"dollar": "💵",
"credit_card": "💳",
"envelope": "✉️",
"email": "📧",
"inbox_tray": "📥",
"outbox_tray": "📤",
"package": "📦",
"memo": "📝",
"pencil": "✏️",
"pencil2": "✏️",
"book": "📖",
"books": "📚",
"clipboard": "📋",
"calendar": "📅",
"chart_with_upwards_trend": "📈",
"chart_with_downwards_trend": "📉",
"bar_chart": "📊",
"pushpin": "📌",
"paperclip": "📎",
"scissors": "✂️",
"file_folder": "📁",
"open_file_folder": "📂",
"wastebasket": "🗑️",
// Symbols
"white_check_mark": "✅",
"ballot_box_with_check": "☑️",
"heavy_check_mark": "✔️",
"x": "❌",
"negative_squared_cross_mark": "❎",
"bangbang": "‼️",
"interrobang": "⁉️",
"warning": "⚠️",
"no_entry": "⛔",
"recycle": "♻️",
"100": "💯",
"arrow_up": "⬆️",
"arrow_down": "⬇️",
"arrow_left": "⬅️",
"arrow_right": "➡️",
"arrow_upper_right": "↗️",
"arrow_lower_right": "↘️",
"arrow_upper_left": "↖️",
"arrow_lower_left": "↙️",
"arrows_counterclockwise": "🔄",
"hash": "#️⃣",
"information_source": "",
"abc": "🔤",
"red_circle": "🔴",
"blue_circle": "🔵",
"large_orange_diamond": "🔶",
"large_blue_diamond": "🔷",
"white_circle": "⚪",
"black_circle": "⚫",
// Flags
"checkered_flag": "🏁",
"triangular_flag_on_post": "🚩",
"crossed_flags": "🎌",
"flag_white": "🏳️",
"flag_black": "🏴",
// Celebration
"tada": "🎉",
"confetti_ball": "🎊",
"balloon": "🎈",
"birthday": "🎂",
"gift": "🎁",
"sparkles": "✨",
"sparkler": "🎇",
"fireworks": "🎆",
"ribbon": "🎀",
"art": "🎨",
"performing_arts": "🎭",
"microphone": "🎤",
"headphones": "🎧",
"musical_keyboard": "🎹",
"guitar": "🎸",
"soccer": "⚽",
"basketball": "🏀",
"football": "🏈",
"baseball": "⚾",
"tennis": "🎾",
"golf": "⛳",
// Places
"house": "🏠",
"office": "🏢",
"hospital": "🏥",
"school": "🏫",
"earth_americas": "🌎",
"earth_africa": "🌍",
"earth_asia": "🌏",
"globe_with_meridians": "🌐",
"camping": "🏕️",
"mount_fuji": "🗻",
"sunrise": "🌅",
"sunset": "🌇",
// Clock
"hourglass": "⌛",
"watch": "⌚",
"alarm_clock": "⏰",
"stopwatch": "⏱️",
"timer_clock": "⏲️",
"clock": "🕐",
}
)
func init() {
@ -71,6 +404,9 @@ func RenderMarkdown(input string, mentions map[string]string) template.HTML {
}
sanitized := string(policy.SanitizeBytes(buf.Bytes()))
sanitized = processMermaid(sanitized)
sanitized = processEmojis(sanitized)
if len(mentions) > 0 {
sanitized = processMentions(sanitized, mentions)
}
@ -136,6 +472,31 @@ func replaceOutsideCode(html, old, replacement string) string {
return result.String()
}
// processEmojis replaces :shortcode: patterns with Unicode emoji characters.
// It skips content inside <code> and <pre> tags using replaceOutsideCode.
func processEmojis(html string) string {
// Find all shortcode matches and collect unique ones that have emoji mappings
matches := emojiRegex.FindAllString(html, -1)
seen := map[string]bool{}
for _, match := range matches {
if seen[match] {
continue
}
seen[match] = true
name := match[1 : len(match)-1]
if emoji, ok := emojiMap[name]; ok {
html = replaceOutsideCode(html, match, emoji)
}
}
return html
}
// processMermaid transforms mermaid code blocks from goldmark's rendered format
// into the format mermaid.js expects: <pre class="mermaid">...content...</pre>
func processMermaid(html string) string {
return mermaidBlockRegex.ReplaceAllString(html, `<pre class="mermaid">$1</pre>`)
}
func isWordChar(b byte) bool {
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') || b == '_'
}

View File

@ -26,6 +26,14 @@
content: none;
}
/* Mermaid diagram styling */
pre.mermaid {
text-align: center;
background: transparent;
border: none;
overflow-x: auto;
}
/* Task list checkbox styling */
.prose input[type="checkbox"] {
margin-right: 0.375rem;

View File

@ -14,5 +14,12 @@
{{block "content" .}}{{end}}
</main>
</div>
<script type="module">
if (document.querySelector('pre.mermaid')) {
const { default: mermaid } = await import('https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs');
mermaid.initialize({ startOnLoad: false, theme: 'default' });
await mermaid.run();
}
</script>
</body>
</html>