Private
Public Access
Add markdown :name: emoji shortcode support in messages (#27)
Typing a complete 😂/😉/etc. shortcode now renders as the emoji, matching Slack/GitHub/Discord. Render-time only, alongside the existing preserveLineBreaks preprocessing step -- stored/sent content keeps the raw :name: text, same as markdown itself is never converted until display. Fenced code blocks and inline code spans are skipped so pasted code (a Ruby symbol, a dict key) isn't silently mangled. frontend/src/lib/emojiShortcodes.ts is generated once from emojibase-data's GitHub shortcode set (same one-time-generator approach as #19's emojiNames.ts, never a runtime dependency) -- 928 of the app's 936 emoji matched, 956 aliases, no collisions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import Markdown from 'markdown-to-jsx'
|
||||
import { EMOJI_SHORTCODES } from '../lib/emojiShortcodes'
|
||||
|
||||
interface MessageContentProps {
|
||||
content: string
|
||||
@@ -23,6 +24,38 @@ function MarkdownImageLink({ src, alt, title }: MarkdownImageLinkProps) {
|
||||
)
|
||||
}
|
||||
|
||||
const SHORTCODE_PATTERN = /:([a-z0-9_+-]+):/g
|
||||
|
||||
// Converts a complete `:name:` shortcode to its emoji, skipping fenced code
|
||||
// blocks and inline code spans -- someone pasting code containing
|
||||
// `:something:` (a Ruby symbol, a dict key) shouldn't get it silently
|
||||
// turned into an emoji. This is render-time only: stored/sent content
|
||||
// always keeps the literal `:name:` text, matching how markdown itself is
|
||||
// never converted until display.
|
||||
function convertShortcodes(text: string): string {
|
||||
const lines = text.split('\n')
|
||||
let inFence = false
|
||||
return lines
|
||||
.map((line) => {
|
||||
if (/^\s*```/.test(line)) {
|
||||
inFence = !inFence
|
||||
return line
|
||||
}
|
||||
if (inFence) return line
|
||||
// Splitting on backtick-delimited spans keeps inline code (`:foo:`)
|
||||
// untouched -- odd-indexed segments are the code spans themselves.
|
||||
return line
|
||||
.split(/(`+[^`]*`+)/g)
|
||||
.map((part, i) =>
|
||||
i % 2 === 0
|
||||
? part.replace(SHORTCODE_PATTERN, (match, name) => EMOJI_SHORTCODES[name] ?? match)
|
||||
: part,
|
||||
)
|
||||
.join('')
|
||||
})
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
// CommonMark treats a single newline as a soft break (rendered as a space),
|
||||
// not a visible line break -- only a trailing double-space or blank line
|
||||
// produces one. The Composer's Shift+Enter has always inserted a plain
|
||||
@@ -60,5 +93,5 @@ export const MARKDOWN_OPTIONS = {
|
||||
}
|
||||
|
||||
export function MessageContent({ content }: MessageContentProps) {
|
||||
return <Markdown options={MARKDOWN_OPTIONS}>{preserveLineBreaks(content)}</Markdown>
|
||||
return <Markdown options={MARKDOWN_OPTIONS}>{preserveLineBreaks(convertShortcodes(content))}</Markdown>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user