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