57 lines
1.6 KiB
TypeScript
57 lines
1.6 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* MentionText — renders user-authored plain text with `@handle` tokens turned
|
|
* into profile links.
|
|
*
|
|
* The single place that decides what a mention looks like. Shared by comment
|
|
* bodies and profile bios so the two can't drift apart, and so the
|
|
* "@handle" grammar lives next to the regex the composer
|
|
* (`MentionTextarea`) uses to detect it.
|
|
*
|
|
* Input is always PLAIN TEXT, never HTML — we split on the token and emit
|
|
* React nodes, so nothing user-supplied is ever interpreted as markup.
|
|
* `whitespace-pre-line` on the wrapper preserves the author's line breaks
|
|
* (bios are multi-line) without needing <br> injection.
|
|
*/
|
|
|
|
import Link from "next/link";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
/** Capturing group keeps the delimiters, so split() yields text and tokens
|
|
* alternately. Handle charset matches MentionTextarea's MENTION_RE. */
|
|
const MENTION_TOKEN = /(@[A-Za-z0-9_]+)/g;
|
|
|
|
/** Split text into plain runs + clickable @handle links. Exported for callers
|
|
* that need the nodes without the wrapper element. */
|
|
export function renderMentions(text: string) {
|
|
return text.split(MENTION_TOKEN).map((part, i) => {
|
|
if (part.length > 1 && part[0] === "@") {
|
|
return (
|
|
<Link
|
|
key={i}
|
|
href={`/u/${part.slice(1)}`}
|
|
className="font-medium text-primary hover:underline"
|
|
>
|
|
{part}
|
|
</Link>
|
|
);
|
|
}
|
|
return part;
|
|
});
|
|
}
|
|
|
|
export function MentionText({
|
|
text,
|
|
className,
|
|
}: {
|
|
text: string;
|
|
className?: string;
|
|
}) {
|
|
return (
|
|
<span className={cn("whitespace-pre-line", className)}>
|
|
{renderMentions(text)}
|
|
</span>
|
|
);
|
|
}
|