1import type { ComposerContextRecord } from "@t3tools/contracts";
2import { contextChipPresentation } from "./composerChipPresentation";
3import type { MarkdownFileIcon } from "./markdownLinks";
4import type { SelectableMarkdownSkill } from "../native/SelectableMarkdownText.types";
5import { parseComposerContextHref } from "@t3tools/shared/composerContextReferences";
6
7const CONTEXT_ICONS: Readonly<Record<string, MarkdownFileIcon>> = {
8 photo: "image",
9 "play.rectangle": "video",
10 terminal: "bash",
11 "cursorarrow.click": "react",
12 "text.bubble": "markdown",
13 "git-pull-request": "git",
14 cube: "mcp",
15 doc: "default",
16};
17
18/** Context identity comes from the URL, even when different chips share a label. */
19export function enrichedContextLinkPresentation(
20 href: string,
21 records?: ReadonlyArray<ComposerContextRecord>,
22) {
23 const reference = parseComposerContextHref(href);
24 if (!reference) return undefined;
25 const record = records?.find((candidate) => candidate.contextId === reference.contextId);
26 const presentation = contextChipPresentation(reference.kind, record);
27 return {
28 color: presentation.accent,
29 icon: CONTEXT_ICONS[presentation.symbol] ?? "default",
30 };
31}
32
33export function enrichedLinkVariantPattern(url: string) {
34 return `^${url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`;
35}
36
37/** Conservative recognition for Enriched's whole-span inline-code link hook. */
38export const ENRICHED_INLINE_FILE_LINK_REGEX =
39 /^(?!\S*:\/\/)(?:(?:\.{1,2}[\\/]|~[\\/]|\/(?:Users|home|tmp|var|etc|opt|mnt|Volumes|private|root|usr|bin|sbin|lib|lib64|srv|dev|proc|sys|run|boot|media|workspace|workspaces)\/|[A-Za-z]:[\\/])[^\s`]+|(?:[A-Za-z0-9._-]+[\\/])+[A-Za-z0-9._-]+\.[A-Za-z0-9_-]+(?::\d+){0,2}|[A-Za-z0-9._-]+\.[A-Za-z0-9_-]+:\d+(?::\d+)?)$/;
40
41export function enrichedSkillLinkRegex(skills?: ReadonlyArray<SelectableMarkdownSkill>) {
42 const names = [...new Set(skills?.map((skill) => skill.name) ?? [])].filter(Boolean);
43 if (names.length === 0) return null;
44 names.sort((a, b) => b.length - a.length);
45 const alternatives = names.map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
46 return new RegExp(
47 `(?<![A-Za-z0-9_-])(?<!\\p{Sc})\\p{Sc}(?:${alternatives.join("|")})(?![A-Za-z0-9_-])`,
48 "u",
49 );
50}
51
52export function enrichedSkillDisplayName(skill: SelectableMarkdownSkill) {
53 return (
54 skill.displayName?.trim() ||
55 skill.name
56 .split(/[\s:_-]+/)
57 .filter(Boolean)
58 .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
59 .join(" ")
60 );
61}
62
63export function enrichedSkillReference(
64 url: string,
65 skills?: ReadonlyArray<SelectableMarkdownSkill>,
66) {
67 const match = /^\p{Sc}(.+)$/u.exec(url);
68 return match ? skills?.find((skill) => skill.name === match[1]) : undefined;
69}