@tanishqkancharla/diffmap 0.3.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/host/home.md CHANGED
@@ -1,156 +1,201 @@
1
1
  # diffmap
2
2
 
3
- A viewer for specs and walkthroughs. Call stacks, mermaid that links into source, and a Diff panel. This page is one — `parseViewerDocument` plus `ViewerApp`, same path as `diffmap serve`.
3
+ **Your coding agent writes more code than you can review.**
4
4
 
5
- TOC on the left, article here, Diff on the right. Click a stack row or a mermaid node.
5
+ **You should be spending time reviewing design decisions and risky changes, not boilerplate.**
6
6
 
7
- ## Install the skill
7
+ `diffmap` is an agent skill that lets your agent visually explain their changes to you. Agents can create interactive diagrams that link back to the source code.
8
8
 
9
- ```sh
10
- npx skills add tanishqkancharla/diffmap --skill generate-diffmap
11
- ```
12
-
13
- That keeps a living spec in `specs/<name>.md`. Same file for the life of the work: what’s already in the tree, and what’s still planned. Don’t start a second walkthrough later.
14
-
15
- ## Serve locally
9
+ You can try `diffmap` before you install it. In a repository that has changes, paste this prompt into your agent:
16
10
 
17
- ```sh
18
- npx @tanishqkancharla/diffmap serve specs/<name>.md
11
+ ```text
12
+ Write a diffmap spec for the changes on this branch. Use the instructions at
13
+ https://raw.githubusercontent.com/tanishqkancharla/diffmap/main/skills/generate-diffmap/SKILL.md
14
+ Then serve the spec with npx @tanishqkancharla/diffmap and send me the link.
19
15
  ```
20
16
 
21
- Bare `npx @tanishqkancharla/diffmap specs/<name>.md` is the same as `serve`. `list` shows running viewers — reuse the URL instead of starting a second server.
22
-
23
- **Close server** is only on that local page. This host has no process to stop.
24
-
25
- ## Share
26
-
27
- ### Gist
17
+ If you like the result, install the skill:
28
18
 
29
19
  ```sh
