Open History to see the last commit
1import { faviconUrlForOrigin } from "@t3tools/shared/favicon";
2import { GlobeIcon } from "lucide-react";
3import { Children, memo, useState, type ReactNode } from "react";
4
5import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients";
6
7import { cn } from "../../lib/utils";
8import { sourceControlIcon } from "../../sourceControlPresentation";
9
10// Inline-level and selectable: triple-click selection stops at block-level and select-none boxes.
11const MARKDOWN_LINK_FAVICON_CLASS_NAME = "inline-block size-[14px] [vertical-align:-0.125em]";
12
13const failedFaviconHosts = new Set<string>();
14
15function sourceControlHostIcon(host: string): ReactNode {
16 const definition = sourceControlClients.findByPublicDomain(host);
17 if (definition === undefined) return null;
18 const Icon = sourceControlIcon(definition);
19 return <Icon className={MARKDOWN_LINK_FAVICON_CLASS_NAME} />;
20}
21
22const MarkdownLinkFavicon = memo(function MarkdownLinkFavicon({ host }: { host: string }) {
23 const [failedHost, setFailedHost] = useState<string | null>(null);
24 const brandIcon = sourceControlHostIcon(host);
25 const faviconUrl = brandIcon ? null : faviconUrlForOrigin(`https://${host}`);
26 return (
27 <span className="ms-[0.25em] me-[0.2em]" aria-hidden>
28 {brandIcon ? (
29 brandIcon
30 ) : faviconUrl === null || failedHost === host || failedFaviconHosts.has(host) ? (
31 <GlobeIcon className={MARKDOWN_LINK_FAVICON_CLASS_NAME} />
32 ) : (
33 <img
34 src={faviconUrl}
35 alt=""
36 loading="lazy"
37 draggable={false}
38 className={cn(MARKDOWN_LINK_FAVICON_CLASS_NAME, "rounded-sm")}
39 onError={() => {
40 failedFaviconHosts.add(host);
41 setFailedHost(host);
42 }}
43 />
44 )}
45 </span>
46 );
47});
48
49function leadingExternalLinkTextLength(text: string): number {
50 const protocol = /^(?:https?:\/\/)/i.exec(text)?.[0];
51 if (protocol) return protocol.length;
52 // A whole code point, so a surrogate pair is not split across elements.
53 return Array.from(text.slice(0, 2))[0]?.length ?? 0;
54}
55
56// Keep short labels such as PR numbers intact in narrow table columns.
57function breakableExternalLinkText(text: string): ReactNode[] {
58 return Array.from(text.matchAll(/\s+|\S+/g), ({ 0: word, index: start }) =>
59 word.length <= 16 ? (
60 word
61 ) : (
62 <span key={start} className="[overflow-wrap:anywhere]">
63 {word}
64 </span>
65 ),
66 );
67}
68
69export function MarkdownExternalLinkContent({
70 host,
71 plainText,
72 children,
73}: {
74 host: string;
75 plainText: string | null;
76 children: ReactNode;
77}) {
78 if (plainText) {
79 const leadingLength = leadingExternalLinkTextLength(plainText);
80 return (
81 <>
82 <span className="whitespace-nowrap">
83 <MarkdownLinkFavicon host={host} />
84 {plainText.slice(0, leadingLength)}
85 </span>
86 {breakableExternalLinkText(plainText.slice(leadingLength))}
87 </>
88 );
89 }
90
91 const childNodes = Children.toArray(children);
92 const firstChild = childNodes[0];
93
94 if (typeof firstChild === "string" && firstChild.length > 0) {
95 const leadingLength = leadingExternalLinkTextLength(firstChild);
96 return (
97 <>
98 <span className="whitespace-nowrap">
99 <MarkdownLinkFavicon host={host} />
100 {firstChild.slice(0, leadingLength)}
101 </span>
102 {breakableExternalLinkText(firstChild.slice(leadingLength))}
103 {childNodes.slice(1)}
104 </>
105 );
106 }
107
108 return (
109 <>
110 <span className="[overflow-wrap:anywhere]">
111 <MarkdownLinkFavicon host={host} />
112 {firstChild}
113 </span>
114 {childNodes.slice(1)}
115 </>
116 );
117}