1import { formatProviderSkillDisplayName } from "@t3tools/shared/inlineSkills";
2import { Children, cloneElement, isValidElement, type ReactNode } from "react";
3import { matchInlineSkills, type InlineSkill } from "@t3tools/shared/inlineSkills";
4
5import { SKILL_CHIP_ICON_SVG } from "../composerInlineChip";
6import { ContextChip, ContextChipLabel } from "../ContextChip";
7
8export function SkillInlineText(props: { text: string; skills: ReadonlyArray<InlineSkill> }) {
9 const nodes: ReactNode[] = [];
10 let cursor = 0;
11
12 for (const { start, end, skill, rawText } of matchInlineSkills(props.text, props.skills)) {
13 if (start > cursor) {
14 nodes.push(props.text.slice(cursor, start));
15 }
16 nodes.push(<SkillChip key={`${start}:${skill.name}`} skill={skill} rawText={rawText} />);
17 cursor = end;
18 }
19
20 if (cursor === 0) {
21 return <>{props.text}</>;
22 }
23 if (cursor < props.text.length) {
24 nodes.push(props.text.slice(cursor));
25 }
26 return <>{nodes}</>;
27}
28
29export function renderSkillInlineMarkdownChildren(
30 children: ReactNode,
31 skills: ReadonlyArray<InlineSkill>,
32): ReactNode {
33 return Children.map(children, (child) => {
34 if (typeof child === "string") {
35 return <SkillInlineText text={child} skills={skills} />;
36 }
37 if (!isValidElement<{ children?: ReactNode; node?: { tagName?: string } }>(child)) {
38 return child;
39 }
40 // Custom react-markdown components replace the intrinsic type, so also
41 // check the hast node they carry.
42 const markdownTagName = typeof child.type === "string" ? child.type : child.props.node?.tagName;
43 if (markdownTagName === "code" || markdownTagName === "a") {
44 return child;
45 }
46 if (!("children" in child.props)) {
47 return child;
48 }
49 return cloneElement(
50 child,
51 undefined,
52 renderSkillInlineMarkdownChildren(child.props.children, skills),
53 );
54 });
55}
56
57function SkillChip(props: { skill: InlineSkill; rawText: string }) {
58 return (
59 <ContextChip kind="skill" data-markdown-copy={props.rawText}>
60 <SkillChipIcon />
61 <ContextChipLabel>{formatProviderSkillDisplayName(props.skill)}</ContextChipLabel>
62 </ContextChip>
63 );
64}
65
66/** The skill glyph; the surrounding chip sizes its svg. */
67export function SkillChipIcon() {
68 return (
69 <span
70 aria-hidden="true"
71 className="contents"
72 dangerouslySetInnerHTML={{ __html: SKILL_CHIP_ICON_SVG }}
73 />
74 );
75}