@tanishqkancharla/diffmap 0.4.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/README.md CHANGED
@@ -76,13 +76,14 @@ import {
76
76
  | ---------------------------------------------- | ------------------------------------------------------------------------------------------------- |
77
77
  | `mermaid` | Beautiful Mermaid |
78
78
  | `services` | Animated service diagram you can play or step through |
79
+ | `mermaid diff` or `services diff` | The same diagram with [added and removed parts marked](#diagram-diffs) |
79
80
  | `callstack` or `diff` containing `└──` / `├──` | Interactive stack rows, no file header |
80
81
  | `diff` or `diff:path` with a file path | Pierre patch with Pierre’s file header |
81
82
  | `diff` with no path | Pierre patch, no file header |
82
83
  | `changes` with a commit, range, or empty body | Files changed card with a Review button; empty means uncommitted changes |
83
84
  | `start:end:path` | Pierre file excerpt with Pierre’s file header |
84
85
  | `html` | Trusted HTML from this file. diffmap does not sanitize it. Only use it for local files you wrote. |
85
- | other langs | Highlighted code with optional [line highlights and notes](#highlight-lines-and-add-notes) |
86
+ | other langs | Highlighted code with optional [titles, highlights, and notes](#highlight-lines-and-add-notes) |
86
87
 
87
88
  Fences keep whitespace. Use them for mermaid, call stacks, and diffs.
88
89
 
@@ -110,6 +111,13 @@ function requestHandler(record: RecordInput) {
110
111
  - A symbol that does not appear, overlapping line notes, or a line number
111
112
  outside the fence is a parse error. A note line above other code stays code.
112
113
 
114
+ Add `[Title]` after the language to show a title above the code, as in
115
+ ` ```ts [src/request.ts] {2-3} `. Call stacks and `mermaid` fences take a
116
+ title the same way, as in ` ```mermaid diff [Request flow] `, or
117
+ `::callstack{title="Request flow"}` and `::mermaid{title="Request flow"}` in
118
+ MDC. A `services` fence uses its [start caption](#animated-service-diagrams)
119
+ instead.
120
+
113
121
  Call stacks take the same highlights and notes, counting rows from the first.
114
122
  Write their notes with `#`. A symbol note matches the row text you see, not
115
123
  hidden references. Inside a linked row, symbol notes show on hover only; the
@@ -146,8 +154,13 @@ startServer
146
154
  ::html
147
155
  <aside>Note.</aside>
148
156
  ::
157
+
158
+ ::details{summary="How it works"}
159
+ Folded content: prose, lists, and any fence above.
160
+ ::
149
161
  ```
150
162
 
163
+ `::details` folds its content under the `summary` label until the reader opens it.
151
164
  `.md` and `.mdx` are both markdown. Curly braces in prose are plain text.
152
165
 
153
166
  ## Link call stacks to source changes
@@ -244,6 +257,8 @@ service API "HTTP server" [[src/api.ts]]
244
257
  state attempts = 0
245
258
  > Failures since the last success. Five locks the account.
246
259
 
260
+ start "Alice logs in"
261
+ > She is signed out.
247
262
  step "The browser submits the form"
248
263
  Browser -> API: POST /login
249
264
  step "The API asks Auth to verify"
@@ -261,6 +276,11 @@ step "The API sets the session cookie"
261
276
  another to nest it. Names are unique within the diagram.
262
277
  - `state <key> = <value>` declares a value and its initial state. Indent it under
263
278
  its service.
279
+ - `start "caption"` at column 0, before the first step, captions the whole
280
+ diagram, such as `start "Alice logs in"`. It shows above the diagram until
281
+ the reader steps, and each step's caption replaces it. A `>` line under it
282
+ can say what the initial state shows. A `services` fence takes no `[Title]`;
283
+ the start caption is its title.
264
284
  - `step "caption"` starts a step at column 0. The caption is required: a
265
285
  sentence that says what happens, shown under the controls. Indented lines
266
286
  under it are `From -> To: label` events and `Service.key = value` updates.
@@ -270,7 +290,7 @@ step "The API sets the session cookie"
270
290
  consecutive `>` lines join.
271
291
  - Under a service or state, it is a description: the name gets a dotted
272
292
  underline and the text shows in a tooltip on hover or focus.
273
- - Under a step, it explains the step and shows under the caption.
293
+ - Under `start` or a step, it explains the caption and shows under it.
274
294
  - Under an event or update, it is a note for that step. Notes appear in a
275
295
  column beside the diagram once the step's events land, each joined by a
276
296
  line to its event label or state row.
@@ -284,6 +304,60 @@ connection between each pair of services that talk. A request and its reply
284
304
  share that connection. Indentation is significant, so `services` has no MDC form.
285
305
  See [the runnable examples](fixtures/services.md).
286
306
 
307
+ ## Diagram diffs
308
+
309
+ Add `diff` after `mermaid` or `services` to show a change in one diagram.
310
+ Start every line with `-` for Today only, `+` for Proposed only, or a space
311
+ for both, as in a call stack diff. Indentation starts after that column.
312
+
313
+ ````md
314
+ ```mermaid diff
315
+ flowchart LR
316
+ Client --> Handler
317
+ - Handler --> Service
318
+ + Handler --> Validate
319
+ + Validate --> Service
320
+ ```
321
+ ````
322
+
323
+ diffmap draws every line once, so both versions share one layout. Nodes,
324
+ edges, sequence participants, and messages that only Today has are red and
325
+ dashed; those only Proposed has are green. A node whose label differs between
326
+ the versions is amber and shows its Proposed label. Each version must also be
327
+ a valid diagram on its own. To relabel a node, declare it on its own line
328
+ (`- B[Old]`, `+ B[New]`) so its edges are not drawn twice. In a diff,
329
+ `%% ref edge:<index>` counts edges as drawn: within each run of changed lines,
330
+ `+` lines come before `-` lines.
331
+
332
+ ````md
333
+ ```services diff
334
+ service API
335
+ - service SessionCache
336
+ - state active = 0
337
+ + service Sessions
338
+ service Postgres
339
+ - state sessions = not used
340
+ + state sessions = 0 rows
341
+
342
+ start "Sessions move to Postgres"
343
+ -step "API caches the session"
344
+ API -> SessionCache: create(alice)
345
+ SessionCache.active = 1
346
+ +step "Sessions writes the session"
347
+ API -> Sessions: create(alice)
348
+ Sessions -> Postgres: INSERT
349
+ Postgres.sessions = 1 row
350
+ ```
351
+ ````
352
+
353
+ In a `services diff`, lines inside a `-` or `+` service or step take its
354
+ sign. Today replays the unmarked and `-` lines and Proposed the unmarked and
355
+ `+` lines, so each state row shows Today's value struck through beside
356
+ Proposed's when they differ. A `-` and a `+` declaration of the same state key
357
+ give each version its own starting value. Added and removed steps color their
358
+ segment of the progress bar, and their caption starts with **Added** or
359
+ **Removed**. See [the runnable examples](fixtures/diagram-diffs.md).
360
+
287
361
  ## Link Mermaid nodes and edges
288
362
 
289
363
  Inside a Mermaid fence, add `%% ref node:<id> [[reference]]` to link a node or
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanishqkancharla/diffmap",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Serve a spec or code walkthrough as a local page",
5
5
  "license": "MIT",
6
6
  "author": "Tanishq Kancharla",
@@ -26,10 +26,26 @@ export type SourceReference = FileReference | ChangesReference;
26
26
  export type SourceAnnotation = {
27
27
  text: string;
28
28
  references: SourceReference[];
29
+ /** Source panel title while selected, such as a call stack row without its tree. */
30
+ title?: string;
29
31
  };
30
32
 
31
33
  export function parseCallStack(source: string): SourceAnnotation[] {
32
- return source.split("\n").map(parseAnnotation);
34
+ return source.split("\n").map((line) => {
35
+ const annotation = parseAnnotation(line);
36
+ return {
37
+ ...annotation,
38
+ title: callStackRow(annotation.text).description.trim(),
39
+ };
40
+ });
41
+ }
42
+
43
+ /** A call stack row's change mark, tree prefix, and the call it describes. */
44
+ export function callStackRow(text: string) {
45
+ const sign = text.startsWith("+") ? "+" : text.startsWith("-") ? "-" : " ";
46
+ const label = text.replace(/^[+ -]/, "");
47
+ const tree = /^[ \t│┃├└─]*/u.exec(label)![0];
48
+ return { sign, tree, description: label.slice(tree.length) };
33
49
  }
34
50
 
35
51
  export function parseAnnotation(line: string): SourceAnnotation {
@@ -12,9 +12,17 @@ export function extractTitle(source: string) {
12
12
  export function extractFences(source: string) {
13
13
  const fences: Fence[] = [];
14
14
  for (const match of source.matchAll(fencePattern)) {
15
- const lang = match[1] ?? "";
15
+ const lang = (match[1] ?? "").trim();
16
16
  const body = match[2] ?? "";
17
- fences.push(parseFence(lang.trim(), body));
17
+ // A diagram's info string can add `diff`, and Mermaid a [Title].
18
+ const diagram = /^(mermaid|services)\b\s*(diff)?\s*(?:\[([^\]]*)\])?$/.exec(
19
+ lang,
20
+ );
21
+ fences.push(
22
+ diagram === null
23
+ ? parseFence(lang, body)
24
+ : parseFence(diagram[1]!, body, [], diagram[3], diagram[2]),
25
+ );
18
26
  }
19
27
  return fences;
20
28
  }
package/src/host/home.md CHANGED
@@ -1,125 +1,122 @@
1
1
  # diffmap
2
2
 
3
- diffmap turns a Markdown file into a page like this one. Your coding agent writes it to show you a change, before and after building it: diagrams you can play, call stacks of what changes, and links that open the code.
3
+ **Your coding agent writes more code than you can review.**
4
4
 
5
- Install the skill, then ask your agent to write a diffmap for a change you are planning or for a branch it just built:
5
+ **You should be spending time reviewing design decisions and risky changes, not boilerplate.**
6
6
 
7
- ```sh
8
- npx skills add tanishqkancharla/diffmap --skill generate-diffmap
9
- ```
10
-
11
- The agent keeps the spec in `specs/<name>.md`, serves it on your machine, and can share it as a link like this page.
12
-
13
- Everything below is an example spec: diffmap's own features, one per phase. Each phase uses the feature it describes. Click a call stack row, a diagram node, or anything marked ↗ to open its code.
14
-
15
- ## Show a change, not a wall of text
16
-
17
- A coding agent explains a change in a long message. You read it, then open each file it names to check that the code says the same thing.
18
-
19
- A diffmap puts the explanation next to the code. Each phase says what changes for you, then shows the change as call stacks and diagrams whose parts open the code they name.
20
-
21
- ### ✅ Phase 1: Call stacks that open the code
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.
22
8
 
23
- **Today**
9
+ You can try `diffmap` before you install it. In a repository that has changes, paste this prompt into your agent:
24
10
 
25
- The agent describes the call path in a paragraph. You search for each function it names and guess how they connect.
26
-
27
- **Proposed**
28
-
29
- The agent draws the path as data flowing through calls: each row reads `result = call(inputs)`, `+` rows are new, `−` rows go away, and each row opens its code. This is how diffmap draws the tree below:
30
-
31
- ```callstack
32
- fence = parseFence(lang, source) [[src/parseFence.ts#parseFence]]
33
- +└── lines = parseCallStack(source) # one row per line, with its links [[src/annotations.ts#parseCallStack]]
34
- view = Fence({ fence }) [[src/viewer/Fence.tsx#Fence]]
35
- -└── return CodeBlock(fence.source) # the tree as plain text
36
- +├── if fence.kind === "callstack":
37
- +│ └── return CallStackDiff(fence.lines) # rows with + and − marks [[src/viewer/CallStackDiff.tsx#CallStackDiff]]
38
- +└── return CodeBlock(fence.source) # other languages stay pseudocode
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.
39
15
  ```
40
16
 
41
- ### ✅ Phase 2: Diagrams you can click
42
-
43
- **Today**
44
-
45
- The agent draws a flow in ASCII boxes. The boxes do not link to anything, and they break when a label gets long.
17
+ If you like the result, install the skill:
46
18
 
47
- **Proposed**
48
-
49
- Mermaid diagrams render in the page, and each node or arrow can open its code. Here is what happens when you click a reference:
50
-
51
- ```mermaid
52
- flowchart LR
53
- Click[Click a reference] --> Find[useSourceReference]
54
- Find -->|diffmap serve| Local[Read the repo with git]
55
- Find -->|diffmap.dev| GitHub[fetchPinnedBlob]
56
- Local --> Panel[SourceDiffPanel]
57
- GitHub --> Panel
58
- %% ref node:Find [[src/viewer/sourceNavigation.ts#useSourceReference]]
59
- %% ref node:GitHub [[src/github/fetchRepo.ts#fetchPinnedBlob]]
60
- %% ref node:Panel [[src/viewer/SourceDiffPanel.tsx#SourceDiffPanel]]
61
- %% ref edge:1 [[src/viewer/sourceNavigation.ts#requestSource]]
19
+ ```sh
20
+ npx skills add tanishqkancharla/diffmap --skill generate-diffmap
62
21
  ```
63
22
 
64
- ### ✅ Phase 3: Services you can play
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
65
26
 
66
- **Today**
27
+ Your agent selects the best diagram for each idea. Each part of the diagram has a link to its code.
67
28
 
68
- A sequence diagram shows the messages between services, but not what each message changes. You keep the state of every service in your head.
29
+ ### Service diagrams
69
30
 
70
- **Proposed**
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.
71
32
 
72
- A service diagram shows each service with its state, then steps through the events. Press **Play** to watch `diffmap serve` open a spec, or hover a dotted name to read what it is:
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:
73
34
 
74
35
  ```services
75
36
  service Laptop
76
- > Your machine. serve reads files here and uploads nothing.
37
+ > This is your computer. diffmap serve reads files here and does not upload them.
77
38
  service Agent "coding agent"
78
- > Writes the spec and runs diffmap for you.
39
+ > The agent writes the spec and runs diffmap for you.
79
40
  service CLI "diffmap serve" [[src/serve.ts#startServer]]
80
- > A small server that reads your repo with git.
41
+ > This small server uses git to read your repository.
81
42
  state url = none
82
- service Repo "your repo"
83
- > Holds the spec and the commits it links to.
43
+ service Repo "your repository"
44
+ > The repository contains the spec and the commits that the spec refers to.
84
45
  service Browser
85
46
  service Page "the spec" [[src/viewer/ViewerApp.tsx#ViewerApp]]
86
47
  state page = empty
87
48
  service Panel "Diff panel" [[src/viewer/SourceDiffPanel.tsx#SourceDiffPanel]]
88
- > Opens beside the spec and shows the code a reference names.
49
+ > The Diff panel opens next to the spec. It shows the code for the link that you clicked.
89
50
  state shows = none
90
51
 
52
+ start "diffmap serve opens a spec in your browser"
53
+ > The agent has written the spec, but nothing serves it yet.
54
+
91
55
  step "The agent serves the spec"
56
+ > Your agent runs diffmap serve with the path of the spec.
92
57
  Agent -> CLI: serve specs/search.md
93
- step "serve checks every commit the spec links to"
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.
94
60
  CLI -> Repo: readDocumentChanges [[src/git.ts#readDocumentChanges]]
95
- > A wrong SHA or line range fails here, so you never meet a broken link.
61
+ > If a SHA or a line range is wrong, the command stops here. Thus, the page has no broken links.
96
62
  step "serve prints a URL for you"
63
+ > The server starts on your computer and gives the agent a local URL.
97
64
  CLI -> Agent: localhost URL
98
65
  CLI.url = localhost:4321
99
66
  step "You open the page"
67
+ > Your browser gets the spec from the server and shows it as a page.
100
68
  Page -> CLI: GET /
101
69
  CLI -> Page: spec
102
70
  Page.page = rendered
103
71
  step "You click a call stack row"
72
+ > The Diff panel asks the server for the code that the row refers to.
104
73
  Panel -> CLI: /__diffmap/source
105
74
  CLI -> Repo: git show
106
- > Reads the file at the commit the row names, not the working tree.
75
+ > The server reads the file from the commit in the row, not from the working tree.
107
76
  step "The Diff panel shows the code"
77
+ > The server sends the lines, and the Diff panel shows them next to the spec.
108
78
  CLI -> Panel: lines
109
79
  Panel.shows = code
110
80
  ```
111
81
 
112
- ### ✅ Phase 4: Open code from prose
82
+ ### Mermaid diagrams
83
+
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:
85
+
86
+ ```mermaid
87
+ flowchart LR
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]]
97
+ ```
98
+
99
+ ### Call stacks
113
100
 
114
- **Today**
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.
115
102
 
116
- A paragraph names a function as plain code, like `shareMarkdownFile`. You search the repo to find it.
103
+ This call stack shows how `diffmap` draws call stacks:
117
104
 
118
- **Proposed**
105
+ ```callstack
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
113
+ ```
114
+
115
+ ## Open the code
119
116
 
120
- A function named in a paragraph opens its code too: [[src/share.ts#shareMarkdownFile]].
117
+ A function name in a sentence can also be a link, for example [[src/share.ts#shareMarkdownFile]].
121
118
 
122
- This phase is done, so it ends the way a finished phase does: with a card for the commit that built it, and references pinned to that commit. Pinned links show the code as the commit left it, even after the file moves on.
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.
123
120
 
124
121
  ```changes
125
122
  6e4b5d3
@@ -136,22 +133,22 @@ This phase is done, so it ends the way a finished phase does: with a card for th
136
133
  + └── return InlineReference(node.annotation) # a code button that opens the Diff panel [[6e4b5d3:src/viewer/ComarkView.tsx#L173-L189]]
137
134
  ```
138
135
 
139
- ### ✅ Phase 5: Share a spec as a link
140
-
141
- **Today**
142
-
143
- To read a spec, a teammate needs diffmap and a checkout of your repo.
136
+ ## Share a spec with a link
144
137
 
145
- **Proposed**
146
-
147
- One command uploads the spec as a secret GitHub gist and prints a link anyone can open:
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:
148
139
 
149
140
  ```sh
150
141
  npx @tanishqkancharla/diffmap share specs/search.md
151
142
  # prints https://diffmap.dev/g/<gistId>
152
143
  ```
153
144
 
154
- The page that opens has no git and no checkout, so the gist carries what the spec reads: a snapshot of each commit it links, and the current contents of each file it quotes. That is why this page can show code at all:
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:
147
+
148
+ - A snapshot of each commit that the spec refers to
149
+ - The current contents of each file that the spec quotes.
150
+
151
+ Because of this data, this page can show code:
155
152
 
156
153
  ```mermaid
157
154
  flowchart LR
@@ -172,7 +169,7 @@ flowchart LR
172
169
  2f96051
173
170
  ```
174
171
 
175
- `share` builds both snapshots before it calls `gh`, so diffmap never stores a GitHub token:
172
+ `share` makes the two snapshots before it calls `gh`. Thus, `diffmap` does not keep a GitHub token:
176
173
 
177
174
  ```callstack
178
175
  result = shareMarkdownFile({ filePath, workspaceRoot }) [[2f96051:src/share.ts#L26-L103]]
@@ -186,7 +183,7 @@ flowchart LR
186
183
  └── return { gistId, viewerUrl: gistViewerUrl(gistId) }
187
184
  ```
188
185
 
189
- This excerpt is one of the quoted files, so a shared copy of this page shows it too:
186
+ This excerpt is from one of the quoted files. Thus, a shared copy of this page also shows it:
190
187
 
191
188
  ```54:63:src/share.ts
192
189
  // It has no files either, so excerpts and references without a commit read these.
@@ -200,3 +197,5 @@ This excerpt is one of the quoted files, so a shared copy of this page shows it
200
197
  if (input.isPublic) args.push("--public");
201
198
  args.push("--desc", extractTitle(source), filePath);
202
199
  ```
200
+
201
+ ::
@@ -62,7 +62,6 @@ 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
  );
package/src/parseFence.ts CHANGED
@@ -8,8 +8,21 @@ import {
8
8
  import { parseServiceDiagram, type ServicesParse } from "./services.js";
9
9
 
10
10
  export type Fence =
11
- | { kind: "mermaid"; source: string; annotations: DiagramAnnotation[] }
12
- | { kind: "services"; source: string; parsed: ServicesParse }
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
+ }
13
26
  | { kind: "html"; source: string }
14
27
  | {
15
28
  kind: "callstack";
@@ -19,6 +32,8 @@ export type Fence =
19
32
  /** 1-based rows from the info string, as in ```callstack {2}. */
20
33
  highlights: number[];
21
34
  notes: CodeNote[];
35
+ /** From the info string, as in ```callstack [Request flow]. */
36
+ title: string | undefined;
22
37
  }
23
38
  | {
24
39
  kind: "changes";
@@ -37,6 +52,8 @@ export type Fence =
37
52
  /** 1-based lines from the info string, as in ```ts {2,4-6}. */
38
53
  highlights: number[];
39
54
  notes: CodeNote[];
55
+ /** From the info string, as in ```ts [src/request.ts]. */
56
+ title: string | undefined;
40
57
  };
41
58
 
42
59
  const fileRefPattern = /^(\d+):(\d+):(.+)$/;
@@ -60,22 +77,21 @@ export function parseFence(
60
77
  lang: string,
61
78
  source: string,
62
79
  highlights: number[] = [],
80
+ title?: string,
81
+ /** The info string after the language and highlights, as in ```mermaid diff. */
82
+ meta?: string,
63
83
  ): Fence {
64
84
  const trimmed = source.replace(/\n$/, "");
65
- if (lang === "mermaid")
66
- return {
67
- kind: "mermaid",
68
- source: trimmed,
69
- annotations: parseDiagramAnnotations(trimmed),
70
- };
85
+ const diff = meta?.split(/\s+/).includes("diff") ?? false;
86
+ if (lang === "mermaid") return mermaidFence(trimmed, diff, title);
71
87
  if (lang === "services")
72
88
  return {
73
89
  kind: "services",
74
90
  source: trimmed,
75
- parsed: parseServiceDiagram(trimmed),
91
+ parsed: parseServiceDiagram(trimmed, diff),
76
92
  };
77
93
  if (lang === "html") return { kind: "html", source: trimmed };
78
- if (lang === "callstack") return callStackFence(trimmed, highlights);
94
+ if (lang === "callstack") return callStackFence(trimmed, highlights, title);
79
95
 
80
96
  // The first line is the rev (blank for uncommitted); each later line is a path to review.
81
97
  if (lang === "changes") {
@@ -101,7 +117,8 @@ export function parseFence(
101
117
 
102
118
  const diffPath = diffPathPattern.exec(lang);
103
119
  if (lang === "diff" || diffPath !== null) {
104
- if (isCallStackSource(trimmed)) return callStackFence(trimmed, highlights);
120
+ if (isCallStackSource(trimmed))
121
+ return callStackFence(trimmed, highlights, title);
105
122
  return {
106
123
  kind: "diff",
107
124
  path: diffPath === null ? pathFromDiffSource(trimmed) : diffPath[1],
@@ -116,10 +133,61 @@ export function parseFence(
116
133
  source: code,
117
134
  highlights,
118
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,
119
183
  };
120
184
  }
121
185
 
122
- function callStackFence(source: string, highlights: number[]): Fence {
186
+ function callStackFence(
187
+ source: string,
188
+ highlights: number[],
189
+ title: string | undefined,
190
+ ): Fence {
123
191
  const { code, notes } = parseCodeNotes(source);
124
192
  return {
125
193
  kind: "callstack",
@@ -127,5 +195,6 @@ function callStackFence(source: string, highlights: number[]): Fence {
127
195
  lines: parseCallStack(code),
128
196
  highlights,
129
197
  notes,
198
+ title,
130
199
  };
131
200
  }