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