Files
2026-07-17 21:48:37 +05:30

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;
}