| 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 | |
| 7 | export type InlinePart = { kind: "text" | "strong" | "em" | "code"; text: string }; |
| 8 | |
| 9 | const MARKS = /`([^`]+)`|\*\*([^*]+?)\*\*|__([^_]+?)__|\*([^*\s][^*]*?)\*|(?<![\w])_([^_\s][^_]*?)_(?![\w])|\[([^\]]+)\]\([^)\s]*\)/g; |
| 10 | |
| 11 | export 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. */ |
| 34 | export function inlinePlain(text: string): string { |
| 35 | return inlineParts(text) |
| 36 | .map((part) => part.text) |
| 37 | .join(""); |
| 38 | } |