@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/src/update.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { DiffmapUpdateError } from "./errors.js";
|
|
4
|
+
|
|
5
|
+
// The unscoped `diffmap` package on npm is unrelated; always use the scoped name.
|
|
6
|
+
export const packageName = "@tanishqkancharla/diffmap";
|
|
7
|
+
const latestSpec = `${packageName}@latest`;
|
|
8
|
+
const registryUrl =
|
|
9
|
+
"https://registry.npmjs.org/@tanishqkancharla%2Fdiffmap/latest";
|
|
10
|
+
|
|
11
|
+
type InstallMethod = "npm" | "pnpm" | "bun" | "npx" | "unknown";
|
|
12
|
+
|
|
13
|
+
type Command = { command: string; args: string[] };
|
|
14
|
+
|
|
15
|
+
type UpdateResult =
|
|
16
|
+
| { status: "up-to-date"; current: string; latest: string; message: string }
|
|
17
|
+
| {
|
|
18
|
+
status: "updated";
|
|
19
|
+
previous: string;
|
|
20
|
+
current: string;
|
|
21
|
+
command: string;
|
|
22
|
+
message: string;
|
|
23
|
+
}
|
|
24
|
+
| {
|
|
25
|
+
status: "manual";
|
|
26
|
+
current: string;
|
|
27
|
+
latest: string;
|
|
28
|
+
command: string;
|
|
29
|
+
message: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type UpdateDeps = {
|
|
33
|
+
currentVersion: string;
|
|
34
|
+
fetchLatestVersion: () => Promise<string | Error>;
|
|
35
|
+
detectInstallMethod: () => Promise<InstallMethod>;
|
|
36
|
+
runCommand: (command: Command) => Promise<void | Error>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Routes `diffmap --update [...]` to the `update` command so incur's built-in updater never runs. */
|
|
40
|
+
export function resolveUpdateArgv(argv: string[]) {
|
|
41
|
+
const separator = argv.indexOf("--");
|
|
42
|
+
const flags = separator === -1 ? argv : argv.slice(0, separator);
|
|
43
|
+
if (!flags.includes("--update")) return argv;
|
|
44
|
+
const rest = argv.filter((token, index) => {
|
|
45
|
+
return token !== "--update" || (separator !== -1 && index > separator);
|
|
46
|
+
});
|
|
47
|
+
return ["update", ...rest];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type ParsedVersion = {
|
|
51
|
+
core: [number, number, number];
|
|
52
|
+
prerelease: string[];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function parseVersion(version: string): ParsedVersion | undefined {
|
|
56
|
+
const match = version
|
|
57
|
+
.trim()
|
|
58
|
+
.replace(/^v/, "")
|
|
59
|
+
.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
|
|
60
|
+
if (!match) return undefined;
|
|
61
|
+
return {
|
|
62
|
+
core: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
63
|
+
prerelease: match[4] === undefined ? [] : match[4].split("."),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Semver precedence: negative when `a < b`, positive when `a > b`, 0 when equal. */
|
|
68
|
+
export function compareVersions(a: string, b: string) {
|
|
69
|
+
const left = parseVersion(a);
|
|
70
|
+
const right = parseVersion(b);
|
|
71
|
+
if (!left || !right) {
|
|
72
|
+
return new DiffmapUpdateError({
|
|
73
|
+
reason: `cannot compare versions "${a}" and "${b}"`,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
for (let index = 0; index < 3; index++) {
|
|
77
|
+
const difference = left.core[index]! - right.core[index]!;
|
|
78
|
+
if (difference !== 0) return Math.sign(difference);
|
|
79
|
+
}
|
|
80
|
+
if (left.prerelease.length === 0 || right.prerelease.length === 0) {
|
|
81
|
+
return Math.sign(right.prerelease.length - left.prerelease.length);
|
|
82
|
+
}
|
|
83
|
+
const length = Math.max(left.prerelease.length, right.prerelease.length);
|
|
84
|
+
for (let index = 0; index < length; index++) {
|
|
85
|
+
const leftPart = left.prerelease[index];
|
|
86
|
+
const rightPart = right.prerelease[index];
|
|
87
|
+
if (leftPart === undefined) return -1;
|
|
88
|
+
if (rightPart === undefined) return 1;
|
|
89
|
+
if (leftPart === rightPart) continue;
|
|
90
|
+
const leftNumeric = /^\d+$/.test(leftPart);
|
|
91
|
+
const rightNumeric = /^\d+$/.test(rightPart);
|
|
92
|
+
if (leftNumeric && rightNumeric) {
|
|
93
|
+
return Math.sign(Number(leftPart) - Number(rightPart));
|
|
94
|
+
}
|
|
95
|
+
if (leftNumeric) return -1;
|
|
96
|
+
if (rightNumeric) return 1;
|
|
97
|
+
return leftPart < rightPart ? -1 : 1;
|
|
98
|
+
}
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function normalizePath(value: string) {
|
|
103
|
+
return value.replaceAll("\\", "/").replace(/\/+$/, "");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Infers the install method from the real path of the running entry script. */
|
|
107
|
+
function detectInstallMethodFromPath(input: {
|
|
108
|
+
entryPath: string;
|
|
109
|
+
env: Record<string, string | undefined>;
|
|
110
|
+
npmGlobalRoot?: string | undefined;
|
|
111
|
+
}): InstallMethod {
|
|
112
|
+
const entry = normalizePath(input.entryPath);
|
|
113
|
+
if (input.env.npm_command === "exec" || entry.includes("/_npx/")) {
|
|
114
|
+
return "npx";
|
|
115
|
+
}
|
|
116
|
+
if (entry.includes("/.bun/install/global/")) return "bun";
|
|
117
|
+
const pnpmHome = input.env.PNPM_HOME;
|
|
118
|
+
if (
|
|
119
|
+
entry.includes("/pnpm/global/") ||
|
|
120
|
+
(pnpmHome !== undefined &&
|
|
121
|
+
pnpmHome !== "" &&
|
|
122
|
+
entry.startsWith(`${normalizePath(pnpmHome)}/global/`))
|
|
123
|
+
) {
|
|
124
|
+
return "pnpm";
|
|
125
|
+
}
|
|
126
|
+
if (input.npmGlobalRoot !== undefined && input.npmGlobalRoot !== "") {
|
|
127
|
+
const root = normalizePath(input.npmGlobalRoot);
|
|
128
|
+
if (entry.startsWith(`${root}/${packageName}/`)) return "npm";
|
|
129
|
+
}
|
|
130
|
+
return "unknown";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function globalInstallCommand(method: "npm" | "pnpm" | "bun"): Command {
|
|
134
|
+
if (method === "pnpm") {
|
|
135
|
+
return { command: "pnpm", args: ["add", "--global", latestSpec] };
|
|
136
|
+
}
|
|
137
|
+
if (method === "bun") {
|
|
138
|
+
return { command: "bun", args: ["add", "--global", latestSpec] };
|
|
139
|
+
}
|
|
140
|
+
return { command: "npm", args: ["install", "--global", latestSpec] };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function formatCommand(command: Command) {
|
|
144
|
+
return [command.command, ...command.args].join(" ");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function runUpdate(deps: UpdateDeps) {
|
|
148
|
+
const current = deps.currentVersion;
|
|
149
|
+
const latest = await deps.fetchLatestVersion();
|
|
150
|
+
if (latest instanceof Error) return latest;
|
|
151
|
+
|
|
152
|
+
const comparison = compareVersions(current, latest);
|
|
153
|
+
if (comparison instanceof Error) return comparison;
|
|
154
|
+
if (comparison >= 0) {
|
|
155
|
+
return {
|
|
156
|
+
message: `diffmap is already up to date (${current})`,
|
|
157
|
+
status: "up-to-date",
|
|
158
|
+
current,
|
|
159
|
+
latest,
|
|
160
|
+
} satisfies UpdateResult;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const method = await deps.detectInstallMethod();
|
|
164
|
+
if (method === "npx") {
|
|
165
|
+
const command = `npx ${latestSpec}`;
|
|
166
|
+
return {
|
|
167
|
+
message: `diffmap ${latest} is available (running ${current} through npx). Run \`${command}\` to use it, or install it globally with \`${formatCommand(globalInstallCommand("npm"))}\`.`,
|
|
168
|
+
status: "manual",
|
|
169
|
+
current,
|
|
170
|
+
latest,
|
|
171
|
+
command,
|
|
172
|
+
} satisfies UpdateResult;
|
|
173
|
+
}
|
|
174
|
+
if (method === "unknown") {
|
|
175
|
+
const command = formatCommand(globalInstallCommand("npm"));
|
|
176
|
+
return {
|
|
177
|
+
message: `diffmap ${latest} is available (running ${current}), but diffmap could not tell how it was installed. Update it with \`${command}\` or your package manager's equivalent.`,
|
|
178
|
+
status: "manual",
|
|
179
|
+
current,
|
|
180
|
+
latest,
|
|
181
|
+
command,
|
|
182
|
+
} satisfies UpdateResult;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const install = globalInstallCommand(method);
|
|
186
|
+
const installed = await deps.runCommand(install);
|
|
187
|
+
if (installed instanceof Error) return installed;
|
|
188
|
+
return {
|
|
189
|
+
message: `Updated diffmap from ${current} to ${latest}`,
|
|
190
|
+
status: "updated",
|
|
191
|
+
previous: current,
|
|
192
|
+
current: latest,
|
|
193
|
+
command: formatCommand(install),
|
|
194
|
+
} satisfies UpdateResult;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function fetchLatestVersion() {
|
|
198
|
+
const response = await fetch(registryUrl, {
|
|
199
|
+
headers: { accept: "application/json" },
|
|
200
|
+
signal: AbortSignal.timeout(10_000),
|
|
201
|
+
}).catch(
|
|
202
|
+
(cause) =>
|
|
203
|
+
new DiffmapUpdateError({
|
|
204
|
+
reason: "could not reach the npm registry",
|
|
205
|
+
cause,
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
if (response instanceof Error) return response;
|
|
209
|
+
if (!response.ok) {
|
|
210
|
+
return new DiffmapUpdateError({
|
|
211
|
+
reason: `npm registry responded ${response.status} for ${packageName}`,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
const body = (await response.json().catch(() => undefined)) as
|
|
215
|
+
| { version?: unknown }
|
|
216
|
+
| undefined;
|
|
217
|
+
if (typeof body?.version !== "string") {
|
|
218
|
+
return new DiffmapUpdateError({
|
|
219
|
+
reason: `npm registry returned no version for ${packageName}`,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return body.version;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function execCommand(command: Command) {
|
|
226
|
+
return new Promise<{ stdout: string; stderr: string } | Error>((resolve) => {
|
|
227
|
+
execFile(
|
|
228
|
+
command.command,
|
|
229
|
+
command.args,
|
|
230
|
+
{ shell: process.platform === "win32", maxBuffer: 10_000_000 },
|
|
231
|
+
(cause, stdout, stderr) => {
|
|
232
|
+
if (cause) {
|
|
233
|
+
const detail = stderr.trim() || stdout.trim() || cause.message;
|
|
234
|
+
resolve(
|
|
235
|
+
new DiffmapUpdateError({
|
|
236
|
+
reason: `\`${formatCommand(command)}\` failed: ${detail}`,
|
|
237
|
+
cause,
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
resolve({ stdout, stderr });
|
|
243
|
+
},
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function readNpmGlobalRoot() {
|
|
249
|
+
const result = await execCommand({
|
|
250
|
+
command: "npm",
|
|
251
|
+
args: ["root", "--global"],
|
|
252
|
+
});
|
|
253
|
+
if (result instanceof Error) return undefined;
|
|
254
|
+
const root = result.stdout.trim();
|
|
255
|
+
if (root === "") return undefined;
|
|
256
|
+
try {
|
|
257
|
+
return fs.realpathSync(root);
|
|
258
|
+
} catch {
|
|
259
|
+
return root;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export async function detectInstallMethod() {
|
|
264
|
+
const entry = process.argv[1];
|
|
265
|
+
if (entry === undefined) return "unknown" satisfies InstallMethod;
|
|
266
|
+
let entryPath = entry;
|
|
267
|
+
try {
|
|
268
|
+
entryPath = fs.realpathSync(entry);
|
|
269
|
+
} catch {}
|
|
270
|
+
const withoutNpm = detectInstallMethodFromPath({
|
|
271
|
+
entryPath,
|
|
272
|
+
env: process.env,
|
|
273
|
+
});
|
|
274
|
+
if (withoutNpm !== "unknown") return withoutNpm;
|
|
275
|
+
return detectInstallMethodFromPath({
|
|
276
|
+
entryPath,
|
|
277
|
+
env: process.env,
|
|
278
|
+
npmGlobalRoot: await readNpmGlobalRoot(),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export async function runCommand(command: Command) {
|
|
283
|
+
const result = await execCommand(command);
|
|
284
|
+
if (result instanceof Error) return result;
|
|
285
|
+
}
|
|
@@ -50,7 +50,12 @@ export function CallStackDiff(
|
|
|
50
50
|
);
|
|
51
51
|
if (line.references.length === 0) {
|
|
52
52
|
return (
|
|
53
|
-
<div
|
|
53
|
+
<div
|
|
54
|
+
key={index}
|
|
55
|
+
className={row}
|
|
56
|
+
data-change={sign}
|
|
57
|
+
title={description}
|
|
58
|
+
>
|
|
54
59
|
{content}
|
|
55
60
|
</div>
|
|
56
61
|
);
|
|
@@ -63,7 +68,7 @@ export function CallStackDiff(
|
|
|
63
68
|
data-change={sign}
|
|
64
69
|
aria-pressed={props.selectedAnnotation === line}
|
|
65
70
|
aria-controls="source-diff-panel"
|
|
66
|
-
title=
|
|
71
|
+
title={description}
|
|
67
72
|
aria-label={description}
|
|
68
73
|
onClick={() => props.onSelectAnnotation(line)}
|
|
69
74
|
>
|
|
@@ -81,20 +86,24 @@ const styles = {
|
|
|
81
86
|
spacing.padding({ y: 2 }),
|
|
82
87
|
{
|
|
83
88
|
backgroundColor: backgroundColor.app,
|
|
84
|
-
overflowX: "
|
|
89
|
+
overflowX: "hidden",
|
|
90
|
+
maxWidth: "100%",
|
|
85
91
|
},
|
|
86
92
|
),
|
|
87
93
|
row: style(
|
|
88
94
|
spacing.padding({ x: 3 }),
|
|
89
95
|
{
|
|
90
96
|
display: "flex",
|
|
97
|
+
alignItems: "center",
|
|
91
98
|
gap: "12px",
|
|
99
|
+
boxSizing: "border-box",
|
|
92
100
|
width: "100%",
|
|
93
|
-
|
|
101
|
+
maxWidth: "100%",
|
|
102
|
+
minWidth: 0,
|
|
103
|
+
overflow: "hidden",
|
|
94
104
|
textAlign: "left",
|
|
95
105
|
font: "inherit",
|
|
96
106
|
lineHeight: "1.6",
|
|
97
|
-
whiteSpace: "pre",
|
|
98
107
|
color: "inherit",
|
|
99
108
|
background: "transparent",
|
|
100
109
|
border: 0,
|
|
@@ -109,9 +118,19 @@ const styles = {
|
|
|
109
118
|
"& .stack-content": {
|
|
110
119
|
display: "flex",
|
|
111
120
|
alignItems: "stretch",
|
|
121
|
+
flex: "1 1 auto",
|
|
122
|
+
minWidth: 0,
|
|
112
123
|
minHeight: "28px",
|
|
124
|
+
overflow: "hidden",
|
|
125
|
+
},
|
|
126
|
+
"& .stack-label": {
|
|
127
|
+
flex: "1 1 auto",
|
|
128
|
+
minWidth: 0,
|
|
129
|
+
paddingBlock: "3px",
|
|
130
|
+
overflow: "hidden",
|
|
131
|
+
textOverflow: "ellipsis",
|
|
132
|
+
whiteSpace: "nowrap",
|
|
113
133
|
},
|
|
114
|
-
"& .stack-label": { paddingBlock: "3px" },
|
|
115
134
|
"& .stack-comment": { color: colors.gray[11], fontWeight: 400 },
|
|
116
135
|
"& .tree-prefix": { display: "inline-flex", alignSelf: "stretch" },
|
|
117
136
|
"& [data-branch]": {
|
|
@@ -147,6 +166,7 @@ const styles = {
|
|
|
147
166
|
"& > .diff-sign": { alignSelf: "center" },
|
|
148
167
|
"& > .source-indicator": {
|
|
149
168
|
alignSelf: "center",
|
|
169
|
+
flexShrink: 0,
|
|
150
170
|
marginLeft: "auto",
|
|
151
171
|
color: colors.blue[11],
|
|
152
172
|
},
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Button, Close, colors } from "maui";
|
|
2
|
+
import { style, useStyles } from "purse-styles";
|
|
3
|
+
|
|
4
|
+
const closeIcon = style({
|
|
5
|
+
color: colors.red[9],
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export function CloseServerButton(props: { onClick?: () => void }) {
|
|
9
|
+
const iconClass = useStyles(closeIcon);
|
|
10
|
+
return (
|
|
11
|
+
<Button variant="default" onClick={props.onClick}>
|
|
12
|
+
<Close className={iconClass} size="sm" />
|
|
13
|
+
Close server
|
|
14
|
+
</Button>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -4,6 +4,9 @@ import { useTheme } from "maui";
|
|
|
4
4
|
import { useStyles } from "purse-styles";
|
|
5
5
|
import { pierreFileOptions, pierreShell } from "./pierre.ts";
|
|
6
6
|
import { useViewerMode } from "./viewerMode.ts";
|
|
7
|
+
import { useGitHubPin } from "../github/pin.ts";
|
|
8
|
+
import { useHostFiles } from "../host/files.ts";
|
|
9
|
+
import { fetchPinnedBlob } from "../github/fetchRepo.ts";
|
|
7
10
|
|
|
8
11
|
type FileExcerptPayload = {
|
|
9
12
|
path: string;
|
|
@@ -40,8 +43,36 @@ export function FileExcerpt(props: {
|
|
|
40
43
|
function useFileExcerpt(input: { path: string; start: number; end: number }) {
|
|
41
44
|
const [excerpt, setExcerpt] = useState<FileExcerptPayload>();
|
|
42
45
|
const mode = useViewerMode();
|
|
46
|
+
const pin = useGitHubPin();
|
|
47
|
+
const hostFiles = useHostFiles();
|
|
48
|
+
const hostExcerpt = hostFileExcerpt(hostFiles, input);
|
|
43
49
|
useEffect(() => {
|
|
44
|
-
if (
|
|
50
|
+
if (hostFiles !== undefined) return;
|
|
51
|
+
if (mode === "github") {
|
|
52
|
+
if (pin === undefined) return;
|
|
53
|
+
// oxlint-disable-next-line typescript/no-floating-promises -- React effects cannot await; this request owns the excerpt update.
|
|
54
|
+
void fetchPinnedBlob(pin, input.path)
|
|
55
|
+
.then((contents) => {
|
|
56
|
+
if (contents instanceof Error) return undefined;
|
|
57
|
+
const lines = contents.split(/\r?\n/);
|
|
58
|
+
const start = input.start;
|
|
59
|
+
const end = Math.min(input.end, lines.length);
|
|
60
|
+
return {
|
|
61
|
+
path: input.path,
|
|
62
|
+
start,
|
|
63
|
+
end,
|
|
64
|
+
contents: lines.slice(start - 1, end).join("\n"),
|
|
65
|
+
} satisfies FileExcerptPayload;
|
|
66
|
+
})
|
|
67
|
+
.then((value) => {
|
|
68
|
+
if (value !== undefined) setExcerpt(value);
|
|
69
|
+
})
|
|
70
|
+
.catch((cause) => {
|
|
71
|
+
console.warn("diffmap file excerpt failed", cause);
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (mode !== "local") return;
|
|
45
76
|
const params = new URLSearchParams({
|
|
46
77
|
path: input.path,
|
|
47
78
|
start: String(input.start),
|
|
@@ -60,6 +91,24 @@ function useFileExcerpt(input: { path: string; start: number; end: number }) {
|
|
|
60
91
|
.catch((cause) => {
|
|
61
92
|
console.warn("diffmap file excerpt failed", cause);
|
|
62
93
|
});
|
|
63
|
-
}, [input.path, input.start, input.end, mode]);
|
|
64
|
-
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
|
+
};
|
|
65
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">
|