Skip to content

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

135 lines5,694 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.

Agents as a team: lifecycle, merge queue, billing and a new shell1/**
2 * g1t's additions to GitHub flavoured markdown. They run on the HTML tree
3 * after it has been sanitized, so what they add is trusted and what the
4 * author wrote never is.
5 */
6
7type Text = { type: "text"; value: string };
8type Element = {
9 type: "element";
10 tagName: string;
11 properties: Record<string, unknown>;
12 children: Node[];
13};
14/** A node of the HTML tree, loosely: only what these plugins read. */
15type Node = { type: string; value?: string; tagName?: string; properties?: Record<string, unknown>; children?: Node[] };
16
17/** The repository a document belongs to, so `#12` can name its issues. */
18export type MarkdownRepo = { namespace: string; name: string };
19
20/** The repository a site path like `/acme/web` names. */
21export function repoAt(base?: string): MarkdownRepo | undefined {
22 const [namespace, name] = (base ?? "").split("/").filter(Boolean);
23 return namespace && name ? { namespace, name } : undefined;
24}
25
26export const ALERTS = ["note", "tip", "important", "warning", "caution"] as const;
27export type AlertKind = (typeof ALERTS)[number];
28
29const ALERT_MARKER = /^\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*\n?/i;
30
31function isElement(node: Node, tag?: string): node is Element {
32 return node.type === "element" && (tag == null || (node as Element).tagName === tag);
33}
34
35/**
36 * GitHub's alerts: a quote that starts with `[!NOTE]`, `[!TIP]`,
37 * `[!IMPORTANT]`, `[!WARNING]` or `[!CAUTION]` becomes a callout.
38 */
39export function rehypeAlerts() {
40 return (tree: Node) => {
41 const walk = (node: Node) => {
42 for (const child of node.children ?? []) {
43 if (isElement(child, "blockquote")) {
44 const first = child.children.find((c): c is Element => isElement(c, "p"));
45 const text = first?.children[0];
46 const match = text?.type === "text" ? ALERT_MARKER.exec((text as Text).value) : null;
47 if (first && text && match) {
48 (text as Text).value = (text as Text).value.slice(match[0].length);
49 // A marker on a line of its own leaves an empty paragraph behind.
50 if (first.children.length === 1 && !(text as Text).value.trim()) {
51 child.children = child.children.filter((c) => c !== first);
52 }
53 child.properties = { ...child.properties, dataAlert: match[1]!.toLowerCase() };
54 }
55 }
56 walk(child);
57 }
58 };
59 walk(tree);
60 };
61}
62
Merge branch 'worktree-agent-a8385d293d42c913a'63/**
64 * Where `@g1t` leads: g1t's own agent, which has no profile. Never `/g1t`,
65 * which staff made an alias of Flagon, Inc.'s workspace: mentioning g1t
66 * always means the agent.
67 */
68export const G1T_MENTION_HREF = "https://docs.g1t.sh/guides/working-with-g1t/";
69
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar70/** `owner/repo#12`, `#12`, `@workspace/team`, `@name` and commit hashes, in one pass. */
Agents as a team: lifecycle, merge queue, billing and a new shell71const REFERENCE =
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar72 /(?<![\w/@#])(?:([a-z0-9][a-z0-9-]*\/[a-z0-9._-]+)#(\d+)|#(\d+)|@([a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38})\/([a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,59})|@([a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38})|([0-9a-f]{7,40}))(?![\w-])/gi;
Agents as a team: lifecycle, merge queue, billing and a new shell73
74function link(href: string, text: string, kind: string): Element {
75 return {
76 type: "element",
77 tagName: "a",
78 properties: { href, dataRef: kind },
79 children: [{ type: "text", value: text }],
80 };
81}
82
83/**
84 * Links what a forge's text refers to: `#12` to an issue or pull request
85 * in this repository (the issue page sends a pull request's number on to
Merge branch 'worktree-agent-a8385d293d42c913a'86 * it), `owner/repo#12` to one elsewhere, `@name` to a person or workspace
87 * (`@g1t` to how g1t's agent works), and a commit hash to its commit.
88 * Nothing inside code or a link changes.
Agents as a team: lifecycle, merge queue, billing and a new shell89 */
90export function rehypeReferences(options: { repo?: MarkdownRepo }) {
91 const { repo } = options;
92 return (tree: Node) => {
93 const walk = (node: Node) => {
94 const children = node.children;
95 if (!children) return;
96 for (let index = 0; index < children.length; index++) {
97 const child = children[index]!;
98 if (isElement(child)) {
99 if (child.tagName === "a" || child.tagName === "code" || child.tagName === "pre") continue;
100 walk(child);
101 continue;
102 }
103 if (child.type !== "text") continue;
104 const value = (child as Text).value;
105 const parts: Node[] = [];
106 let last = 0;
107 for (const match of value.matchAll(REFERENCE)) {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar108 const [whole, other, otherNumber, number, workspace, team, name, hash] = match;
Agents as a team: lifecycle, merge queue, billing and a new shell109 let replacement: Element | null = null;
110 if (other && otherNumber) {
111 replacement = link(`/${other}/issues/${otherNumber}`, whole, "issue");
112 } else if (number && repo) {
113 replacement = link(`/${repo.namespace}/${repo.name}/issues/${number}`, whole, "issue");
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar114 } else if (workspace && team) {
115 replacement = link(`/${workspace.toLowerCase()}/-/teams/${team.toLowerCase()}`, whole, "team");
Agents as a team: lifecycle, merge queue, billing and a new shell116 } else if (name) {
Merge branch 'worktree-agent-a8385d293d42c913a'117 const href = name.toLowerCase() === "g1t" ? G1T_MENTION_HREF : `/${name.toLowerCase()}`;
118 replacement = link(href, whole, "mention");
Agents as a team: lifecycle, merge queue, billing and a new shell119 } else if (hash && repo && /\d/.test(hash) && /[a-f]/i.test(hash)) {
120 replacement = link(`/${repo.namespace}/${repo.name}/commit/${hash}`, hash.slice(0, 7), "commit");
121 }
122 if (!replacement) continue;
123 if (match.index! > last) parts.push({ type: "text", value: value.slice(last, match.index) });
124 parts.push(replacement);
125 last = match.index! + whole.length;
126 }
127 if (parts.length === 0) continue;
128 if (last < value.length) parts.push({ type: "text", value: value.slice(last) });
129 children.splice(index, 1, ...parts);
130 index += parts.length - 1;
131 }
132 };
133 walk(tree);
134 };
135}

This file's history is long; its oldest lines are credited to the oldest commit read.