@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/README.md +171 -1
- package/index.html +6 -0
- package/package.json +3 -2
- package/src/annotations.ts +17 -1
- package/src/changes.ts +15 -0
- package/src/codeNotes.ts +123 -0
- package/src/errors.ts +5 -0
- package/src/extractDocument.ts +10 -2
- package/src/git.test.ts +13 -1
- package/src/git.ts +15 -2
- package/src/host/HostHome.tsx +22 -6
- package/src/host/home.md +154 -109
- package/src/host/home.test.ts +6 -8
- package/src/parseFence.ts +132 -23
- package/src/parseViewer.test.ts +359 -2
- package/src/parseViewer.ts +126 -25
- package/src/serve.test.ts +22 -1
- package/src/services.ts +396 -0
- package/src/share.ts +1 -1
- package/src/viewer/CallStackDiff.tsx +119 -55
- package/src/viewer/ChangesCard.tsx +133 -45
- package/src/viewer/CodeBlock.tsx +204 -0
- package/src/viewer/ComarkView.tsx +34 -0
- package/src/viewer/Fence.tsx +26 -2
- package/src/viewer/HtmlBlock.tsx +2 -1
- package/src/viewer/MermaidBlock.tsx +55 -14
- package/src/viewer/ServiceDiagramBlock.tsx +358 -0
- package/src/viewer/ServiceStage.tsx +1129 -0
- package/src/viewer/SourceDiffPanel.tsx +147 -48
- package/src/viewer/TableOfContents.tsx +47 -18
- package/src/viewer/TocButton.tsx +28 -0
- package/src/viewer/ViewerApp.tsx +28 -3
- package/src/viewer/diagramDiff.ts +69 -0
- package/src/viewer/diagramLinks.ts +54 -6
- package/src/viewer/notes.tsx +126 -0
- package/src/viewer/pierre.ts +20 -2
- package/src/viewer/serviceLayout.ts +200 -0
- package/vite.config.ts +5 -0
- package/vite.gist.config.ts +1 -0
package/src/host/home.md
CHANGED
|
@@ -1,156 +1,201 @@
|
|
|
1
1
|
# diffmap
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Your coding agent writes more code than you can review.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**You should be spending time reviewing design decisions and risky changes, not boilerplate.**
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
18
|
-
|
|
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
|
-
|
|
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
|
|
20
|
+
npx skills add tanishqkancharla/diffmap --skill generate-diffmap
|
|
31
21
|
```
|
|
32
22
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
`
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
###
|
|
82
|
+
### Mermaid diagrams
|
|
50
83
|
|
|
51
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
%% ref node:
|
|
61
|
-
%% ref node:
|
|
62
|
-
%% ref
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
115
|
+
## Open the code
|
|
91
116
|
|
|
92
|
-
A
|
|
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
|
-
|
|
122
|
+
6e4b5d3
|
|
96
123
|
```
|
|
97
124
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
136
|
+
## Share a spec with a link
|
|
123
137
|
|
|
124
|
-
|
|
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
|
-
```
|
|
127
|
-
|
|
140
|
+
```sh
|
|
141
|
+
npx @tanishqkancharla/diffmap share specs/search.md
|
|
142
|
+
# prints https://diffmap.dev/g/<gistId>
|
|
128
143
|
```
|
|
129
144
|
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
+
Because of this data, this page can show code:
|
|
135
152
|
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
168
|
+
```changes
|
|
169
|
+
2f96051
|
|
170
|
+
```
|
|
145
171
|
|
|
146
|
-
|
|
172
|
+
`share` makes the two snapshots before it calls `gh`. Thus, `diffmap` does not keep a GitHub token:
|
|
147
173
|
|
|
148
174
|
```callstack
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
156
|
-
- [x] PR URL when the spec lives in the repo (pin to one SHA).
|
|
201
|
+
::
|
package/src/host/home.test.ts
CHANGED
|
@@ -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("
|
|
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/
|
|
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
|
|
76
|
-
const
|
|
77
|
-
const lines =
|
|
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, /
|
|
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
|
-
| {
|
|
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
|
-
| {
|
|
12
|
-
|
|
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
|
-
| {
|
|
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(
|
|
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
|
-
|
|
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: "
|
|
89
|
+
kind: "services",
|
|
39
90
|
source: trimmed,
|
|
40
|
-
|
|
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:
|
|
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
|
}
|