@tanishqkancharla/diffmap 0.1.4 → 0.2.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 +69 -61
- package/index.html +18 -0
- package/package.json +7 -5
- package/src/annotations.ts +36 -29
- package/src/changes.ts +229 -0
- package/src/cli.test.ts +50 -0
- package/src/cli.ts +41 -5
- package/src/errors.ts +10 -0
- package/src/gist/GistApp.tsx +39 -20
- package/src/gist/GistStatus.tsx +2 -51
- package/src/gist/fetchGist.ts +23 -0
- package/src/git.test.ts +402 -0
- package/src/git.ts +498 -0
- package/src/github/GitHubApp.tsx +39 -22
- package/src/github/fetchRepo.test.ts +59 -1
- package/src/github/fetchRepo.ts +99 -43
- package/src/github/patch.test.ts +18 -9
- package/src/github/patch.ts +30 -19
- package/src/host/HostHome.tsx +86 -0
- package/src/host/files.ts +10 -0
- package/src/host/home.md +156 -0
- package/src/host/home.test.ts +83 -0
- package/src/index.ts +1 -0
- package/src/packageVersion.ts +7 -0
- package/src/parseFence.ts +6 -6
- package/src/parseViewer.test.ts +87 -0
- package/src/parseViewer.ts +103 -77
- package/src/serve.test.ts +306 -0
- package/src/serve.ts +48 -5
- package/src/share.ts +18 -0
- package/src/update.test.ts +19 -0
- package/src/update.ts +285 -0
- package/src/viewer/CallStackDiff.tsx +15 -3
- package/src/viewer/ChangesCard.tsx +267 -0
- package/src/viewer/CloseServerButton.tsx +16 -0
- package/src/viewer/ComarkView.tsx +83 -15
- package/src/viewer/Fence.tsx +9 -1
- package/src/viewer/FileExcerpt.tsx +25 -3
- package/src/viewer/HtmlPlaceholder.tsx +6 -1
- package/src/viewer/MermaidBlock.tsx +99 -11
- package/src/viewer/RangePicker.tsx +526 -0
- package/src/viewer/SourceDiffPanel.tsx +277 -121
- package/src/viewer/TableOfContents.tsx +313 -39
- package/src/viewer/ViewerApp.tsx +120 -68
- package/src/viewer/changesSource.ts +91 -0
- package/src/viewer/sourceHeader.test.ts +2 -87
- package/src/viewer/sourceHeader.ts +4 -63
- package/src/viewer/sourceNavigation.ts +33 -42
- package/src/viewer/viewerMode.ts +1 -1
- package/vite.config.ts +2 -1
- package/vite.gist.config.ts +57 -0
- package/src/viewer/Check.tsx +0 -24
- package/src/viewer/DoneButton.tsx +0 -32
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createElement, type ReactNode, useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Checkbox,
|
|
4
|
+
Code,
|
|
4
5
|
colors,
|
|
5
|
-
|
|
6
|
+
focusRing,
|
|
6
7
|
radius,
|
|
7
8
|
spacing,
|
|
8
9
|
Table,
|
|
@@ -19,7 +20,7 @@ import type {
|
|
|
19
20
|
ViewerElement,
|
|
20
21
|
ViewerNode,
|
|
21
22
|
} from "../parseViewer.js";
|
|
22
|
-
import type { SourceNavigation } from "../annotations.js";
|
|
23
|
+
import type { SourceAnnotation, SourceNavigation } from "../annotations.js";
|
|
23
24
|
import { Fence } from "./Fence.tsx";
|
|
24
25
|
import { HtmlPlaceholder } from "./HtmlPlaceholder.tsx";
|
|
25
26
|
import { useViewerMode } from "./viewerMode.ts";
|
|
@@ -60,6 +61,11 @@ function renderNode(
|
|
|
60
61
|
if (node.type === "view") {
|
|
61
62
|
return <Fence key={key} fence={node.fence} {...navigation} />;
|
|
62
63
|
}
|
|
64
|
+
if (node.type === "reference") {
|
|
65
|
+
return (
|
|
66
|
+
<InlineReference key={key} annotation={node.annotation} {...navigation} />
|
|
67
|
+
);
|
|
68
|
+
}
|
|
63
69
|
return renderElement(node, key, navigation, trustedHtml);
|
|
64
70
|
}
|
|
65
71
|
|
|
@@ -89,7 +95,15 @@ function renderElement(
|
|
|
89
95
|
);
|
|
90
96
|
}
|
|
91
97
|
if (node.tag === "code") {
|
|
92
|
-
return
|
|
98
|
+
return (
|
|
99
|
+
<Code
|
|
100
|
+
key={key}
|
|
101
|
+
{...domAttrs(node)}
|
|
102
|
+
style={{ whiteSpace: "normal", overflowWrap: "anywhere" }}
|
|
103
|
+
>
|
|
104
|
+
{children}
|
|
105
|
+
</Code>
|
|
106
|
+
);
|
|
93
107
|
}
|
|
94
108
|
if (voidTags.has(node.tag)) {
|
|
95
109
|
return createElement(node.tag, { key, ...domAttrs(node) });
|
|
@@ -156,12 +170,33 @@ function renderTable(
|
|
|
156
170
|
);
|
|
157
171
|
}
|
|
158
172
|
|
|
173
|
+
function InlineReference(
|
|
174
|
+
props: { annotation: SourceAnnotation } & SourceNavigation,
|
|
175
|
+
) {
|
|
176
|
+
const className = useStyles(inlineReferenceClass);
|
|
177
|
+
return (
|
|
178
|
+
<button
|
|
179
|
+
type="button"
|
|
180
|
+
className={className}
|
|
181
|
+
aria-pressed={props.selectedAnnotation === props.annotation}
|
|
182
|
+
aria-controls="source-diff-panel"
|
|
183
|
+
title={props.annotation.text}
|
|
184
|
+
onClick={() => props.onSelectAnnotation(props.annotation)}
|
|
185
|
+
>
|
|
186
|
+
<Code>{props.annotation.text}</Code>
|
|
187
|
+
</button>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
159
191
|
function TaskListItem(props: { node: ViewerElement; children: ReactNode }) {
|
|
160
192
|
const [checked, setChecked] = useState(props.node.attrs.checked === true);
|
|
193
|
+
const className = useStyles(taskListItemClass);
|
|
161
194
|
return (
|
|
162
195
|
<li
|
|
163
196
|
id={props.node.attrs.id}
|
|
164
|
-
className={props.node.attrs.className
|
|
197
|
+
className={[className, props.node.attrs.className]
|
|
198
|
+
.filter(Boolean)
|
|
199
|
+
.join(" ")}
|
|
165
200
|
data-task=""
|
|
166
201
|
>
|
|
167
202
|
<span className="diffmap-task-checkbox">
|
|
@@ -171,13 +206,14 @@ function TaskListItem(props: { node: ViewerElement; children: ReactNode }) {
|
|
|
171
206
|
setChecked={setChecked}
|
|
172
207
|
/>
|
|
173
208
|
</span>
|
|
174
|
-
{props.children}
|
|
209
|
+
<div className="diffmap-task-content">{props.children}</div>
|
|
175
210
|
</li>
|
|
176
211
|
);
|
|
177
212
|
}
|
|
178
213
|
|
|
179
214
|
function nodeText(node: ViewerNode): string {
|
|
180
215
|
if (node.type === "text") return node.value;
|
|
216
|
+
if (node.type === "reference") return node.annotation.text;
|
|
181
217
|
if (node.type !== "element") return "";
|
|
182
218
|
return node.children.map(nodeText).join("");
|
|
183
219
|
}
|
|
@@ -194,16 +230,48 @@ function domAttrs(node: ViewerElement) {
|
|
|
194
230
|
};
|
|
195
231
|
}
|
|
196
232
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
return <code className={className}>{props.children}</code>;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const inlineCodeClass = style(
|
|
203
|
-
monospace,
|
|
204
|
-
radius.md,
|
|
205
|
-
spacing.padding({ x: 2, y: 1 }),
|
|
233
|
+
const inlineReferenceClass = style(
|
|
234
|
+
radius.sm,
|
|
206
235
|
{
|
|
207
|
-
|
|
236
|
+
display: "inline",
|
|
237
|
+
padding: 0,
|
|
238
|
+
border: 0,
|
|
239
|
+
font: "inherit",
|
|
240
|
+
color: "inherit",
|
|
241
|
+
background: "transparent",
|
|
242
|
+
cursor: "pointer",
|
|
243
|
+
"& > code": { color: colors.blue[11] },
|
|
244
|
+
"&:hover > code": { backgroundColor: colors.blue[3] },
|
|
245
|
+
"&[aria-pressed='true'] > code": {
|
|
246
|
+
backgroundColor: colors.blue[4],
|
|
247
|
+
color: colors.blue[12],
|
|
248
|
+
},
|
|
208
249
|
},
|
|
250
|
+
focusRing(),
|
|
209
251
|
);
|
|
252
|
+
|
|
253
|
+
const taskListItemClass = style({
|
|
254
|
+
display: "grid",
|
|
255
|
+
gridTemplateColumns: "auto minmax(0, 1fr)",
|
|
256
|
+
alignItems: "start",
|
|
257
|
+
columnGap: spacing.value(3),
|
|
258
|
+
"& > .diffmap-task-checkbox": {
|
|
259
|
+
display: "flex",
|
|
260
|
+
alignItems: "center",
|
|
261
|
+
minHeight: "1lh",
|
|
262
|
+
},
|
|
263
|
+
"& > .diffmap-task-content": {
|
|
264
|
+
minWidth: 0,
|
|
265
|
+
},
|
|
266
|
+
"& .diffmap-task-checkbox label > span:last-child": {
|
|
267
|
+
position: "absolute",
|
|
268
|
+
width: "1px",
|
|
269
|
+
height: "1px",
|
|
270
|
+
padding: 0,
|
|
271
|
+
margin: "-1px",
|
|
272
|
+
overflow: "hidden",
|
|
273
|
+
clip: "rect(0, 0, 0, 0)",
|
|
274
|
+
whiteSpace: "nowrap",
|
|
275
|
+
border: 0,
|
|
276
|
+
},
|
|
277
|
+
});
|
package/src/viewer/Fence.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { CodeBlock } from "maui";
|
|
|
2
2
|
import type { Fence as FenceModel } from "../parseFence.js";
|
|
3
3
|
import type { SourceNavigation } from "../annotations.js";
|
|
4
4
|
import { CallStackDiff } from "./CallStackDiff.tsx";
|
|
5
|
+
import { ChangesCard } from "./ChangesCard.tsx";
|
|
5
6
|
import { CodeDiff } from "./CodeDiff.tsx";
|
|
6
7
|
import { FileExcerpt } from "./FileExcerpt.tsx";
|
|
7
8
|
import { HtmlBlock } from "./HtmlBlock.tsx";
|
|
@@ -25,7 +26,14 @@ export function Fence(props: { fence: FenceModel } & SourceNavigation) {
|
|
|
25
26
|
if (!trustedHtml) return <HtmlPlaceholder />;
|
|
26
27
|
return <HtmlBlock source={fence.source} />;
|
|
27
28
|
}
|
|
28
|
-
if (fence.kind === "
|
|
29
|
+
if (fence.kind === "changes")
|
|
30
|
+
return (
|
|
31
|
+
<ChangesCard
|
|
32
|
+
rev={fence.rev}
|
|
33
|
+
selectedAnnotation={props.selectedAnnotation}
|
|
34
|
+
onSelectAnnotation={props.onSelectAnnotation}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
29
37
|
if (fence.kind === "callstack")
|
|
30
38
|
return (
|
|
31
39
|
<CallStackDiff
|
|
@@ -5,6 +5,7 @@ import { useStyles } from "purse-styles";
|
|
|
5
5
|
import { pierreFileOptions, pierreShell } from "./pierre.ts";
|
|
6
6
|
import { useViewerMode } from "./viewerMode.ts";
|
|
7
7
|
import { useGitHubPin } from "../github/pin.ts";
|
|
8
|
+
import { useHostFiles } from "../host/files.ts";
|
|
8
9
|
import { fetchPinnedBlob } from "../github/fetchRepo.ts";
|
|
9
10
|
|
|
10
11
|
type FileExcerptPayload = {
|
|
@@ -43,8 +44,10 @@ function useFileExcerpt(input: { path: string; start: number; end: number }) {
|
|
|
43
44
|
const [excerpt, setExcerpt] = useState<FileExcerptPayload>();
|
|
44
45
|
const mode = useViewerMode();
|
|
45
46
|
const pin = useGitHubPin();
|
|
47
|
+
const hostFiles = useHostFiles();
|
|
48
|
+
const hostExcerpt = hostFileExcerpt(hostFiles, input);
|
|
46
49
|
useEffect(() => {
|
|
47
|
-
if (
|
|
50
|
+
if (hostFiles !== undefined) return;
|
|
48
51
|
if (mode === "github") {
|
|
49
52
|
if (pin === undefined) return;
|
|
50
53
|
// oxlint-disable-next-line typescript/no-floating-promises -- React effects cannot await; this request owns the excerpt update.
|
|
@@ -69,6 +72,7 @@ function useFileExcerpt(input: { path: string; start: number; end: number }) {
|
|
|
69
72
|
});
|
|
70
73
|
return;
|
|
71
74
|
}
|
|
75
|
+
if (mode !== "local") return;
|
|
72
76
|
const params = new URLSearchParams({
|
|
73
77
|
path: input.path,
|
|
74
78
|
start: String(input.start),
|
|
@@ -87,6 +91,24 @@ function useFileExcerpt(input: { path: string; start: number; end: number }) {
|
|
|
87
91
|
.catch((cause) => {
|
|
88
92
|
console.warn("diffmap file excerpt failed", cause);
|
|
89
93
|
});
|
|
90
|
-
}, [input.path, input.start, input.end, mode, pin]);
|
|
91
|
-
return excerpt;
|
|
94
|
+
}, [input.path, input.start, input.end, mode, pin, hostFiles]);
|
|
95
|
+
return hostExcerpt ?? excerpt;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function hostFileExcerpt(
|
|
99
|
+
files: Record<string, string> | undefined,
|
|
100
|
+
input: { path: string; start: number; end: number },
|
|
101
|
+
): FileExcerptPayload | undefined {
|
|
102
|
+
if (files === undefined) return undefined;
|
|
103
|
+
const contents = files[input.path];
|
|
104
|
+
if (contents === undefined) return undefined;
|
|
105
|
+
const lines = contents.split(/\r?\n/);
|
|
106
|
+
const start = input.start;
|
|
107
|
+
const end = Math.min(input.end, lines.length);
|
|
108
|
+
return {
|
|
109
|
+
path: input.path,
|
|
110
|
+
start,
|
|
111
|
+
end,
|
|
112
|
+
contents: lines.slice(start - 1, end).join("\n"),
|
|
113
|
+
};
|
|
92
114
|
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { colors, radius, spacing, text } from "maui";
|
|
2
2
|
import { style, useStyles } from "purse-styles";
|
|
3
|
+
import { useViewerMode } from "./viewerMode.ts";
|
|
3
4
|
|
|
4
5
|
export function HtmlPlaceholder(props: { inline?: boolean }) {
|
|
5
6
|
const inline = props.inline === true;
|
|
6
7
|
const className = useStyles(inline ? styles.inline : styles.block);
|
|
7
|
-
const
|
|
8
|
+
const mode = useViewerMode();
|
|
9
|
+
const copy =
|
|
10
|
+
mode === "gist"
|
|
11
|
+
? "HTML from this gist is not rendered."
|
|
12
|
+
: "HTML is not rendered on the hosted viewer.";
|
|
8
13
|
if (inline) return <span className={className}>{copy}</span>;
|
|
9
14
|
return (
|
|
10
15
|
<aside className={className} data-diffmap-kind="html-disabled">
|
|
@@ -1,19 +1,80 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef } from "react";
|
|
2
|
-
import {
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Close,
|
|
5
|
+
Dialog,
|
|
6
|
+
Maximize,
|
|
7
|
+
backgroundColor,
|
|
8
|
+
colors,
|
|
9
|
+
fontFamily,
|
|
10
|
+
spacing,
|
|
11
|
+
} from "maui";
|
|
3
12
|
import { style, useStyles } from "purse-styles";
|
|
4
13
|
import { mermaidSvg } from "../mermaid.js";
|
|
5
14
|
import type { DiagramAnnotation } from "../diagramAnnotations.js";
|
|
6
15
|
import type { SourceNavigation } from "../annotations.js";
|
|
7
16
|
import { linkDiagram } from "./diagramLinks.js";
|
|
8
17
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
) {
|
|
18
|
+
type MermaidProps = {
|
|
19
|
+
source: string;
|
|
20
|
+
annotations: DiagramAnnotation[];
|
|
21
|
+
} & SourceNavigation;
|
|
22
|
+
|
|
23
|
+
export function MermaidBlock(props: MermaidProps) {
|
|
24
|
+
const [fullscreen, setFullscreen] = useState(false);
|
|
25
|
+
const frame = useStyles(styles.frame);
|
|
26
|
+
const expand = useStyles(styles.expand);
|
|
27
|
+
const dialog = useStyles(styles.dialog);
|
|
28
|
+
const header = useStyles(styles.header);
|
|
29
|
+
return (
|
|
30
|
+
<div className={frame}>
|
|
31
|
+
<MermaidDiagram {...props} fullscreen={false} />
|
|
32
|
+
<Button
|
|
33
|
+
className={expand}
|
|
34
|
+
size="sm"
|
|
35
|
+
variant="quiet"
|
|
36
|
+
aria-label="Full screen"
|
|
37
|
+
onClick={() => setFullscreen(true)}
|
|
38
|
+
>
|
|
39
|
+
<Maximize size="xs" />
|
|
40
|
+
</Button>
|
|
41
|
+
<Dialog
|
|
42
|
+
isOpen={fullscreen}
|
|
43
|
+
onOpenChange={setFullscreen}
|
|
44
|
+
size="lg"
|
|
45
|
+
aria-label="Diagram"
|
|
46
|
+
className={dialog}
|
|
47
|
+
>
|
|
48
|
+
<div className={header}>
|
|
49
|
+
<Button
|
|
50
|
+
size="sm"
|
|
51
|
+
variant="quiet"
|
|
52
|
+
slot="close"
|
|
53
|
+
aria-label="Exit full screen"
|
|
54
|
+
>
|
|
55
|
+
<Close size="xs" />
|
|
56
|
+
</Button>
|
|
57
|
+
</div>
|
|
58
|
+
<MermaidDiagram
|
|
59
|
+
{...props}
|
|
60
|
+
fullscreen
|
|
61
|
+
onSelectAnnotation={(annotation) => {
|
|
62
|
+
// The Diff panel opens behind the dialog, so leave full screen.
|
|
63
|
+
setFullscreen(false);
|
|
64
|
+
props.onSelectAnnotation(annotation);
|
|
65
|
+
}}
|
|
66
|
+
/>
|
|
67
|
+
</Dialog>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function MermaidDiagram(props: MermaidProps & { fullscreen: boolean }) {
|
|
15
73
|
const container = useRef<HTMLDivElement>(null);
|
|
16
|
-
const shell = useStyles(
|
|
74
|
+
const shell = useStyles(
|
|
75
|
+
styles.shell,
|
|
76
|
+
props.fullscreen ? styles.fullscreenShell : styles.inlineShell,
|
|
77
|
+
);
|
|
17
78
|
const svg = useMemo(
|
|
18
79
|
() =>
|
|
19
80
|
mermaidSvg({
|
|
@@ -91,9 +152,37 @@ export function MermaidBlock(
|
|
|
91
152
|
}
|
|
92
153
|
|
|
93
154
|
const styles = {
|
|
94
|
-
|
|
155
|
+
frame: style({
|
|
156
|
+
position: "relative",
|
|
95
157
|
width: "100%",
|
|
158
|
+
minWidth: 0,
|
|
159
|
+
}),
|
|
160
|
+
expand: style({
|
|
161
|
+
position: "absolute",
|
|
162
|
+
top: 0,
|
|
163
|
+
insetInlineEnd: 0,
|
|
164
|
+
}),
|
|
165
|
+
dialog: style({
|
|
166
|
+
width: "calc(100vw - 32px)",
|
|
167
|
+
height: "calc(100dvh - 32px)",
|
|
168
|
+
padding: spacing.value(6),
|
|
169
|
+
"& > [role='dialog']": { flex: 1, minHeight: 0, gap: spacing.value(2) },
|
|
170
|
+
}),
|
|
171
|
+
header: style({
|
|
172
|
+
display: "flex",
|
|
173
|
+
justifyContent: "flex-end",
|
|
174
|
+
}),
|
|
175
|
+
inlineShell: style({
|
|
96
176
|
maxHeight: "60vh",
|
|
177
|
+
"& svg": { maxHeight: "60vh" },
|
|
178
|
+
}),
|
|
179
|
+
fullscreenShell: style({
|
|
180
|
+
flex: 1,
|
|
181
|
+
"& svg": { height: "100%" },
|
|
182
|
+
}),
|
|
183
|
+
shell: style({
|
|
184
|
+
width: "100%",
|
|
185
|
+
minHeight: 0,
|
|
97
186
|
overflow: "auto",
|
|
98
187
|
minWidth: 0,
|
|
99
188
|
border: 0,
|
|
@@ -120,7 +209,6 @@ const styles = {
|
|
|
120
209
|
display: "block",
|
|
121
210
|
width: "100%",
|
|
122
211
|
height: "auto",
|
|
123
|
-
maxHeight: "60vh",
|
|
124
212
|
},
|
|
125
213
|
}),
|
|
126
214
|
};
|