@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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +212 -0
  3. package/bin.js +5 -0
  4. package/index.html +38 -0
  5. package/package.json +71 -0
  6. package/src/annotations.ts +82 -0
  7. package/src/cli.ts +94 -0
  8. package/src/codeFont.ts +2 -0
  9. package/src/contentPlugin.ts +47 -0
  10. package/src/definitions.ts +320 -0
  11. package/src/diagramAnnotations.ts +34 -0
  12. package/src/errors.ts +41 -0
  13. package/src/extractDocument.ts +20 -0
  14. package/src/gist/GistApp.tsx +292 -0
  15. package/src/gist/GistStatus.tsx +137 -0
  16. package/src/gist/fetchGist.ts +128 -0
  17. package/src/gist/main.tsx +25 -0
  18. package/src/gist/route.test.ts +90 -0
  19. package/src/gist/route.ts +134 -0
  20. package/src/index.ts +22 -0
  21. package/src/invokeCwd.ts +10 -0
  22. package/src/mermaid.ts +29 -0
  23. package/src/normalizeDiff.ts +39 -0
  24. package/src/parseFence.ts +91 -0
  25. package/src/parseViewer.ts +399 -0
  26. package/src/registry.ts +121 -0
  27. package/src/serve.ts +350 -0
  28. package/src/share.ts +94 -0
  29. package/src/viewer/CallStackDiff.tsx +156 -0
  30. package/src/viewer/Check.tsx +24 -0
  31. package/src/viewer/CodeDiff.tsx +26 -0
  32. package/src/viewer/ComarkView.tsx +209 -0
  33. package/src/viewer/DiffButton.tsx +24 -0
  34. package/src/viewer/DoneButton.tsx +32 -0
  35. package/src/viewer/Fence.tsx +51 -0
  36. package/src/viewer/FileExcerpt.tsx +65 -0
  37. package/src/viewer/HtmlBlock.tsx +19 -0
  38. package/src/viewer/HtmlPlaceholder.tsx +29 -0
  39. package/src/viewer/MermaidBlock.tsx +126 -0
  40. package/src/viewer/SourceDiffPanel.tsx +305 -0
  41. package/src/viewer/TableOfContents.tsx +211 -0
  42. package/src/viewer/ViewerApp.tsx +257 -0
  43. package/src/viewer/diagramLinks.ts +74 -0
  44. package/src/viewer/diffsTheme.ts +4 -0
  45. package/src/viewer/main.tsx +17 -0
  46. package/src/viewer/pierre.ts +47 -0
  47. package/src/viewer/sourceNavigation.ts +104 -0
  48. package/src/viewer/styles.css +9 -0
  49. package/src/viewer/viewerMode.ts +9 -0
  50. package/src/viewer/vite-env.d.ts +4 -0
  51. 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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { register } from "tsx/esm/api";
3
+
4
+ register();
5
+ await import("./src/cli.ts");
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();
@@ -0,0 +1,2 @@
1
+ export const codeFontFamily =
2
+ 'ui-monospace, "SF Mono", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace';
@@ -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("&", "&amp;")
44
+ .replaceAll("<", "&lt;")
45
+ .replaceAll(">", "&gt;")
46
+ .replaceAll('"', "&quot;");
47
+ }