59 lines
2.5 KiB
TypeScript
59 lines
2.5 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
import { shortcodeToEmoji } from "@/lib/emoji";
|
|
|
|
/**
|
|
* Small, safe inline renderer for message bodies:
|
|
* **bold** _italic_ ~~strike~~ `code` [label](url) @mention #channel :shortcode: bare-url
|
|
* Mentions/channels render as highlighted blue chips. No dangerouslySetInnerHTML.
|
|
*/
|
|
export function RichText({ text }: { text: string }) {
|
|
return <>{parseInline(text)}</>;
|
|
}
|
|
|
|
const TOKEN =
|
|
/(\*\*[^*]+\*\*|~~[^~]+~~|_[^_]+_|`[^`]+`|\[[^\]]+\]\(https?:\/\/[^)\s]+\)|:[a-z0-9_+-]{2,30}:|@[a-z0-9_.-]+|#[a-z0-9_-]+|https?:\/\/[^\s]+)/gi;
|
|
|
|
function parseInline(text: string): React.ReactNode[] {
|
|
const nodes: React.ReactNode[] = [];
|
|
let last = 0;
|
|
let m: RegExpExecArray | null;
|
|
let i = 0;
|
|
TOKEN.lastIndex = 0;
|
|
while ((m = TOKEN.exec(text)) !== null) {
|
|
if (m.index > last) nodes.push(text.slice(last, m.index));
|
|
const tok = m[0];
|
|
if (tok.startsWith("**")) {
|
|
nodes.push(<strong key={i++} className="font-semibold text-ink">{tok.slice(2, -2)}</strong>);
|
|
} else if (tok.startsWith("~~")) {
|
|
nodes.push(<span key={i++} className="line-through opacity-80">{tok.slice(2, -2)}</span>);
|
|
} else if (tok.startsWith("_")) {
|
|
nodes.push(<em key={i++}>{tok.slice(1, -1)}</em>);
|
|
} else if (tok.startsWith("`")) {
|
|
nodes.push(<code key={i++} className="rounded bg-hover px-1 py-0.5 font-mono text-[12.5px] text-accent">{tok.slice(1, -1)}</code>);
|
|
} else if (tok.startsWith("[")) {
|
|
const mm = /^\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)$/.exec(tok);
|
|
nodes.push(mm ? (
|
|
<a key={i++} href={mm[2]} target="_blank" rel="noreferrer" className="text-info underline underline-offset-2 hover:text-accent">{mm[1]}</a>
|
|
) : tok);
|
|
} else if (tok.startsWith(":") && tok.endsWith(":")) {
|
|
const emoji = shortcodeToEmoji(tok.slice(1, -1));
|
|
nodes.push(emoji ? <span key={i++}>{emoji}</span> : tok);
|
|
} else if (tok.startsWith("@")) {
|
|
nodes.push(
|
|
<span key={i++} className="rounded px-1 font-medium text-info" style={{ background: "rgba(91,157,255,0.14)" }}>{tok}</span>,
|
|
);
|
|
} else if (tok.startsWith("#")) {
|
|
nodes.push(
|
|
<span key={i++} className="rounded px-1 font-medium text-info" style={{ background: "rgba(91,157,255,0.14)" }}>{tok}</span>,
|
|
);
|
|
} else {
|
|
nodes.push(<a key={i++} href={tok} target="_blank" rel="noreferrer" className="text-info underline underline-offset-2 hover:text-accent">{tok}</a>);
|
|
}
|
|
last = m.index + tok.length;
|
|
}
|
|
if (last < text.length) nodes.push(text.slice(last));
|
|
return nodes;
|
|
}
|