Emojis and mermaid
This commit is contained in:
parent
02af677306
commit
da50fe4dde
|
|
@ -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 == '_'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue