108 lines
4.2 KiB
TypeScript
108 lines
4.2 KiB
TypeScript
/**
|
|
* Renders a single `[[wiki-link]]` string as a colored Link.
|
|
* Accepts either the raw `[[id]]` form OR an already-stripped `id`.
|
|
*/
|
|
import Link from "next/link";
|
|
|
|
const CLASS_MAP: Record<string, string> = {
|
|
people: "people", person: "people",
|
|
org: "organizations", organization: "organizations", organizations: "organizations",
|
|
loc: "locations", location: "locations", locations: "locations",
|
|
event: "events", events: "events",
|
|
uap: "uap-objects", "uap-object": "uap-objects", "uap-objects": "uap-objects",
|
|
vehicle: "vehicles", vehicles: "vehicles",
|
|
op: "operations", operation: "operations", operations: "operations",
|
|
concept: "concepts", concepts: "concepts",
|
|
};
|
|
|
|
const COLOR_BY_CLASS: Record<string, string> = {
|
|
people: "text-[#ff6ec7] border-[rgba(255,110,199,0.35)] hover:bg-[rgba(255,110,199,0.08)]",
|
|
organizations: "text-[#ff8a4d] border-[rgba(255,138,77,0.35)] hover:bg-[rgba(255,138,77,0.08)]",
|
|
locations: "text-[#3fde6a] border-[rgba(63,222,106,0.35)] hover:bg-[rgba(63,222,106,0.08)]",
|
|
events: "text-[#ffa500] border-[rgba(255,165,0,0.35)] hover:bg-[rgba(255,165,0,0.08)]",
|
|
"uap-objects": "text-[#ff3344] border-[rgba(255,51,68,0.45)] hover:bg-[rgba(255,51,68,0.08)] font-semibold",
|
|
vehicles: "text-[#5b9bd5] border-[rgba(91,155,213,0.35)] hover:bg-[rgba(91,155,213,0.08)]",
|
|
operations: "text-[#9b5de5] border-[rgba(155,93,229,0.35)] hover:bg-[rgba(155,93,229,0.08)]",
|
|
concepts: "text-[#06d6a0] border-[rgba(6,214,160,0.35)] hover:bg-[rgba(6,214,160,0.08)]",
|
|
document: "text-[#f5c542] border-[rgba(245,197,66,0.35)] hover:bg-[rgba(245,197,66,0.08)]",
|
|
page: "text-[#7fdbff] border-[rgba(127,219,255,0.35)] hover:bg-[rgba(127,219,255,0.08)]",
|
|
table: "text-[#1e9eb5] border-[rgba(30,158,181,0.35)]",
|
|
image: "text-[#ffeb99] border-[rgba(255,235,153,0.35)]",
|
|
unknown: "text-[#c8d4e6] border-[rgba(127,219,255,0.18)]",
|
|
};
|
|
|
|
export interface WikiLinkResolved {
|
|
href: string;
|
|
cls: keyof typeof COLOR_BY_CLASS;
|
|
display: string;
|
|
}
|
|
|
|
/** Parses `[[X]]`, `[[X|alias]]`, or bare `X` into a route + display. */
|
|
export function resolveWikiLink(input: string): WikiLinkResolved {
|
|
let target = input.trim();
|
|
let alias: string | undefined;
|
|
const m = target.match(/^\[\[(.+?)\]\]$/);
|
|
if (m) target = m[1];
|
|
if (target.includes("|")) {
|
|
const [t, a] = target.split("|");
|
|
target = t;
|
|
alias = a;
|
|
}
|
|
const display = (alias ?? target).trim();
|
|
|
|
if (target.startsWith("table/")) return { href: `/t/${target.slice(6)}`, cls: "table", display };
|
|
if (target.startsWith("image/")) return { href: `/i/${target.slice(6)}`, cls: "image", display };
|
|
|
|
// doc-id/pNNN
|
|
const pageMatch = target.match(/^([a-z0-9-]+)\/p\d{3}$/);
|
|
if (pageMatch) return { href: `/d/${target}`, cls: "page", display };
|
|
|
|
// class/id
|
|
const slash = target.match(/^([a-z-]+)\/([A-Za-z0-9._-]+)$/);
|
|
if (slash) {
|
|
const cls = CLASS_MAP[slash[1]];
|
|
if (cls) return { href: `/e/${cls}/${slash[2]}`, cls: cls as keyof typeof COLOR_BY_CLASS, display };
|
|
}
|
|
// bare doc-id
|
|
return { href: `/d/${target}`, cls: "document", display };
|
|
}
|
|
|
|
export function FmWikiLink({ target, size = "sm" }: { target: string; size?: "xs" | "sm" | "md" }) {
|
|
const { href, cls, display } = resolveWikiLink(target);
|
|
const sz =
|
|
size === "xs" ? "text-[10px] px-1.5 py-0.5" :
|
|
size === "md" ? "text-sm px-2.5 py-1" :
|
|
"text-xs px-2 py-0.5";
|
|
return (
|
|
<Link
|
|
href={href}
|
|
className={`inline-flex items-center font-mono border rounded transition-colors ${sz} ${COLOR_BY_CLASS[cls]}`}
|
|
>
|
|
{display}
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
export function FmWikiLinkList({
|
|
items,
|
|
size = "sm",
|
|
emptyLabel = "—",
|
|
max = 64,
|
|
}: {
|
|
items: string[] | undefined;
|
|
size?: "xs" | "sm" | "md";
|
|
emptyLabel?: string;
|
|
max?: number;
|
|
}) {
|
|
if (!items || items.length === 0) return <span className="text-[#5a6678] text-xs italic">{emptyLabel}</span>;
|
|
const shown = items.slice(0, max);
|
|
const overflow = items.length - shown.length;
|
|
return (
|
|
<div className="flex flex-wrap gap-1">
|
|
{shown.map((t, i) => <FmWikiLink key={i} target={t} size={size} />)}
|
|
{overflow > 0 && (
|
|
<span className="font-mono text-[10px] text-[#5a6678] px-2 py-0.5">+{overflow}</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|