Emojis and mermaid
This commit is contained in:
parent
02af677306
commit
da50fe4dde
|
|
@ -23,6 +23,339 @@ var (
|
||||||
|
|
||||||
// Matches @username in raw markdown for extraction
|
// Matches @username in raw markdown for extraction
|
||||||
RawMentionRegex = regexp.MustCompile(`(?:^|[\s(])@(\w+)`)
|
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() {
|
func init() {
|
||||||
|
|
@ -71,6 +404,9 @@ func RenderMarkdown(input string, mentions map[string]string) template.HTML {
|
||||||
}
|
}
|
||||||
sanitized := string(policy.SanitizeBytes(buf.Bytes()))
|
sanitized := string(policy.SanitizeBytes(buf.Bytes()))
|
||||||
|
|
||||||
|
sanitized = processMermaid(sanitized)
|
||||||
|
sanitized = processEmojis(sanitized)
|
||||||
|
|
||||||
if len(mentions) > 0 {
|
if len(mentions) > 0 {
|
||||||
sanitized = processMentions(sanitized, mentions)
|
sanitized = processMentions(sanitized, mentions)
|
||||||
}
|
}
|
||||||
|
|
@ -136,6 +472,31 @@ func replaceOutsideCode(html, old, replacement string) string {
|
||||||
return result.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 {
|
func isWordChar(b byte) bool {
|
||||||
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') || b == '_'
|
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') || b == '_'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,14 @@
|
||||||
content: none;
|
content: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mermaid diagram styling */
|
||||||
|
pre.mermaid {
|
||||||
|
text-align: center;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
/* Task list checkbox styling */
|
/* Task list checkbox styling */
|
||||||
.prose input[type="checkbox"] {
|
.prose input[type="checkbox"] {
|
||||||
margin-right: 0.375rem;
|
margin-right: 0.375rem;
|
||||||
|
|
|
||||||
|
|
@ -14,5 +14,12 @@
|
||||||
{{block "content" .}}{{end}}
|
{{block "content" .}}{{end}}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue