Skip to content
38 linesCodeBlameRaw
1/**
2 * The few marks an agent's one-line summary carries, read so a row shows
3 * them as marks rather than as asterisks and backticks: **bold**, *italic*
4 * or _italic_, `code`, and [links](…) as their words. Nothing else; a row is one line, not a document.
5 */
6
7export type InlinePart = { kind: "text" | "strong" | "em" | "code"; text: string };
8
9const MARKS = /`([^`]+)`|\*\*([^*]+?)\*\*|__([^_]+?)__|\*([^*\s][^*]*?)\*|(?<![\w])_([^_\s][^_]*?)_(?![\w])|\[([^\]]+)\]\([^)\s]*\)/g;
10
11export function inlineParts(text: string): InlinePart[] {
12 const parts: InlinePart[] = [];
13 const push = (kind: InlinePart["kind"], value: string) => {
14 if (!value) return;
15 const last = parts.at(-1);
16 if (kind === "text" && last?.kind === "text") last.text += value;
17 else parts.push({ kind, text: value });
18 };
19 let at = 0;
20 for (const match of text.matchAll(MARKS)) {
21 push("text", text.slice(at, match.index));
22 const [, code, strong, strongU, em, emU, link] = match;
23 if (code != null) push("code", code);
24 else if (strong != null || strongU != null) push("strong", (strong ?? strongU)!);
25 else if (em != null || emU != null) push("em", (em ?? emU)!);
26 else push("text", link ?? "");
27 at = match.index + match[0].length;
28 }
29 push("text", text.slice(at));
30 return parts;
31}
32
33/** The words alone, for a tooltip or an attribute. */
34export function inlinePlain(text: string): string {
35 return inlineParts(text)
36 .map((part) => part.text)
37 .join("");
38}