30
- npx @tanishqkancharla/diffmap share specs/<name>.md
20
+ npx skills add tanishqkancharla/diffmap --skill generate-diffmap
31
21
  ```
32
22
 
33
- Prints `https://diffmap.dev/g/<gistId>` (optional `/<file.md>`). Secret gist: unlisted, not private. Anyone with the id can read it. Does not start a local server.
34
-
35
- ### GitHub PR vs commit
36
-
37
- A spec on a public PR:
38
-
39
- `https://diffmap.dev/<owner>/<repo>/pull/<n>/specs/<name>.md`
40
-
41
- Pinned commit:
42
-
43
- `https://diffmap.dev/<owner>/<repo>/commit/<sha>/specs/<name>.md`
44
-
45
- The pin is one SHA. The markdown and every `[[path]]`, mermaid `%% ref`, and excerpt load from that commit, not `main`. Commit references load their diffs from the GitHub API. A PR number only chooses the SHA (head at load, then pin). Refresh stays on the commit URL.
46
-
47
- ## What the fences are
23
+ `diffmap` is [open source](https://github.com/tanishqkancharla/diffmap) and MIT licensed. Diffmaps are hosted locally by default, but you can choose to [upload a diffmap as a secret gist](#share-a-spec-with-a-link) and open it on diffmap.dev.
24
+
25
+ ## See the main ideas
26
+
27
+ Your agent selects the best diagram for each idea. Each part of the diagram has a link to its code.
28
+
29
+ ### Service diagrams
30
+
31
+ A service diagram shows each service and its state. Each step shows one event and the state that the event changes. Thus, you do not have to remember the state.
32
+
33
+ To see how `diffmap serve` opens a spec, press **Play**. To read about a name with a dotted line, put the pointer on the name:
34
+
35
+ ```services
36
+ service Laptop
37
+ > This is your computer. diffmap serve reads files here and does not upload them.
38
+ service Agent "coding agent"
39
+ > The agent writes the spec and runs diffmap for you.
40
+ service CLI "diffmap serve" [[src/serve.ts#startServer]]
41
+ > This small server uses git to read your repository.
42
+ state url = none
43
+ service Repo "your repository"
44
+ > The repository contains the spec and the commits that the spec refers to.
45
+ service Browser
46
+ service Page "the spec" [[src/viewer/ViewerApp.tsx#ViewerApp]]
47
+ state page = empty
48
+ service Panel "Diff panel" [[src/viewer/SourceDiffPanel.tsx#SourceDiffPanel]]
49
+ > The Diff panel opens next to the spec. It shows the code for the link that you clicked.
50
+ state shows = none
51
+
52
+ start "diffmap serve opens a spec in your browser"
53
+ > The agent has written the spec, but nothing serves it yet.
54
+
55
+ step "The agent serves the spec"
56
+ > Your agent runs diffmap serve with the path of the spec.
57
+ Agent -> CLI: serve specs/search.md
58
+ step "serve examines each commit in the spec"
59
+ > Before the server starts, it uses git to read each commit and line range that the spec refers to.
60
+ CLI -> Repo: readDocumentChanges [[src/git.ts#readDocumentChanges]]
61
+ > If a SHA or a line range is wrong, the command stops here. Thus, the page has no broken links.
62
+ step "serve prints a URL for you"
63
+ > The server starts on your computer and gives the agent a local URL.
64
+ CLI -> Agent: localhost URL
65
+ CLI.url = localhost:4321
66
+ step "You open the page"
67
+ > Your browser gets the spec from the server and shows it as a page.
68
+ Page -> CLI: GET /
69
+ CLI -> Page: spec
70
+ Page.page = rendered
71
+ step "You click a call stack row"
72
+ > The Diff panel asks the server for the code that the row refers to.
73
+ Panel -> CLI: /__diffmap/source
74
+ CLI -> Repo: git show
75
+ > The server reads the file from the commit in the row, not from the working tree.
76
+ step "The Diff panel shows the code"
77
+ > The server sends the lines, and the Diff panel shows them next to the spec.
78
+ CLI -> Panel: lines
79
+ Panel.shows = code
80
+ ```
48
81
 
49
- ### System flow
82
+ ### Mermaid diagrams
50
83
 
51
- Click a node. `%% ref` is a mermaid comment; `[[path]]`, `[[path#symbol]]`, and `[[<sha>:path#L12-L30]]` are the same references as on a stack row.
84
+ The page shows Mermaid diagrams. Each node and each arrow can have a link to its code. This diagram shows what occurs when you click a link:
52
85
 
53
86
  ```mermaid
54
87
  flowchart LR
55
- Spec[Living spec] --> Serve[serve]
56
- Spec --> Share[share]
57
- Spec --> Pin[GitHub pin]
58
- %% ref node:Spec [[src/parseViewer.ts#parseViewerDocument]]
59
- %% ref node:Serve [[src/cli.ts]]
60
- %% ref node:Share [[src/share.ts#shareMarkdownFile]]
61
- %% ref node:Pin [[src/github/route.ts]]
62
- %% ref edge:0 [[1257fe8:src/cli.ts#L49-L50]]
88
+ Click[Click a link] --> Find[useSourceReference]
89
+ Find -->|diffmap serve| Local[Read the repo with git]
90
+ Find -->|diffmap.dev| GitHub[fetchPinnedBlob]
91
+ Local --> Panel[SourceDiffPanel]
92
+ GitHub --> Panel
93
+ %% ref node:Find [[src/viewer/sourceNavigation.ts#useSourceReference]]
94
+ %% ref node:GitHub [[src/github/fetchRepo.ts#fetchPinnedBlob]]
95
+ %% ref node:Panel [[src/viewer/SourceDiffPanel.tsx#SourceDiffPanel]]
96
+ %% ref edge:1 [[src/viewer/sourceNavigation.ts#requestSource]]
63
97
  ```
64
98
 
65
- ````md
66
- ```mermaid
67
- flowchart LR
68
- Spec[Living spec] --> Serve[serve]
69
- %% ref node:Serve [[src/cli.ts]]
70
- ```
71
- ````
72
-
73
99
  ### Call stacks
74
100
 
75
- `-` is current, `+` is proposed. After a phase lands, commit it and retarget `[[path]]` to the commit: `[[<sha>:path#L12-L30]]`. Line numbers are the file after the commit.
101
+ Each row has the form `result = call(inputs)`. Thus, you can follow the data from one call to the next call. A `+` row is new code, and a `−` row is old code that the change removes. Each row has a link to its code.
76
102
 
77
- ```callstack
78
- ViewerApp
79
- -└── DoneButton # a loud Done control
80
- +└── CloseServerButton # quiet, local mode only [[dae6b98:src/viewer/ViewerApp.tsx#L219-L227]] [[dae6b98:src/viewer/CloseServerButton.tsx#L8-L16]]
81
- └── DiffButton
82
- ```
103
+ This call stack shows how `diffmap` draws call stacks:
83
104
 
84
105
  ```callstack
85
- shareMarkdownFile [[src/share.ts#shareMarkdownFile]]
86
- -└── print the gist URL
87
- +└── gistViewerUrl # https://diffmap.dev/g/<id> [[d14ae1a:src/share.ts#L55-L59]]
106
+ fence = parseFence(lang, source) [[src/parseFence.ts#parseFence]]
107
+ +└── lines = parseCallStack(source) # one row per line, with its links [[src/annotations.ts#parseCallStack]]
108
+ view = Fence({ fence }) [[src/viewer/Fence.tsx#Fence]]
109
+ -└── return CodeBlock(fence.source) # the tree as plain text
110
+ +├── if fence.kind === "callstack":
111
+ +│ └── return CallStackDiff(fence.lines) # rows with + and − marks [[src/viewer/CallStackDiff.tsx#CallStackDiff]]
112
+ +└── return CodeBlock(fence.source) # other languages stay pseudocode
88
113
  ```
89
114
 
90
- ### Changes
115
+ ## Open the code
91
116
 
92
- A `changes` fence names a commit. It renders a card with every file that commit changed. **Review** opens the Diff panel on all of them, with a sticky header per file; a row jumps to its file. An empty body means uncommitted changes.
117
+ A function name in a sentence can also be a link, for example [[src/share.ts#shareMarkdownFile]].
118
+
119
+ When the work is complete, the spec shows a card for the commit. The links in the spec then refer to that commit. These links always show the code from that commit, also when the file changes later.
93
120
 
94
121
  ```changes
95
- dae6b98
122
+ 6e4b5d3
96
123
  ```
97
124
 
98
- Commit references must name a file the commit changed, and the range must fit the file after the commit. `serve` and `share` check this at startup.
99
-
100
- ### File excerpts
101
-
102
- `start:end:path`. Locally the server reads the workspace file. Here the bundled file is sliced the same way.
103
-
104
- ```58:72:src/cli.ts
105
- const cli = Cli.create("diffmap", {
106
- description: "Serve a spec locally, or share it as a gist",
107
- version: packageVersion,
108
- args: serveArgs,
109
- options: serveOptions,
110
- output: serveOutput,
111
- hint: "`serve <spec.md>` runs the local viewer. `share <spec.md>` publishes a secret gist. `update` (or `--update`) installs the latest diffmap.",
112
- examples: [
113
- {
114
- args: { file: "spec.md" },
115
- description: "Same as `diffmap serve spec.md`",
116
- },
117
- ],
118
- run: runServe,
119
- })
125
+ ```callstack
126
+ nodes = fromNode(text) [[6e4b5d3:src/parseViewer.ts#L318-L326]]
127
+ -└── return [{ type: "text", value: text }] # prose stays plain text
128
+ +└── return proseText(text) [[6e4b5d3:src/parseViewer.ts#L329-L353]]
129
+ + ├── reference = parseReference(match) # each link in double brackets
130
+ + └── return [...textBefore, { type: "reference", annotation }]
131
+ element = renderNode(node) [[6e4b5d3:src/viewer/ComarkView.tsx#L64-L68]]
132
+ +└── if node.type === "reference":
133
+ + └── return InlineReference(node.annotation) # a code button that opens the Diff panel [[6e4b5d3:src/viewer/ComarkView.tsx#L173-L189]]
120
134
  ```
121
135
 
122
- ### HTML
136
+ ## Share a spec with a link
123
137
 
124
- Local `serve` trusts `html` in the file you pointed at. This host does not. Gist and GitHub pages are the same — no `dangerouslySetInnerHTML` from untrusted markdown.
138
+ A teammate can read your spec without a copy of your branch. One command uploads the spec as a secret GitHub gist and prints the link:
125
139
 
126
- ```html
127
- <aside>If this renders, the host leaked HTML.</aside>
140
+ ```sh
141
+ npx @tanishqkancharla/diffmap share specs/search.md
142
+ # prints https://diffmap.dev/g/<gistId>
128
143
  ```
129
144
 
130
- ## How a spec stays alive
145
+ ::details{summary="How sharing works"}
146
+ The shared page cannot use git or a copy of your repository. Thus, the gist also contains the data that the spec uses:
131
147
 
132
- Write `specs/<name>.md`. Serve it while you plan. When the plan changes, edit this file. When a phase lands, commit it, add a `changes` fence, and retarget the links to the commit so the Diff panel opens the change, not just the file.
148
+ - A snapshot of each commit that the spec refers to
149
+ - The current contents of each file that the spec quotes.
133
150
 
134
- ### Phase 1: Spec and serve
151
+ Because of this data, this page can show code:
135
152
 
136
- ```callstack
137
- generate-diffmap
138
- └── specs/name.md
139
- └── diffmap serve [[src/cli.ts]]
153
+ ```mermaid
154
+ flowchart LR
155
+ subgraph Gist
156
+ Spec[spec.md]
157
+ Changes[diffmap-changes.json]
158
+ Files[diffmap-files.json]
159
+ end
160
+ Spec -->|Markdown| Page[The spec page]
161
+ Changes -->|commits| Cards[Changes cards and commit links]
162
+ Files -->|file contents| Excerpts[Excerpts and plain links]
163
+ %% ref node:Changes [[2f96051:src/share.ts#L44-L53]]
164
+ %% ref node:Files [[2f96051:src/share.ts#L105-L125]]
165
+ %% ref node:Excerpts [[src/host/files.ts#HostFilesContext]]
140
166
  ```
141
167
 
142
- - [x] Install the skill.
143
- - [x] Keep one living spec.
144
- - [x] `diffmap serve` / `list`.
168
+ ```changes
169
+ 2f96051
170
+ ```
145
171
 
146
- ### Phase 2: Share it
172
+ `share` makes the two snapshots before it calls `gh`. Thus, `diffmap` does not keep a GitHub token:
147
173
 
148
174
  ```callstack
149
- specs/name.md
150
- -└── localhost only
151
- +├── diffmap share [[d14ae1a:src/share.ts#L55-L59]]
152
- +└── GitHub PR URL [[src/github/route.ts]]
175
+ result = shareMarkdownFile({ filePath, workspaceRoot }) [[2f96051:src/share.ts#L26-L103]]
176
+ ├── parsed = parseViewerDocument(source)
177
+ ├── if parsed is an error: return parsed # a broken diagram stops here
178
+ ├── changes = readDocumentChanges(workspaceRoot, parsed) # each linked commit
179
+ +├── files = readDocumentFiles(workspaceRoot, parsed) # each quoted file [[2f96051:src/share.ts#L105-L125]]
180
+ ├── await gh("auth", "status")
181
+ -├── created = await gh("gist", "create", spec, changes)
182
+ +├── created = await gh("gist", "create", spec, changes, files)
183
+ └── return { gistId, viewerUrl: gistViewerUrl(gistId) }
184
+ ```
185
+
186
+ This excerpt is from one of the quoted files. Thus, a shared copy of this page also shows it:
187
+
188
+ ```54:63:src/share.ts
189
+ // It has no files either, so excerpts and references without a commit read these.
190
+ const files = readDocumentFiles(input.workspaceRoot, parsed);
191
+ if (files instanceof Error) return files;
192
+
193
+ const auth = await runGh(["auth", "status"]);
194
+ if (auth instanceof DiffmapShareError) return auth;
195
+
196
+ const args = ["gist", "create"];
197
+ if (input.isPublic) args.push("--public");
198
+ args.push("--desc", extractTitle(source), filePath);
153
199
  ```
154
200
 
155
- - [x] Gist URL for a snapshot.
156
- - [x] PR URL when the spec lives in the repo (pin to one SHA).
201
+ ::
@@ -32,7 +32,7 @@ test("homepage markdown is a real viewer document", async () => {
32
32
  assert.equal(kinds.has("callstack"), true);
33
33
  assert.equal(kinds.has("changes"), true);
34
34
  assert.equal(kinds.has("file"), true);
35
- assert.equal(kinds.has("html"), true);
35
+ assert.equal(kinds.has("services"), true);
36
36
 
37
37
  const mermaid = fences.find((fence) => fence.kind === "mermaid");
38
38
  assert.notEqual(mermaid, undefined);
@@ -62,22 +62,20 @@ test("homepage markdown is a real viewer document", async () => {
62
62
  const levels = new Set(document.headings.map((heading) => heading.level));
63
63
  assert.equal(levels.has(1), true);
64
64
  assert.equal(levels.has(2), true);
65
- assert.equal(levels.has(3), true);
66
65
  assert.ok(
67
66
  document.references.filter((ref) => ref.rev !== undefined).length >= 3,
68
67
  );
69
68
 
70
69
  const excerpt = fences.find(
71
- (fence) => fence.kind === "file" && fence.path === "src/cli.ts",
70
+ (fence) => fence.kind === "file" && fence.path === "src/share.ts",
72
71
  );
73
72
  assert.equal(excerpt?.kind, "file");
74
73
  if (excerpt?.kind !== "file") return;
75
- const cliPath = path.join(path.dirname(homePath), "..", "cli.ts");
76
- const cli = await fs.readFile(cliPath, "utf8");
77
- const lines = cli.split(/\n/);
74
+ const sharePath = path.join(path.dirname(homePath), "..", "share.ts");
75
+ const share = await fs.readFile(sharePath, "utf8");
76
+ const lines = share.split(/\n/);
78
77
  const slice = lines.slice(excerpt.start - 1, excerpt.end).join("\n");
79
78
  assert.equal(lines[excerpt.start - 1]?.trim().length !== 0, true);
80
- assert.match(slice, /packageVersion/);
81
- assert.match(slice, /\}\)\s*$/);
79
+ assert.match(slice, /"gist", "create"/);
82
80
  assert.equal(excerpt.source, slice);
83
81
  });
package/src/parseFence.ts CHANGED
@@ -1,18 +1,60 @@
1
1
  import { parseCallStack, type SourceAnnotation } from "./annotations.js";
2
2
  import { WORKING, type Rev } from "./changes.js";
3
+ import { parseCodeNotes, type CodeNote } from "./codeNotes.js";
3
4
  import {
4
5
  parseDiagramAnnotations,
5
6
  type DiagramAnnotation,
6
7
  } from "./diagramAnnotations.js";
8
+ import { parseServiceDiagram, type ServicesParse } from "./services.js";
7
9
 
8
10
  export type Fence =
9
- | { kind: "mermaid"; source: string; annotations: DiagramAnnotation[] }
11
+ | {
12
+ kind: "mermaid";
13
+ /** In a diff, every line with its sign removed; see mermaidFence. */
14
+ source: string;
15
+ annotations: DiagramAnnotation[];
16
+ /** From ```mermaid diff: the diagram without `+` lines, and without `-` lines. */
17
+ sides: { today: string; proposed: string } | undefined;
18
+ /** From the info string, as in ```mermaid [Request flow]. */
19
+ title: string | undefined;
20
+ }
21
+ | {
22
+ kind: "services";
23
+ source: string;
24
+ parsed: ServicesParse;
25
+ }
10
26
  | { kind: "html"; source: string }
11
- | { kind: "callstack"; source: string; lines: SourceAnnotation[] }
12
- | { kind: "changes"; rev: Rev; source: string }
27
+ | {
28
+ kind: "callstack";
29
+ /** Rows with trailing note lines removed. */
30
+ source: string;
31
+ lines: SourceAnnotation[];
32
+ /** 1-based rows from the info string, as in ```callstack {2}. */
33
+ highlights: number[];
34
+ notes: CodeNote[];
35
+ /** From the info string, as in ```callstack [Request flow]. */
36
+ title: string | undefined;
37
+ }
38
+ | {
39
+ kind: "changes";
40
+ rev: Rev;
41
+ /** Paths to review first, in order; other changed files fold away. */
42
+ review: string[];
43
+ source: string;
44
+ }
13
45
  | { kind: "file"; path: string; start: number; end: number; source: string }
14
46
  | { kind: "diff"; path: string | undefined; source: string }
15
- | { kind: "code"; lang: string; source: string };
47
+ | {
48
+ kind: "code";
49
+ lang: string;
50
+ /** Code with trailing note lines removed. */
51
+ source: string;
52
+ /** 1-based lines from the info string, as in ```ts {2,4-6}. */
53
+ highlights: number[];
54
+ notes: CodeNote[];
55
+ /** From the info string, as in ```ts [src/request.ts]. */
56
+ title: string | undefined;
57
+ };
16
58
 
17
59
  const fileRefPattern = /^(\d+):(\d+):(.+)$/;
18
60
  const diffPathPattern = /^diff:(.+)$/;
@@ -31,27 +73,33 @@ export function pathFromDiffSource(source: string) {
31
73
  return undefined;
32
74
  }
33
75
 
34
- export function parseFence(lang: string, source: string): Fence {
76
+ export function parseFence(
77
+ lang: string,
78
+ source: string,
79
+ highlights: number[] = [],
80
+ title?: string,
81
+ /** The info string after the language and highlights, as in ```mermaid diff. */
82
+ meta?: string,
83
+ ): Fence {
35
84
  const trimmed = source.replace(/\n$/, "");
36
- if (lang === "mermaid")
85
+ const diff = meta?.split(/\s+/).includes("diff") ?? false;
86
+ if (lang === "mermaid") return mermaidFence(trimmed, diff, title);
87
+ if (lang === "services")
37
88
  return {
38
- kind: "mermaid",
89
+ kind: "services",
39
90
  source: trimmed,
40
- annotations: parseDiagramAnnotations(trimmed),
91
+ parsed: parseServiceDiagram(trimmed, diff),
41
92
  };
42
93
  if (lang === "html") return { kind: "html", source: trimmed };
43
- if (lang === "callstack")
44
- return {
45
- kind: "callstack",
46
- source: trimmed,
47
- lines: parseCallStack(trimmed),
48
- };
94
+ if (lang === "callstack") return callStackFence(trimmed, highlights, title);
49
95
 
96
+ // The first line is the rev (blank for uncommitted); each later line is a path to review.
50
97
  if (lang === "changes") {
51
- const rev = trimmed.trim();
98
+ const [rev = "", ...paths] = trimmed.split("\n").map((line) => line.trim());
52
99
  return {
53
100
  kind: "changes",
54
101
  rev: rev.length === 0 ? WORKING : rev,
102
+ review: paths.filter((line) => line.length > 0),
55
103
  source: trimmed,
56
104
  };
57
105
  }
@@ -69,13 +117,8 @@ export function parseFence(lang: string, source: string): Fence {
69
117
 
70
118
  const diffPath = diffPathPattern.exec(lang);
71
119
  if (lang === "diff" || diffPath !== null) {
72
- if (isCallStackSource(trimmed)) {
73
- return {
74
- kind: "callstack",
75
- source: trimmed,
76
- lines: parseCallStack(trimmed),
77
- };
78
- }
120
+ if (isCallStackSource(trimmed))
121
+ return callStackFence(trimmed, highlights, title);
79
122
  return {
80
123
  kind: "diff",
81
124
  path: diffPath === null ? pathFromDiffSource(trimmed) : diffPath[1],
@@ -83,9 +126,75 @@ export function parseFence(lang: string, source: string): Fence {
83
126
  };
84
127
  }
85
128
 
129
+ const { code, notes } = parseCodeNotes(trimmed);
86
130
  return {
87
131
  kind: "code",
88
132
  lang: lang.length === 0 ? "text" : lang,
89
- source: trimmed,
133
+ source: code,
134
+ highlights,
135
+ notes,
136
+ title,
137
+ };
138
+ }
139
+
140
+ /**
141
+ * A diff's lines start with `-` (Today only), `+` (Proposed only), or
142
+ * anything else (both). The viewer draws every line, then marks what each
143
+ * side lacks, so Mermaid syntax never needs parsing here.
144
+ */
145
+ function mermaidFence(
146
+ source: string,
147
+ diff: boolean,
148
+ title: string | undefined,
149
+ ): Fence {
150
+ if (!diff)
151
+ return {
152
+ kind: "mermaid",
153
+ source,
154
+ annotations: parseDiagramAnnotations(source),
155
+ sides: undefined,
156
+ title,
157
+ };
158
+ const rows = source.split("\n").map((line) => ({
159
+ sign: line[0],
160
+ text: line.replace(/^[+ -]/, ""),
161
+ }));
162
+ const without = (sign: string) =>
163
+ rows
164
+ .filter((row) => row.sign !== sign)
165
+ .map((row) => row.text)
166
+ .join("\n");
167
+ // Mermaid keeps a node's first label, so within each run of changed lines
168
+ // the `+` lines come first and a relabeled node shows its Proposed label.
169
+ const merged: string[] = [];
170
+ const removed: string[] = [];
171
+ for (const row of rows) {
172
+ if (row.sign === "-") removed.push(row.text);
173
+ else if (row.sign === "+") merged.push(row.text);
174
+ else merged.push(...removed.splice(0), row.text);
175
+ }
176
+ merged.push(...removed);
177
+ return {
178
+ kind: "mermaid",
179
+ source: merged.join("\n"),
180
+ annotations: parseDiagramAnnotations(merged.join("\n")),
181
+ sides: { today: without("+"), proposed: without("-") },
182
+ title,
183
+ };
184
+ }
185
+
186
+ function callStackFence(
187
+ source: string,
188
+ highlights: number[],
189
+ title: string | undefined,
190
+ ): Fence {
191
+ const { code, notes } = parseCodeNotes(source);
192
+ return {
193
+ kind: "callstack",
194
+ source: code,
195
+ lines: parseCallStack(code),
196
+ highlights,
197
+ notes,
198
+ title,
90
199
  };
91
200
  }