@tanishqkancharla/diffmap 0.1.3
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/LICENSE +21 -0
- package/README.md +212 -0
- package/bin.js +5 -0
- package/index.html +38 -0
- package/package.json +71 -0
- package/src/annotations.ts +82 -0
- package/src/cli.ts +94 -0
- package/src/codeFont.ts +2 -0
- package/src/contentPlugin.ts +47 -0
- package/src/definitions.ts +320 -0
- package/src/diagramAnnotations.ts +34 -0
- package/src/errors.ts +41 -0
- package/src/extractDocument.ts +20 -0
- package/src/gist/GistApp.tsx +292 -0
- package/src/gist/GistStatus.tsx +137 -0
- package/src/gist/fetchGist.ts +128 -0
- package/src/gist/main.tsx +25 -0
- package/src/gist/route.test.ts +90 -0
- package/src/gist/route.ts +134 -0
- package/src/index.ts +22 -0
- package/src/invokeCwd.ts +10 -0
- package/src/mermaid.ts +29 -0
- package/src/normalizeDiff.ts +39 -0
- package/src/parseFence.ts +91 -0
- package/src/parseViewer.ts +399 -0
- package/src/registry.ts +121 -0
- package/src/serve.ts +350 -0
- package/src/share.ts +94 -0
- package/src/viewer/CallStackDiff.tsx +156 -0
- package/src/viewer/Check.tsx +24 -0
- package/src/viewer/CodeDiff.tsx +26 -0
- package/src/viewer/ComarkView.tsx +209 -0
- package/src/viewer/DiffButton.tsx +24 -0
- package/src/viewer/DoneButton.tsx +32 -0
- package/src/viewer/Fence.tsx +51 -0
- package/src/viewer/FileExcerpt.tsx +65 -0
- package/src/viewer/HtmlBlock.tsx +19 -0
- package/src/viewer/HtmlPlaceholder.tsx +29 -0
- package/src/viewer/MermaidBlock.tsx +126 -0
- package/src/viewer/SourceDiffPanel.tsx +305 -0
- package/src/viewer/TableOfContents.tsx +211 -0
- package/src/viewer/ViewerApp.tsx +257 -0
- package/src/viewer/diagramLinks.ts +74 -0
- package/src/viewer/diffsTheme.ts +4 -0
- package/src/viewer/main.tsx +17 -0
- package/src/viewer/pierre.ts +47 -0
- package/src/viewer/sourceNavigation.ts +104 -0
- package/src/viewer/styles.css +9 -0
- package/src/viewer/viewerMode.ts +9 -0
- package/src/viewer/vite-env.d.ts +4 -0
- package/vite.config.ts +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tanishq Kancharla
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# diffmap
|
|
2
|
+
|
|
3
|
+
<img width="2031" height="1212" alt="Screenshot 2026-09-10 at 10 41 03 AM" src="https://github.com/user-attachments/assets/8650f8be-44ca-4c21-b9bf-4ce938b4083e" />
|
|
4
|
+
|
|
5
|
+
diffmap is a set of personal skills as well as a web-viewer for Markdown files. It has a unique form of showing diffs using a mixture of callstack diffs and mermaid diagram links, which I've found to be personally extremely helpful in understanding massive diffs.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npx diffmap path/to/file.md
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Share a secret gist and print a `diffmap.dev` link (requires [`gh`](https://cli.github.com/) logged in). Does not start a local server:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npx diffmap share path/to.md
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Hosted viewer: `https://diffmap.dev/g/<gistId>` (optional `/<file.md>`; `#heading` is the table of contents). The page fetches the gist in the browser from `api.github.com`. Gists are unlisted, not private.
|
|
18
|
+
|
|
19
|
+
## Agent skills
|
|
20
|
+
|
|
21
|
+
Install skills from this repository:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npx skills add tanishqkancharla/diffmap --skill code-walkthrough
|
|
25
|
+
npx skills add tanishqkancharla/diffmap --skill generate-spec
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`code-walkthrough` explains landed changes. `generate-spec` writes a phased spec for work that has not happened yet, then serves `specs/<name>.md` with diffmap.
|
|
29
|
+
|
|
30
|
+
Options:
|
|
31
|
+
|
|
32
|
+
- `--port <n>` — listen port (default `4177`)
|
|
33
|
+
- `--root <dir>` — workspace root for file excerpts (default the directory you ran the command from)
|
|
34
|
+
|
|
35
|
+
**Done** in the top right stops the server.
|
|
36
|
+
|
|
37
|
+
The server also stops after 24 hours without a page or source request.
|
|
38
|
+
Loading or refreshing the page resets the timer; background health checks and
|
|
39
|
+
Vite connections do not keep it alive.
|
|
40
|
+
|
|
41
|
+
List every running viewer, including viewers using custom ports:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
npx diffmap list
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Request the page with `Accept: text/markdown` to read the current source file
|
|
48
|
+
instead of the rendered HTML:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
curl -H 'Accept: text/markdown' http://127.0.0.1:4177/
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Library
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { startServer, parseFence, parseViewerDocument } from "diffmap";
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`parseViewerDocument` turns markdown into the page document with [md4x](https://github.com/unjs/md4x). `startServer` listens. The generate-spec and code-walkthrough skills own spec vs walkthrough section order; the viewer does not.
|
|
61
|
+
|
|
62
|
+
## Fences
|
|
63
|
+
|
|
64
|
+
| Fence info string | Viewer |
|
|
65
|
+
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
66
|
+
| `mermaid` | Beautiful Mermaid |
|
|
67
|
+
| `callstack` or `diff` containing `└──` / `├──` | Interactive stack rows, no file header |
|
|
68
|
+
| `diff` or `diff:path` with a file path | Pierre patch with Pierre’s file header |
|
|
69
|
+
| `diff` with no path | Pierre patch, no file header |
|
|
70
|
+
| `start:end:path` | Pierre file excerpt with Pierre’s file header |
|
|
71
|
+
| `html` | Trusted HTML from this file. diffmap does not sanitize it. Only use it for local files you wrote. |
|
|
72
|
+
| other langs | Maui `CodeBlock` |
|
|
73
|
+
|
|
74
|
+
Fences keep whitespace. Use them for mermaid, call stacks, and diffs.
|
|
75
|
+
|
|
76
|
+
## MDC
|
|
77
|
+
|
|
78
|
+
md4x Comark components map to the same views:
|
|
79
|
+
|
|
80
|
+
```md
|
|
81
|
+
::mermaid
|
|
82
|
+
flowchart TD
|
|
83
|
+
A --> B
|
|
84
|
+
::
|
|
85
|
+
|
|
86
|
+
::callstack
|
|
87
|
+
startServer
|
|
88
|
+
+└── createViteServer
|
|
89
|
+
::
|
|
90
|
+
|
|
91
|
+
::diff{path="src/cli.ts"}
|
|
92
|
+
--- a/src/cli.ts
|
|
93
|
+
+++ b/src/cli.ts
|
|
94
|
+
::
|
|
95
|
+
|
|
96
|
+
::file{path="src/cli.ts" start="1" end="20"}
|
|
97
|
+
::
|
|
98
|
+
|
|
99
|
+
::html
|
|
100
|
+
<aside>Note.</aside>
|
|
101
|
+
::
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`.md` and `.mdx` are both markdown. Curly braces in prose are plain text.
|
|
105
|
+
|
|
106
|
+
## Link call stacks to source changes
|
|
107
|
+
|
|
108
|
+
Append `[[path/to/file.ts#symbolName]]` to a call stack line to link a symbol.
|
|
109
|
+
Paths are relative to `--root`. Symbol lookup supports TypeScript and JavaScript
|
|
110
|
+
declarations, including qualified names such as `[[src/store.ts#Store.save]]`.
|
|
111
|
+
Ambiguous names show the qualified names you can use instead.
|
|
112
|
+
|
|
113
|
+
```callstack
|
|
114
|
+
handleRequest [[src/request.ts#handleRequest]]
|
|
115
|
+
+└── validateInput # reject invalid input [[src/validation.ts#validateInput]]
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The panel highlights the declaration in the current workspace file. If an
|
|
119
|
+
included source patch overlaps that declaration and its new-side text matches
|
|
120
|
+
the workspace, the panel highlights the patch instead. Symbol references do not
|
|
121
|
+
require a `source-diff` fence. Use `[[path/to/file]]` for a whole file or
|
|
122
|
+
`[[path/to/file#L12-L30]]` for a file range; these work with other languages too.
|
|
123
|
+
|
|
124
|
+
To reference an exact change, including removed code, use the patch syntax below.
|
|
125
|
+
Symbol names resolve against current files; use an `old` reference for a deleted
|
|
126
|
+
symbol or a historical version.
|
|
127
|
+
|
|
128
|
+
Append `[[id:side:start-end]]` to a call stack line. `side` is `old` or `new`;
|
|
129
|
+
line numbers refer to that version of the source file, not the patch. A single
|
|
130
|
+
line can use `[[id:new:12]]`. Multiple references on one stack line are allowed.
|
|
131
|
+
The references are hidden in the rendered stack, and linked rows support mouse
|
|
132
|
+
clicks and keyboard activation.
|
|
133
|
+
|
|
134
|
+
Define each ID once in a `source-diff:id:path` fence anywhere in the document.
|
|
135
|
+
Copy the file's actual Git patch, including its `diff --git`, file headers, and
|
|
136
|
+
`@@` hunk headers. Keep enough context to explain the change. Each reference
|
|
137
|
+
range must fit within one included hunk. Use the new path for renamed files and
|
|
138
|
+
the old path for deleted files.
|
|
139
|
+
|
|
140
|
+
````md
|
|
141
|
+
```callstack
|
|
142
|
+
handleRequest
|
|
143
|
+
-└── saveUnchecked [[request:old:12]]
|
|
144
|
+
+├── validateInput # reject invalid input [[request:new:12-13]]
|
|
145
|
+
└── saveRecord
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
```source-diff:request:src/request.ts
|
|
149
|
+
diff --git a/src/request.ts b/src/request.ts
|
|
150
|
+
--- a/src/request.ts
|
|
151
|
+
+++ b/src/request.ts
|
|
152
|
+
@@ -10,5 +10,6 @@
|
|
153
|
+
export function handleRequest(input: Input) {
|
|
154
|
+
const record = input.record;
|
|
155
|
+
- saveUnchecked(record);
|
|
156
|
+
+ const error = validateInput(record);
|
|
157
|
+
+ if (error instanceof Error) return error;
|
|
158
|
+
return saveRecord(record);
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
````
|
|
162
|
+
|
|
163
|
+
References open in one shared panel beside the walkthrough. Selecting a stack
|
|
164
|
+
line or diagram element highlights it and scrolls the panel to the linked code
|
|
165
|
+
range. When a target has several references, buttons above the source viewer
|
|
166
|
+
select among them. On narrow windows the source panel sits below the walkthrough.
|
|
167
|
+
Documents without references or source patches keep the single-panel layout.
|
|
168
|
+
|
|
169
|
+
Unknown IDs, duplicate definitions, malformed references, mismatched paths, and
|
|
170
|
+
ranges outside the included hunks produce a parse error. Source patches are
|
|
171
|
+
embedded snapshots; the viewer does not regenerate them from the working tree.
|
|
172
|
+
|
|
173
|
+
Cmd-click (or Ctrl-click) a TypeScript or JavaScript symbol in the source panel
|
|
174
|
+
to open its definition. Use Back to retrace navigation and return to the reference.
|
|
175
|
+
Resolution uses the workspace's TypeScript configuration and current files.
|
|
176
|
+
Deleted files and lines that no longer match the workspace show a message;
|
|
177
|
+
their call stack references still highlight the embedded old-side diff.
|
|
178
|
+
|
|
179
|
+
Run `npx diffmap fixtures/annotations.md` for an example
|
|
180
|
+
with old/new references, multiple files, and unchanged context.
|
|
181
|
+
|
|
182
|
+
## Link Mermaid nodes and edges
|
|
183
|
+
|
|
184
|
+
Inside a Mermaid fence, add `%% ref node:<id> [[reference]]` to link a node or
|
|
185
|
+
participant, or `%% ref edge:<index> [[reference]]` to link an edge or sequence
|
|
186
|
+
message. Edge indices start at zero in declaration order. The same file, symbol,
|
|
187
|
+
and old/new patch references work here. Keep one directive per target; append
|
|
188
|
+
multiple references to that directive when needed.
|
|
189
|
+
|
|
190
|
+
````md
|
|
191
|
+
```mermaid
|
|
192
|
+
flowchart LR
|
|
193
|
+
A[Request] -->|Validate| B[Validation]
|
|
194
|
+
%% ref node:A [[src/request.ts#handleRequest]]
|
|
195
|
+
%% ref node:B [[src/validation.ts#validateInput]]
|
|
196
|
+
%% ref edge:0 [[request:new:12-13]]
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
```mermaid
|
|
200
|
+
sequenceDiagram
|
|
201
|
+
participant App
|
|
202
|
+
participant Store
|
|
203
|
+
App->>Store: Save record
|
|
204
|
+
%% ref node:Store [[src/store.ts#Store]]
|
|
205
|
+
%% ref edge:0 [[src/store.ts#Store.save]]
|
|
206
|
+
```
|
|
207
|
+
````
|
|
208
|
+
|
|
209
|
+
Click a linked shape, edge line, or edge label, or focus it with Tab and press
|
|
210
|
+
Enter or Space. Directives are Mermaid comments, so they remain hidden and do
|
|
211
|
+
not change the diagram when rendered by other Mermaid tools. Missing diagram
|
|
212
|
+
targets display an error. See [the runnable example](fixtures/references.md).
|
package/bin.js
ADDED
package/index.html
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<meta name="theme-color" content="#ffffff" />
|
|
7
|
+
<script>
|
|
8
|
+
(function () {
|
|
9
|
+
var preference = "system";
|
|
10
|
+
try {
|
|
11
|
+
var storedPreference = window.localStorage.getItem("maui-theme");
|
|
12
|
+
if (
|
|
13
|
+
storedPreference === "system" ||
|
|
14
|
+
storedPreference === "light" ||
|
|
15
|
+
storedPreference === "dark"
|
|
16
|
+
) {
|
|
17
|
+
preference = storedPreference;
|
|
18
|
+
}
|
|
19
|
+
} catch {}
|
|
20
|
+
|
|
21
|
+
var theme =
|
|
22
|
+
preference === "system"
|
|
23
|
+
? window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
24
|
+
? "dark"
|
|
25
|
+
: "light"
|
|
26
|
+
: preference;
|
|
27
|
+
|
|
28
|
+
document.documentElement.dataset.theme = theme;
|
|
29
|
+
document.documentElement.style.colorScheme = theme;
|
|
30
|
+
})();
|
|
31
|
+
</script>
|
|
32
|
+
<title>diffmap</title>
|
|
33
|
+
</head>
|
|
34
|
+
<body>
|
|
35
|
+
<div id="root"></div>
|
|
36
|
+
<script type="module" src="/src/viewer/main.tsx"></script>
|
|
37
|
+
</body>
|
|
38
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tanishqkancharla/diffmap",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Serve a spec or code walkthrough as a local page",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Tanishq Kancharla",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/tanishqkancharla/diffmap.git"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"diffmap": "bin.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin.js",
|
|
16
|
+
"index.html",
|
|
17
|
+
"src",
|
|
18
|
+
"vite.config.ts"
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": "./src/index.ts"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public",
|
|
26
|
+
"registry": "https://registry.npmjs.org/"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"cli": "node --import tsx src/cli.ts",
|
|
30
|
+
"format": "oxfmt .",
|
|
31
|
+
"format:check": "oxfmt --check .",
|
|
32
|
+
"lint": "oxlint --deny-warnings src vite.config.ts vite.gist.config.ts bin.js",
|
|
33
|
+
"lint:fix": "oxlint --fix src vite.config.ts vite.gist.config.ts bin.js",
|
|
34
|
+
"prepublishOnly": "npm run typecheck && npm run lint && npm run format:check",
|
|
35
|
+
"release": "npm publish",
|
|
36
|
+
"test": "tsx --test src/gist/route.test.ts",
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"build": "vite build",
|
|
39
|
+
"build:gist": "vite build",
|
|
40
|
+
"preview:gist": "vite preview --config vite.gist.config.ts --host 127.0.0.1 --port 4178"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@pierre/diffs": "1.4.1",
|
|
44
|
+
"@vitejs/plugin-react": "^6.1.1",
|
|
45
|
+
"beautiful-mermaid": "^1.1.3",
|
|
46
|
+
"errore": "^0.14.1",
|
|
47
|
+
"incur": "^0.4.20",
|
|
48
|
+
"maui": "npm:@tanishqkancharla/maui@0.0.16",
|
|
49
|
+
"md4x": "^0.0.29",
|
|
50
|
+
"purse-styles": "^0.2.3",
|
|
51
|
+
"react": "^19.2.8",
|
|
52
|
+
"react-dom": "^19.2.8",
|
|
53
|
+
"tsx": "^4.23.13",
|
|
54
|
+
"vite": "^8.2.2"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/node": "^22.20.1",
|
|
58
|
+
"@types/react": "19.2.18",
|
|
59
|
+
"@types/react-dom": "19.2.5",
|
|
60
|
+
"oxfmt": "^0.65.0",
|
|
61
|
+
"oxlint": "^1.80.0",
|
|
62
|
+
"typescript": "7.0.2"
|
|
63
|
+
},
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"typescript": "^7.0.2"
|
|
66
|
+
},
|
|
67
|
+
"engines": {
|
|
68
|
+
"node": ">=22"
|
|
69
|
+
},
|
|
70
|
+
"packageManager": "npm@11.5.1"
|
|
71
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
type DiffReference = {
|
|
2
|
+
kind: "diff";
|
|
3
|
+
id: string;
|
|
4
|
+
side: "old" | "new";
|
|
5
|
+
start: number;
|
|
6
|
+
end: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type FileReference = {
|
|
10
|
+
kind: "file";
|
|
11
|
+
path: string;
|
|
12
|
+
symbol?: string;
|
|
13
|
+
start?: number;
|
|
14
|
+
end?: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type SourceReference = DiffReference | FileReference;
|
|
18
|
+
|
|
19
|
+
export type SourceAnnotation = {
|
|
20
|
+
text: string;
|
|
21
|
+
references: SourceReference[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function parseCallStack(source: string): SourceAnnotation[] {
|
|
25
|
+
return source.split("\n").map(parseAnnotation);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function parseAnnotation(line: string): SourceAnnotation {
|
|
29
|
+
const references: SourceReference[] = [];
|
|
30
|
+
const text = line.replace(
|
|
31
|
+
/\s*\[\[([^[\]\n]+)\]\]/g,
|
|
32
|
+
(match, value: string) => {
|
|
33
|
+
const reference = parseReference(value);
|
|
34
|
+
if (reference === undefined) return match;
|
|
35
|
+
references.push(reference);
|
|
36
|
+
return "";
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
return { text, references };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseReference(value: string): SourceReference | undefined {
|
|
43
|
+
const diff = /^([\w-]+):(old|new):([1-9]\d*)(?:-([1-9]\d*))?$/.exec(value);
|
|
44
|
+
if (diff !== null)
|
|
45
|
+
return {
|
|
46
|
+
kind: "diff",
|
|
47
|
+
id: diff[1]!,
|
|
48
|
+
side: diff[2] === "old" ? "old" : "new",
|
|
49
|
+
start: Number(diff[3]),
|
|
50
|
+
end: Number(diff[4] === undefined ? diff[3] : diff[4]),
|
|
51
|
+
};
|
|
52
|
+
const file = /^([^#:[\]\n]+)(?:#(.+))?$/.exec(value);
|
|
53
|
+
if (file === null || file[1]!.trim().length === 0) return undefined;
|
|
54
|
+
const path = file[1]!.trim();
|
|
55
|
+
const fragment = file[2];
|
|
56
|
+
if (fragment === undefined) return { kind: "file", path };
|
|
57
|
+
const range = /^L([1-9]\d*)(?:-L?([1-9]\d*))?$/.exec(fragment);
|
|
58
|
+
if (range !== null)
|
|
59
|
+
return {
|
|
60
|
+
kind: "file",
|
|
61
|
+
path,
|
|
62
|
+
start: Number(range[1]),
|
|
63
|
+
end: Number(range[2] === undefined ? range[1] : range[2]),
|
|
64
|
+
};
|
|
65
|
+
if (!/^[\w$]+(?:\.[\w$]+)*$/.test(fragment)) return undefined;
|
|
66
|
+
return { kind: "file", path, symbol: fragment };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function referenceLabel(reference: SourceReference) {
|
|
70
|
+
if (reference.kind === "diff")
|
|
71
|
+
return `${reference.id} · ${reference.side} ${reference.start}–${reference.end}`;
|
|
72
|
+
if (reference.symbol !== undefined)
|
|
73
|
+
return `${reference.path}#${reference.symbol}`;
|
|
74
|
+
if (reference.start !== undefined)
|
|
75
|
+
return `${reference.path}#L${reference.start}-L${reference.end}`;
|
|
76
|
+
return reference.path;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type SourceNavigation = {
|
|
80
|
+
selectedAnnotation: SourceAnnotation | undefined;
|
|
81
|
+
onSelectAnnotation: (annotation: SourceAnnotation) => void;
|
|
82
|
+
};
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --import tsx
|
|
2
|
+
|
|
3
|
+
import { Cli, z } from "incur";
|
|
4
|
+
import { resolveFromInvokeCwd } from "./invokeCwd.js";
|
|
5
|
+
import { listRunningDiffmaps } from "./registry.js";
|
|
6
|
+
import { startServer } from "./serve.js";
|
|
7
|
+
import { shareMarkdownFile } from "./share.js";
|
|
8
|
+
|
|
9
|
+
const cli = Cli.create("diffmap", {
|
|
10
|
+
description: "Serve a spec or code walkthrough as a local page",
|
|
11
|
+
version: "0.1.3",
|
|
12
|
+
args: z.object({
|
|
13
|
+
file: z.string().describe("Path to the markdown file"),
|
|
14
|
+
}),
|
|
15
|
+
options: z.object({
|
|
16
|
+
port: z.coerce.number().optional().describe("Port (default 4177)"),
|
|
17
|
+
root: z.string().optional().describe("Workspace root for file excerpts"),
|
|
18
|
+
}),
|
|
19
|
+
output: z.object({
|
|
20
|
+
url: z.string(),
|
|
21
|
+
file: z.string(),
|
|
22
|
+
}),
|
|
23
|
+
async *run(c) {
|
|
24
|
+
const started = await startServer({
|
|
25
|
+
filePath: resolveFromInvokeCwd(c.args.file),
|
|
26
|
+
workspaceRoot:
|
|
27
|
+
c.options.root === undefined
|
|
28
|
+
? resolveFromInvokeCwd(".")
|
|
29
|
+
: resolveFromInvokeCwd(c.options.root),
|
|
30
|
+
port: c.options.port === undefined ? 4177 : c.options.port,
|
|
31
|
+
});
|
|
32
|
+
if (started instanceof Error) {
|
|
33
|
+
return c.error({
|
|
34
|
+
code: "DIFFMAP",
|
|
35
|
+
message: started.message,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
yield { url: started.url, file: started.filePath };
|
|
39
|
+
await started.closed;
|
|
40
|
+
return { url: started.url, file: started.filePath };
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
.command("list", {
|
|
44
|
+
description: "List running diffmap viewers",
|
|
45
|
+
output: z.object({
|
|
46
|
+
instances: z.array(
|
|
47
|
+
z.object({
|
|
48
|
+
pid: z.number(),
|
|
49
|
+
title: z.string(),
|
|
50
|
+
url: z.string(),
|
|
51
|
+
file: z.string(),
|
|
52
|
+
}),
|
|
53
|
+
),
|
|
54
|
+
}),
|
|
55
|
+
async run(c) {
|
|
56
|
+
const instances = await listRunningDiffmaps();
|
|
57
|
+
if (instances instanceof Error) {
|
|
58
|
+
return c.error({
|
|
59
|
+
code: "DIFFMAP",
|
|
60
|
+
message: instances.message,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return c.ok({ instances });
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
.command("share", {
|
|
67
|
+
description: "Upload a walkthrough gist and print the diffmap.dev URL",
|
|
68
|
+
args: z.object({
|
|
69
|
+
file: z.string().describe("Path to the markdown file"),
|
|
70
|
+
}),
|
|
71
|
+
options: z.object({
|
|
72
|
+
public: z.boolean().optional().describe("Create a public gist"),
|
|
73
|
+
}),
|
|
74
|
+
output: z.object({
|
|
75
|
+
gistId: z.string(),
|
|
76
|
+
gistUrl: z.string(),
|
|
77
|
+
viewerUrl: z.string(),
|
|
78
|
+
}),
|
|
79
|
+
async run(c) {
|
|
80
|
+
const shared = await shareMarkdownFile({
|
|
81
|
+
filePath: resolveFromInvokeCwd(c.args.file),
|
|
82
|
+
isPublic: c.options.public === true,
|
|
83
|
+
});
|
|
84
|
+
if (shared instanceof Error) {
|
|
85
|
+
return c.error({
|
|
86
|
+
code: "DIFFMAP",
|
|
87
|
+
message: shared.message,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return c.ok(shared);
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await cli.serve();
|
package/src/codeFont.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import type { Plugin } from "vite";
|
|
3
|
+
import { parseViewerDocument } from "./parseViewer.js";
|
|
4
|
+
|
|
5
|
+
export function diffmapContentPlugin(input: {
|
|
6
|
+
filePath: string;
|
|
7
|
+
title: string;
|
|
8
|
+
}): Plugin {
|
|
9
|
+
const virtualId = "\0virtual:diffmap";
|
|
10
|
+
return {
|
|
11
|
+
name: "diffmap-content",
|
|
12
|
+
resolveId(id) {
|
|
13
|
+
if (id === "virtual:diffmap") return virtualId;
|
|
14
|
+
},
|
|
15
|
+
async load(id) {
|
|
16
|
+
if (id !== virtualId) return;
|
|
17
|
+
const source = await fs.readFile(input.filePath, "utf8");
|
|
18
|
+
const document = parseViewerDocument(source);
|
|
19
|
+
if (document instanceof Error) throw document;
|
|
20
|
+
return `export const viewerDocument = ${JSON.stringify(document)};`;
|
|
21
|
+
},
|
|
22
|
+
transformIndexHtml(html) {
|
|
23
|
+
return html.replaceAll(
|
|
24
|
+
"<title>diffmap</title>",
|
|
25
|
+
`<title>${escapeHtml(input.title)}</title>`,
|
|
26
|
+
);
|
|
27
|
+
},
|
|
28
|
+
configureServer(server) {
|
|
29
|
+
server.watcher.add(input.filePath);
|
|
30
|
+
server.watcher.on("change", (file) => {
|
|
31
|
+
if (file !== input.filePath) return;
|
|
32
|
+
const mod = server.moduleGraph.getModuleById(virtualId);
|
|
33
|
+
if (mod === undefined) return;
|
|
34
|
+
// oxlint-disable-next-line typescript/no-floating-promises -- Vite watcher callbacks cannot await module reloads.
|
|
35
|
+
void server.reloadModule(mod);
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function escapeHtml(value: string) {
|
|
42
|
+
return value
|
|
43
|
+
.replaceAll("&", "&")
|
|
44
|
+
.replaceAll("<", "<")
|
|
45
|
+
.replaceAll(">", ">")
|
|
46
|
+
.replaceAll('"', """);
|
|
47
|
+
}
|