1import { useMemo } from "react";
2import { Image } from "react-native";
3import type {
4 LinkPillContent,
5 MarkdownDocumentAsset,
6 MarkdownStyle,
7} from "react-native-enriched-markdown";
8import { markdownFileIconSource } from "../lib/markdownFileIcons";
9import { markdownLinkIconSource } from "../lib/markdownLinkIcons";
10import { resolveMarkdownLinkIcon, resolveMarkdownLinkPresentation } from "../lib/markdownLinks";
11import type { SelectableMarkdownTextProps } from "./SelectableMarkdownText.types";
12import {
13 enrichedContextLinkPresentation,
14 enrichedLinkVariantPattern,
15 enrichedSkillDisplayName,
16 enrichedSkillReference,
17} from "../lib/enrichedLinkPresentation";
18
19function presentation(
20 url: string,
21 props: Pick<SelectableMarkdownTextProps, "linkCustomization" | "skills" | "textStyle">,
22) {
23 const custom = props.linkCustomization?.(url);
24 if (custom) return custom;
25 const context = enrichedContextLinkPresentation(url);
26 if (context) return { color: context.color, icon: markdownFileIconSource(context.icon) };
27 const skill = enrichedSkillReference(url, props.skills);
28 if (skill)
29 return {
30 color: props.textStyle.skillTextColor,
31 label: enrichedSkillDisplayName(skill),
32 icon: markdownFileIconSource("mcp"),
33 };
34 const link = resolveMarkdownLinkPresentation(url);
35 if (link.kind === "file")
36 return {
37 color: props.textStyle.fileTextColor,
38 label: link.label,
39 icon: markdownFileIconSource(link.icon),
40 };
41 const icon = link.kind === "external" ? resolveMarkdownLinkIcon(link.host) : null;
42 return icon
43 ? {
44 color: props.textStyle.linkColor,
45 icon: markdownLinkIconSource(icon),
46 iconTintColor: props.textStyle.linkColor,
47 }
48 : undefined;
49}
50
51export function useEnrichedLinkVariants(
52 assets: ReadonlyArray<MarkdownDocumentAsset>,
53 props: SelectableMarkdownTextProps,
54) {
55 const { linkCustomization, skills, textStyle } = props;
56 const links = useMemo(() => {
57 const byUrl = new Map<string, NonNullable<ReturnType<typeof presentation>>>();
58 for (const asset of assets) {
59 if (asset.kind !== "link" || byUrl.has(asset.url)) continue;
60 const value = presentation(asset.url, { linkCustomization, skills, textStyle });
61 if (value) byUrl.set(asset.url, value);
62 }
63 return byUrl;
64 }, [assets, linkCustomization, skills, textStyle]);
65 const linkVariants = useMemo<MarkdownStyle["linkVariants"]>(
66 () =>
67 Object.fromEntries(
68 [...links].map(([url, link]) => {
69 const color = link.color ?? textStyle.fileTextColor;
70 return [
71 enrichedLinkVariantPattern(url),
72 {
73 pill: {
74 borderColor: "transparent",
75 borderWidth: 0,
76 borderRadius: 6,
77 paddingHorizontal: 5,
78 paddingVertical: 2,
79 },
80 fontFamily: textStyle.fontFamily,
81 color,
82 underline: false,
83 backgroundColor: "transparent",
84 },
85 ];
86 }),
87 ),
88 [links, textStyle],
89 );
90 const linkPillContent = useMemo<Record<string, LinkPillContent>>(
91 () =>
92 Object.fromEntries(
93 [...links].map(([url, link]) => [
94 url,
95 {
96 label: link.label,
97 iconUri: link.icon ? Image.resolveAssetSource(link.icon)?.uri : undefined,
98 iconTintColor: link.iconTintColor,
99 },
100 ]),
101 ),
102 [links],
103 );
104 return { linkVariants, linkPillContent };
105}