@tanishqkancharla/diffmap 0.4.0 → 0.6.1

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
+ > The API checks her password with Auth, then sets a session cookie.
247
262
  step "The browser submits the form"
248
263
  Browser -> API: POST /login
249
264
  step "The API asks Auth to verify"
@@ -261,16 +276,21 @@ 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
+ gives an overview of the whole process. A `services` fence takes no
283
+ `[Title]`; 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.
267
- Every event in a step travels at once, and the updates land when the events
268
- arrive.
287
+ A step's events travel one after another, in the order written, and the
288
+ updates land when the last one arrives.
269
289
  - A `>` line adds text to the line above it. Indent it further than that line;
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.1",
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,109 @@
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
+
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
+
9
+ You can try `diffmap` before you install it. In a repository that has changes, paste this prompt into your agent:
10
+
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.
15
+ ```
16
+
17
+ If you like the result, install the skill:
6
18
 
7
19
  ```sh
8
20
  npx skills add tanishqkancharla/diffmap --skill generate-diffmap
9
21
  ```
10
22
 
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.
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.
12
24
 
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.
25
+ ## See the main ideas
14
26
 
15
- ## Show a change, not a wall of text
27
+ Your agent selects the best diagram for each idea. Each part of the diagram has a link to its code.
16
28
 
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.
29
+ ### Service diagrams
18
30
 
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.
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.
20
32
 
21
- ### ✅ Phase 1: Call stacks that open the code
33
+ A service diagram can also show a change. Red parts exist only today, and green parts are new. As you step, each value shows today's state crossed out next to the new state.
22
34
 
23
- **Today**
35
+ This change moves login sessions from memory to a database. Press **Play** to step through it:
24
36
 
25
- The agent describes the call path in a paragraph. You search for each function it names and guess how they connect.
37
+ ```services diff
38
+ service Browser "login page"
39
+ state cookie = none
40
+ service API "HTTP server"
41
+ - service Cache "in-memory sessions"
42
+ - state sessions = 0
43
+ + service Sessions
44
+ service Postgres
45
+ - state sessions = not used
46
+ + state sessions = 0 rows
26
47
 
