@kubohiroya/turbowarp-plus-editor-git 0.0.0-stage → 0.8.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 (63) hide show
  1. package/LICENSE +373 -0
  2. package/README.ja.md +121 -0
  3. package/README.md +43 -2
  4. package/THIRD_PARTY_NOTICES.md +5 -0
  5. package/dist/THIRD_PARTY_NOTICES.md +1186 -0
  6. package/dist/block-diff-location.d.ts +22 -0
  7. package/dist/block-diff-preview.d.ts +33 -0
  8. package/dist/browser.d.ts +4 -0
  9. package/dist/connection-settings.d.ts +35 -0
  10. package/dist/file-diff-view.d.ts +18 -0
  11. package/dist/filesystem.d.ts +89 -0
  12. package/dist/github-pull-requests.d.ts +38 -0
  13. package/dist/index.d.ts +29 -0
  14. package/dist/index.js +3563 -0
  15. package/dist/messages.d.ts +112 -0
  16. package/dist/mutation-fs.d.ts +58 -0
  17. package/dist/objectSpread2-Dkd68nJH.js +101 -0
  18. package/dist/project-asset-paths-KV715791.js +761 -0
  19. package/dist/project-asset-paths.d.ts +12 -0
  20. package/dist/project-assets.d.ts +19 -0
  21. package/dist/project-source-diff.d.ts +26 -0
  22. package/dist/project-source-diff.js +172 -0
  23. package/dist/project-source-patch.d.ts +39 -0
  24. package/dist/project-source-review-model.d.ts +23 -0
  25. package/dist/project-source-review.d.ts +38 -0
  26. package/dist/pull-request-panel.d.ts +8 -0
  27. package/dist/remote-operations.d.ts +5 -0
  28. package/dist/repository-BQkADJmK.js +17280 -0
  29. package/dist/repository.d.ts +71 -0
  30. package/dist/repository.js +1 -0
  31. package/dist/scratch-block-diff-renderer-xS8OYsRl.js +1830 -0
  32. package/dist/scratch-block-diff-renderer.d.ts +128 -0
  33. package/dist/smart-http-BmHEkE7K.js +303 -0
  34. package/dist/smart-http.d.ts +60 -0
  35. package/dist/smart-http.js +1 -0
  36. package/dist/style.css +138 -0
  37. package/package.json +65 -4
  38. package/src/block-diff-location.ts +63 -0
  39. package/src/block-diff-preview.tsx +117 -0
  40. package/src/browser.ts +5 -0
  41. package/src/connection-settings.tsx +103 -0
  42. package/src/file-diff-view.tsx +44 -0
  43. package/src/filesystem.ts +125 -0
  44. package/src/github-pull-requests.ts +229 -0
  45. package/src/index.tsx +352 -0
  46. package/src/messages.ts +64 -0
  47. package/src/mutation-fs.ts +204 -0
  48. package/src/project-asset-paths.ts +33 -0
  49. package/src/project-assets.ts +66 -0
  50. package/src/project-source-diff.css +108 -0
  51. package/src/project-source-diff.tsx +139 -0
  52. package/src/project-source-patch.ts +126 -0
  53. package/src/project-source-review-model.ts +116 -0
  54. package/src/project-source-review.tsx +250 -0
  55. package/src/pull-request-panel.tsx +134 -0
  56. package/src/remote-operations.ts +90 -0
  57. package/src/repository.ts +364 -0
  58. package/src/scratch-block-diff-renderer.ts +331 -0
  59. package/src/smart-http.ts +190 -0
  60. package/src/style.css +29 -0
  61. package/src/styles.d.ts +1 -0
  62. package/tsconfig.json +18 -0
  63. package/vite.config.ts +26 -0
