@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.
Files changed (53) hide show
  1. package/README.md +69 -61
  2. package/index.html +18 -0
  3. package/package.json +7 -5
  4. package/src/annotations.ts +36 -29
  5. package/src/changes.ts +229 -0
  6. package/src/cli.test.ts +50 -0
  7. package/src/cli.ts +41 -5
  8. package/src/errors.ts +10 -0
  9. package/src/gist/GistApp.tsx +39 -20
  10. package/src/gist/GistStatus.tsx +2 -51
  11. package/src/gist/fetchGist.ts +23 -0
  12. package/src/git.test.ts +402 -0
  13. package/src/git.ts +498 -0
  14. package/src/github/GitHubApp.tsx +39 -22
  15. package/src/github/fetchRepo.test.ts +59 -1
  16. package/src/github/fetchRepo.ts +99 -43
  17. package/src/github/patch.test.ts +18 -9
  18. package/src/github/patch.ts +30 -19
  19. package/src/host/HostHome.tsx +86 -0
  20. package/src/host/files.ts +10 -0
  21. package/src/host/home.md +156 -0
  22. package/src/host/home.test.ts +83 -0
  23. package/src/index.ts +1 -0
  24. package/src/packageVersion.ts +7 -0
  25. package/src/parseFence.ts +6 -6
  26. package/src/parseViewer.test.ts +87 -0
  27. package/src/parseViewer.ts +103 -77
  28. package/src/serve.test.ts +306 -0
  29. package/src/serve.ts +48 -5
  30. package/src/share.ts +18 -0
  31. package/src/update.test.ts +19 -0
  32. package/src/update.ts +285 -0
  33. package/src/viewer/CallStackDiff.tsx +15 -3
  34. package/src/viewer/ChangesCard.tsx +267 -0
  35. package/src/viewer/CloseServerButton.tsx +16 -0
  36. package/src/viewer/ComarkView.tsx +83 -15
  37. package/src/viewer/Fence.tsx +9 -1
  38. package/src/viewer/FileExcerpt.tsx +25 -3
  39. package/src/viewer/HtmlPlaceholder.tsx +6 -1
  40. package/src/viewer/MermaidBlock.tsx +99 -11
  41. package/src/viewer/RangePicker.tsx +526 -0
  42. package/src/viewer/SourceDiffPanel.tsx +277 -121
  43. package/src/viewer/TableOfContents.tsx +313 -39
  44. package/src/viewer/ViewerApp.tsx +120 -68
  45. package/src/viewer/changesSource.ts +91 -0
  46. package/src/viewer/sourceHeader.test.ts +2 -87
  47. package/src/viewer/sourceHeader.ts +4 -63
  48. package/src/viewer/sourceNavigation.ts +33 -42
  49. package/src/viewer/viewerMode.ts +1 -1
  50. package/vite.config.ts +2 -1
  51. package/vite.gist.config.ts +57 -0
  52. package/src/viewer/Check.tsx +0 -24
  53. 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
- monospace,
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 <InlineChip key={key}>{children}</InlineChip>;
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
- function InlineChip(props: { children?: ReactNode }) {
198
- const className = useStyles(inlineCodeClass);
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
- backgroundColor: colors.gray[4],
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
+ });
@@ -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 === "source-diff") return undefined;
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 (mode === "gist") return;
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 copy = "HTML from this gist is not rendered.";
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 { backgroundColor, colors, fontFamily } from "maui";
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
- export function MermaidBlock(
10
- props: {
11
- source: string;
12
- annotations: DiagramAnnotation[];
13
- } & SourceNavigation,
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(styles.shell);
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
- shell: style({
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
  };