Skip to content

g1t/apps/web/app/lib/inline-markdown.ts

39 lines1,514 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Usage, Billing settings and prepaid AI credit; fixes from the UX audit1/**
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
8export type InlinePart = { kind: "text" | "strong" | "em" | "code"; text: string };
9
10const MARKS = /`([^`]+)`|\*\*([^*]+?)\*\*|__([^_]+?)__|\*([^*\s][^*]*?)\*|(?<![\w])_([^_\s][^_]*?)_(?![\w])|\[([^\]]+)\]\([^)\s]*\)/g;
11
12export 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. */
35export function inlinePlain(text: string): string {
36 return inlineParts(text)
37 .map((part) => part.text)
38 .join("");
39}