@@ -0,0 +1,1830 @@
1
+ import { n as init_objectSpread2, t as _objectSpread2 } from "./objectSpread2-Dkd68nJH.js";
2
+ import { a as createTwoFilesPatch, i as isAssetName, n as assetsDirectoryParts, o as hasOnlyUnixLineEndings, r as folderSettingsLimit, s as hasOnlyWinLineEndings, t as assetLimit } from "./project-asset-paths-KV715791.js";
3
+ import React, { useEffect, useMemo, useRef, useState } from "react";
4
+ // node_modules/diff/libesm/patch/line-endings.js
5
+ function unixToWin(patch) {
6
+ if (Array.isArray(patch)) return patch.map((p) => unixToWin(p));
7
+ return Object.assign(Object.assign({}, patch), { hunks: patch.hunks.map((hunk) => Object.assign(Object.assign({}, hunk), { lines: hunk.lines.map((line, i) => {
8
+ var _a;
9
+ return line.startsWith("\\") || line.endsWith("\r") || ((_a = hunk.lines[i + 1]) === null || _a === void 0 ? void 0 : _a.startsWith("\\")) ? line : line + "\r";
10
+ }) })) });
11
+ }
12
+ function winToUnix(patch) {
13
+ if (Array.isArray(patch)) return patch.map((p) => winToUnix(p));
14
+ return Object.assign(Object.assign({}, patch), { hunks: patch.hunks.map((hunk) => Object.assign(Object.assign({}, hunk), { lines: hunk.lines.map((line) => line.endsWith("\r") ? line.substring(0, line.length - 1) : line) })) });
15
+ }
16
+ /**
17
+ * Returns true if the patch consistently uses Unix line endings (or only involves one line and has
18
+ * no line endings).
19
+ */
20
+ function isUnix(patch) {
21
+ if (!Array.isArray(patch)) patch = [patch];
22
+ return !patch.some((index) => index.hunks.some((hunk) => hunk.lines.some((line) => !line.startsWith("\\") && line.endsWith("\r"))));
23
+ }
24
+ /**
25
+ * Returns true if the patch uses Windows line endings and only Windows line endings.
26
+ */
27
+ function isWin(patch) {
28
+ if (!Array.isArray(patch)) patch = [patch];
29
+ return patch.some((index) => index.hunks.some((hunk) => hunk.lines.some((line) => line.endsWith("\r")))) && patch.every((index) => index.hunks.every((hunk) => hunk.lines.every((line, i) => {
30
+ var _a;
31
+ return line.startsWith("\\") || line.endsWith("\r") || ((_a = hunk.lines[i + 1]) === null || _a === void 0 ? void 0 : _a.startsWith("\\"));
32
+ })));
33
+ }
34
+ // node_modules/diff/libesm/patch/parse.js
35
+ /**
36
+ * Parses a unified diff format patch into a structured patch object.
37
+ *
38
+ * `parsePatch` has some understanding of Git's particular dialect of unified diff format.
39
+ * When parsing a Git patch, each index in the result may contain additional
40
+ * fields (`isRename`, `isBinary`, etc) not included in the data structure returned by
41
+ * `structuredPatch`; see the `StructuredPatch` interface for a full list.
42
+ *
43
+ * @return a JSON object representation of the patch, suitable for use with the `applyPatch`
44
+ * method. This parses to the same structure returned by `structuredPatch`, except that
45
+ * `oldFileName` and `newFileName` may be `undefined` if the patch doesn't contain enough
46
+ * information to determine them (e.g. a hunk-only patch with no file headers).
47
+ */
48
+ function parsePatch(uniDiff) {
49
+ const diffstr = uniDiff.split(/\n/), list = [];
50
+ let i = 0;
51
+ function isGitDiffHeader(line) {
52
+ return /^diff --git /.test(line);
53
+ }
54
+ function isDiffHeader(line) {
55
+ return isGitDiffHeader(line) || /^Index:\s/.test(line) || /^diff(?: -r \w+)+\s/.test(line);
56
+ }
57
+ function isFileHeader(line) {
58
+ return /^(---|\+\+\+)\s/.test(line);
59
+ }
60
+ function isHunkHeader(line) {
61
+ return /^@@\s/.test(line);
62
+ }
63
+ function parseIndex() {
64
+ var _a;
65
+ const index = {};
66
+ index.hunks = [];
67
+ list.push(index);
68
+ let seenDiffHeader = false;
69
+ while (i < diffstr.length) {
70
+ const line = diffstr[i];
71
+ if (isFileHeader(line) || isHunkHeader(line)) break;
72
+ if (isGitDiffHeader(line)) {
73
+ if (seenDiffHeader) return;
74
+ seenDiffHeader = true;
75
+ index.isGit = true;
76
+ const paths = parseGitDiffHeader(line);
77
+ if (paths) {
78
+ index.oldFileName = paths.oldFileName;
79
+ index.newFileName = paths.newFileName;
80
+ }
81
+ i++;
82
+ while (i < diffstr.length) {
83
+ const extLine = diffstr[i];
84
+ if (isFileHeader(extLine) || isHunkHeader(extLine) || isDiffHeader(extLine)) break;
85
+ const renameFromMatch = /^rename from (.*)/.exec(extLine);
86
+ if (renameFromMatch) {
87
+ index.oldFileName = "a/" + unquoteIfQuoted(renameFromMatch[1]);
88
+ index.isRename = true;
89
+ }
90
+ const renameToMatch = /^rename to (.*)/.exec(extLine);
91
+ if (renameToMatch) {
92
+ index.newFileName = "b/" + unquoteIfQuoted(renameToMatch[1]);
93
+ index.isRename = true;
94
+ }
95
+ const copyFromMatch = /^copy from (.*)/.exec(extLine);
96
+ if (copyFromMatch) {
97
+ index.oldFileName = "a/" + unquoteIfQuoted(copyFromMatch[1]);
98
+ index.isCopy = true;
99
+ }
100
+ const copyToMatch = /^copy to (.*)/.exec(extLine);
101
+ if (copyToMatch) {
102
+ index.newFileName = "b/" + unquoteIfQuoted(copyToMatch[1]);
103
+ index.isCopy = true;
104
+ }
105
+ const newFileModeMatch = /^new file mode (\d+)/.exec(extLine);
106
+ if (newFileModeMatch) {
107
+ index.isCreate = true;
108
+ index.newMode = newFileModeMatch[1];
109
+ }
110
+ const deletedFileModeMatch = /^deleted file mode (\d+)/.exec(extLine);
111
+ if (deletedFileModeMatch) {
112
+ index.isDelete = true;
113
+ index.oldMode = deletedFileModeMatch[1];
114
+ }
115
+ const oldModeMatch = /^old mode (\d+)/.exec(extLine);
116
+ if (oldModeMatch) index.oldMode = oldModeMatch[1];
117
+ const newModeMatch = /^new mode (\d+)/.exec(extLine);
118
+ if (newModeMatch) index.newMode = newModeMatch[1];
119
+ if (/^Binary files /.test(extLine)) index.isBinary = true;
120
+ i++;
121
+ }
122
+ continue;
123
+ } else if (isDiffHeader(line)) {
124
+ if (seenDiffHeader) return;
125
+ seenDiffHeader = true;
126
+ const headerMatch = /^(?:Index:|diff(?: -r \w+)+)\s+/.exec(line);
127
+ if (headerMatch) index.index = line.substring(headerMatch[0].length).trim();
128
+ }
129
+ i++;
130
+ }
131
+ parseFileHeader(index);
132
+ parseFileHeader(index);
133
+ if (index.oldFileName === void 0 !== (index.newFileName === void 0)) throw new Error("Missing " + (index.oldFileName !== void 0 ? "\"+++ ...\"" : "\"--- ...\"") + " file header for " + ((_a = index.oldFileName) !== null && _a !== void 0 ? _a : index.newFileName));
134
+ while (i < diffstr.length) {
135
+ const line = diffstr[i];
136
+ if (isDiffHeader(line) || isFileHeader(line) || /^===================================================================/.test(line)) break;
137
+ else if (isHunkHeader(line)) index.hunks.push(parseHunk());
138
+ else i++;
139
+ }
140
+ }
141
+ /**
142
+ * Parses the old and new filenames from a `diff --git` header line.
143
+ *
144
+ * The format is:
145
+ * diff --git a/<old-path> b/<new-path>
146
+ *
147
+ * When filenames contain special characters (including newlines, tabs,
148
+ * backslashes, or double quotes), Git quotes them with C-style escaping:
149
+ * diff --git "a/file\twith\ttabs.txt" "b/file\twith\ttabs.txt"
150
+ *
151
+ * When filenames don't contain special characters and the old and new names
152
+ * are the same, we can unambiguously split on ` b/` by finding where the
153
+ * two halves (including their a/ and b/ prefixes) yield matching bare names.
154
+ *
155
+ * A pathological case exists in which we cannot reliably determine the paths
156
+ * from the `diff --git` header. This case is when the following are true:
157
+ * - the old and new file paths differ
158
+ * - they are both unquoted (i.e. contain no special characters)
159
+ * - at least one of the underlying file paths includes the substring ` b/`
160
+ * In this scenario, we do not know which occurrence of ` b/` indicates the
161
+ * start of the new file path, so the header is inherently ambiguous. We thus
162
+ * select a possible interpretation arbitrarily and return that.
163
+ *
164
+ * Fortunately, this ambiguity should never matter, because in any patch
165
+ * genuinely output by Git in which this pathological scenario occurs, there
166
+ * must also be `rename from`/`rename to` or `copy from`/`copy to` extended
167
+ * headers present below the `diff --git` header. `parseIndex` will parse
168
+ * THOSE headers, from which we CAN unambiguously determine the filenames,
169
+ * and will discard the result returned by this function.
170
+ *
171
+ * Returns null if the header can't be parsed at all — e.g. a quoted filename
172
+ * has an unterminated quote, or if the unquoted header doesn't match the
173
+ * expected `a/... b/...` format. In that case, the caller (parseIndex)
174
+ * skips setting oldFileName/newFileName from this header, but they may
175
+ * still be set later from `---`/`+++` lines or `rename from`/`rename to`
176
+ * extended headers; if none of those are present either, they'll remain
177
+ * undefined in the output.
178
+ */
179
+ function parseGitDiffHeader(line) {
180
+ const rest = line.substring(11);
181
+ if (rest.startsWith("\"")) {
182
+ const oldPath = parseQuotedFileName(rest);
183
+ if (oldPath === null) return null;
184
+ const afterOld = rest.substring(oldPath.rawLength + 1);
185
+ let newFileName;
186
+ if (afterOld.startsWith("\"")) {
187
+ const newPath = parseQuotedFileName(afterOld);
188
+ if (newPath === null) return null;
189
+ newFileName = newPath.fileName;
190
+ } else newFileName = afterOld;
191
+ return {
192
+ oldFileName: oldPath.fileName,
193
+ newFileName
194
+ };
195
+ }
196
+ const quoteIdx = rest.indexOf("\"");
197
+ if (quoteIdx > 0) {
198
+ const oldFileName = rest.substring(0, quoteIdx - 1);
199
+ const newPath = parseQuotedFileName(rest.substring(quoteIdx));
200
+ if (newPath === null) return null;
201
+ return {
202
+ oldFileName,
203
+ newFileName: newPath.fileName
204
+ };
205
+ }
206
+ if (rest.startsWith("a/")) {
207
+ const splits = [];
208
+ let idx = 0;
209
+ while (true) {
210
+ idx = rest.indexOf(" b/", idx + 1);
211
+ if (idx === -1) break;
212
+ splits.push(idx);
213
+ }
214
+ if (splits.length > 0) {
215
+ const mid = splits[Math.floor(splits.length / 2)];
216
+ return {
217
+ oldFileName: rest.substring(0, mid),
218
+ newFileName: rest.substring(mid + 1)
219
+ };
220
+ }
221
+ }
222
+ return null;
223
+ }
224
+ /**
225
+ * If `s` starts with a double quote, unquotes it using C-style escape
226
+ * rules (as used by Git). Otherwise returns `s` as-is.
227
+ */
228
+ function unquoteIfQuoted(s) {
229
+ if (s.startsWith("\"")) {
230
+ const parsed = parseQuotedFileName(s);
231
+ if (parsed) return parsed.fileName;
232
+ }
233
+ return s;
234
+ }
235
+ /**
236
+ * Parses a C-style quoted filename as used by Git or GNU `diff -u`.
237
+ * Returns the unescaped filename and the raw length consumed (including quotes).
238
+ */
239
+ function parseQuotedFileName(s) {
240
+ if (!s.startsWith("\"")) return null;
241
+ let result = "";
242
+ let j = 1;
243
+ while (j < s.length) {
244
+ if (s[j] === "\"") return {
245
+ fileName: result,
246
+ rawLength: j + 1
247
+ };
248
+ if (s[j] === "\\" && j + 1 < s.length) {
249
+ j++;
250
+ switch (s[j]) {
251
+ case "a":
252
+ result += "\x07";
253
+ break;
254
+ case "b":
255
+ result += "\b";
256
+ break;
257
+ case "f":
258
+ result += "\f";
259
+ break;
260
+ case "n":
261
+ result += "\n";
262
+ break;
263
+ case "r":
264
+ result += "\r";
265
+ break;
266
+ case "t":
267
+ result += " ";
268
+ break;
269
+ case "v":
270
+ result += "\v";
271
+ break;
272
+ case "\\":
273
+ result += "\\";
274
+ break;
275
+ case "\"":
276
+ result += "\"";
277
+ break;
278
+ case "0":
279
+ case "1":
280
+ case "2":
281
+ case "3":
282
+ case "4":
283
+ case "5":
284
+ case "6":
285
+ case "7": {
286
+ if (j + 2 >= s.length || s[j + 1] < "0" || s[j + 1] > "7" || s[j + 2] < "0" || s[j + 2] > "7") return null;
287
+ const bytes = [parseInt(s.substring(j, j + 3), 8)];
288
+ j += 3;
289
+ while (s[j] === "\\" && s[j + 1] >= "0" && s[j + 1] <= "7") {
290
+ if (j + 3 >= s.length || s[j + 2] < "0" || s[j + 2] > "7" || s[j + 3] < "0" || s[j + 3] > "7") return null;
291
+ bytes.push(parseInt(s.substring(j + 1, j + 4), 8));
292
+ j += 4;
293
+ }
294
+ result += new TextDecoder("utf-8").decode(new Uint8Array(bytes));
295
+ continue;
296
+ }
297
+ default: return null;
298
+ }
299
+ } else result += s[j];
300
+ j++;
301
+ }
302
+ return null;
303
+ }
304
+ function parseFileHeader(index) {
305
+ const fileHeaderMatch = /^(---|\+\+\+)\s+/.exec(diffstr[i]);
306
+ if (fileHeaderMatch) {
307
+ const prefix = fileHeaderMatch[1], data = diffstr[i].substring(3).trim().split(" ", 2), header = (data[1] || "").trim();
308
+ let fileName = data[0];
309
+ if (fileName.startsWith("\"")) fileName = unquoteIfQuoted(fileName);
310
+ else fileName = fileName.replace(/\\\\/g, "\\");
311
+ if (prefix === "---") {
312
+ index.oldFileName = fileName;
313
+ index.oldHeader = header;
314
+ } else {
315
+ index.newFileName = fileName;
316
+ index.newHeader = header;
317
+ }
318
+ i++;
319
+ }
320
+ }
321
+ function parseHunk() {
322
+ var _a;
323
+ const chunkHeaderIndex = i, chunkHeader = diffstr[i++].split(/@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
324
+ const hunk = {
325
+ oldStart: +chunkHeader[1],
326
+ oldLines: typeof chunkHeader[2] === "undefined" ? 1 : +chunkHeader[2],
327
+ newStart: +chunkHeader[3],
328
+ newLines: typeof chunkHeader[4] === "undefined" ? 1 : +chunkHeader[4],
329
+ lines: []
330
+ };
331
+ if (hunk.oldLines === 0) hunk.oldStart += 1;
332
+ if (hunk.newLines === 0) hunk.newStart += 1;
333
+ let addCount = 0, removeCount = 0;
334
+ for (; i < diffstr.length && (removeCount < hunk.oldLines || addCount < hunk.newLines || ((_a = diffstr[i]) === null || _a === void 0 ? void 0 : _a.startsWith("\\"))); i++) {
335
+ const operation = diffstr[i].length == 0 && i != diffstr.length - 1 ? " " : diffstr[i][0];
336
+ if (operation === "+" || operation === "-" || operation === " " || operation === "\\") {
337
+ hunk.lines.push(diffstr[i]);
338
+ if (operation === "+") addCount++;
339
+ else if (operation === "-") removeCount++;
340
+ else if (operation === " ") {
341
+ addCount++;
342
+ removeCount++;
343
+ }
344
+ } else throw new Error(`Hunk at line ${chunkHeaderIndex + 1} contained invalid line ${diffstr[i]}`);
345
+ }
346
+ if (!addCount && hunk.newLines === 1) hunk.newLines = 0;
347
+ if (!removeCount && hunk.oldLines === 1) hunk.oldLines = 0;
348
+ if (addCount !== hunk.newLines) throw new Error("Added line count did not match for hunk at line " + (chunkHeaderIndex + 1));
349
+ if (removeCount !== hunk.oldLines) throw new Error("Removed line count did not match for hunk at line " + (chunkHeaderIndex + 1));
350
+ if (i < diffstr.length && diffstr[i] && /^[+ -]/.test(diffstr[i]) && !isFileHeader(diffstr[i])) throw new Error("Hunk at line " + (chunkHeaderIndex + 1) + " has more lines than expected (expected " + hunk.oldLines + " old lines and " + hunk.newLines + " new lines)");
351
+ return hunk;
352
+ }
353
+ while (i < diffstr.length) parseIndex();
354
+ return list;
355
+ }
356
+ // node_modules/diff/libesm/util/distance-iterator.js
357
+ function distance_iterator_default(start, minLine, maxLine) {
358
+ let wantForward = true, backwardExhausted = false, forwardExhausted = false, localOffset = 1;
359
+ return function iterator() {
360
+ if (wantForward && !forwardExhausted) {
361
+ if (backwardExhausted) localOffset++;
362
+ else wantForward = false;
363
+ if (start + localOffset <= maxLine) return start + localOffset;
364
+ forwardExhausted = true;
365
+ }
366
+ if (!backwardExhausted) {
367
+ if (!forwardExhausted) wantForward = true;
368
+ if (minLine <= start - localOffset) return start - localOffset++;
369
+ backwardExhausted = true;
370
+ return iterator();
371
+ }
372
+ };
373
+ }
374
+ // node_modules/diff/libesm/patch/apply.js
375
+ /**
376
+ * attempts to apply a unified diff patch.
377
+ *
378
+ * Hunks are applied first to last.
379
+ * `applyPatch` first tries to apply the first hunk at the line number specified in the hunk header, and with all context lines matching exactly.
380
+ * If that fails, it tries scanning backwards and forwards, one line at a time, to find a place to apply the hunk where the context lines match exactly.
381
+ * If that still fails, and `fuzzFactor` is greater than zero, it increments the maximum number of mismatches (missing, extra, or changed context lines) that there can be between the hunk context and a region where we are trying to apply the patch such that the hunk will still be considered to match.
382
+ * Regardless of `fuzzFactor`, lines to be deleted in the hunk *must* be present for a hunk to match, and the context lines *immediately* before and after an insertion must match exactly.
383
+ *
384
+ * Once a hunk is successfully fitted, the process begins again with the next hunk.
385
+ * Regardless of `fuzzFactor`, later hunks must be applied later in the file than earlier hunks.
386
+ *
387
+ * If a hunk cannot be successfully fitted *anywhere* with fewer than `fuzzFactor` mismatches, `applyPatch` fails and returns `false`.
388
+ *
389
+ * If a hunk is successfully fitted but not at the line number specified by the hunk header, all subsequent hunks have their target line number adjusted accordingly.
390
+ * (e.g. if the first hunk is applied 10 lines below where the hunk header said it should fit, `applyPatch` will *start* looking for somewhere to apply the second hunk 10 lines below where its hunk header says it goes.)
391
+ *
392
+ * If the patch was applied successfully, returns a string containing the patched text.
393
+ * If the patch could not be applied (because some hunks in the patch couldn't be fitted to the text in `source`), `applyPatch` returns false.
394
+ *
395
+ * @param patch a string diff or the output from the `parsePatch` or `structuredPatch` methods.
396
+ */
397
+ function applyPatch(source, patch, options = {}) {
398
+ let patches;
399
+ if (typeof patch === "string") patches = parsePatch(patch);
400
+ else if (Array.isArray(patch)) patches = patch;
401
+ else patches = [patch];
402
+ if (patches.length > 1) throw new Error("applyPatch only works with a single input.");
403
+ return applyStructuredPatch(source, patches[0], options);
404
+ }
405
+ function applyStructuredPatch(source, patch, options = {}) {
406
+ if (options.autoConvertLineEndings || options.autoConvertLineEndings == null) {
407
+ if (hasOnlyWinLineEndings(source) && isUnix(patch)) patch = unixToWin(patch);
408
+ else if (hasOnlyUnixLineEndings(source) && isWin(patch)) patch = winToUnix(patch);
409
+ }
410
+ const lines = source.split("\n"), hunks = patch.hunks, compareLine = options.compareLine || ((lineNumber, line, operation, patchContent) => line === patchContent), fuzzFactor = options.fuzzFactor || 0;
411
+ let minLine = 0;
412
+ if (fuzzFactor < 0 || !Number.isInteger(fuzzFactor)) throw new Error("fuzzFactor must be a non-negative integer");
413
+ if (!hunks.length) return source;
414
+ let prevLine = "", removeEOFNL = false, addEOFNL = false;
415
+ for (let i = 0; i < hunks[hunks.length - 1].lines.length; i++) {
416
+ const line = hunks[hunks.length - 1].lines[i];
417
+ if (line[0] == "\\") {
418
+ if (prevLine[0] == "+") removeEOFNL = true;
419
+ else if (prevLine[0] == "-") addEOFNL = true;
420
+ }
421
+ prevLine = line;
422
+ }
423
+ if (removeEOFNL) {
424
+ if (addEOFNL) {
425
+ if (!fuzzFactor && lines[lines.length - 1] == "") return false;
426
+ } else if (lines[lines.length - 1] == "") lines.pop();
427
+ else if (!fuzzFactor) return false;
428
+ } else if (addEOFNL) {
429
+ if (lines[lines.length - 1] != "") lines.push("");
430
+ else if (!fuzzFactor) return false;
431
+ }
432
+ /**
433
+ * Checks if the hunk can be made to fit at the provided location with at most `maxErrors`
434
+ * insertions, substitutions, or deletions, while ensuring also that:
435
+ * - lines deleted in the hunk match exactly, and
436
+ * - wherever an insertion operation or block of insertion operations appears in the hunk, the
437
+ * immediately preceding and following lines of context match exactly
438
+ *
439
+ * `toPos` should be set such that lines[toPos] is meant to match hunkLines[0].
440
+ *
441
+ * If the hunk can be applied, returns an object with properties `oldLineLastI` and
442
+ * `replacementLines`. Otherwise, returns null.
443
+ */
444
+ function applyHunk(hunkLines, toPos, maxErrors, hunkLinesI = 0, lastContextLineMatched = true, patchedLines = [], patchedLinesLength = 0) {
445
+ let nConsecutiveOldContextLines = 0;
446
+ let nextContextLineMustMatch = false;
447
+ for (; hunkLinesI < hunkLines.length; hunkLinesI++) {
448
+ const hunkLine = hunkLines[hunkLinesI], operation = hunkLine.length > 0 ? hunkLine[0] : " ", content = hunkLine.length > 0 ? hunkLine.substr(1) : hunkLine;
449
+ if (operation === "-") {
450
+ if (compareLine(toPos + 1, lines[toPos], operation, content)) {
451
+ toPos++;
452
+ nConsecutiveOldContextLines = 0;
453
+ } else {
454
+ if (!maxErrors || lines[toPos] == null) return null;
455
+ patchedLines[patchedLinesLength] = lines[toPos];
456
+ return applyHunk(hunkLines, toPos + 1, maxErrors - 1, hunkLinesI, false, patchedLines, patchedLinesLength + 1);
457
+ }
458
+ }
459
+ if (operation === "+") {
460
+ if (!lastContextLineMatched) return null;
461
+ patchedLines[patchedLinesLength] = content;
462
+ patchedLinesLength++;
463
+ nConsecutiveOldContextLines = 0;
464
+ nextContextLineMustMatch = true;
465
+ }
466
+ if (operation === " ") {
467
+ nConsecutiveOldContextLines++;
468
+ patchedLines[patchedLinesLength] = lines[toPos];
469
+ if (compareLine(toPos + 1, lines[toPos], operation, content)) {
470
+ patchedLinesLength++;
471
+ lastContextLineMatched = true;
472
+ nextContextLineMustMatch = false;
473
+ toPos++;
474
+ } else {
475
+ if (nextContextLineMustMatch || !maxErrors) return null;
476
+ return lines[toPos] && (applyHunk(hunkLines, toPos + 1, maxErrors - 1, hunkLinesI + 1, false, patchedLines, patchedLinesLength + 1) || applyHunk(hunkLines, toPos + 1, maxErrors - 1, hunkLinesI, false, patchedLines, patchedLinesLength + 1)) || applyHunk(hunkLines, toPos, maxErrors - 1, hunkLinesI + 1, false, patchedLines, patchedLinesLength);
477
+ }
478
+ }
479
+ }
480
+ patchedLinesLength -= nConsecutiveOldContextLines;
481
+ toPos -= nConsecutiveOldContextLines;
482
+ patchedLines.length = patchedLinesLength;
483
+ return {
484
+ patchedLines,
485
+ oldLineLastI: toPos - 1
486
+ };
487
+ }
488
+ const resultLines = [];
489
+ let prevHunkOffset = 0;
490
+ for (let i = 0; i < hunks.length; i++) {
491
+ const hunk = hunks[i];
492
+ let hunkResult;
493
+ const maxLine = lines.length - hunk.oldLines + fuzzFactor;
494
+ let toPos;
495
+ for (let maxErrors = 0; maxErrors <= fuzzFactor; maxErrors++) {
496
+ toPos = hunk.oldStart + prevHunkOffset - 1;
497
+ const iterator = distance_iterator_default(toPos, minLine, maxLine);
498
+ for (; toPos !== void 0; toPos = iterator()) {
499
+ hunkResult = applyHunk(hunk.lines, toPos, maxErrors);
500
+ if (hunkResult) break;
501
+ }
502
+ if (hunkResult) break;
503
+ }
504
+ if (!hunkResult) return false;
505
+ for (let i = minLine; i < toPos; i++) resultLines.push(lines[i]);
506
+ for (let i = 0; i < hunkResult.patchedLines.length; i++) {
507
+ const line = hunkResult.patchedLines[i];
508
+ resultLines.push(line);
509
+ }
510
+ minLine = hunkResult.oldLineLastI + 1;
511
+ prevHunkOffset = toPos + 1 - hunk.oldStart;
512
+ }
513
+ for (let i = minLine; i < lines.length; i++) resultLines.push(lines[i]);
514
+ return resultLines.join("\n");
515
+ }
516
+ // \0@oxc-project+runtime@0.156.0/helpers/esm/objectWithoutPropertiesLoose.js
517
+ init_objectSpread2();
518
+ function _objectWithoutPropertiesLoose(r, e) {
519
+ if (null == r) return {};
520
+ var t = {};
521
+ for (var n in r) if ({}.hasOwnProperty.call(r, n)) {
522
+ if (e.includes(n)) continue;
523
+ t[n] = r[n];
524
+ }
525
+ return t;
526
+ }
527
+ // \0@oxc-project+runtime@0.156.0/helpers/esm/objectWithoutProperties.js
528
+ function _objectWithoutProperties(e, t) {
529
+ if (null == e) return {};
530
+ var o, r, i = _objectWithoutPropertiesLoose(e, t);
531
+ if (Object.getOwnPropertySymbols) {
532
+ var s = Object.getOwnPropertySymbols(e);
533
+ for (r = 0; r < s.length; r++) o = s[r], t.includes(o) || {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]);
534
+ }
535
+ return i;
536
+ }
537
+ // src/project-source-patch.ts
538
+ var _excluded = ["blocks"];
539
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
540
+ function readProject(source) {
541
+ const project = JSON.parse(source);
542
+ if (!isRecord(project) || !Array.isArray(project.targets) || !project.targets.every((target) => isRecord(target) && typeof target.name === "string" && typeof target.isStage === "boolean" && (target.blocks === void 0 || isRecord(target.blocks)))) throw new Error("Expected a project.source.json with Scratch targets and blocks.");
543
+ const parsed = project;
544
+ return _objectSpread2(_objectSpread2({}, parsed), {}, { targets: parsed.targets.map((target) => {
545
+ var _target$blocks;
546
+ return _objectSpread2(_objectSpread2({}, target), {}, { blocks: (_target$blocks = target.blocks) !== null && _target$blocks !== void 0 ? _target$blocks : {} });
547
+ }) });
548
+ }
549
+ /** Reads one version of a project.source.json. `null` stands for a version in which the file does not exist. */
550
+ function parseProjectSource(source) {
551
+ return source === null ? { targets: [] } : readProject(source);
552
+ }
553
+ /** Keep the original lines and include enough context to render both sides from the patch alone. */
554
+ function createProjectSourceDiff(before, after) {
555
+ readProject(before);
556
+ readProject(after);
557
+ const patch = createTwoFilesPatch("a/project.source.json", "b/project.source.json", before, after, void 0, void 0, {
558
+ context: Infinity,
559
+ timeout: 1e3
560
+ });
561
+ if (patch === void 0) throw new Error("Project source diff exceeded its time limit.");
562
+ return "diff --git a/project.source.json b/project.source.json\n" + patch.replace(/^=+\n/, "");
563
+ }
564
+ function readProjectSourceDiff(patch, beforeSource) {
565
+ const [file, extra] = parsePatch(patch);
566
+ if (!file || extra || !file.oldFileName || !file.newFileName) throw new Error("Expected a single unified text diff.");
567
+ if (beforeSource !== void 0) {
568
+ const before = readProject(beforeSource);
569
+ const after = applyPatch(beforeSource, file);
570
+ if (after === false) throw new Error("The original project does not match this diff.");
571
+ return [before, readProject(after)];
572
+ }
573
+ if (!file.hunks.length) return null;
574
+ const before = [], after = [];
575
+ for (const hunk of file.hunks) {
576
+ if (hunk.oldStart !== before.length + 1 || hunk.newStart !== after.length + 1) throw new Error("The diff omits project context.");
577
+ for (const line of hunk.lines) {
578
+ if (line.startsWith(" ") || line.startsWith("-")) before.push(line.slice(1));
579
+ if (line.startsWith(" ") || line.startsWith("+")) after.push(line.slice(1));
580
+ }
581
+ }
582
+ return [readProject(before.join("\n")), readProject(after.join("\n"))];
583
+ }
584
+ function equal(a, b) {
585
+ if (a === b) return true;
586
+ if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((value, i) => equal(value, b[i]));
587
+ if (!isRecord(a) || !isRecord(b)) return false;
588
+ const keys = Object.keys(a);
589
+ return keys.length === Object.keys(b).length && keys.every((key) => Object.hasOwn(b, key) && equal(a[key], b[key]));
590
+ }
591
+ var targetKey = (target) => target.isStage ? "stage" : "sprite:" + target.name;
592
+ function changedParts(before, after) {
593
+ if (!isRecord(before) || !isRecord(after)) return [];
594
+ const result = [];
595
+ const visibleInput = (input) => {
596
+ var _input$;
597
+ return Array.isArray(input) ? (_input$ = input[1]) !== null && _input$ !== void 0 ? _input$ : input[2] : input;
598
+ };
599
+ for (const group of ["inputs", "fields"]) {
600
+ const oldGroup = isRecord(before[group]) ? before[group] : {};
601
+ const newGroup = isRecord(after[group]) ? after[group] : {};
602
+ for (const name of /* @__PURE__ */ new Set([...Object.keys(oldGroup), ...Object.keys(newGroup)])) {
603
+ const a = oldGroup[name], b = newGroup[name];
604
+ if (group === "inputs" ? !equal(visibleInput(a), visibleInput(b)) : !equal(a, b)) result.push({
605
+ kind: group === "inputs" ? "input" : "field",
606
+ name
607
+ });
608
+ }
609
+ }
610
+ return result;
611
+ }
612
+ function compareProjectBlocks(before, after) {
613
+ const oldTargets = new Map(before.targets.map((target) => [targetKey(target), target]));
614
+ const newTargets = new Map(after.targets.map((target) => [targetKey(target), target]));
615
+ const changes = [];
616
+ let unchanged = 0;
617
+ for (const key of /* @__PURE__ */ new Set([...oldTargets.keys(), ...newTargets.keys()])) {
618
+ var _oldTarget$blocks, _newTarget$blocks;
619
+ const oldTarget = oldTargets.get(key), newTarget = newTargets.get(key);
620
+ const oldBlocks = (_oldTarget$blocks = oldTarget === null || oldTarget === void 0 ? void 0 : oldTarget.blocks) !== null && _oldTarget$blocks !== void 0 ? _oldTarget$blocks : {}, newBlocks = (_newTarget$blocks = newTarget === null || newTarget === void 0 ? void 0 : newTarget.blocks) !== null && _newTarget$blocks !== void 0 ? _newTarget$blocks : {};
621
+ for (const id of /* @__PURE__ */ new Set([...Object.keys(oldBlocks), ...Object.keys(newBlocks)])) {
622
+ const hadBlock = Object.hasOwn(oldBlocks, id), hasBlock = Object.hasOwn(newBlocks, id);
623
+ const a = oldBlocks[id], b = newBlocks[id];
624
+ if (hadBlock && hasBlock && equal(a, b)) {
625
+ unchanged++;
626
+ continue;
627
+ }
628
+ const fields = isRecord(a) && isRecord(b) ? [.../* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])].filter((field) => !equal(a[field], b[field])) : [];
629
+ changes.push({
630
+ key: JSON.stringify([key, id]),
631
+ before: hadBlock && oldTarget ? {
632
+ target: oldTarget,
633
+ id
634
+ } : void 0,
635
+ after: hasBlock && newTarget ? {
636
+ target: newTarget,
637
+ id
638
+ } : void 0,
639
+ kind: !hadBlock ? "added" : !hasBlock ? "removed" : "updated",
640
+ fields,
641
+ parts: changedParts(a, b)
642
+ });
643
+ }
644
+ }
645
+ const withoutBlocks = (project) => _objectSpread2(_objectSpread2({}, project), {}, { targets: project.targets.map((_ref) => {
646
+ let { blocks: _ } = _ref;
647
+ return _objectWithoutProperties(_ref, _excluded);
648
+ }) });
649
+ return {
650
+ changes,
651
+ unchanged,
652
+ otherChanges: !equal(withoutBlocks(before), withoutBlocks(after))
653
+ };
654
+ }
655
+ // src/project-assets.ts
656
+ var imageTypes = {
657
+ svg: "image/svg+xml",
658
+ png: "image/png",
659
+ jpg: "image/jpeg",
660
+ jpeg: "image/jpeg",
661
+ gif: "image/gif",
662
+ bmp: "image/bmp",
663
+ webp: "image/webp"
664
+ };
665
+ /** The costume a sprite shows, or the backdrop the stage shows, when the project data names an image file for it. */
666
+ function currentCostume(target) {
667
+ const { costumes, currentCostume: index } = target;
668
+ if (!Array.isArray(costumes)) return null;
669
+ const costume = costumes[typeof index === "number" && Number.isInteger(index) && index >= 0 ? index : 0];
670
+ if (!isRecord(costume)) return null;
671
+ const md5ext = typeof costume.md5ext === "string" ? costume.md5ext : typeof costume.assetId === "string" && typeof costume.dataFormat === "string" ? costume.assetId + "." + costume.dataFormat : "";
672
+ const match = /^[0-9A-Za-z_-]{1,128}\.([0-9A-Za-z]{1,8})$/.exec(md5ext);
673
+ const type = match ? imageTypes[match[1].toLowerCase()] : void 0;
674
+ if (!type) return null;
675
+ return {
676
+ name: typeof costume.name === "string" ? costume.name : "",
677
+ md5ext,
678
+ type
679
+ };
680
+ }
681
+ var reads = /* @__PURE__ */ new WeakMap();
682
+ function readAsset(loadAsset, version, md5ext) {
683
+ let cache = reads.get(loadAsset);
684
+ if (!cache) reads.set(loadAsset, cache = /* @__PURE__ */ new Map());
685
+ const key = version + ":" + md5ext;
686
+ let read = cache.get(key);
687
+ if (!read) {
688
+ read = loadAsset({
689
+ version,
690
+ md5ext,
691
+ signal: new AbortController().signal
692
+ }).catch(() => null);
693
+ cache.set(key, read);
694
+ }
695
+ return read;
696
+ }
697
+ /** Loads the current costume of one version of a target as an object URL for an image, or `null` while it is unavailable. */
698
+ function useCostumeImage(target, version, loadAsset) {
699
+ const costume = target && loadAsset ? currentCostume(target) : null;
700
+ const key = costume ? version + ":" + costume.md5ext : "";
701
+ const [image, setImage] = useState();
702
+ useEffect(() => {
703
+ if (!costume || !loadAsset) return;
704
+ let active = true;
705
+ let url;
706
+ readAsset(loadAsset, version, costume.md5ext).then((blob) => {
707
+ if (!blob || !active) return;
708
+ url = URL.createObjectURL(new Blob([blob], { type: costume.type }));
709
+ setImage({
710
+ key,
711
+ url
712
+ });
713
+ });
714
+ return () => {
715
+ active = false;
716
+ if (url) URL.revokeObjectURL(url);
717
+ };
718
+ }, [key, loadAsset]);
719
+ return (image === null || image === void 0 ? void 0 : image.key) === key ? image.url : null;
720
+ }
721
+ // src/block-diff-preview.tsx
722
+ init_objectSpread2();
723
+ function createPreviewSync() {
724
+ const members = /* @__PURE__ */ new Map();
725
+ let linked = true;
726
+ let moving = false;
727
+ const align = () => {
728
+ if (!linked || !members.size) return;
729
+ const views = [...members.keys()];
730
+ const fit = {
731
+ scale: Math.min(...views.map((view) => view.fit.scale)),
732
+ height: Math.max(...views.map((view) => view.fit.height))
733
+ };
734
+ for (const view of views) view.layout(fit);
735
+ };
736
+ return {
737
+ add(view) {
738
+ const stop = view.onChange(() => {
739
+ if (!linked || moving) return;
740
+ moving = true;
741
+ try {
742
+ const viewport = view.viewport();
743
+ for (const other of members.keys()) if (other !== view) other.setViewport(viewport);
744
+ } finally {
745
+ moving = false;
746
+ }
747
+ });
748
+ members.set(view, stop);
749
+ align();
750
+ return () => {
751
+ stop();
752
+ members.delete(view);
753
+ };
754
+ },
755
+ setLinked(value) {
756
+ if (linked === value) return;
757
+ linked = value;
758
+ align();
759
+ }
760
+ };
761
+ }
762
+ var linkLabels = {
763
+ en: "Move both versions together",
764
+ ja: "変更前と変更後を連動して動かす"
765
+ };
766
+ /** One sync group per comparison, with the checkbox that links or unlinks its previews. */
767
+ function usePreviewSync(locale) {
768
+ const sync = useMemo(createPreviewSync, []);
769
+ const [linked, setLinked] = useState(true);
770
+ useEffect(() => sync.setLinked(linked), [sync, linked]);
771
+ return {
772
+ sync,
773
+ toggle: /* @__PURE__ */ React.createElement("label", { className: "twgit-block-diff-link" }, /* @__PURE__ */ React.createElement("input", {
774
+ type: "checkbox",
775
+ checked: linked,
776
+ onChange: (event) => setLinked(event.currentTarget.checked)
777
+ }), " ", locale.startsWith("ja") ? linkLabels.ja : linkLabels.en)
778
+ };
779
+ }
780
+ var rendering = (result) => typeof result === "function" ? { release: result } : {
781
+ release: result && (() => result.dispose()),
782
+ view: result === null || result === void 0 ? void 0 : result.view
783
+ };
784
+ function BlockPreview({ side, title, parts, marks, renderBlock, withContext, labels, onLocate, locationStatus, version, loadAsset, sync }) {
785
+ const host = useRef(null);
786
+ const watermark = useRef(null);
787
+ const [status, setStatus] = useState("loading");
788
+ const thumbnail = !!(side && version && loadAsset && currentCostume(side.target));
789
+ const image = useCostumeImage(thumbnail ? side.target : void 0, version !== null && version !== void 0 ? version : "after", loadAsset);
790
+ useEffect(() => {
791
+ if (!side || !host.current) return;
792
+ const container = host.current;
793
+ const abort = new AbortController();
794
+ let dispose;
795
+ setStatus("loading");
796
+ Promise.resolve().then(() => {
797
+ var _watermark$current;
798
+ return renderBlock(_objectSpread2(_objectSpread2({}, side), {}, {
799
+ container,
800
+ withContext,
801
+ parts,
802
+ marks,
803
+ signal: abort.signal,
804
+ watermark: thumbnail ? (_watermark$current = watermark.current) !== null && _watermark$current !== void 0 ? _watermark$current : void 0 : void 0
805
+ }));
806
+ }).then((result) => {
807
+ const { release, view } = rendering(result);
808
+ if (abort.signal.aborted) {
809
+ release === null || release === void 0 || release();
810
+ return;
811
+ }
812
+ const leave = view && (sync === null || sync === void 0 ? void 0 : sync.add(view));
813
+ dispose = () => {
814
+ leave === null || leave === void 0 || leave();
815
+ release === null || release === void 0 || release();
816
+ };
817
+ setStatus("ready");
818
+ }, () => {
819
+ if (!abort.signal.aborted) setStatus("failed");
820
+ });
821
+ return () => {
822
+ abort.abort();
823
+ dispose === null || dispose === void 0 || dispose();
824
+ };
825
+ }, [
826
+ side,
827
+ renderBlock,
828
+ withContext,
829
+ parts,
830
+ marks,
831
+ sync,
832
+ thumbnail
833
+ ]);
834
+ return /* @__PURE__ */ React.createElement("div", { className: "twgit-block-diff-side" }, /* @__PURE__ */ React.createElement("h4", null, title), !side && /* @__PURE__ */ React.createElement("p", null, labels.absent), side && status === "loading" && /* @__PURE__ */ React.createElement("p", { role: "status" }, labels.loading), side && status === "failed" && /* @__PURE__ */ React.createElement("p", { role: "status" }, labels.unavailable), side && /* @__PURE__ */ React.createElement("div", {
835
+ ref: host,
836
+ className: "twgit-block-diff-preview"
837
+ }, thumbnail && /* @__PURE__ */ React.createElement("div", {
838
+ ref: watermark,
839
+ className: "twgit-block-diff-watermark",
840
+ "data-dockview-costume-thumbnail": ""
841
+ }, image && /* @__PURE__ */ React.createElement("img", {
842
+ src: image,
843
+ alt: ""
844
+ }))), side && onLocate && /* @__PURE__ */ React.createElement("button", {
845
+ type: "button",
846
+ "aria-label": `${title} · ${labels.locate}`,
847
+ onClick: onLocate
848
+ }, labels.locate), locationStatus && /* @__PURE__ */ React.createElement("p", { role: "status" }, labels[locationStatus]));
849
+ }
850
+ // src/project-source-review-model.ts
851
+ init_objectSpread2();
852
+ function scriptRoots(target) {
853
+ const roots = /* @__PURE__ */ new Map();
854
+ if (!target) return roots;
855
+ for (const id of Object.keys(target.blocks)) {
856
+ var _roots$get;
857
+ if (roots.has(id)) continue;
858
+ const chain = /* @__PURE__ */ new Set();
859
+ let current = id;
860
+ while (!roots.has(current) && !chain.has(current)) {
861
+ chain.add(current);
862
+ const block = target.blocks[current];
863
+ if (!isRecord(block) || typeof block.parent !== "string" || !Object.hasOwn(target.blocks, block.parent)) break;
864
+ current = block.parent;
865
+ }
866
+ const root = (_roots$get = roots.get(current)) !== null && _roots$get !== void 0 ? _roots$get : current;
867
+ for (const block of chain) roots.set(block, root);
868
+ }
869
+ return roots;
870
+ }
871
+ function groupScripts(before, after, changes) {
872
+ const roots = {
873
+ before: scriptRoots(before),
874
+ after: scriptRoots(after)
875
+ };
876
+ const parents = /* @__PURE__ */ new Map();
877
+ function groupOf(key) {
878
+ let parent = key;
879
+ while (parents.has(parent)) parent = parents.get(parent);
880
+ for (let current = key; current !== parent;) {
881
+ const next = parents.get(current);
882
+ parents.set(current, parent);
883
+ current = next;
884
+ }
885
+ return parent;
886
+ }
887
+ for (const [id, oldRoot] of roots.before) {
888
+ const newRoot = roots.after.get(id);
889
+ if (newRoot === void 0) continue;
890
+ const a = groupOf("before:" + oldRoot), b = groupOf("after:" + newRoot);
891
+ if (a !== b) parents.set(b, a);
892
+ }
893
+ const groups = /* @__PURE__ */ new Map();
894
+ const sides = /* @__PURE__ */ new Map();
895
+ for (const version of ["before", "after"]) {
896
+ const target = version === "before" ? before : after;
897
+ if (!target) continue;
898
+ for (const root of new Set(roots[version].values())) {
899
+ var _groups$get;
900
+ const key = version + ":" + root;
901
+ const id = groupOf(key);
902
+ const group = (_groups$get = groups.get(id)) !== null && _groups$get !== void 0 ? _groups$get : {
903
+ id,
904
+ before: [],
905
+ after: []
906
+ };
907
+ const side = {
908
+ target,
909
+ id: root,
910
+ marks: []
911
+ };
912
+ group[version].push(side);
913
+ groups.set(id, group);
914
+ sides.set(key, side);
915
+ }
916
+ }
917
+ const affected = /* @__PURE__ */ new Set();
918
+ for (const change of changes) for (const version of ["before", "after"]) {
919
+ var _sides$get;
920
+ const block = change[version];
921
+ if (!block) continue;
922
+ const key = version + ":" + roots[version].get(block.id);
923
+ (_sides$get = sides.get(key)) === null || _sides$get === void 0 || _sides$get.marks.push({
924
+ id: block.id,
925
+ kind: change.kind,
926
+ parts: change.parts
927
+ });
928
+ affected.add(groupOf(key));
929
+ }
930
+ return [...affected].map((id) => groups.get(id));
931
+ }
932
+ function changedProperties(before, after, except) {
933
+ return [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].filter((key) => key !== except && !equal(before[key], after[key]));
934
+ }
935
+ function compareProjectForReview(before, after) {
936
+ const { changes } = compareProjectBlocks(before, after);
937
+ const byTarget = /* @__PURE__ */ new Map();
938
+ for (const change of changes) {
939
+ var _change$after;
940
+ const key = targetKey(((_change$after = change.after) !== null && _change$after !== void 0 ? _change$after : change.before).target);
941
+ const existing = byTarget.get(key);
942
+ if (existing) existing.push(change);
943
+ else byTarget.set(key, [change]);
944
+ }
945
+ const oldTargets = new Map(before.targets.map((target) => [targetKey(target), target]));
946
+ const newTargets = new Map(after.targets.map((target) => [targetKey(target), target]));
947
+ const targets = [];
948
+ for (const key of /* @__PURE__ */ new Set([...oldTargets.keys(), ...newTargets.keys()])) {
949
+ const a = oldTargets.get(key), b = newTargets.get(key);
950
+ const edits = byTarget.get(key);
951
+ const scripts = edits ? groupScripts(a, b, edits) : [];
952
+ const properties = changedProperties(_objectSpread2({}, a), _objectSpread2({}, b), "blocks");
953
+ if (scripts.length || properties.length) {
954
+ const { name, isStage } = b !== null && b !== void 0 ? b : a;
955
+ targets.push({
956
+ name,
957
+ isStage,
958
+ scripts,
959
+ properties,
960
+ before: a,
961
+ after: b
962
+ });
963
+ }
964
+ }
965
+ return {
966
+ targets,
967
+ properties: changedProperties(before, after, "targets")
968
+ };
969
+ }
970
+ // src/project-source-review.tsx
971
+ var ja = {
972
+ review: "変更レビュー",
973
+ fullDescription: "PR 本文を表示",
974
+ checks: "確認してほしいこと",
975
+ files: "変更ファイル",
976
+ targets: "影響するステージ・スプライト",
977
+ scripts: "コードの変更",
978
+ scriptCount: (count) => `コードの変更 · ${count} か所`,
979
+ reviewed: "確認済み",
980
+ before: "変更前",
981
+ after: "変更後",
982
+ updated: "変更",
983
+ added: "追加",
984
+ removed: "削除",
985
+ reorganized: "再編",
986
+ absent: "この版にはこのコードがありません",
987
+ loading: "コードを描画中…",
988
+ unavailable: "このコードを描画できません。",
989
+ related: "関連ファイル",
990
+ properties: "コード以外の変更",
991
+ invalid: "この作品の差分を読み込めません。",
992
+ flag: "緑の旗が押されたとき",
993
+ broadcast: "を受け取ったとき",
994
+ key: "キーが押されたとき",
995
+ click: "このスプライトが押されたとき",
996
+ script: "コード",
997
+ noChanges: "作品に内容の変更はありません。",
998
+ comparison: "作品の変更",
999
+ unreadable: "作品データとして読み込めないため、テキストの差分を表示します。"
1000
+ };
1001
+ var en = {
1002
+ review: "Change review",
1003
+ fullDescription: "Show full PR description",
1004
+ checks: "Review checklist",
1005
+ files: "Changed files",
1006
+ targets: "Affected stages and sprites",
1007
+ scripts: "Code changes",
1008
+ scriptCount: (count) => `Code changes · ${count}`,
1009
+ reviewed: "Reviewed",
1010
+ before: "Before",
1011
+ after: "After",
1012
+ updated: "Changed",
1013
+ added: "Added",
1014
+ removed: "Removed",
1015
+ reorganized: "Reorganized",
1016
+ absent: "No code in this version",
1017
+ loading: "Rendering code…",
1018
+ unavailable: "This code could not be rendered.",
1019
+ related: "Related file",
1020
+ properties: "Changes outside the code",
1021
+ invalid: "This project diff could not be read.",
1022
+ flag: "When green flag clicked",
1023
+ broadcast: "received",
1024
+ key: "When key pressed",
1025
+ click: "When this sprite clicked",
1026
+ script: "Code",
1027
+ noChanges: "No changes to the project contents.",
1028
+ comparison: "Project changes",
1029
+ unreadable: "This file could not be read as project data, so its text diff is shown."
1030
+ };
1031
+ var properties = /* @__PURE__ */ new Map([
1032
+ ["variables", ["変数", "Variables"]],
1033
+ ["lists", ["リスト", "Lists"]],
1034
+ ["broadcasts", ["メッセージ", "Broadcast messages"]],
1035
+ ["costumes", ["コスチューム・背景", "Costumes and backdrops"]],
1036
+ ["sounds", ["音", "Sounds"]],
1037
+ ["comments", ["コメント", "Comments"]],
1038
+ ["monitors", ["モニター", "Monitors"]],
1039
+ ["extensions", ["拡張機能", "Extensions"]],
1040
+ ["x", ["X 座標", "X position"]],
1041
+ ["y", ["Y 座標", "Y position"]],
1042
+ ["visible", ["表示状態", "Visibility"]],
1043
+ ["direction", ["向き", "Direction"]],
1044
+ ["size", ["大きさ", "Size"]],
1045
+ ["currentCostume", ["表示するコスチューム", "Selected costume"]]
1046
+ ]);
1047
+ function scriptTitle(side, labels) {
1048
+ const block = side.target.blocks[side.id];
1049
+ if (!isRecord(block)) return labels.script;
1050
+ if (block.opcode === "event_whenflagclicked") return labels.flag;
1051
+ if (block.opcode === "event_whenthisspriteclicked") return labels.click;
1052
+ if (block.opcode === "event_whenkeypressed") return labels.key;
1053
+ if (block.opcode === "event_whenbroadcastreceived") {
1054
+ const field = isRecord(block.fields) ? block.fields.BROADCAST_OPTION : void 0;
1055
+ const message = Array.isArray(field) && typeof field[0] === "string" ? field[0] : "";
1056
+ if (message) return labels === ja ? `「${message}」${labels.broadcast}` : `“${message}” ${labels.broadcast}`;
1057
+ }
1058
+ return typeof block.opcode === "string" ? block.opcode : labels.script;
1059
+ }
1060
+ function ScriptComparison({ script, labels, reviewed = false, setReviewed, renderBlock, loadAsset }) {
1061
+ const [open, setOpen] = useState(false);
1062
+ const { sync, toggle } = usePreviewSync(labels === ja ? "ja" : "en");
1063
+ const status = !script.before.length ? "added" : !script.after.length ? "removed" : script.before.length !== 1 || script.after.length !== 1 ? "reorganized" : "updated";
1064
+ const beforeTitle = script.before[0] && scriptTitle(script.before[0], labels);
1065
+ const afterTitle = script.after[0] && scriptTitle(script.after[0], labels);
1066
+ const title = beforeTitle && afterTitle && beforeTitle !== afterTitle ? beforeTitle + " → " + afterTitle : afterTitle !== null && afterTitle !== void 0 ? afterTitle : beforeTitle;
1067
+ return /* @__PURE__ */ React.createElement("details", {
1068
+ className: "twgit-review-script",
1069
+ onToggle: (event) => setOpen(event.currentTarget.open)
1070
+ }, /* @__PURE__ */ React.createElement("summary", null, /* @__PURE__ */ React.createElement("span", { className: "twgit-review-status" }, labels[status]), /* @__PURE__ */ React.createElement("strong", null, title), status === "reorganized" && /* @__PURE__ */ React.createElement("small", null, script.before.length, " → ", script.after.length), reviewed && /* @__PURE__ */ React.createElement("small", null, labels.reviewed)), open && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "twgit-block-diff-sides" }, ["before", "after"].map((version) => /* @__PURE__ */ React.createElement("div", { key: version }, script[version].length ? script[version].map((side, index, sides) => /* @__PURE__ */ React.createElement(BlockPreview, {
1071
+ key: side.id,
1072
+ side,
1073
+ title: sides.length > 1 ? `${labels[version]} ${index + 1}` : labels[version],
1074
+ marks: side.marks,
1075
+ renderBlock,
1076
+ withContext: true,
1077
+ labels,
1078
+ version,
1079
+ loadAsset,
1080
+ sync
1081
+ })) : /* @__PURE__ */ React.createElement(BlockPreview, {
1082
+ title: labels[version],
1083
+ renderBlock,
1084
+ withContext: true,
1085
+ labels
1086
+ })))), toggle, setReviewed && /* @__PURE__ */ React.createElement("label", { className: "twgit-review-checked" }, /* @__PURE__ */ React.createElement("input", {
1087
+ type: "checkbox",
1088
+ checked: reviewed,
1089
+ onChange: (event) => setReviewed(event.currentTarget.checked)
1090
+ }), " ", labels.reviewed)));
1091
+ }
1092
+ function propertyNames(keys, labels) {
1093
+ return keys.map((key) => {
1094
+ var _properties$get, _properties$get2;
1095
+ return (_properties$get = (_properties$get2 = properties.get(key)) === null || _properties$get2 === void 0 ? void 0 : _properties$get2[labels === ja ? 0 : 1]) !== null && _properties$get !== void 0 ? _properties$get : key;
1096
+ }).join(" · ");
1097
+ }
1098
+ /**
1099
+ * The current costume (the backdrop for the stage) of each version of a target that has one. A changed costume shows
1100
+ * before → after. Nothing is shown until an image is read, so the heading falls back to the name alone.
1101
+ */
1102
+ function TargetThumbnails({ target, labels, loadAsset }) {
1103
+ const images = {
1104
+ before: useCostumeImage(target.before, "before", loadAsset),
1105
+ after: useCostumeImage(target.after, "after", loadAsset)
1106
+ };
1107
+ const costumes = {
1108
+ before: target.before && currentCostume(target.before),
1109
+ after: target.after && currentCostume(target.after)
1110
+ };
1111
+ const changed = !!costumes.before && !!costumes.after && costumes.before.md5ext !== costumes.after.md5ext;
1112
+ const shown = (changed ? ["before", "after"] : [images.after ? "after" : "before"]).filter((version) => images[version]);
1113
+ if (!shown.length) return null;
1114
+ return /* @__PURE__ */ React.createElement("span", { className: "twgit-review-thumbnails" }, shown.map((version, index) => {
1115
+ var _costumes$version, _costumes$version$nam, _costumes$version2;
1116
+ return /* @__PURE__ */ React.createElement(React.Fragment, { key: version }, index > 0 && /* @__PURE__ */ React.createElement("span", { "aria-hidden": "true" }, "→"), /* @__PURE__ */ React.createElement("img", {
1117
+ src: images[version],
1118
+ title: ((_costumes$version = costumes[version]) === null || _costumes$version === void 0 ? void 0 : _costumes$version.name) || void 0,
1119
+ alt: changed ? `${labels[version]}: ${(_costumes$version$nam = (_costumes$version2 = costumes[version]) === null || _costumes$version2 === void 0 ? void 0 : _costumes$version2.name) !== null && _costumes$version$nam !== void 0 ? _costumes$version$nam : ""}` : ""
1120
+ }));
1121
+ }));
1122
+ }
1123
+ /** Lists the changed stages and sprites of one project file, each with its code comparisons. */
1124
+ function ComparisonTargets({ comparison, labels, renderBlock, loadAsset, review }) {
1125
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, comparison.properties.length > 0 && /* @__PURE__ */ React.createElement("p", null, labels.properties, " · ", propertyNames(comparison.properties, labels)), !comparison.targets.length && !comparison.properties.length && /* @__PURE__ */ React.createElement("p", null, labels.noChanges), comparison.targets.map((target) => /* @__PURE__ */ React.createElement("section", {
1126
+ className: "twgit-review-target",
1127
+ key: JSON.stringify([target.isStage, target.name])
1128
+ }, /* @__PURE__ */ React.createElement("h5", null, /* @__PURE__ */ React.createElement(TargetThumbnails, {
1129
+ target,
1130
+ labels,
1131
+ loadAsset
1132
+ }), target.name, target.scripts.length > 0 && /* @__PURE__ */ React.createElement("small", null, labels.scriptCount(target.scripts.length))), target.properties.length > 0 && /* @__PURE__ */ React.createElement("p", null, labels.properties, " · ", propertyNames(target.properties, labels)), target.scripts.map((script) => {
1133
+ const key = JSON.stringify([
1134
+ review === null || review === void 0 ? void 0 : review.file,
1135
+ target.isStage,
1136
+ target.name,
1137
+ script.id
1138
+ ]);
1139
+ return /* @__PURE__ */ React.createElement(ScriptComparison, {
1140
+ key,
1141
+ script,
1142
+ labels,
1143
+ renderBlock,
1144
+ loadAsset,
1145
+ reviewed: review === null || review === void 0 ? void 0 : review.keys.has(key),
1146
+ setReviewed: review && ((value) => review.set(key, value))
1147
+ });
1148
+ }))));
1149
+ }
1150
+ var countScripts = (comparison) => comparison.targets.reduce((count, target) => count + target.scripts.length, 0);
1151
+ /** Compares two versions of a project.source.json by stage, sprite and code, as the pull request review does. */
1152
+ function ProjectSourceComparison({ before, after, renderBlock, locale = "en", fallback, loadAsset }) {
1153
+ const labels = locale.startsWith("ja") ? ja : en;
1154
+ const comparison = useMemo(() => {
1155
+ try {
1156
+ return compareProjectForReview(parseProjectSource(before), parseProjectSource(after));
1157
+ } catch (_unused) {
1158
+ return null;
1159
+ }
1160
+ }, [before, after]);
1161
+ if (!comparison) return fallback === void 0 ? /* @__PURE__ */ React.createElement("p", { role: "alert" }, labels.invalid) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("p", {
1162
+ role: "alert",
1163
+ className: "twgit-review-unreadable"
1164
+ }, labels.unreadable), fallback);
1165
+ return /* @__PURE__ */ React.createElement("section", {
1166
+ className: "twgit-block-diff twgit-review twgit-review-comparison",
1167
+ "aria-label": labels.comparison
1168
+ }, /* @__PURE__ */ React.createElement("header", { className: "twgit-review-overview" }, /* @__PURE__ */ React.createElement("dl", null, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("dt", null, labels.targets), /* @__PURE__ */ React.createElement("dd", null, comparison.targets.length)), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("dt", null, labels.scripts), /* @__PURE__ */ React.createElement("dd", null, countScripts(comparison))))), /* @__PURE__ */ React.createElement(ComparisonTargets, {
1169
+ comparison,
1170
+ labels,
1171
+ renderBlock,
1172
+ loadAsset
1173
+ }));
1174
+ }
1175
+ function ProjectSourceReview({ title, description, verification, files, renderBlock, locale = "en" }) {
1176
+ const labels = locale.startsWith("ja") ? ja : en;
1177
+ const entries = useMemo(() => files.map((file) => {
1178
+ if (!("patch" in file)) return { file };
1179
+ try {
1180
+ const projects = readProjectSourceDiff(file.patch, file.beforeSource);
1181
+ return {
1182
+ file,
1183
+ comparison: projects ? compareProjectForReview(...projects) : {
1184
+ targets: [],
1185
+ properties: []
1186
+ }
1187
+ };
1188
+ } catch (_unused2) {
1189
+ return {
1190
+ file,
1191
+ invalid: true
1192
+ };
1193
+ }
1194
+ }), [files]);
1195
+ const [checked, setChecked] = useState();
1196
+ const reviewed = (checked === null || checked === void 0 ? void 0 : checked.entries) === entries ? checked.keys : /* @__PURE__ */ new Set();
1197
+ const setReviewed = (key, value) => setChecked((previous) => {
1198
+ const keys = new Set((previous === null || previous === void 0 ? void 0 : previous.entries) === entries ? previous.keys : []);
1199
+ if (value) keys.add(key);
1200
+ else keys.delete(key);
1201
+ return {
1202
+ entries,
1203
+ keys
1204
+ };
1205
+ });
1206
+ const targets = entries.reduce((total, entry) => {
1207
+ var _entry$comparison$tar, _entry$comparison;
1208
+ return total + ((_entry$comparison$tar = (_entry$comparison = entry.comparison) === null || _entry$comparison === void 0 ? void 0 : _entry$comparison.targets.length) !== null && _entry$comparison$tar !== void 0 ? _entry$comparison$tar : 0);
1209
+ }, 0);
1210
+ const scripts = entries.reduce((total, entry) => total + (entry.comparison ? countScripts(entry.comparison) : 0), 0);
1211
+ const body = description === null || description === void 0 ? void 0 : description.trim();
1212
+ const longBody = !!body && body.length > 420;
1213
+ const excerpt = longBody ? body.slice(0, 250).trimEnd() + "…" : body;
1214
+ return /* @__PURE__ */ React.createElement("section", {
1215
+ className: "twgit-block-diff twgit-review",
1216
+ "aria-label": labels.review
1217
+ }, /* @__PURE__ */ React.createElement("header", { className: "twgit-review-overview" }, /* @__PURE__ */ React.createElement("p", null, labels.review), /* @__PURE__ */ React.createElement("h2", null, title), excerpt && /* @__PURE__ */ React.createElement("p", { className: "twgit-review-description" }, excerpt), /* @__PURE__ */ React.createElement("dl", null, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("dt", null, labels.files), /* @__PURE__ */ React.createElement("dd", null, entries.length)), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("dt", null, labels.targets), /* @__PURE__ */ React.createElement("dd", null, targets)), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("dt", null, labels.scripts), /* @__PURE__ */ React.createElement("dd", null, scripts)), scripts > 0 && /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("dt", null, labels.reviewed), /* @__PURE__ */ React.createElement("dd", null, reviewed.size, " / ", scripts))), longBody && /* @__PURE__ */ React.createElement("details", { className: "twgit-review-body" }, /* @__PURE__ */ React.createElement("summary", null, labels.fullDescription), /* @__PURE__ */ React.createElement("p", { className: "twgit-review-description" }, body)), !!(verification === null || verification === void 0 ? void 0 : verification.length) && /* @__PURE__ */ React.createElement("div", { className: "twgit-review-checklist" }, /* @__PURE__ */ React.createElement("h3", null, labels.checks), /* @__PURE__ */ React.createElement("ul", null, verification.map((item, index) => /* @__PURE__ */ React.createElement("li", { key: index }, item))))), /* @__PURE__ */ React.createElement("h3", { className: "twgit-review-files-title" }, labels.files), entries.map(({ file, comparison, invalid }) => /* @__PURE__ */ React.createElement("section", {
1218
+ key: file.path,
1219
+ className: "twgit-review-file"
1220
+ }, /* @__PURE__ */ React.createElement("header", null, /* @__PURE__ */ React.createElement("h4", null, file.previousPath ? file.previousPath + " → " + file.path : file.path), file.summary && /* @__PURE__ */ React.createElement("p", null, file.summary)), "status" in file && /* @__PURE__ */ React.createElement("p", null, labels.related, " · ", labels[file.status]), (invalid || file.unavailable) && /* @__PURE__ */ React.createElement("p", { role: "alert" }, labels.invalid), comparison && /* @__PURE__ */ React.createElement(ComparisonTargets, {
1221
+ comparison,
1222
+ labels,
1223
+ renderBlock,
1224
+ loadAsset: "loadAsset" in file ? file.loadAsset : void 0,
1225
+ review: {
1226
+ file: file.path,
1227
+ keys: reviewed,
1228
+ set: setReviewed
1229
+ }
1230
+ }))));
1231
+ }
1232
+ // src/github-pull-requests.ts
1233
+ init_objectSpread2();
1234
+ var PullRequestError = class extends Error {
1235
+ constructor(code) {
1236
+ super(code);
1237
+ this.code = code;
1238
+ }
1239
+ };
1240
+ var repositoryName = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
1241
+ var validRepository = (value) => typeof value === "string" && repositoryName.test(value) && value.split("/").every((segment) => segment !== "." && segment !== "..");
1242
+ var isSha = (value) => typeof value === "string" && /^[a-f0-9]{40}$/i.test(value);
1243
+ function gitHubRepositoryFromOrigin(origin) {
1244
+ var _origin$trim$match;
1245
+ const name = origin === null || origin === void 0 || (_origin$trim$match = origin.trim().match(/^(?:https:\/\/github\.com\/|ssh:\/\/git@github\.com\/|git@github\.com:)([^/?#]+\/[^/?#]+)\/?$/i)) === null || _origin$trim$match === void 0 ? void 0 : _origin$trim$match[1];
1246
+ const repository = name === null || name === void 0 ? void 0 : name.replace(/\.git$/i, "");
1247
+ return validRepository(repository) ? repository : null;
1248
+ }
1249
+ function repoPath(repository) {
1250
+ return "/repos/" + repository.split("/").map(encodeURIComponent).join("/");
1251
+ }
1252
+ var invalidResponse = () => new PullRequestError("failed");
1253
+ var emptyProject = "{\"targets\":[]}\n";
1254
+ var sourceLimit = 1048576;
1255
+ async function readBody(response, limit, chunk) {
1256
+ if (Number(response.headers.get("Content-Length")) > limit || !response.body) throw invalidResponse();
1257
+ const reader = response.body.getReader();
1258
+ let length = 0;
1259
+ try {
1260
+ for (;;) {
1261
+ const { done, value } = await reader.read();
1262
+ if (done) return;
1263
+ length += value.byteLength;
1264
+ if (length > limit) throw invalidResponse();
1265
+ chunk(value);
1266
+ }
1267
+ } catch (error) {
1268
+ reader.cancel().catch(() => {});
1269
+ throw error;
1270
+ }
1271
+ }
1272
+ async function readText(response, limit) {
1273
+ const decoder = new TextDecoder("utf-8", { fatal: true });
1274
+ const chunks = [];
1275
+ await readBody(response, limit, (value) => chunks.push(decoder.decode(value, { stream: true })));
1276
+ return chunks.join("") + decoder.decode();
1277
+ }
1278
+ function readSummary(value) {
1279
+ if (!isRecord(value) || !Number.isSafeInteger(value.number) || Number(value.number) < 1 || typeof value.title !== "string") throw invalidResponse();
1280
+ return {
1281
+ number: Number(value.number),
1282
+ title: value.title
1283
+ };
1284
+ }
1285
+ function readRevision(value) {
1286
+ if (!isRecord(value) || !isSha(value.sha) || typeof value.ref !== "string") throw invalidResponse();
1287
+ const repository = isRecord(value.repo) ? value.repo.full_name : null;
1288
+ if (repository !== null && !validRepository(repository)) throw invalidResponse();
1289
+ return {
1290
+ sha: value.sha,
1291
+ ref: value.ref,
1292
+ repository
1293
+ };
1294
+ }
1295
+ function readPullRequest(value) {
1296
+ const summary = readSummary(value);
1297
+ if (!isRecord(value) || value.state !== "open" && value.state !== "closed" || !Number.isSafeInteger(value.changed_files) || Number(value.changed_files) < 0) throw invalidResponse();
1298
+ const author = isRecord(value.user) && typeof value.user.login === "string" ? value.user.login : void 0;
1299
+ const description = typeof value.body_text === "string" ? value.body_text : typeof value.body === "string" ? value.body : "";
1300
+ const state = value.merged_at ? "merged" : value.draft ? "draft" : value.state;
1301
+ return _objectSpread2(_objectSpread2({}, summary), {}, {
1302
+ author,
1303
+ description,
1304
+ base: readRevision(value.base),
1305
+ head: readRevision(value.head),
1306
+ state,
1307
+ changedFiles: Number(value.changed_files)
1308
+ });
1309
+ }
1310
+ function readFile(value) {
1311
+ if (!isRecord(value) || typeof value.filename !== "string" || typeof value.status !== "string") throw invalidResponse();
1312
+ return {
1313
+ path: value.filename,
1314
+ previous: typeof value.previous_filename === "string" ? value.previous_filename : void 0,
1315
+ status: value.status
1316
+ };
1317
+ }
1318
+ function contentPath(path) {
1319
+ const parts = path.split("/");
1320
+ if (parts.some((part) => !part || part === "." || part === "..")) throw invalidResponse();
1321
+ return parts.map(encodeURIComponent).join("/");
1322
+ }
1323
+ /**
1324
+ * Reads the assets of both versions of a project file from raw.githubusercontent.com, which does not count against the
1325
+ * API rate limit. Each version's sb3-source.json names its assets directory. Every read is capped, honors both its own
1326
+ * signal and the pull request's, and resolves `null` on any failure.
1327
+ */
1328
+ function rawAssetLoader(versions, send, review) {
1329
+ const directories = /* @__PURE__ */ new Map();
1330
+ const get = (url, signal) => send(url, {
1331
+ credentials: "omit",
1332
+ redirect: "error",
1333
+ signal
1334
+ });
1335
+ const folderUrl = ({ repository, sha, path }) => "https://raw.githubusercontent.com/" + repository.split("/").map(encodeURIComponent).join("/") + "/" + sha + "/" + contentPath(path).split("/").slice(0, -1).map((part) => part + "/").join("");
1336
+ const readDirectory = async (folder, signal) => {
1337
+ let settings = null;
1338
+ try {
1339
+ var _response$body;
1340
+ const response = await get(folder + "sb3-source.json", signal);
1341
+ if (response.ok) settings = await readText(response, folderSettingsLimit);
1342
+ else (_response$body = response.body) === null || _response$body === void 0 || _response$body.cancel().catch(() => {});
1343
+ } catch (error) {
1344
+ if (signal.aborted) throw error;
1345
+ }
1346
+ return assetsDirectoryParts(settings);
1347
+ };
1348
+ return async ({ version, md5ext, signal: own }) => {
1349
+ const location = versions[version];
1350
+ if (!location || !isAssetName(md5ext)) return null;
1351
+ try {
1352
+ const signal = AbortSignal.any([own, review]);
1353
+ if (signal.aborted) return null;
1354
+ const folder = folderUrl(location);
1355
+ let directory = directories.get(version);
1356
+ if (!directory) {
1357
+ directory = readDirectory(folder, signal);
1358
+ directories.set(version, directory);
1359
+ const pending = directory;
1360
+ pending.catch(() => {
1361
+ if (directories.get(version) === pending) directories.delete(version);
1362
+ });
1363
+ }
1364
+ const parts = await directory;
1365
+ if (!parts || signal.aborted) return null;
1366
+ const response = await get(folder + parts.map(encodeURIComponent).join("/") + "/" + md5ext, signal);
1367
+ if (!response.ok) {
1368
+ var _response$body2;
1369
+ (_response$body2 = response.body) === null || _response$body2 === void 0 || _response$body2.cancel().catch(() => {});
1370
+ return null;
1371
+ }
1372
+ const chunks = [];
1373
+ await readBody(response, assetLimit, (value) => chunks.push(value));
1374
+ return signal.aborted ? null : new Blob(chunks);
1375
+ } catch (_unused) {
1376
+ return null;
1377
+ }
1378
+ };
1379
+ }
1380
+ function createGitHubPullRequests(repository, send = fetch) {
1381
+ if (!validRepository(repository)) throw invalidResponse();
1382
+ const api = "https://api.github.com";
1383
+ const repo = repoPath(repository);
1384
+ const request = async (path, signal, accept = "application/vnd.github+json") => {
1385
+ const response = await send(api + path, {
1386
+ headers: { Accept: accept },
1387
+ credentials: "omit",
1388
+ redirect: "error",
1389
+ signal
1390
+ });
1391
+ if (!response.ok) {
1392
+ if (response.status === 429 || response.status === 403 && (response.headers.get("X-RateLimit-Remaining") === "0" || response.headers.has("Retry-After"))) throw new PullRequestError("rateLimit");
1393
+ if (response.status === 404 || response.status === 403) throw new PullRequestError("unavailable");
1394
+ throw invalidResponse();
1395
+ }
1396
+ return response;
1397
+ };
1398
+ const json = async (path, signal, accept) => JSON.parse(await readText(await request(path, signal, accept), 12 * sourceLimit));
1399
+ const metadata = async (number, signal) => readPullRequest(await json(repo + "/pulls/" + number, signal, "application/vnd.github.text+json"));
1400
+ const source = async (name, path, revision, signal) => readText(await request(repoPath(name) + "/contents/" + contentPath(path) + "?ref=" + revision, signal, "application/vnd.github.raw+json"), sourceLimit);
1401
+ return {
1402
+ async list(page, signal) {
1403
+ const result = await json(repo + "/pulls?state=all&sort=updated&per_page=20&page=" + page, signal);
1404
+ if (!Array.isArray(result)) throw invalidResponse();
1405
+ return {
1406
+ items: result.map(readSummary),
1407
+ more: result.length === 20
1408
+ };
1409
+ },
1410
+ async load(number, signal) {
1411
+ if (!Number.isSafeInteger(number) || number < 1) throw invalidResponse();
1412
+ const pr = await metadata(number, signal);
1413
+ if (pr.number !== number) throw invalidResponse();
1414
+ if (pr.changedFiles > 3e3) throw new PullRequestError("tooManyFiles");
1415
+ const records = [];
1416
+ for (let page = 1; page <= Math.ceil(pr.changedFiles / 100); page++) {
1417
+ const result = await json(repo + "/pulls/" + number + "/files?per_page=100&page=" + page, signal);
1418
+ if (!Array.isArray(result)) throw invalidResponse();
1419
+ records.push(...result.map(readFile));
1420
+ }
1421
+ const current = await metadata(number, signal);
1422
+ if (current.base.sha !== pr.base.sha || current.head.sha !== pr.head.sha || current.changedFiles !== pr.changedFiles || records.length !== pr.changedFiles) throw new PullRequestError("changed");
1423
+ let base;
1424
+ if (records.some((file) => file.path.split("/").pop() === "project.source.json" && file.status !== "added" && file.status !== "copied")) try {
1425
+ const comparison = await json(repo + "/compare/" + pr.base.sha + "..." + pr.head.sha + "?per_page=1", signal);
1426
+ const merge = isRecord(comparison) ? comparison.merge_base_commit : null;
1427
+ if (isRecord(merge) && isSha(merge.sha)) base = merge.sha;
1428
+ } catch (error) {
1429
+ if (signal.aborted) throw error;
1430
+ }
1431
+ const files = [];
1432
+ for (const file of records) {
1433
+ const status = file.status === "removed" ? "removed" : file.status === "added" || file.status === "copied" ? "added" : "updated";
1434
+ const related = {
1435
+ path: file.path,
1436
+ status,
1437
+ previousPath: file.previous
1438
+ };
1439
+ if (file.path.split("/").pop() !== "project.source.json") {
1440
+ files.push(related);
1441
+ continue;
1442
+ }
1443
+ try {
1444
+ var _file$previous, _pr$head$repository, _file$previous2, _pr$head$repository2;
1445
+ if (status !== "added" && !base) throw invalidResponse();
1446
+ const [before, after] = await Promise.all([status === "added" ? emptyProject : source(repository, (_file$previous = file.previous) !== null && _file$previous !== void 0 ? _file$previous : file.path, base, signal), status === "removed" ? emptyProject : source((_pr$head$repository = pr.head.repository) !== null && _pr$head$repository !== void 0 ? _pr$head$repository : repository, file.path, pr.head.sha, signal)]);
1447
+ const loadAsset = rawAssetLoader({
1448
+ before: status === "added" ? null : {
1449
+ repository,
1450
+ sha: base,
1451
+ path: (_file$previous2 = file.previous) !== null && _file$previous2 !== void 0 ? _file$previous2 : file.path
1452
+ },
1453
+ after: status === "removed" ? null : {
1454
+ repository: (_pr$head$repository2 = pr.head.repository) !== null && _pr$head$repository2 !== void 0 ? _pr$head$repository2 : repository,
1455
+ sha: pr.head.sha,
1456
+ path: file.path
1457
+ }
1458
+ }, send, signal);
1459
+ files.push({
1460
+ path: file.path,
1461
+ previousPath: file.previous,
1462
+ patch: createProjectSourceDiff(before, after),
1463
+ loadAsset
1464
+ });
1465
+ } catch (error) {
1466
+ if (signal.aborted) throw error;
1467
+ files.push(_objectSpread2(_objectSpread2({}, related), {}, { unavailable: true }));
1468
+ }
1469
+ }
1470
+ return _objectSpread2(_objectSpread2({}, pr), {}, {
1471
+ files,
1472
+ url: "https://github.com/" + repository + "/pull/" + number
1473
+ });
1474
+ }
1475
+ };
1476
+ }
1477
+ // src/block-diff-location.ts
1478
+ function markBlockLocation(block, parts) {
1479
+ const root = block.getSvgRoot();
1480
+ if (!root) return;
1481
+ root.classList.add("twgit-changed-block");
1482
+ const marked = /* @__PURE__ */ new Set();
1483
+ for (const { kind, name } of parts) {
1484
+ var _block$getInputTarget, _block$getField;
1485
+ const part = kind === "input" ? (_block$getInputTarget = block.getInputTargetBlock) === null || _block$getInputTarget === void 0 ? void 0 : _block$getInputTarget.call(block, name) : (_block$getField = block.getField) === null || _block$getField === void 0 ? void 0 : _block$getField.call(block, name);
1486
+ const element = part === null || part === void 0 ? void 0 : part.getSvgRoot();
1487
+ if (!element || element === root || marked.has(element)) continue;
1488
+ element.classList.add("twgit-changed-block-part");
1489
+ marked.add(element);
1490
+ }
1491
+ return () => {
1492
+ root.classList.remove("twgit-changed-block");
1493
+ for (const element of marked) element.classList.remove("twgit-changed-block-part");
1494
+ };
1495
+ }
1496
+ /** Resolve the matching project revision and target in the host before navigating its workspace. */
1497
+ function createScratchBlocksDiffLocator(resolveWorkspace) {
1498
+ return async (location) => {
1499
+ const workspace = await resolveWorkspace(location);
1500
+ if (!workspace || location.signal.aborted) return false;
1501
+ const block = workspace.getBlockById(location.id);
1502
+ const expected = location.target.blocks[location.id];
1503
+ if (!block || isRecord(expected) && typeof expected.opcode === "string" && block.type !== expected.opcode) return false;
1504
+ const clear = markBlockLocation(block, location.parts);
1505
+ if (!clear) return false;
1506
+ try {
1507
+ workspace.centerOnBlock(location.id);
1508
+ } catch (error) {
1509
+ clear();
1510
+ throw error;
1511
+ }
1512
+ if (location.signal.aborted) {
1513
+ clear();
1514
+ return false;
1515
+ }
1516
+ location.signal.addEventListener("abort", clear, { once: true });
1517
+ return true;
1518
+ };
1519
+ }
1520
+ // src/scratch-block-diff-renderer.ts
1521
+ init_objectSpread2();
1522
+ function scriptRoot({ target, id }) {
1523
+ const visited = /* @__PURE__ */ new Set();
1524
+ for (;;) {
1525
+ const block = target.blocks[id];
1526
+ if (!isRecord(block) || typeof block.parent !== "string" || !Object.hasOwn(target.blocks, block.parent)) return id;
1527
+ if (visited.has(id)) throw new Error("Cyclic block parents.");
1528
+ visited.add(id);
1529
+ id = block.parent;
1530
+ }
1531
+ }
1532
+ function safeMutation(value, root, depth = 0) {
1533
+ if (!isRecord(value) || depth > 32 || value.tagName !== (root ? "mutation" : "arg") || !Array.isArray(value.children)) return false;
1534
+ return Object.entries(value).every(([key, item]) => key === "children" || /^[a-zA-Z][\w-]*$/.test(key) && [
1535
+ "string",
1536
+ "number",
1537
+ "boolean"
1538
+ ].includes(typeof item)) && value.children.every((child) => safeMutation(child, false, depth + 1));
1539
+ }
1540
+ function selectBlocks({ target, id }, root, withContext) {
1541
+ const selected = Object.create(null);
1542
+ const visiting = /* @__PURE__ */ new Set();
1543
+ let blockCount = 0;
1544
+ function visit(blockId, followNext, depth) {
1545
+ if (visiting.has(blockId) || depth > 128) throw new Error("Block preview exceeds its graph limit.");
1546
+ if (Object.hasOwn(selected, blockId) || !Object.hasOwn(target.blocks, blockId)) return;
1547
+ if (++blockCount > 800) throw new Error("Block preview exceeds its graph limit.");
1548
+ if (blockId === "__proto__") throw new Error("Invalid block ID.");
1549
+ const data = target.blocks[blockId];
1550
+ if (Array.isArray(data)) {
1551
+ selected[blockId] = data.length > 3 ? [
1552
+ ...data.slice(0, 3),
1553
+ 20,
1554
+ 20
1555
+ ] : [...data];
1556
+ return;
1557
+ }
1558
+ if (!isRecord(data) || typeof data.opcode !== "string") throw new Error("Invalid Scratch block.");
1559
+ const block = structuredClone(data);
1560
+ if (block.mutation && !safeMutation(block.mutation, true)) throw new Error("Unsupported block mutation.");
1561
+ block.x = 20;
1562
+ block.y = 20;
1563
+ block.topLevel = blockId === root;
1564
+ if (blockId === root) block.shadow = false;
1565
+ delete block.comment;
1566
+ if (!followNext) block.next = null;
1567
+ selected[blockId] = block;
1568
+ visiting.add(blockId);
1569
+ if (isRecord(block.inputs)) for (const [name, input] of Object.entries(block.inputs)) {
1570
+ if (!withContext && /^SUBSTACK\d*$/.test(name)) {
1571
+ delete block.inputs[name];
1572
+ continue;
1573
+ }
1574
+ if (!Array.isArray(input)) continue;
1575
+ for (const child of input.slice(1, 3)) if (typeof child === "string") visit(child, withContext, depth + 1);
1576
+ }
1577
+ if (typeof block.next === "string") visit(block.next, true, depth + 1);
1578
+ visiting.delete(blockId);
1579
+ }
1580
+ visit(root, withContext, 0);
1581
+ if (!Object.hasOwn(selected, id)) throw new Error("The changed block is not reachable.");
1582
+ return selected;
1583
+ }
1584
+ var placeholderPrefix = "twgit_review_placeholder_";
1585
+ var placeholderSerial = 0;
1586
+ function statementInput(element) {
1587
+ var _element$getAttribute;
1588
+ return element.localName === "statement" || /^SUBSTACK\d*$/.test((_element$getAttribute = element.getAttribute("name")) !== null && _element$getAttribute !== void 0 ? _element$getAttribute : "");
1589
+ }
1590
+ function placeholderDefinition(element, opcode, type, blockly, locale) {
1591
+ const args = [{
1592
+ type: "field_label",
1593
+ text: (locale.startsWith("ja") ? "未登録: " : "Unregistered: ") + opcode.slice(0, 120)
1594
+ }];
1595
+ const branches = [];
1596
+ for (const child of Array.from(element.children)) {
1597
+ const name = child.getAttribute("name");
1598
+ if (!name) continue;
1599
+ if (child.localName === "field") args.push({
1600
+ type: "field_input",
1601
+ name,
1602
+ text: ""
1603
+ });
1604
+ else if (statementInput(child)) branches.push(name);
1605
+ else if (child.localName === "value") args.push({
1606
+ type: "input_value",
1607
+ name
1608
+ });
1609
+ }
1610
+ const parent = element.parentElement;
1611
+ const reporter = (parent === null || parent === void 0 ? void 0 : parent.localName) === "value" && !statementInput(parent);
1612
+ const definition = _objectSpread2({
1613
+ type,
1614
+ message0: args.map((_, index) => "%" + (index + 1)).join(" "),
1615
+ args0: args,
1616
+ inputsInline: true,
1617
+ colour: "#687488",
1618
+ colourSecondary: "#566274",
1619
+ colourTertiary: "#4b5564"
1620
+ }, reporter ? {
1621
+ output: null,
1622
+ outputShape: blockly.OUTPUT_SHAPE_ROUND
1623
+ } : {
1624
+ previousStatement: null,
1625
+ nextStatement: null
1626
+ });
1627
+ for (const [index, name] of branches.entries()) {
1628
+ definition["message" + (index + 1)] = "%1";
1629
+ definition["args" + (index + 1)] = [{
1630
+ type: "input_statement",
1631
+ name
1632
+ }];
1633
+ }
1634
+ return definition;
1635
+ }
1636
+ var defaultScale = .675;
1637
+ var padding = 16;
1638
+ var minHeight = 160;
1639
+ var maxHeight = 420;
1640
+ /**
1641
+ * Sizes a preview frame to its blocks and reports scrolling and zooming, so both versions can move together.
1642
+ * The frame keeps a gutter on the right for the zoom buttons, and for the minimap and thumbnail when present.
1643
+ */
1644
+ function createView(workspace, blockly, surface, frame, startScale, navigation) {
1645
+ var _bounds$x, _bounds$y, _workspace$translate, _surface$ownerDocumen;
1646
+ const bounds = workspace.getBlocksBoundingBox();
1647
+ const left = (_bounds$x = bounds.x) !== null && _bounds$x !== void 0 ? _bounds$x : 0, top = (_bounds$y = bounds.y) !== null && _bounds$y !== void 0 ? _bounds$y : 0;
1648
+ const gutter = navigation ? 150 : 56;
1649
+ const width = frame.clientWidth || 400;
1650
+ const scale = Math.max(.25, Math.min(startScale, (width - padding - gutter) / Math.max(1, bounds.width), 388 / Math.max(1, bounds.height)));
1651
+ const fit = {
1652
+ scale,
1653
+ height: Math.min(maxHeight, Math.max(minHeight, Math.ceil(bounds.height * scale + 32)))
1654
+ };
1655
+ const listeners = /* @__PURE__ */ new Set();
1656
+ const translate = (_workspace$translate = workspace.translate) === null || _workspace$translate === void 0 ? void 0 : _workspace$translate.bind(workspace);
1657
+ let quiet = false;
1658
+ if (translate) workspace.translate = (x, y) => {
1659
+ translate(x, y);
1660
+ if (!quiet) for (const listener of listeners) listener();
1661
+ };
1662
+ const setViewport = ({ scale, x, y }) => {
1663
+ if (!translate) return;
1664
+ quiet = true;
1665
+ try {
1666
+ var _workspace$getMetrics, _metrics$absoluteLeft, _metrics$absoluteTop, _workspace$scrollbar;
1667
+ workspace.scale = scale;
1668
+ workspace.scrollX = x;
1669
+ workspace.scrollY = y;
1670
+ const metrics = (_workspace$getMetrics = workspace.getMetrics) === null || _workspace$getMetrics === void 0 ? void 0 : _workspace$getMetrics.call(workspace);
1671
+ translate(x + ((_metrics$absoluteLeft = metrics === null || metrics === void 0 ? void 0 : metrics.absoluteLeft) !== null && _metrics$absoluteLeft !== void 0 ? _metrics$absoluteLeft : 0), y + ((_metrics$absoluteTop = metrics === null || metrics === void 0 ? void 0 : metrics.absoluteTop) !== null && _metrics$absoluteTop !== void 0 ? _metrics$absoluteTop : 0));
1672
+ (_workspace$scrollbar = workspace.scrollbar) === null || _workspace$scrollbar === void 0 || _workspace$scrollbar.resize();
1673
+ } finally {
1674
+ quiet = false;
1675
+ }
1676
+ };
1677
+ const layout = ({ scale, height }) => {
1678
+ frame.style.height = height + "px";
1679
+ blockly.svgResize(workspace);
1680
+ setViewport({
1681
+ scale,
1682
+ x: padding - left * scale,
1683
+ y: padding - top * scale
1684
+ });
1685
+ };
1686
+ layout(fit);
1687
+ const resize = new ((_surface$ownerDocumen = surface.ownerDocument.defaultView) !== null && _surface$ownerDocumen !== void 0 ? _surface$ownerDocumen : globalThis).ResizeObserver(() => blockly.svgResize(workspace));
1688
+ resize.observe(surface);
1689
+ const wheel = (event) => {
1690
+ if (!event.ctrlKey) event.stopPropagation();
1691
+ };
1692
+ surface.addEventListener("wheel", wheel, { capture: true });
1693
+ return {
1694
+ api: {
1695
+ fit,
1696
+ layout,
1697
+ setViewport,
1698
+ viewport: () => {
1699
+ var _workspace$scale, _workspace$scrollX, _workspace$scrollY;
1700
+ return {
1701
+ scale: (_workspace$scale = workspace.scale) !== null && _workspace$scale !== void 0 ? _workspace$scale : fit.scale,
1702
+ x: (_workspace$scrollX = workspace.scrollX) !== null && _workspace$scrollX !== void 0 ? _workspace$scrollX : 0,
1703
+ y: (_workspace$scrollY = workspace.scrollY) !== null && _workspace$scrollY !== void 0 ? _workspace$scrollY : 0
1704
+ };
1705
+ },
1706
+ onChange(listener) {
1707
+ listeners.add(listener);
1708
+ return () => listeners.delete(listener);
1709
+ }
1710
+ },
1711
+ dispose() {
1712
+ frame.style.removeProperty("height");
1713
+ listeners.clear();
1714
+ resize.disconnect();
1715
+ surface.removeEventListener("wheel", wheel, { capture: true });
1716
+ if (translate) workspace.translate = translate;
1717
+ }
1718
+ };
1719
+ }
1720
+ /** Use the host's Scratch VM serializer and Scratch Blocks definitions in an isolated workspace. */
1721
+ function createScratchBlocksDiffRenderer(options) {
1722
+ return async ({ container, target, id, withContext, parts = [], marks, signal, watermark }) => {
1723
+ var _Object$values$find;
1724
+ const blockly = await options.getBlockly();
1725
+ if (signal.aborted) return;
1726
+ const root = withContext ? scriptRoot({
1727
+ target,
1728
+ id
1729
+ }) : id;
1730
+ const selected = selectBlocks({
1731
+ target,
1732
+ id
1733
+ }, root, withContext);
1734
+ const compactRoot = Array.isArray(selected[root]);
1735
+ const blocks = options.deserializeBlocks(selected);
1736
+ const serializedRoot = compactRoot ? (_Object$values$find = Object.values(blocks).find((block) => block.topLevel)) === null || _Object$values$find === void 0 ? void 0 : _Object$values$find.id : root;
1737
+ if (!serializedRoot) throw new Error("Missing Scratch block.");
1738
+ const vmBlocks = options.createBlockContainer();
1739
+ for (const block of Object.values(blocks)) vmBlocks.createBlock(block, true);
1740
+ const xml = vmBlocks.blockToXML(serializedRoot);
1741
+ if (!xml) throw new Error("Missing Scratch block XML.");
1742
+ const dom = blockly.Xml.textToDom("<xml>" + xml + "</xml>");
1743
+ const main = blockly.getMainWorkspace();
1744
+ const surface = container.ownerDocument.createElement("div");
1745
+ surface.className = "twgit-block-diff-surface";
1746
+ container.append(surface);
1747
+ let workspace;
1748
+ let view;
1749
+ let navigation;
1750
+ const cleanups = [];
1751
+ const placeholders = [];
1752
+ const dispose = () => {
1753
+ const active = blockly.getMainWorkspace();
1754
+ for (const cleanup of cleanups.splice(0)) cleanup();
1755
+ blockly.Events.disable();
1756
+ try {
1757
+ navigation === null || navigation === void 0 || navigation.dispose();
1758
+ workspace === null || workspace === void 0 || workspace.dispose();
1759
+ } finally {
1760
+ if (active === workspace) blockly.mainWorkspace = main;
1761
+ for (const type of placeholders) delete blockly.Blocks[type];
1762
+ blockly.Events.enable();
1763
+ surface.remove();
1764
+ }
1765
+ };
1766
+ blockly.Events.disable();
1767
+ try {
1768
+ var _main$options$zoomOpt, _main$options, _main$options2, _options$attachNaviga;
1769
+ for (const element of Array.from(dom.querySelectorAll("block,shadow"))) {
1770
+ var _element$getAttribute2, _options$locale;
1771
+ const opcode = (_element$getAttribute2 = element.getAttribute("type")) !== null && _element$getAttribute2 !== void 0 ? _element$getAttribute2 : "";
1772
+ if (!opcode.startsWith(placeholderPrefix) && Object.hasOwn(blockly.Blocks, opcode)) continue;
1773
+ if (!options.showUnregisteredBlocks || !blockly.defineBlocksWithJsonArray) throw new Error("Unregistered Scratch block.");
1774
+ let alias;
1775
+ do
1776
+ alias = placeholderPrefix + ++placeholderSerial;
1777
+ while (Object.hasOwn(blockly.Blocks, alias));
1778
+ placeholders.push(alias);
1779
+ blockly.defineBlocksWithJsonArray([placeholderDefinition(element, opcode, alias, blockly, (_options$locale = options.locale) !== null && _options$locale !== void 0 ? _options$locale : container.ownerDocument.documentElement.lang)]);
1780
+ element.setAttribute("type", alias);
1781
+ }
1782
+ const startScale = (_main$options$zoomOpt = main === null || main === void 0 || (_main$options = main.options) === null || _main$options === void 0 || (_main$options = _main$options.zoomOptions) === null || _main$options === void 0 ? void 0 : _main$options.startScale) !== null && _main$options$zoomOpt !== void 0 ? _main$options$zoomOpt : defaultScale;
1783
+ workspace = blockly.inject(surface, {
1784
+ readOnly: true,
1785
+ sounds: false,
1786
+ media: main === null || main === void 0 || (_main$options2 = main.options) === null || _main$options2 === void 0 ? void 0 : _main$options2.pathToMedia,
1787
+ scrollbars: true,
1788
+ zoom: {
1789
+ controls: true,
1790
+ wheel: true,
1791
+ startScale,
1792
+ maxScale: 4,
1793
+ minScale: .25,
1794
+ scaleSpeed: 1.2
1795
+ }
1796
+ });
1797
+ workspace.markFocused = () => {};
1798
+ blockly.Xml.domToWorkspace(dom, workspace);
1799
+ if (marks) for (const mark of marks) {
1800
+ var _changed$getSvgRoot;
1801
+ const changed = workspace.getBlockById(mark.id === root ? serializedRoot : mark.id);
1802
+ if (!changed) continue;
1803
+ (_changed$getSvgRoot = changed.getSvgRoot()) === null || _changed$getSvgRoot === void 0 || _changed$getSvgRoot.classList.add("twgit-block-diff-" + mark.kind);
1804
+ markBlockLocation(changed, mark.parts);
1805
+ }
1806
+ else {
1807
+ const changed = workspace.getBlockById(id === root ? serializedRoot : id);
1808
+ if (changed) markBlockLocation(changed, parts);
1809
+ }
1810
+ view = createView(workspace, blockly, surface, container, startScale, !!options.attachNavigation);
1811
+ cleanups.push(view.dispose);
1812
+ navigation = (_options$attachNaviga = options.attachNavigation) === null || _options$attachNaviga === void 0 ? void 0 : _options$attachNaviga.call(options, workspace, blockly, watermark);
1813
+ if (navigation && watermark) {
1814
+ watermark.dataset.navigation = "";
1815
+ cleanups.push(() => delete watermark.dataset.navigation);
1816
+ }
1817
+ } catch (error) {
1818
+ dispose();
1819
+ throw error;
1820
+ } finally {
1821
+ blockly.mainWorkspace = main;
1822
+ blockly.Events.enable();
1823
+ }
1824
+ return {
1825
+ dispose,
1826
+ view: view.api
1827
+ };
1828
+ };
1829
+ }
1830
+ export { gitHubRepositoryFromOrigin as a, BlockPreview as c, currentCostume as d, compareProjectBlocks as f, createGitHubPullRequests as i, createPreviewSync as l, readProjectSourceDiff as m, createScratchBlocksDiffLocator as n, ProjectSourceComparison as o, createProjectSourceDiff as p, PullRequestError as r, ProjectSourceReview as s, createScratchBlocksDiffRenderer as t, usePreviewSync as u };