@tanishqkancharla/diffmap 0.1.3 → 0.1.5
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 +25 -14
- package/package.json +3 -3
- package/src/cli.test.ts +50 -0
- package/src/cli.ts +126 -51
- package/src/contentPlugin.ts +5 -3
- package/src/errors.ts +6 -1
- package/src/gist/GistApp.tsx +34 -21
- package/src/gist/GistStatus.tsx +8 -43
- package/src/gist/main.tsx +2 -2
- package/src/gist/pin.ts +14 -0
- package/src/github/GitHubApp.tsx +235 -0
- package/src/github/fetchRepo.test.ts +17 -0
- package/src/github/fetchRepo.ts +300 -0
- package/src/github/patch.test.ts +30 -0
- package/src/github/patch.ts +40 -0
- package/src/github/pin.ts +14 -0
- package/src/github/route.test.ts +68 -0
- package/src/github/route.ts +175 -0
- package/src/host/HostApp.tsx +22 -0
- package/src/host/HostHome.tsx +73 -0
- package/src/host/files.ts +10 -0
- package/src/host/home.md +194 -0
- package/src/host/home.test.ts +82 -0
- package/src/index.ts +1 -0
- package/src/mermaid.ts +29 -1
- package/src/packageVersion.ts +7 -0
- package/src/parseViewer.test.ts +185 -0
- package/src/parseViewer.ts +27 -3
- package/src/serve.test.ts +306 -0
- package/src/serve.ts +23 -5
- package/src/share.ts +5 -1
- package/src/update.test.ts +19 -0
- package/src/update.ts +285 -0
- package/src/viewer/CallStackDiff.tsx +26 -6
- package/src/viewer/CloseServerButton.tsx +16 -0
- package/src/viewer/FileExcerpt.tsx +52 -3
- package/src/viewer/HtmlPlaceholder.tsx +6 -1
- package/src/viewer/SourceDiffPanel.tsx +155 -59
- package/src/viewer/TableOfContents.tsx +230 -39
- package/src/viewer/TocButton.tsx +27 -0
- package/src/viewer/ViewerApp.tsx +182 -47
- package/src/viewer/main.tsx +10 -3
- package/src/viewer/sourceHeader.test.ts +131 -0
- package/src/viewer/sourceHeader.ts +103 -0
- package/src/viewer/sourceNavigation.ts +81 -3
- package/src/viewer/viewerMode.ts +1 -1
- package/src/viewer/vite-env.d.ts +2 -1
- package/vite.config.ts +1 -1
- package/src/viewer/Check.tsx +0 -24
- package/src/viewer/DoneButton.tsx +0 -32
package/README.md
CHANGED
|
@@ -5,34 +5,33 @@
|
|
|
5
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
6
|
|
|
7
7
|
```sh
|
|
8
|
-
npx diffmap path/to/file.md
|
|
8
|
+
npx @tanishqkancharla/diffmap serve path/to/file.md
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
Bare `npx @tanishqkancharla/diffmap path/to/file.md` is the same as `serve`.
|
|
12
|
+
|
|
11
13
|
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
14
|
|
|
13
15
|
```sh
|
|
14
|
-
npx diffmap share path/to.md
|
|
16
|
+
npx @tanishqkancharla/diffmap share path/to.md
|
|
15
17
|
```
|
|
16
18
|
|
|
17
19
|
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
20
|
|
|
19
21
|
## Agent skills
|
|
20
22
|
|
|
21
|
-
Install skills from this repository:
|
|
22
|
-
|
|
23
23
|
```sh
|
|
24
|
-
npx skills add tanishqkancharla/diffmap --skill code-walkthrough
|
|
25
24
|
npx skills add tanishqkancharla/diffmap --skill generate-spec
|
|
26
25
|
```
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
Keeps a spec in `specs/` that stays true to what's planned versus already in the tree. Serve it with diffmap. Share from a GitHub PR (`https://diffmap.dev/<owner>/<repo>/pull/<n>/specs/<name>.md`) or `npx @tanishqkancharla/diffmap share`.
|
|
29
28
|
|
|
30
29
|
Options:
|
|
31
30
|
|
|
32
|
-
- `--port <n>` — listen port (default
|
|
31
|
+
- `--port <n>` — pin a listen port (default: a free port)
|
|
33
32
|
- `--root <dir>` — workspace root for file excerpts (default the directory you ran the command from)
|
|
34
33
|
|
|
35
|
-
**
|
|
34
|
+
**Close server** in the top right stops the server. Hosted gist and GitHub viewers have no local server, so they omit this control.
|
|
36
35
|
|
|
37
36
|
The server also stops after 24 hours without a page or source request.
|
|
38
37
|
Loading or refreshing the page resets the timer; background health checks and
|
|
@@ -41,23 +40,35 @@ Vite connections do not keep it alive.
|
|
|
41
40
|
List every running viewer, including viewers using custom ports:
|
|
42
41
|
|
|
43
42
|
```sh
|
|
44
|
-
npx diffmap list
|
|
43
|
+
npx @tanishqkancharla/diffmap list
|
|
45
44
|
```
|
|
46
45
|
|
|
46
|
+
Update a global install to the latest `@tanishqkancharla/diffmap` on npm. `diffmap --update` does the same thing:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
diffmap update
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
If you are already on the latest version, it says so and exits. Otherwise it runs the global install for the package manager that installed diffmap (`npm install --global`, `pnpm add --global`, or `bun add --global`) and prints the old and new versions. Under `npx`, or when it cannot tell how diffmap was installed, it prints the command to run instead of installing anything.
|
|
53
|
+
|
|
47
54
|
Request the page with `Accept: text/markdown` to read the current source file
|
|
48
|
-
instead of the rendered HTML
|
|
55
|
+
instead of the rendered HTML. Use the URL printed by `serve`:
|
|
49
56
|
|
|
50
57
|
```sh
|
|
51
|
-
curl -H 'Accept: text/markdown' http://127.0.0.1
|
|
58
|
+
curl -H 'Accept: text/markdown' http://127.0.0.1:<port>/
|
|
52
59
|
```
|
|
53
60
|
|
|
54
61
|
## Library
|
|
55
62
|
|
|
56
63
|
```ts
|
|
57
|
-
import {
|
|
64
|
+
import {
|
|
65
|
+
startServer,
|
|
66
|
+
parseFence,
|
|
67
|
+
parseViewerDocument,
|
|
68
|
+
} from "@tanishqkancharla/diffmap";
|
|
58
69
|
```
|
|
59
70
|
|
|
60
|
-
`parseViewerDocument` turns markdown into the page document with [md4x](https://github.com/unjs/md4x). `startServer` listens. The generate-spec
|
|
71
|
+
`parseViewerDocument` turns markdown into the page document with [md4x](https://github.com/unjs/md4x). `startServer` listens. The generate-spec skill owns document shape; the viewer does not.
|
|
61
72
|
|
|
62
73
|
## Fences
|
|
63
74
|
|
|
@@ -176,7 +187,7 @@ Resolution uses the workspace's TypeScript configuration and current files.
|
|
|
176
187
|
Deleted files and lines that no longer match the workspace show a message;
|
|
177
188
|
their call stack references still highlight the embedded old-side diff.
|
|
178
189
|
|
|
179
|
-
Run `npx diffmap fixtures/annotations.md` for an example
|
|
190
|
+
Run `npx @tanishqkancharla/diffmap fixtures/annotations.md` for an example
|
|
180
191
|
with old/new references, multiple files, and unchanged context.
|
|
181
192
|
|
|
182
193
|
## Link Mermaid nodes and edges
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanishqkancharla/diffmap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Serve a spec or code walkthrough as a local page",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Tanishq Kancharla",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"lint:fix": "oxlint --fix src vite.config.ts vite.gist.config.ts bin.js",
|
|
34
34
|
"prepublishOnly": "npm run typecheck && npm run lint && npm run format:check",
|
|
35
35
|
"release": "npm publish",
|
|
36
|
-
"test": "tsx --test src/gist/route.test.ts",
|
|
36
|
+
"test": "tsx --test src/cli.test.ts src/update.test.ts src/parseViewer.test.ts src/serve.test.ts src/gist/route.test.ts src/github/route.test.ts src/github/patch.test.ts src/github/fetchRepo.test.ts src/viewer/sourceHeader.test.ts src/host/home.test.ts",
|
|
37
37
|
"typecheck": "tsc --noEmit",
|
|
38
38
|
"build": "vite build",
|
|
39
39
|
"build:gist": "vite build",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"beautiful-mermaid": "^1.1.3",
|
|
46
46
|
"errore": "^0.14.1",
|
|
47
47
|
"incur": "^0.4.20",
|
|
48
|
-
"maui": "npm:@tanishqkancharla/maui@0.0.
|
|
48
|
+
"maui": "npm:@tanishqkancharla/maui@0.0.30",
|
|
49
49
|
"md4x": "^0.0.29",
|
|
50
50
|
"purse-styles": "^0.2.3",
|
|
51
51
|
"react": "^19.2.8",
|
package/src/cli.test.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
const srcDir = fileURLToPath(new URL(".", import.meta.url));
|
|
11
|
+
const packageRoot = path.resolve(srcDir, "..");
|
|
12
|
+
const cliPath = path.join(srcDir, "cli.ts");
|
|
13
|
+
|
|
14
|
+
async function runCli(args: string[]) {
|
|
15
|
+
try {
|
|
16
|
+
const result = await execFileAsync(
|
|
17
|
+
process.execPath,
|
|
18
|
+
["--import", "tsx", cliPath, ...args],
|
|
19
|
+
{
|
|
20
|
+
cwd: packageRoot,
|
|
21
|
+
timeout: 20_000,
|
|
22
|
+
maxBuffer: 1_000_000,
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
return { exitCode: 0, stdout: result.stdout, stderr: result.stderr };
|
|
26
|
+
} catch (cause) {
|
|
27
|
+
if (typeof cause !== "object" || !cause) throw cause;
|
|
28
|
+
const exitCode =
|
|
29
|
+
"code" in cause && typeof cause.code === "number"
|
|
30
|
+
? cause.code
|
|
31
|
+
: undefined;
|
|
32
|
+
const stdout =
|
|
33
|
+
"stdout" in cause && typeof cause.stdout === "string" ? cause.stdout : "";
|
|
34
|
+
const stderr =
|
|
35
|
+
"stderr" in cause && typeof cause.stderr === "string" ? cause.stderr : "";
|
|
36
|
+
return { exitCode, stdout, stderr };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test("diffmap --version matches package.json", async () => {
|
|
41
|
+
const pkg = JSON.parse(
|
|
42
|
+
await fs.readFile(path.join(packageRoot, "package.json"), "utf8"),
|
|
43
|
+
) as { version: string };
|
|
44
|
+
const source = await fs.readFile(cliPath, "utf8");
|
|
45
|
+
assert.doesNotMatch(source, /version:\s*["'`]\d+\.\d+\.\d+/);
|
|
46
|
+
const result = await runCli(["--version"]);
|
|
47
|
+
assert.equal(result.exitCode, 0, result.stderr);
|
|
48
|
+
const printed = `${result.stdout}${result.stderr}`.trim();
|
|
49
|
+
assert.equal(printed, pkg.version);
|
|
50
|
+
});
|
package/src/cli.ts
CHANGED
|
@@ -2,44 +2,120 @@
|
|
|
2
2
|
|
|
3
3
|
import { Cli, z } from "incur";
|
|
4
4
|
import { resolveFromInvokeCwd } from "./invokeCwd.js";
|
|
5
|
+
import { packageVersion } from "./packageVersion.js";
|
|
5
6
|
import { listRunningDiffmaps } from "./registry.js";
|
|
6
7
|
import { startServer } from "./serve.js";
|
|
7
8
|
import { shareMarkdownFile } from "./share.js";
|
|
9
|
+
import {
|
|
10
|
+
detectInstallMethod,
|
|
11
|
+
fetchLatestVersion,
|
|
12
|
+
packageName,
|
|
13
|
+
resolveUpdateArgv,
|
|
14
|
+
runCommand,
|
|
15
|
+
runUpdate,
|
|
16
|
+
} from "./update.js";
|
|
8
17
|
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
18
|
+
const serveArgs = z.object({
|
|
19
|
+
file: z.string().describe("Path to the markdown file"),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const serveOptions = z.object({
|
|
23
|
+
port: z.coerce.number().optional().describe("Port (default: a free port)"),
|
|
24
|
+
root: z.string().optional().describe("Workspace root for file excerpts"),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const serveOutput = z.object({
|
|
28
|
+
url: z.string(),
|
|
29
|
+
file: z.string(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
type ServeContext = {
|
|
33
|
+
args: { file: string };
|
|
34
|
+
options: { port?: number | undefined; root?: string | undefined };
|
|
35
|
+
error: (input: { code: string; message: string }) => never;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
async function* runServe(c: ServeContext) {
|
|
39
|
+
const started = await startServer({
|
|
40
|
+
filePath: resolveFromInvokeCwd(c.args.file),
|
|
41
|
+
workspaceRoot:
|
|
42
|
+
c.options.root === undefined
|
|
43
|
+
? resolveFromInvokeCwd(".")
|
|
44
|
+
: resolveFromInvokeCwd(c.options.root),
|
|
45
|
+
port: c.options.port,
|
|
46
|
+
});
|
|
47
|
+
if (started instanceof Error) {
|
|
48
|
+
return c.error({
|
|
49
|
+
code: "DIFFMAP",
|
|
50
|
+
message: started.message,
|
|
31
51
|
});
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
52
|
+
}
|
|
53
|
+
yield { url: started.url, file: started.filePath };
|
|
54
|
+
await started.closed;
|
|
55
|
+
return { url: started.url, file: started.filePath };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const cli = Cli.create("diffmap", {
|
|
59
|
+
description: "Serve a spec locally, or share it as a gist",
|
|
60
|
+
version: packageVersion,
|
|
61
|
+
args: serveArgs,
|
|
62
|
+
options: serveOptions,
|
|
63
|
+
output: serveOutput,
|
|
64
|
+
hint: "`serve <spec.md>` runs the local viewer. `share <spec.md>` publishes a secret gist. `update` (or `--update`) installs the latest diffmap.",
|
|
65
|
+
examples: [
|
|
66
|
+
{
|
|
67
|
+
args: { file: "spec.md" },
|
|
68
|
+
description: "Same as `diffmap serve spec.md`",
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
run: runServe,
|
|
42
72
|
})
|
|
73
|
+
.command("serve", {
|
|
74
|
+
description: "Run the local viewer server",
|
|
75
|
+
args: serveArgs,
|
|
76
|
+
options: serveOptions,
|
|
77
|
+
output: serveOutput,
|
|
78
|
+
examples: [
|
|
79
|
+
{
|
|
80
|
+
args: { file: "spec.md" },
|
|
81
|
+
description: "Open the local viewer",
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
run: runServe,
|
|
85
|
+
})
|
|
86
|
+
.command("share", {
|
|
87
|
+
description: "Publish a secret gist and print https://diffmap.dev/g/<id>",
|
|
88
|
+
args: z.object({
|
|
89
|
+
file: z.string().describe("Path to the markdown file"),
|
|
90
|
+
}),
|
|
91
|
+
options: z.object({
|
|
92
|
+
public: z.boolean().optional().describe("Create a public gist"),
|
|
93
|
+
}),
|
|
94
|
+
output: z.object({
|
|
95
|
+
gistId: z.string(),
|
|
96
|
+
gistUrl: z.string(),
|
|
97
|
+
viewerUrl: z.string(),
|
|
98
|
+
}),
|
|
99
|
+
examples: [
|
|
100
|
+
{
|
|
101
|
+
args: { file: "spec.md" },
|
|
102
|
+
description: "Secret gist → https://diffmap.dev/g/<id>",
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
async run(c) {
|
|
106
|
+
const shared = await shareMarkdownFile({
|
|
107
|
+
filePath: resolveFromInvokeCwd(c.args.file),
|
|
108
|
+
isPublic: c.options.public === true,
|
|
109
|
+
});
|
|
110
|
+
if (shared instanceof Error) {
|
|
111
|
+
return c.error({
|
|
112
|
+
code: "DIFFMAP",
|
|
113
|
+
message: shared.message,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return c.ok(shared);
|
|
117
|
+
},
|
|
118
|
+
})
|
|
43
119
|
.command("list", {
|
|
44
120
|
description: "List running diffmap viewers",
|
|
45
121
|
output: z.object({
|
|
@@ -63,32 +139,31 @@ const cli = Cli.create("diffmap", {
|
|
|
63
139
|
return c.ok({ instances });
|
|
64
140
|
},
|
|
65
141
|
})
|
|
66
|
-
.command("
|
|
67
|
-
description:
|
|
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
|
-
}),
|
|
142
|
+
.command("update", {
|
|
143
|
+
description: `Update diffmap to the latest ${packageName} on npm (same as --update)`,
|
|
74
144
|
output: z.object({
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
145
|
+
status: z.enum(["up-to-date", "updated", "manual"]),
|
|
146
|
+
message: z.string(),
|
|
147
|
+
current: z.string(),
|
|
148
|
+
previous: z.string().optional(),
|
|
149
|
+
latest: z.string().optional(),
|
|
150
|
+
command: z.string().optional(),
|
|
78
151
|
}),
|
|
79
152
|
async run(c) {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
153
|
+
const result = await runUpdate({
|
|
154
|
+
currentVersion: packageVersion,
|
|
155
|
+
fetchLatestVersion,
|
|
156
|
+
detectInstallMethod,
|
|
157
|
+
runCommand,
|
|
83
158
|
});
|
|
84
|
-
if (
|
|
159
|
+
if (result instanceof Error) {
|
|
85
160
|
return c.error({
|
|
86
|
-
code: "
|
|
87
|
-
message:
|
|
161
|
+
code: "UPDATE_FAILED",
|
|
162
|
+
message: result.message,
|
|
88
163
|
});
|
|
89
164
|
}
|
|
90
|
-
return c.ok(
|
|
165
|
+
return c.ok(result);
|
|
91
166
|
},
|
|
92
167
|
});
|
|
93
168
|
|
|
94
|
-
await cli.serve();
|
|
169
|
+
await cli.serve(resolveUpdateArgv(process.argv.slice(2)));
|
package/src/contentPlugin.ts
CHANGED
|
@@ -15,9 +15,11 @@ export function diffmapContentPlugin(input: {
|
|
|
15
15
|
async load(id) {
|
|
16
16
|
if (id !== virtualId) return;
|
|
17
17
|
const source = await fs.readFile(input.filePath, "utf8");
|
|
18
|
-
const document = parseViewerDocument(source);
|
|
19
|
-
if (document instanceof Error)
|
|
20
|
-
|
|
18
|
+
const document = parseViewerDocument(source, input.filePath);
|
|
19
|
+
if (document instanceof Error) {
|
|
20
|
+
return `export const viewerDocument = null; export const parseError = ${JSON.stringify(document.message)};`;
|
|
21
|
+
}
|
|
22
|
+
return `export const viewerDocument = ${JSON.stringify(document)}; export const parseError = null;`;
|
|
21
23
|
},
|
|
22
24
|
transformIndexHtml(html) {
|
|
23
25
|
return html.replaceAll(
|
package/src/errors.ts
CHANGED
|
@@ -17,7 +17,7 @@ export class DiffmapRegistryError extends errore.createTaggedError({
|
|
|
17
17
|
|
|
18
18
|
export class DiffmapMermaidError extends errore.createTaggedError({
|
|
19
19
|
name: "DiffmapMermaidError",
|
|
20
|
-
message: "diffmap could not
|
|
20
|
+
message: "diffmap could not parse mermaid in $path ($where): $reason",
|
|
21
21
|
}) {}
|
|
22
22
|
|
|
23
23
|
export class DiffmapParseError extends errore.createTaggedError({
|
|
@@ -39,3 +39,8 @@ export class DiffmapShareError extends errore.createTaggedError({
|
|
|
39
39
|
name: "DiffmapShareError",
|
|
40
40
|
message: "diffmap share failed: $reason",
|
|
41
41
|
}) {}
|
|
42
|
+
|
|
43
|
+
export class DiffmapUpdateError extends errore.createTaggedError({
|
|
44
|
+
name: "DiffmapUpdateError",
|
|
45
|
+
message: "diffmap update failed: $reason",
|
|
46
|
+
}) {}
|
package/src/gist/GistApp.tsx
CHANGED
|
@@ -6,7 +6,9 @@ import { parseViewerDocument } from "../parseViewer.ts";
|
|
|
6
6
|
import type { ViewerDocument } from "../parseViewer.ts";
|
|
7
7
|
import { ViewerApp } from "../viewer/ViewerApp.tsx";
|
|
8
8
|
import { loadGistMarkdown, type GistDocument } from "./fetchGist.ts";
|
|
9
|
-
import {
|
|
9
|
+
import { GistPinContext } from "./pin.ts";
|
|
10
|
+
import { HostHome } from "../host/HostHome.tsx";
|
|
11
|
+
import { GistLoading, GistStatus } from "./GistStatus.tsx";
|
|
10
12
|
import {
|
|
11
13
|
gistPath,
|
|
12
14
|
parseGistPath,
|
|
@@ -82,7 +84,7 @@ export function GistApp() {
|
|
|
82
84
|
return () => abort.abort();
|
|
83
85
|
}, [route]);
|
|
84
86
|
|
|
85
|
-
if (route.kind === "home") return <
|
|
87
|
+
if (route.kind === "home") return <HostHome />;
|
|
86
88
|
if (route.kind === "invalid") {
|
|
87
89
|
return (
|
|
88
90
|
<GistStatus title="Not a gist id">
|
|
@@ -151,24 +153,32 @@ export function GistApp() {
|
|
|
151
153
|
}
|
|
152
154
|
|
|
153
155
|
return (
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
156
|
+
<GistPinContext.Provider
|
|
157
|
+
value={{
|
|
158
|
+
gistId: view.gist.id,
|
|
159
|
+
file: view.file,
|
|
160
|
+
htmlUrl: view.gist.htmlUrl,
|
|
161
|
+
}}
|
|
162
|
+
>
|
|
163
|
+
<ViewerApp
|
|
164
|
+
key={`${view.gist.id}:${view.file}`}
|
|
165
|
+
document={view.document}
|
|
166
|
+
mode="gist"
|
|
167
|
+
headerActions={
|
|
168
|
+
<GistHeader
|
|
169
|
+
gistId={view.gist.id}
|
|
170
|
+
htmlUrl={view.gist.htmlUrl}
|
|
171
|
+
files={view.gist.files}
|
|
172
|
+
current={view.file}
|
|
173
|
+
onSelect={(file) => {
|
|
174
|
+
const next = gistPath(view.gist.id, file);
|
|
175
|
+
window.history.pushState({}, "", next);
|
|
176
|
+
setRoute(parseGistPath(next));
|
|
177
|
+
}}
|
|
178
|
+
/>
|
|
179
|
+
}
|
|
180
|
+
/>
|
|
181
|
+
</GistPinContext.Provider>
|
|
172
182
|
);
|
|
173
183
|
}
|
|
174
184
|
|
|
@@ -196,7 +206,10 @@ async function loadView(
|
|
|
196
206
|
file: file ?? "",
|
|
197
207
|
};
|
|
198
208
|
}
|
|
199
|
-
const parsed = parseViewerDocument(
|
|
209
|
+
const parsed = parseViewerDocument(
|
|
210
|
+
loaded.gist.chosen.content,
|
|
211
|
+
loaded.gist.chosen.name,
|
|
212
|
+
);
|
|
200
213
|
if (parsed instanceof Error) {
|
|
201
214
|
return {
|
|
202
215
|
kind: "parse-error",
|
package/src/gist/GistStatus.tsx
CHANGED
|
@@ -19,6 +19,7 @@ export function GistStatus(props: {
|
|
|
19
19
|
children: ReactNode;
|
|
20
20
|
gistId?: string;
|
|
21
21
|
gistUrl?: string;
|
|
22
|
+
linkLabel?: string;
|
|
22
23
|
}) {
|
|
23
24
|
const shell = useStyles(styles.shell);
|
|
24
25
|
const header = useStyles(styles.header);
|
|
@@ -39,7 +40,9 @@ export function GistStatus(props: {
|
|
|
39
40
|
{props.children}
|
|
40
41
|
{gistUrl !== undefined && (
|
|
41
42
|
<P>
|
|
42
|
-
<Link href={gistUrl}>
|
|
43
|
+
<Link href={gistUrl}>
|
|
44
|
+
{props.linkLabel ?? "Open gist on GitHub"}
|
|
45
|
+
</Link>
|
|
43
46
|
</P>
|
|
44
47
|
)}
|
|
45
48
|
</main>
|
|
@@ -47,10 +50,10 @@ export function GistStatus(props: {
|
|
|
47
50
|
);
|
|
48
51
|
}
|
|
49
52
|
|
|
50
|
-
export function GistLoading() {
|
|
53
|
+
export function GistLoading(props: { title?: string }) {
|
|
51
54
|
const row = useStyles(styles.loading);
|
|
52
55
|
return (
|
|
53
|
-
<GistStatus title="Loading gist…">
|
|
56
|
+
<GistStatus title={props.title ?? "Loading gist…"}>
|
|
54
57
|
<div className={row}>
|
|
55
58
|
<Thinking />
|
|
56
59
|
</div>
|
|
@@ -58,41 +61,6 @@ export function GistLoading() {
|
|
|
58
61
|
);
|
|
59
62
|
}
|
|
60
63
|
|
|
61
|
-
export function GistLanding() {
|
|
62
|
-
const code = useStyles(styles.code);
|
|
63
|
-
return (
|
|
64
|
-
<GistStatus title="diffmap">
|
|
65
|
-
<P>
|
|
66
|
-
A hosted viewer for diffmap walkthroughs. This page fetches a GitHub
|
|
67
|
-
gist in your browser. There is no server holding the markdown.
|
|
68
|
-
</P>
|
|
69
|
-
<P>
|
|
70
|
-
Canonical URL:{" "}
|
|
71
|
-
<code className={code}>https://diffmap.dev/g/<gistId></code>.
|
|
72
|
-
Optional file:{" "}
|
|
73
|
-
<code className={code}>/g/<gistId>/<file.md></code>. Heading
|
|
74
|
-
hashes stay in the fragment, same as the local viewer.
|
|
75
|
-
</P>
|
|
76
|
-
<P>
|
|
77
|
-
Share a secret gist (requires <code className={code}>gh</code>
|
|
78
|
-
):
|
|
79
|
-
</P>
|
|
80
|
-
<P>
|
|
81
|
-
<code className={code}>npx diffmap share path/to.md</code>
|
|
82
|
-
</P>
|
|
83
|
-
<P>
|
|
84
|
-
Secret gists are unlisted, not private. Anyone with the id can read
|
|
85
|
-
them. Unauthenticated GitHub API is 60 requests per hour per IP.
|
|
86
|
-
</P>
|
|
87
|
-
<P>
|
|
88
|
-
<Link href="https://github.com/tanishqkancharla/diffmap">
|
|
89
|
-
tanishqkancharla/diffmap
|
|
90
|
-
</Link>
|
|
91
|
-
</P>
|
|
92
|
-
</GistStatus>
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
64
|
const styles = {
|
|
97
65
|
shell: style(flex({ direction: "column" }), {
|
|
98
66
|
width: "100%",
|
|
@@ -103,7 +71,7 @@ const styles = {
|
|
|
103
71
|
backgroundColor: colors.gray[4],
|
|
104
72
|
}),
|
|
105
73
|
header: style(
|
|
106
|
-
flex({ direction: "row",
|
|
74
|
+
flex({ direction: "row", alignItems: "center", justifyContent: "between" }),
|
|
107
75
|
spacing.padding({ x: 6, y: 3 }),
|
|
108
76
|
flexItem({ size: "hug" }),
|
|
109
77
|
border(["bottom"], "border"),
|
|
@@ -128,10 +96,7 @@ const styles = {
|
|
|
128
96
|
title: style(text({ size: "xl", fontWeight: 600, color: "highContrast" }), {
|
|
129
97
|
marginBottom: spacing.value(4),
|
|
130
98
|
}),
|
|
131
|
-
loading: style(flex({ direction: "row",
|
|
99
|
+
loading: style(flex({ direction: "row", alignItems: "center" }), {
|
|
132
100
|
minHeight: spacing.value(12),
|
|
133
101
|
}),
|
|
134
|
-
code: style(text({ size: "sm", color: "highContrast" }), {
|
|
135
|
-
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
136
|
-
}),
|
|
137
102
|
};
|
package/src/gist/main.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import { StrictMode } from "react";
|
|
|
2
2
|
import { createRoot } from "react-dom/client";
|
|
3
3
|
import { MauiProvider } from "maui";
|
|
4
4
|
import { rewritePiGistUrl } from "./route.ts";
|
|
5
|
-
import {
|
|
5
|
+
import { HostApp } from "../host/HostApp.tsx";
|
|
6
6
|
import "../viewer/styles.css";
|
|
7
7
|
|
|
8
8
|
const rewritten = rewritePiGistUrl(
|
|
@@ -19,7 +19,7 @@ if (root === null) throw new Error("diffmap root element is missing");
|
|
|
19
19
|
createRoot(root).render(
|
|
20
20
|
<StrictMode>
|
|
21
21
|
<MauiProvider>
|
|
22
|
-
<
|
|
22
|
+
<HostApp />
|
|
23
23
|
</MauiProvider>
|
|
24
24
|
</StrictMode>,
|
|
25
25
|
);
|
package/src/gist/pin.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
|
|
3
|
+
/** Gist snapshot: identify the gist file, never a fake owner/repo. */
|
|
4
|
+
export type GistPin = {
|
|
5
|
+
gistId: string;
|
|
6
|
+
file: string;
|
|
7
|
+
htmlUrl: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const GistPinContext = createContext<GistPin | undefined>(undefined);
|
|
11
|
+
|
|
12
|
+
export function useGistPin() {
|
|
13
|
+
return useContext(GistPinContext);
|
|
14
|
+
}
|