27
- **Proposed**
48
+ start "Sessions move from memory to Postgres"
49
+ > Alice logs in, and the API stores her session. Today it keeps the session in memory, so a server restart signs her out. After the change, Sessions writes it to Postgres, where it survives a restart.
28
50
 
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
51
+ step "Alice logs in"
52
+ Browser -> API: POST /login
53
+ -step "The API keeps the session in memory"
54
+ API -> Cache: create(alice)
55
+ Cache.sessions = 1
56
+ > The server loses this when it restarts.
57
+ +step "Sessions writes the session to Postgres"
58
+ API -> Sessions: create(alice)
59
+ Sessions -> Postgres: INSERT s_81f
60
+ Postgres.sessions = 1 row
61
+ > The row survives a restart.
62
+ step "The API sets the session cookie"
63
+ API -> Browser: 200 Set-Cookie
64
+ Browser.cookie = sid=s_81f
39
65
  ```
40
66
 
41
- ### ✅ Phase 2: Diagrams you can click
42
-
43
- **Today**
67
+ ### Mermaid diagrams
44
68
 
45
- The agent draws a flow in ASCII boxes. The boxes do not link to anything, and they break when a label gets long.
69
+ The page shows Mermaid diagrams. Each node and each arrow can have a link to its code. A diagram can show a change in one picture: green parts are new, and red parts are removed.
46
70
 
47
- **Proposed**
71
+ This diagram shows what occurs when you click a link, and what changed when diffmap.dev began to read code from GitHub:
48
72
 
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]]
73
+ ```mermaid diff [diffmap.dev reads linked code from GitHub]
74
+ flowchart LR
75
+ Click[Click a link] --> Find[useSourceReference]
76
+ Find -->|diffmap serve| Local[Read the repo with git]
77
+ + Find -->|diffmap.dev| GitHub[fetchPinnedBlob]
78
+ Local --> Panel[SourceDiffPanel]
79
+ + GitHub --> Panel
80
+ %% ref node:Find [[src/viewer/sourceNavigation.ts#useSourceReference]]
81
+ %% ref node:GitHub [[src/github/fetchRepo.ts#fetchPinnedBlob]]
82
+ %% ref node:Panel [[src/viewer/SourceDiffPanel.tsx#SourceDiffPanel]]
83
+ %% ref edge:1 [[src/viewer/sourceNavigation.ts#requestSource]]
62
84
  ```
63
85
 
64
- ### ✅ Phase 3: Services you can play
65
-
66
- **Today**
67
-
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.
69
-
70
- **Proposed**
71
-
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:
73
-
74
- ```services
75
- service Laptop
76
- > Your machine. serve reads files here and uploads nothing.
77
- service Agent "coding agent"
78
- > Writes the spec and runs diffmap for you.
79
- service CLI "diffmap serve" [[src/serve.ts#startServer]]
80
- > A small server that reads your repo with git.
81
- state url = none
82
- service Repo "your repo"
83
- > Holds the spec and the commits it links to.
84
- service Browser
85
- service Page "the spec" [[src/viewer/ViewerApp.tsx#ViewerApp]]
86
- state page = empty
87
- service Panel "Diff panel" [[src/viewer/SourceDiffPanel.tsx#SourceDiffPanel]]
88
- > Opens beside the spec and shows the code a reference names.
89
- state shows = none
90
-
91
- step "The agent serves the spec"
92
- Agent -> CLI: serve specs/search.md
93
- step "serve checks every commit the spec links to"
94
- CLI -> Repo: readDocumentChanges [[src/git.ts#readDocumentChanges]]
95
- > A wrong SHA or line range fails here, so you never meet a broken link.
96
- step "serve prints a URL for you"
97
- CLI -> Agent: localhost URL
98
- CLI.url = localhost:4321
99
- step "You open the page"
100
- Page -> CLI: GET /
101
- CLI -> Page: spec
102
- Page.page = rendered
103
- step "You click a call stack row"
104
- Panel -> CLI: /__diffmap/source
105
- CLI -> Repo: git show
106
- > Reads the file at the commit the row names, not the working tree.
107
- step "The Diff panel shows the code"
108
- CLI -> Panel: lines
109
- Panel.shows = code
110
- ```
86
+ ### Call stacks
111
87
 
112
- ### ✅ Phase 4: Open code from prose
88
+ 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.
113
89
 
114
- **Today**
90
+ This call stack shows how `diffmap` draws call stacks:
115
91
 
116
- A paragraph names a function as plain code, like `shareMarkdownFile`. You search the repo to find it.
92
+ ```callstack
93
+ fence = parseFence(lang, source) [[src/parseFence.ts#parseFence]]
94
+ +└── lines = parseCallStack(source) # one row per line, with its links [[src/annotations.ts#parseCallStack]]
95
+ view = Fence({ fence }) [[src/viewer/Fence.tsx#Fence]]
96
+ -└── return CodeBlock(fence.source) # the tree as plain text
97
+ +├── if fence.kind === "callstack":
98
+ +│ └── return CallStackDiff(fence.lines) # rows with + and − marks [[src/viewer/CallStackDiff.tsx#CallStackDiff]]
99
+ +└── return CodeBlock(fence.source) # other languages stay pseudocode
100
+ ```
117
101
 
118
- **Proposed**
102
+ ## Open the code
119
103
 
120
- A function named in a paragraph opens its code too: [[src/share.ts#shareMarkdownFile]].
104
+ A function name in a sentence can also be a link, for example [[src/share.ts#shareMarkdownFile]].
121
105
 
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.
106
+ 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
107
 
124
108
  ```changes
125
109
  6e4b5d3
@@ -136,22 +120,22 @@ This phase is done, so it ends the way a finished phase does: with a card for th
136
120
  + └── return InlineReference(node.annotation) # a code button that opens the Diff panel [[6e4b5d3:src/viewer/ComarkView.tsx#L173-L189]]
137
121
  ```
138
122
 
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.
123
+ ## Share a spec with a link
144
124
 
145
- **Proposed**
146
-
147
- One command uploads the spec as a secret GitHub gist and prints a link anyone can open:
125
+ 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
126
 
149
127
  ```sh
150
128
  npx @tanishqkancharla/diffmap share specs/search.md
151
129
  # prints https://diffmap.dev/g/<gistId>
152
130
  ```
153
131
 
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:
132
+ ::details{summary="How sharing works"}
133
+ The shared page cannot use git or a copy of your repository. Thus, the gist also contains the data that the spec uses:
134
+
135
+ - A snapshot of each commit that the spec refers to
136
+ - The current contents of each file that the spec quotes.
137
+
138
+ Because of this data, this page can show code:
155
139
 
156
140
  ```mermaid
157
141
  flowchart LR
@@ -172,7 +156,7 @@ flowchart LR
172
156
  2f96051
173
157
  ```
174
158
 
175
- `share` builds both snapshots before it calls `gh`, so diffmap never stores a GitHub token:
159
+ `share` makes the two snapshots before it calls `gh`. Thus, `diffmap` does not keep a GitHub token:
176
160
 
177
161
  ```callstack
178
162
  result = shareMarkdownFile({ filePath, workspaceRoot }) [[2f96051:src/share.ts#L26-L103]]
@@ -186,7 +170,7 @@ flowchart LR
186
170
  └── return { gistId, viewerUrl: gistViewerUrl(gistId) }
187
171
  ```
188
172
 
189
- This excerpt is one of the quoted files, so a shared copy of this page shows it too:
173
+ This excerpt is from one of the quoted files. Thus, a shared copy of this page also shows it:
190
174
 
191
175
  ```54:63:src/share.ts
192
176
  // It has no files either, so excerpts and references without a commit read these.
@@ -200,3 +184,5 @@ This excerpt is one of the quoted files, so a shared copy of this page shows it
200
184
  if (input.isPublic) args.push("--public");
201
185
  args.push("--desc", extractTitle(source), filePath);
202
186
  ```
187
+
188
+ ::
@@ -37,6 +37,17 @@ test("homepage markdown is a real viewer document", async () => {
37
37
  const mermaid = fences.find((fence) => fence.kind === "mermaid");
38
38
  assert.notEqual(mermaid, undefined);
39
39
  if (mermaid?.kind !== "mermaid") return;
40
+ // The homepage shows diagram diffs, not only call stack diffs.
41
+ assert.notEqual(mermaid.sides, undefined);
42
+ assert.equal(
43
+ fences.some(
44
+ (fence) =>
45
+ fence.kind === "services" &&
46
+ fence.parsed.ok &&
47
+ fence.parsed.diagram.diff,
48
+ ),
49
+ true,
50
+ );
40
51
  assert.equal(
41
52
  mermaid.annotations.some((link) =>
42
53
  link.annotation.references.some((ref) => ref.kind === "file"),
@@ -62,7 +73,6 @@ test("homepage markdown is a real viewer document", async () => {
62
73
  const levels = new Set(document.headings.map((heading) => heading.level));
63
74
  assert.equal(levels.has(1), true);
64
75
  assert.equal(levels.has(2), true);
65
- assert.equal(levels.has(3), true);
66
76
  assert.ok(
67
77
  document.references.filter((ref) => ref.rev !== undefined).length >= 3,
68
78
  );
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
  }