@actagent/diffs 2026.6.2
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 +236 -0
- package/actagent.plugin.json +218 -0
- package/api.ts +11 -0
- package/assets/viewer-runtime.js +259 -0
- package/index.ts +12 -0
- package/npm-shrinkwrap.json +723 -0
- package/package.json +51 -0
- package/runtime-api.ts +2 -0
- package/skills/diffs/SKILL.md +23 -0
- package/src/browser.test.ts +687 -0
- package/src/browser.ts +576 -0
- package/src/config.test.ts +633 -0
- package/src/config.ts +444 -0
- package/src/http.ts +325 -0
- package/src/language-hints.test.ts +240 -0
- package/src/language-hints.ts +159 -0
- package/src/manifest.test.ts +17 -0
- package/src/pierre-themes.ts +60 -0
- package/src/plugin.ts +111 -0
- package/src/prompt-guidance.ts +8 -0
- package/src/render-target.test.ts +133 -0
- package/src/render.test.ts +300 -0
- package/src/render.ts +622 -0
- package/src/shiki-curated-languages.ts +87 -0
- package/src/store.test.ts +491 -0
- package/src/store.ts +399 -0
- package/src/test-helpers.ts +62 -0
- package/src/tool-render-output.test.ts +108 -0
- package/src/tool.test.ts +672 -0
- package/src/tool.ts +552 -0
- package/src/types.ts +130 -0
- package/src/url.ts +61 -0
- package/src/viewer-assets.ts +190 -0
- package/src/viewer-client.test.ts +175 -0
- package/src/viewer-client.ts +361 -0
- package/src/viewer-payload.ts +95 -0
- package/tsconfig.json +16 -0
package/src/render.ts
ADDED
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
// Diffs plugin module implements render behavior.
|
|
2
|
+
import type { FileContents, FileDiffMetadata, SupportedLanguages } from "@pierre/diffs";
|
|
3
|
+
import { parsePatchFiles } from "@pierre/diffs";
|
|
4
|
+
import { preloadFileDiff, preloadMultiFileDiff } from "@pierre/diffs/ssr";
|
|
5
|
+
import { normalizeDiffFontSize, normalizeDiffLineSpacing } from "./config.js";
|
|
6
|
+
import {
|
|
7
|
+
collectDiffPayloadLanguageHints,
|
|
8
|
+
isBaseDiffViewerLanguage,
|
|
9
|
+
normalizeDiffViewerPayloadLanguages,
|
|
10
|
+
normalizeSupportedLanguageHint,
|
|
11
|
+
} from "./language-hints.js";
|
|
12
|
+
import { ensurePierreThemesRegistered } from "./pierre-themes.js";
|
|
13
|
+
import type {
|
|
14
|
+
DiffInput,
|
|
15
|
+
DiffRenderOptions,
|
|
16
|
+
DiffRenderTarget,
|
|
17
|
+
DiffViewerOptions,
|
|
18
|
+
DiffViewerPayload,
|
|
19
|
+
RenderedDiffDocument,
|
|
20
|
+
} from "./types.js";
|
|
21
|
+
|
|
22
|
+
const DEFAULT_FILE_NAME = "diff.txt";
|
|
23
|
+
const MAX_PATCH_FILE_COUNT = 128;
|
|
24
|
+
const MAX_PATCH_TOTAL_LINES = 120_000;
|
|
25
|
+
const VIEWER_LOADER_DOCUMENT_PATH = "../../assets/viewer.js";
|
|
26
|
+
const LANGUAGE_PACK_VIEWER_LOADER_DOCUMENT_PATH = "../../../diffs-language-pack/assets/viewer.js";
|
|
27
|
+
|
|
28
|
+
function escapeCssString(value: string): string {
|
|
29
|
+
return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function escapeHtml(value: string): string {
|
|
33
|
+
return value
|
|
34
|
+
.replaceAll("&", "&")
|
|
35
|
+
.replaceAll("<", "<")
|
|
36
|
+
.replaceAll(">", ">")
|
|
37
|
+
.replaceAll('"', """)
|
|
38
|
+
.replaceAll("'", "'");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function escapeJsonScript(value: unknown): string {
|
|
42
|
+
return JSON.stringify(value).replaceAll("<", "\\u003c");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildDiffTitle(input: DiffInput): string {
|
|
46
|
+
if (input.title?.trim()) {
|
|
47
|
+
return input.title.trim();
|
|
48
|
+
}
|
|
49
|
+
if (input.kind === "before_after") {
|
|
50
|
+
return input.path?.trim() || "Text diff";
|
|
51
|
+
}
|
|
52
|
+
return "Patch diff";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveBeforeAfterFileName(params: {
|
|
56
|
+
input: Extract<DiffInput, { kind: "before_after" }>;
|
|
57
|
+
lang?: SupportedLanguages;
|
|
58
|
+
}): string {
|
|
59
|
+
const { input, lang } = params;
|
|
60
|
+
if (input.path?.trim()) {
|
|
61
|
+
return input.path.trim();
|
|
62
|
+
}
|
|
63
|
+
if (lang && lang !== "text") {
|
|
64
|
+
return `diff.${lang.replace(/^\.+/, "")}`;
|
|
65
|
+
}
|
|
66
|
+
return DEFAULT_FILE_NAME;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildDiffOptions(options: DiffRenderOptions): DiffViewerOptions {
|
|
70
|
+
const fontFamily = escapeCssString(options.presentation.fontFamily);
|
|
71
|
+
const fontSize = normalizeDiffFontSize(options.presentation.fontSize);
|
|
72
|
+
const lineSpacing = normalizeDiffLineSpacing(options.presentation.lineSpacing);
|
|
73
|
+
const lineHeight = Math.max(20, Math.round(fontSize * lineSpacing));
|
|
74
|
+
return {
|
|
75
|
+
theme: {
|
|
76
|
+
light: "pierre-light",
|
|
77
|
+
dark: "pierre-dark",
|
|
78
|
+
},
|
|
79
|
+
diffStyle: options.presentation.layout,
|
|
80
|
+
diffIndicators: options.presentation.diffIndicators,
|
|
81
|
+
disableLineNumbers: !options.presentation.showLineNumbers,
|
|
82
|
+
expandUnchanged: options.expandUnchanged,
|
|
83
|
+
themeType: options.presentation.theme,
|
|
84
|
+
backgroundEnabled: options.presentation.background,
|
|
85
|
+
overflow: options.presentation.wordWrap ? "wrap" : "scroll",
|
|
86
|
+
unsafeCSS: `
|
|
87
|
+
:host {
|
|
88
|
+
--diffs-font-family: "${fontFamily}", "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
89
|
+
--diffs-header-font-family: "${fontFamily}", "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
90
|
+
--diffs-font-size: ${fontSize}px;
|
|
91
|
+
--diffs-line-height: ${lineHeight}px;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
[data-diffs-header] {
|
|
95
|
+
min-height: 64px;
|
|
96
|
+
padding-inline: 18px 14px;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
[data-header-content] {
|
|
100
|
+
gap: 10px;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
[data-metadata] {
|
|
104
|
+
gap: 10px;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.oc-diff-toolbar {
|
|
108
|
+
display: inline-flex;
|
|
109
|
+
align-items: center;
|
|
110
|
+
gap: 6px;
|
|
111
|
+
margin-inline-start: 6px;
|
|
112
|
+
flex: 0 0 auto;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.oc-diff-toolbar-button {
|
|
116
|
+
display: inline-flex;
|
|
117
|
+
align-items: center;
|
|
118
|
+
justify-content: center;
|
|
119
|
+
width: 24px;
|
|
120
|
+
height: 24px;
|
|
121
|
+
padding: 0;
|
|
122
|
+
margin: 0;
|
|
123
|
+
border: 0;
|
|
124
|
+
border-radius: 0;
|
|
125
|
+
background: transparent;
|
|
126
|
+
color: inherit;
|
|
127
|
+
cursor: pointer;
|
|
128
|
+
opacity: 0.6;
|
|
129
|
+
line-height: 0;
|
|
130
|
+
overflow: visible;
|
|
131
|
+
transition: opacity 120ms ease;
|
|
132
|
+
flex: 0 0 auto;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.oc-diff-toolbar-button:hover {
|
|
136
|
+
opacity: 1;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.oc-diff-toolbar-button[data-active="true"] {
|
|
140
|
+
opacity: 0.92;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.oc-diff-toolbar-button svg {
|
|
144
|
+
display: block;
|
|
145
|
+
width: 16px;
|
|
146
|
+
height: 16px;
|
|
147
|
+
min-width: 16px;
|
|
148
|
+
min-height: 16px;
|
|
149
|
+
overflow: visible;
|
|
150
|
+
flex: 0 0 auto;
|
|
151
|
+
color: inherit;
|
|
152
|
+
fill: currentColor;
|
|
153
|
+
pointer-events: none;
|
|
154
|
+
}
|
|
155
|
+
`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function buildImageRenderOptions(options: DiffRenderOptions): DiffRenderOptions {
|
|
160
|
+
return {
|
|
161
|
+
...options,
|
|
162
|
+
presentation: {
|
|
163
|
+
...options.presentation,
|
|
164
|
+
fontSize: Math.max(16, normalizeDiffFontSize(options.presentation.fontSize)),
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function shouldRenderViewer(target: DiffRenderTarget): boolean {
|
|
170
|
+
return target === "viewer" || target === "both";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function shouldRenderImage(target: DiffRenderTarget): boolean {
|
|
174
|
+
return target === "image" || target === "both";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function buildRenderVariants(params: { options: DiffRenderOptions; target: DiffRenderTarget }): {
|
|
178
|
+
viewerOptions?: DiffViewerOptions;
|
|
179
|
+
imageOptions?: DiffViewerOptions;
|
|
180
|
+
} {
|
|
181
|
+
return {
|
|
182
|
+
...(shouldRenderViewer(params.target)
|
|
183
|
+
? { viewerOptions: buildDiffOptions(params.options) }
|
|
184
|
+
: {}),
|
|
185
|
+
...(shouldRenderImage(params.target)
|
|
186
|
+
? { imageOptions: buildDiffOptions(buildImageRenderOptions(params.options)) }
|
|
187
|
+
: {}),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function renderDiffCard(payload: DiffViewerPayload): string {
|
|
192
|
+
return `<section class="oc-diff-card">
|
|
193
|
+
<diffs-container class="oc-diff-host" data-actagent-diff-host>
|
|
194
|
+
<template shadowrootmode="open">${payload.prerenderedHTML}</template>
|
|
195
|
+
</diffs-container>
|
|
196
|
+
<script type="application/json" data-actagent-diff-payload>${escapeJsonScript(payload)}</script>
|
|
197
|
+
</section>`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function buildHtmlDocument(params: {
|
|
201
|
+
title: string;
|
|
202
|
+
bodyHtml: string;
|
|
203
|
+
theme: DiffRenderOptions["presentation"]["theme"];
|
|
204
|
+
imageMaxWidth: number;
|
|
205
|
+
runtimeMode: "viewer" | "image";
|
|
206
|
+
viewerRuntime: "base" | "language-pack";
|
|
207
|
+
}): string {
|
|
208
|
+
const viewerLoaderPath =
|
|
209
|
+
params.viewerRuntime === "language-pack"
|
|
210
|
+
? LANGUAGE_PACK_VIEWER_LOADER_DOCUMENT_PATH
|
|
211
|
+
: VIEWER_LOADER_DOCUMENT_PATH;
|
|
212
|
+
return `<!doctype html>
|
|
213
|
+
<html lang="en">
|
|
214
|
+
<head>
|
|
215
|
+
<meta charset="utf-8" />
|
|
216
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
217
|
+
<meta name="color-scheme" content="dark light" />
|
|
218
|
+
<title>${escapeHtml(params.title)}</title>
|
|
219
|
+
<style>
|
|
220
|
+
* {
|
|
221
|
+
box-sizing: border-box;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
html,
|
|
225
|
+
body {
|
|
226
|
+
min-height: 100%;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
html {
|
|
230
|
+
background: #05070b;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
body {
|
|
234
|
+
margin: 0;
|
|
235
|
+
min-height: 100vh;
|
|
236
|
+
padding: 22px;
|
|
237
|
+
font-family:
|
|
238
|
+
"Fira Code",
|
|
239
|
+
"SF Mono",
|
|
240
|
+
Monaco,
|
|
241
|
+
Consolas,
|
|
242
|
+
monospace;
|
|
243
|
+
background: #05070b;
|
|
244
|
+
color: #f8fafc;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
body[data-theme="light"] {
|
|
248
|
+
background: #f3f5f8;
|
|
249
|
+
color: #0f172a;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.oc-frame {
|
|
253
|
+
max-width: 1560px;
|
|
254
|
+
margin: 0 auto;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.oc-frame[data-render-mode="image"] {
|
|
258
|
+
max-width: ${Math.max(640, Math.round(params.imageMaxWidth))}px;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
[data-actagent-diff-root] {
|
|
262
|
+
display: grid;
|
|
263
|
+
gap: 18px;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.oc-diff-card {
|
|
267
|
+
overflow: hidden;
|
|
268
|
+
border-radius: 18px;
|
|
269
|
+
border: 1px solid rgba(148, 163, 184, 0.16);
|
|
270
|
+
background: rgba(15, 23, 42, 0.14);
|
|
271
|
+
box-shadow: 0 18px 48px rgba(2, 6, 23, 0.22);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
body[data-theme="light"] .oc-diff-card {
|
|
275
|
+
border-color: rgba(148, 163, 184, 0.22);
|
|
276
|
+
background: rgba(255, 255, 255, 0.92);
|
|
277
|
+
box-shadow: 0 14px 32px rgba(15, 23, 42, 0.08);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.oc-diff-host {
|
|
281
|
+
display: block;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.oc-frame[data-render-mode="image"] .oc-diff-card {
|
|
285
|
+
min-height: 240px;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
@media (max-width: 720px) {
|
|
289
|
+
body {
|
|
290
|
+
padding: 12px;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
[data-actagent-diff-root] {
|
|
294
|
+
gap: 12px;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
</style>
|
|
298
|
+
</head>
|
|
299
|
+
<body data-theme="${params.theme}">
|
|
300
|
+
<main class="oc-frame" data-render-mode="${params.runtimeMode}">
|
|
301
|
+
<div data-actagent-diff-root>
|
|
302
|
+
${params.bodyHtml}
|
|
303
|
+
</div>
|
|
304
|
+
</main>
|
|
305
|
+
<script type="module" src="${viewerLoaderPath}"></script>
|
|
306
|
+
</body>
|
|
307
|
+
</html>`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
type RenderedSection = {
|
|
311
|
+
viewer?: string;
|
|
312
|
+
image?: string;
|
|
313
|
+
usesLanguagePack?: boolean;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
function payloadUsesLanguagePack(payload: DiffViewerPayload | undefined): boolean {
|
|
317
|
+
return payload?.langs.some((lang) => !isBaseDiffViewerLanguage(lang)) ?? false;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function buildRenderedSection(params: {
|
|
321
|
+
viewerPayload?: DiffViewerPayload;
|
|
322
|
+
imagePayload?: DiffViewerPayload;
|
|
323
|
+
}): RenderedSection {
|
|
324
|
+
return {
|
|
325
|
+
...(params.viewerPayload ? { viewer: renderDiffCard(params.viewerPayload) } : {}),
|
|
326
|
+
...(params.imagePayload ? { image: renderDiffCard(params.imagePayload) } : {}),
|
|
327
|
+
usesLanguagePack:
|
|
328
|
+
payloadUsesLanguagePack(params.viewerPayload) || payloadUsesLanguagePack(params.imagePayload),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function buildRenderedBodies(sections: ReadonlyArray<RenderedSection>): {
|
|
333
|
+
viewerBodyHtml?: string;
|
|
334
|
+
imageBodyHtml?: string;
|
|
335
|
+
} {
|
|
336
|
+
const viewerSections = sections.flatMap((section) => (section.viewer ? [section.viewer] : []));
|
|
337
|
+
const imageSections = sections.flatMap((section) => (section.image ? [section.image] : []));
|
|
338
|
+
return {
|
|
339
|
+
...(viewerSections.length > 0 ? { viewerBodyHtml: viewerSections.join("\n") } : {}),
|
|
340
|
+
...(imageSections.length > 0 ? { imageBodyHtml: imageSections.join("\n") } : {}),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function renderBeforeAfterDiff(
|
|
345
|
+
input: Extract<DiffInput, { kind: "before_after" }>,
|
|
346
|
+
options: DiffRenderOptions,
|
|
347
|
+
target: DiffRenderTarget,
|
|
348
|
+
): Promise<{
|
|
349
|
+
viewerBodyHtml?: string;
|
|
350
|
+
imageBodyHtml?: string;
|
|
351
|
+
fileCount: number;
|
|
352
|
+
usesLanguagePack: boolean;
|
|
353
|
+
}> {
|
|
354
|
+
ensurePierreThemesRegistered();
|
|
355
|
+
|
|
356
|
+
const languagePackAvailable = options.languagePackAvailable === true;
|
|
357
|
+
const lang = await normalizeSupportedLanguageHint(input.lang, { languagePackAvailable });
|
|
358
|
+
const fileName = resolveBeforeAfterFileName({ input, lang });
|
|
359
|
+
const oldFile: FileContents = {
|
|
360
|
+
name: fileName,
|
|
361
|
+
contents: input.before,
|
|
362
|
+
...(lang ? { lang } : {}),
|
|
363
|
+
};
|
|
364
|
+
const newFile: FileContents = {
|
|
365
|
+
name: fileName,
|
|
366
|
+
contents: input.after,
|
|
367
|
+
...(lang ? { lang } : {}),
|
|
368
|
+
};
|
|
369
|
+
const { viewerOptions, imageOptions } = buildRenderVariants({ options, target });
|
|
370
|
+
const [viewerResult, imageResult] = await Promise.all([
|
|
371
|
+
viewerOptions
|
|
372
|
+
? preloadMultiFileDiffWithFallback({
|
|
373
|
+
oldFile,
|
|
374
|
+
newFile,
|
|
375
|
+
options: viewerOptions,
|
|
376
|
+
})
|
|
377
|
+
: Promise.resolve(undefined),
|
|
378
|
+
imageOptions
|
|
379
|
+
? preloadMultiFileDiffWithFallback({
|
|
380
|
+
oldFile,
|
|
381
|
+
newFile,
|
|
382
|
+
options: imageOptions,
|
|
383
|
+
})
|
|
384
|
+
: Promise.resolve(undefined),
|
|
385
|
+
]);
|
|
386
|
+
const [viewerPayload, imagePayload] = await Promise.all([
|
|
387
|
+
viewerResult && viewerOptions
|
|
388
|
+
? normalizeDiffViewerPayloadLanguages(
|
|
389
|
+
{
|
|
390
|
+
prerenderedHTML: viewerResult.prerenderedHTML,
|
|
391
|
+
oldFile: viewerResult.oldFile,
|
|
392
|
+
newFile: viewerResult.newFile,
|
|
393
|
+
options: viewerOptions,
|
|
394
|
+
langs: collectDiffPayloadLanguageHints({
|
|
395
|
+
oldFile: viewerResult.oldFile,
|
|
396
|
+
newFile: viewerResult.newFile,
|
|
397
|
+
}),
|
|
398
|
+
},
|
|
399
|
+
{ languagePackAvailable },
|
|
400
|
+
)
|
|
401
|
+
: Promise.resolve(undefined),
|
|
402
|
+
imageResult && imageOptions
|
|
403
|
+
? normalizeDiffViewerPayloadLanguages(
|
|
404
|
+
{
|
|
405
|
+
prerenderedHTML: imageResult.prerenderedHTML,
|
|
406
|
+
oldFile: imageResult.oldFile,
|
|
407
|
+
newFile: imageResult.newFile,
|
|
408
|
+
options: imageOptions,
|
|
409
|
+
langs: collectDiffPayloadLanguageHints({
|
|
410
|
+
oldFile: imageResult.oldFile,
|
|
411
|
+
newFile: imageResult.newFile,
|
|
412
|
+
}),
|
|
413
|
+
},
|
|
414
|
+
{ languagePackAvailable },
|
|
415
|
+
)
|
|
416
|
+
: Promise.resolve(undefined),
|
|
417
|
+
]);
|
|
418
|
+
const section = buildRenderedSection({
|
|
419
|
+
...(viewerPayload ? { viewerPayload } : {}),
|
|
420
|
+
...(imagePayload ? { imagePayload } : {}),
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
...buildRenderedBodies([section]),
|
|
425
|
+
fileCount: 1,
|
|
426
|
+
usesLanguagePack: section.usesLanguagePack === true,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function renderPatchDiff(
|
|
431
|
+
input: Extract<DiffInput, { kind: "patch" }>,
|
|
432
|
+
options: DiffRenderOptions,
|
|
433
|
+
target: DiffRenderTarget,
|
|
434
|
+
): Promise<{
|
|
435
|
+
viewerBodyHtml?: string;
|
|
436
|
+
imageBodyHtml?: string;
|
|
437
|
+
fileCount: number;
|
|
438
|
+
usesLanguagePack: boolean;
|
|
439
|
+
}> {
|
|
440
|
+
ensurePierreThemesRegistered();
|
|
441
|
+
|
|
442
|
+
const languagePackAvailable = options.languagePackAvailable === true;
|
|
443
|
+
const files = await Promise.all(
|
|
444
|
+
parsePatchFiles(input.patch)
|
|
445
|
+
.flatMap((entry) => entry.files ?? [])
|
|
446
|
+
.map((fileDiff) => normalizePatchFileLanguage(fileDiff, { languagePackAvailable })),
|
|
447
|
+
);
|
|
448
|
+
if (files.length === 0) {
|
|
449
|
+
throw new Error("Patch input did not contain any file diffs.");
|
|
450
|
+
}
|
|
451
|
+
if (files.length > MAX_PATCH_FILE_COUNT) {
|
|
452
|
+
throw new Error(`Patch input contains too many files (max ${MAX_PATCH_FILE_COUNT}).`);
|
|
453
|
+
}
|
|
454
|
+
const totalLines = files.reduce((sum, fileDiff) => {
|
|
455
|
+
const splitLines = Number.isFinite(fileDiff.splitLineCount) ? fileDiff.splitLineCount : 0;
|
|
456
|
+
const unifiedLines = Number.isFinite(fileDiff.unifiedLineCount) ? fileDiff.unifiedLineCount : 0;
|
|
457
|
+
return sum + Math.max(splitLines, unifiedLines, 0);
|
|
458
|
+
}, 0);
|
|
459
|
+
if (totalLines > MAX_PATCH_TOTAL_LINES) {
|
|
460
|
+
throw new Error(`Patch input is too large to render (max ${MAX_PATCH_TOTAL_LINES} lines).`);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const { viewerOptions, imageOptions } = buildRenderVariants({ options, target });
|
|
464
|
+
const sections = await Promise.all(
|
|
465
|
+
files.map(async (fileDiff) => {
|
|
466
|
+
const [viewerResult, imageResult] = await Promise.all([
|
|
467
|
+
viewerOptions
|
|
468
|
+
? preloadFileDiffWithFallback({
|
|
469
|
+
fileDiff,
|
|
470
|
+
options: viewerOptions,
|
|
471
|
+
})
|
|
472
|
+
: Promise.resolve(undefined),
|
|
473
|
+
imageOptions
|
|
474
|
+
? preloadFileDiffWithFallback({
|
|
475
|
+
fileDiff,
|
|
476
|
+
options: imageOptions,
|
|
477
|
+
})
|
|
478
|
+
: Promise.resolve(undefined),
|
|
479
|
+
]);
|
|
480
|
+
|
|
481
|
+
const [viewerPayload, imagePayload] = await Promise.all([
|
|
482
|
+
viewerResult && viewerOptions
|
|
483
|
+
? normalizeDiffViewerPayloadLanguages(
|
|
484
|
+
{
|
|
485
|
+
prerenderedHTML: viewerResult.prerenderedHTML,
|
|
486
|
+
fileDiff: viewerResult.fileDiff,
|
|
487
|
+
options: viewerOptions,
|
|
488
|
+
langs: collectDiffPayloadLanguageHints({ fileDiff: viewerResult.fileDiff }),
|
|
489
|
+
},
|
|
490
|
+
{ languagePackAvailable },
|
|
491
|
+
)
|
|
492
|
+
: Promise.resolve(undefined),
|
|
493
|
+
imageResult && imageOptions
|
|
494
|
+
? normalizeDiffViewerPayloadLanguages(
|
|
495
|
+
{
|
|
496
|
+
prerenderedHTML: imageResult.prerenderedHTML,
|
|
497
|
+
fileDiff: imageResult.fileDiff,
|
|
498
|
+
options: imageOptions,
|
|
499
|
+
langs: collectDiffPayloadLanguageHints({ fileDiff: imageResult.fileDiff }),
|
|
500
|
+
},
|
|
501
|
+
{ languagePackAvailable },
|
|
502
|
+
)
|
|
503
|
+
: Promise.resolve(undefined),
|
|
504
|
+
]);
|
|
505
|
+
|
|
506
|
+
return buildRenderedSection({
|
|
507
|
+
...(viewerPayload ? { viewerPayload } : {}),
|
|
508
|
+
...(imagePayload ? { imagePayload } : {}),
|
|
509
|
+
});
|
|
510
|
+
}),
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
return {
|
|
514
|
+
...buildRenderedBodies(sections),
|
|
515
|
+
fileCount: files.length,
|
|
516
|
+
usesLanguagePack: sections.some((section) => section.usesLanguagePack === true),
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async function normalizePatchFileLanguage(
|
|
521
|
+
fileDiff: FileDiffMetadata,
|
|
522
|
+
options: { languagePackAvailable: boolean },
|
|
523
|
+
): Promise<FileDiffMetadata> {
|
|
524
|
+
const lang = await normalizeSupportedLanguageHint(fileDiff.lang, options);
|
|
525
|
+
if (lang === fileDiff.lang) {
|
|
526
|
+
return fileDiff;
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
...fileDiff,
|
|
530
|
+
...(lang ? { lang } : { lang: "text" }),
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export async function renderDiffDocument(
|
|
535
|
+
input: DiffInput,
|
|
536
|
+
options: DiffRenderOptions,
|
|
537
|
+
target: DiffRenderTarget = "both",
|
|
538
|
+
): Promise<RenderedDiffDocument> {
|
|
539
|
+
const title = buildDiffTitle(input);
|
|
540
|
+
const rendered =
|
|
541
|
+
input.kind === "before_after"
|
|
542
|
+
? await renderBeforeAfterDiff(input, options, target)
|
|
543
|
+
: await renderPatchDiff(input, options, target);
|
|
544
|
+
const viewerRuntime = rendered.usesLanguagePack ? "language-pack" : "base";
|
|
545
|
+
|
|
546
|
+
return {
|
|
547
|
+
...(rendered.viewerBodyHtml
|
|
548
|
+
? {
|
|
549
|
+
html: buildHtmlDocument({
|
|
550
|
+
title,
|
|
551
|
+
bodyHtml: rendered.viewerBodyHtml,
|
|
552
|
+
theme: options.presentation.theme,
|
|
553
|
+
imageMaxWidth: options.image.maxWidth,
|
|
554
|
+
runtimeMode: "viewer",
|
|
555
|
+
viewerRuntime,
|
|
556
|
+
}),
|
|
557
|
+
}
|
|
558
|
+
: {}),
|
|
559
|
+
...(rendered.imageBodyHtml
|
|
560
|
+
? {
|
|
561
|
+
imageHtml: buildHtmlDocument({
|
|
562
|
+
title,
|
|
563
|
+
bodyHtml: rendered.imageBodyHtml,
|
|
564
|
+
theme: options.presentation.theme,
|
|
565
|
+
imageMaxWidth: options.image.maxWidth,
|
|
566
|
+
runtimeMode: "image",
|
|
567
|
+
viewerRuntime,
|
|
568
|
+
}),
|
|
569
|
+
}
|
|
570
|
+
: {}),
|
|
571
|
+
title,
|
|
572
|
+
fileCount: rendered.fileCount,
|
|
573
|
+
inputKind: input.kind,
|
|
574
|
+
viewerRuntime,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
type PreloadedFileDiffResult = Awaited<ReturnType<typeof preloadFileDiff>>;
|
|
579
|
+
type PreloadedMultiFileDiffResult = Awaited<ReturnType<typeof preloadMultiFileDiff>>;
|
|
580
|
+
|
|
581
|
+
function shouldFallbackToClientHydration(error: unknown): boolean {
|
|
582
|
+
return (
|
|
583
|
+
error instanceof TypeError &&
|
|
584
|
+
error.message.includes('needs an import attribute of "type: json"')
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
async function preloadFileDiffWithFallback(params: {
|
|
589
|
+
fileDiff: FileDiffMetadata;
|
|
590
|
+
options: DiffViewerOptions;
|
|
591
|
+
}): Promise<PreloadedFileDiffResult> {
|
|
592
|
+
try {
|
|
593
|
+
return await preloadFileDiff(params);
|
|
594
|
+
} catch (error) {
|
|
595
|
+
if (!shouldFallbackToClientHydration(error)) {
|
|
596
|
+
throw error;
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
fileDiff: params.fileDiff,
|
|
600
|
+
prerenderedHTML: "",
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async function preloadMultiFileDiffWithFallback(params: {
|
|
606
|
+
oldFile: FileContents;
|
|
607
|
+
newFile: FileContents;
|
|
608
|
+
options: DiffViewerOptions;
|
|
609
|
+
}): Promise<PreloadedMultiFileDiffResult> {
|
|
610
|
+
try {
|
|
611
|
+
return await preloadMultiFileDiff(params);
|
|
612
|
+
} catch (error) {
|
|
613
|
+
if (!shouldFallbackToClientHydration(error)) {
|
|
614
|
+
throw error;
|
|
615
|
+
}
|
|
616
|
+
return {
|
|
617
|
+
oldFile: params.oldFile,
|
|
618
|
+
newFile: params.newFile,
|
|
619
|
+
prerenderedHTML: "",
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Diffs plugin module implements shiki curated languages behavior.
|
|
2
|
+
const javascript = () => import("@shikijs/langs/javascript");
|
|
3
|
+
const typescript = () => import("@shikijs/langs/typescript");
|
|
4
|
+
const tsx = () => import("@shikijs/langs/tsx");
|
|
5
|
+
const jsx = () => import("@shikijs/langs/jsx");
|
|
6
|
+
const json = () => import("@shikijs/langs/json");
|
|
7
|
+
const markdown = () => import("@shikijs/langs/markdown");
|
|
8
|
+
const yaml = () => import("@shikijs/langs/yaml");
|
|
9
|
+
const css = () => import("@shikijs/langs/css");
|
|
10
|
+
const html = () => import("@shikijs/langs/html");
|
|
11
|
+
const sh = () => import("@shikijs/langs/sh");
|
|
12
|
+
const python = () => import("@shikijs/langs/python");
|
|
13
|
+
const go = () => import("@shikijs/langs/go");
|
|
14
|
+
const rust = () => import("@shikijs/langs/rust");
|
|
15
|
+
const java = () => import("@shikijs/langs/java");
|
|
16
|
+
const c = () => import("@shikijs/langs/c");
|
|
17
|
+
const cpp = () => import("@shikijs/langs/cpp");
|
|
18
|
+
const csharp = () => import("@shikijs/langs/csharp");
|
|
19
|
+
const php = () => import("@shikijs/langs/php");
|
|
20
|
+
const sql = () => import("@shikijs/langs/sql");
|
|
21
|
+
const docker = () => import("@shikijs/langs/docker");
|
|
22
|
+
const ruby = () => import("@shikijs/langs/ruby");
|
|
23
|
+
const swift = () => import("@shikijs/langs/swift");
|
|
24
|
+
const kotlin = () => import("@shikijs/langs/kotlin");
|
|
25
|
+
const r = () => import("@shikijs/langs/r");
|
|
26
|
+
const dart = () => import("@shikijs/langs/dart");
|
|
27
|
+
const lua = () => import("@shikijs/langs/lua");
|
|
28
|
+
const powershell = () => import("@shikijs/langs/powershell");
|
|
29
|
+
const xml = () => import("@shikijs/langs/xml");
|
|
30
|
+
const toml = () => import("@shikijs/langs/toml");
|
|
31
|
+
|
|
32
|
+
type CuratedLanguageInfo = {
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly name: string;
|
|
35
|
+
readonly aliases?: readonly string[];
|
|
36
|
+
readonly import: () => Promise<unknown>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const bundledLanguagesInfo = [
|
|
40
|
+
{ id: "javascript", name: "JavaScript", aliases: ["js", "mjs", "cjs"], import: javascript },
|
|
41
|
+
{ id: "typescript", name: "TypeScript", aliases: ["ts", "mts", "cts"], import: typescript },
|
|
42
|
+
{ id: "tsx", name: "TSX", import: tsx },
|
|
43
|
+
{ id: "jsx", name: "JSX", import: jsx },
|
|
44
|
+
{ id: "json", name: "JSON", aliases: ["jsonc", "json5", "jsonl"], import: json },
|
|
45
|
+
{ id: "markdown", name: "Markdown", aliases: ["md"], import: markdown },
|
|
46
|
+
{ id: "yaml", name: "YAML", aliases: ["yml"], import: yaml },
|
|
47
|
+
{ id: "css", name: "CSS", import: css },
|
|
48
|
+
{ id: "html", name: "HTML", import: html },
|
|
49
|
+
{ id: "sh", name: "Shell", aliases: ["bash", "shell", "shellscript", "zsh"], import: sh },
|
|
50
|
+
{ id: "python", name: "Python", aliases: ["py"], import: python },
|
|
51
|
+
{ id: "go", name: "Go", import: go },
|
|
52
|
+
{ id: "rust", name: "Rust", aliases: ["rs"], import: rust },
|
|
53
|
+
{ id: "java", name: "Java", import: java },
|
|
54
|
+
{ id: "c", name: "C", import: c },
|
|
55
|
+
{ id: "cpp", name: "C++", aliases: ["c++"], import: cpp },
|
|
56
|
+
{ id: "csharp", name: "C#", aliases: ["c#", "cs"], import: csharp },
|
|
57
|
+
{ id: "php", name: "PHP", import: php },
|
|
58
|
+
{ id: "sql", name: "SQL", import: sql },
|
|
59
|
+
{ id: "docker", name: "Docker", aliases: ["dockerfile"], import: docker },
|
|
60
|
+
{ id: "ruby", name: "Ruby", aliases: ["rb"], import: ruby },
|
|
61
|
+
{ id: "swift", name: "Swift", import: swift },
|
|
62
|
+
{ id: "kotlin", name: "Kotlin", aliases: ["kt", "kts"], import: kotlin },
|
|
63
|
+
{ id: "r", name: "R", import: r },
|
|
64
|
+
{ id: "dart", name: "Dart", import: dart },
|
|
65
|
+
{ id: "lua", name: "Lua", import: lua },
|
|
66
|
+
{ id: "powershell", name: "PowerShell", aliases: ["ps", "ps1"], import: powershell },
|
|
67
|
+
{ id: "xml", name: "XML", import: xml },
|
|
68
|
+
{ id: "toml", name: "TOML", import: toml },
|
|
69
|
+
] as const satisfies readonly CuratedLanguageInfo[];
|
|
70
|
+
|
|
71
|
+
export const bundledLanguagesBase = Object.fromEntries(
|
|
72
|
+
bundledLanguagesInfo.map((language) => [language.id, language.import]),
|
|
73
|
+
);
|
|
74
|
+
export function getBundledLanguageAliases(
|
|
75
|
+
language: (typeof bundledLanguagesInfo)[number],
|
|
76
|
+
): readonly string[] {
|
|
77
|
+
return "aliases" in language ? language.aliases : [];
|
|
78
|
+
}
|
|
79
|
+
export const bundledLanguagesAlias = Object.fromEntries(
|
|
80
|
+
bundledLanguagesInfo.flatMap((language) =>
|
|
81
|
+
getBundledLanguageAliases(language).map((alias) => [alias, language.import]),
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
export const bundledLanguages = {
|
|
85
|
+
...bundledLanguagesBase,
|
|
86
|
+
...bundledLanguagesAlias,
|
|
87
|
+
};
|