@kubohiroya/turbowarp-plus-editor-git 0.11.0 → 0.12.1

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.
@@ -912,7 +912,9 @@ function groupScripts(before, after, changes) {
912
912
  const group = (_groups$get = groups.get(id)) !== null && _groups$get !== void 0 ? _groups$get : {
913
913
  id,
914
914
  before: [],
915
- after: []
915
+ after: [],
916
+ kind: "updated",
917
+ summary: []
916
918
  };
917
919
  const side = {
918
920
  target,
@@ -924,20 +926,151 @@ function groupScripts(before, after, changes) {
924
926
  sides.set(key, side);
925
927
  }
926
928
  }
927
- const affected = /* @__PURE__ */ new Set();
929
+ const affected = /* @__PURE__ */ new Map();
928
930
  for (const change of changes) for (const version of ["before", "after"]) {
929
- var _sides$get;
931
+ var _sides$get, _affected$get;
930
932
  const block = change[version];
931
933
  if (!block) continue;
932
934
  const key = version + ":" + roots[version].get(block.id);
933
- (_sides$get = sides.get(key)) === null || _sides$get === void 0 || _sides$get.marks.push({
935
+ if (!isLayoutOnly(change)) (_sides$get = sides.get(key)) === null || _sides$get === void 0 || _sides$get.marks.push({
934
936
  id: block.id,
935
937
  kind: change.kind,
936
938
  parts: change.parts
937
939
  });
938
- affected.add(groupOf(key));
940
+ const id = groupOf(key);
941
+ const list = (_affected$get = affected.get(id)) !== null && _affected$get !== void 0 ? _affected$get : [];
942
+ if (!list.includes(change)) list.push(change);
943
+ affected.set(id, list);
939
944
  }
940
- return [...affected].map((id) => groups.get(id));
945
+ return [...affected].map(([id, list]) => {
946
+ const group = groups.get(id);
947
+ group.kind = scriptKind(group, list);
948
+ group.summary = summarize(group, list, before, after);
949
+ return group;
950
+ });
951
+ }
952
+ var connectionFields = /* @__PURE__ */ new Set([
953
+ "next",
954
+ "parent",
955
+ "topLevel"
956
+ ]);
957
+ var positionFields = /* @__PURE__ */ new Set(["x", "y"]);
958
+ var compactPositionOnly = (a, b) => Array.isArray(a) && Array.isArray(b) && equal(a.slice(0, 3), b.slice(0, 3)) && !equal(a, b);
959
+ function blockOf(side) {
960
+ return side ? side.target.blocks[side.id] : void 0;
961
+ }
962
+ /** True for an updated block whose content is the same: only its connections or its position changed. */
963
+ function isLayoutOnly(change) {
964
+ if (change.kind !== "updated") return false;
965
+ if (compactPositionOnly(blockOf(change.before), blockOf(change.after))) return true;
966
+ return change.fields.length > 0 && change.fields.every((field) => connectionFields.has(field) || positionFields.has(field));
967
+ }
968
+ function isPositionOnly(change) {
969
+ if (change.kind !== "updated") return false;
970
+ if (compactPositionOnly(blockOf(change.before), blockOf(change.after))) return true;
971
+ return change.fields.length > 0 && change.fields.every((field) => positionFields.has(field));
972
+ }
973
+ function scriptKind(group, changes) {
974
+ if (!group.before.length) return "added";
975
+ if (!group.after.length) return "removed";
976
+ if (group.before.length !== 1 || group.after.length !== 1) return "reorganized";
977
+ if (changes.every(isPositionOnly)) return "moved";
978
+ if (changes.every(isLayoutOnly)) return "reordered";
979
+ return "updated";
980
+ }
981
+ var opcodeOf = (block) => isRecord(block) && typeof block.opcode === "string" ? block.opcode : void 0;
982
+ var isShadow = (block) => !isRecord(block) || block.shadow === true;
983
+ var isHat = (opcode) => opcode !== void 0 && (opcode === "control_start_as_clone" || opcode === "procedures_definition" || /^[a-z0-9]+_when/i.test(opcode));
984
+ var substackInputs = ["SUBSTACK", "SUBSTACK2"];
985
+ function inputBlock(block, name) {
986
+ if (!isRecord(block) || !isRecord(block.inputs)) return void 0;
987
+ const input = block.inputs[name];
988
+ return Array.isArray(input) && typeof input[1] === "string" ? input[1] : void 0;
989
+ }
990
+ function summarize(group, changes, before, after) {
991
+ const items = [];
992
+ const seen = /* @__PURE__ */ new Set();
993
+ const add = (item) => {
994
+ const key = JSON.stringify(item);
995
+ if (seen.has(key)) return;
996
+ seen.add(key);
997
+ items.push(item);
998
+ };
999
+ if (group.kind === "moved" || group.kind === "reordered") {
1000
+ add({ kind: group.kind });
1001
+ return items;
1002
+ }
1003
+ const explained = /* @__PURE__ */ new Set();
1004
+ if (group.before.length === 1 && group.after.length === 1) {
1005
+ const oldRoot = group.before[0].id, newRoot = group.after[0].id;
1006
+ const a = opcodeOf(before === null || before === void 0 ? void 0 : before.blocks[oldRoot]), b = opcodeOf(after === null || after === void 0 ? void 0 : after.blocks[newRoot]);
1007
+ if (a !== b && isHat(a) && isHat(b)) {
1008
+ add({
1009
+ kind: "hatReplaced",
1010
+ before: a,
1011
+ after: b
1012
+ });
1013
+ for (const change of changes) {
1014
+ var _change$after, _change$before;
1015
+ if (change.kind === "added" && ((_change$after = change.after) === null || _change$after === void 0 ? void 0 : _change$after.id) === newRoot || change.kind === "removed" && ((_change$before = change.before) === null || _change$before === void 0 ? void 0 : _change$before.id) === oldRoot) explained.add(change);
1016
+ }
1017
+ }
1018
+ }
1019
+ const branches = [];
1020
+ for (const change of changes) {
1021
+ if (change.kind !== "added" || !after) continue;
1022
+ const block = after.blocks[change.after.id];
1023
+ const opcode = opcodeOf(block);
1024
+ if (!opcode) continue;
1025
+ if (substackInputs.some((name) => {
1026
+ const visited = /* @__PURE__ */ new Set();
1027
+ for (let id = inputBlock(block, name); id !== void 0 && !visited.has(id);) {
1028
+ visited.add(id);
1029
+ if (before && Object.hasOwn(before.blocks, id)) return true;
1030
+ const next = after.blocks[id];
1031
+ id = isRecord(next) && typeof next.next === "string" ? next.next : void 0;
1032
+ }
1033
+ return false;
1034
+ })) {
1035
+ add({
1036
+ kind: "wrapped",
1037
+ opcode
1038
+ });
1039
+ explained.add(change);
1040
+ } else if (opcode === "control_if" || opcode === "control_if_else") branches.push(change);
1041
+ }
1042
+ for (const change of branches) {
1043
+ add({
1044
+ kind: "branchAdded",
1045
+ opcode: opcodeOf(after.blocks[change.after.id])
1046
+ });
1047
+ explained.add(change);
1048
+ }
1049
+ for (const change of changes) {
1050
+ var _opcodeOf;
1051
+ if (change.kind !== "updated") continue;
1052
+ const opcode = (_opcodeOf = opcodeOf(blockOf(change.after))) !== null && _opcodeOf !== void 0 ? _opcodeOf : opcodeOf(blockOf(change.before));
1053
+ if (!opcode) continue;
1054
+ for (const part of change.parts) {
1055
+ if (part.kind === "input" && substackInputs.includes(part.name)) continue;
1056
+ add({
1057
+ kind: "valueChanged",
1058
+ opcode,
1059
+ name: part.name
1060
+ });
1061
+ }
1062
+ }
1063
+ const count = (kind) => changes.filter((change) => change.kind === kind && !explained.has(change) && !isShadow(blockOf(kind === "added" ? change.after : change.before))).length;
1064
+ const added = count("added"), removed = count("removed");
1065
+ if (added) add({
1066
+ kind: "blocksAdded",
1067
+ count: added
1068
+ });
1069
+ if (removed) add({
1070
+ kind: "blocksRemoved",
1071
+ count: removed
1072
+ });
1073
+ return items;
941
1074
  }
942
1075
  function changedProperties(before, after, except) {
943
1076
  return [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].filter((key) => key !== except && !equal(before[key], after[key]));
@@ -946,8 +1079,8 @@ function compareProjectForReview(before, after) {
946
1079
  const { changes } = compareProjectBlocks(before, after);
947
1080
  const byTarget = /* @__PURE__ */ new Map();
948
1081
  for (const change of changes) {
949
- var _change$after;
950
- const key = targetKey(((_change$after = change.after) !== null && _change$after !== void 0 ? _change$after : change.before).target);
1082
+ var _change$after2;
1083
+ const key = targetKey(((_change$after2 = change.after) !== null && _change$after2 !== void 0 ? _change$after2 : change.before).target);
951
1084
  const existing = byTarget.get(key);
952
1085
  if (existing) existing.push(change);
953
1086
  else byTarget.set(key, [change]);
@@ -959,12 +1092,14 @@ function compareProjectForReview(before, after) {
959
1092
  const a = oldTargets.get(key), b = newTargets.get(key);
960
1093
  const edits = byTarget.get(key);
961
1094
  const scripts = edits ? groupScripts(a, b, edits) : [];
962
- const properties = changedProperties(_objectSpread2({}, a), _objectSpread2({}, b), "blocks");
963
- if (scripts.length || properties.length) {
1095
+ const status = !a ? "added" : !b ? "removed" : "updated";
1096
+ const properties = status === "updated" ? changedProperties(_objectSpread2({}, a), _objectSpread2({}, b), "blocks") : [];
1097
+ if (scripts.length || properties.length || status !== "updated") {
964
1098
  const { name, isStage } = b !== null && b !== void 0 ? b : a;
965
1099
  targets.push({
966
1100
  name,
967
1101
  isStage,
1102
+ status,
968
1103
  scripts,
969
1104
  properties,
970
1105
  before: a,
@@ -977,6 +1112,308 @@ function compareProjectForReview(before, after) {
977
1112
  properties: changedProperties(before, after, "targets")
978
1113
  };
979
1114
  }
1115
+ // src/project-change-model.ts
1116
+ init_objectSpread2();
1117
+ var targetDataKeys = [
1118
+ "variables",
1119
+ "lists",
1120
+ "broadcasts"
1121
+ ];
1122
+ var nonSettingKeys = /* @__PURE__ */ new Set([
1123
+ "blocks",
1124
+ "costumes",
1125
+ "sounds",
1126
+ ...targetDataKeys
1127
+ ]);
1128
+ var propertyLabels = /* @__PURE__ */ new Map([
1129
+ ["variables", ["変数", "Variables"]],
1130
+ ["lists", ["リスト", "Lists"]],
1131
+ ["broadcasts", ["メッセージ", "Broadcast messages"]],
1132
+ ["costumes", ["コスチューム・背景", "Costumes and backdrops"]],
1133
+ ["sounds", ["音", "Sounds"]],
1134
+ ["blocks", ["コード", "Code"]],
1135
+ ["x", ["X 座標", "X position"]],
1136
+ ["y", ["Y 座標", "Y position"]],
1137
+ ["size", ["大きさ", "Size"]],
1138
+ ["direction", ["向き", "Direction"]],
1139
+ ["visible", ["表示状態", "Visibility"]],
1140
+ ["currentCostume", ["表示するコスチューム", "Selected costume"]],
1141
+ ["layerOrder", ["重なり順", "Layer order"]],
1142
+ ["volume", ["音量", "Volume"]],
1143
+ ["comments", ["コメント", "Comments"]],
1144
+ ["draggable", ["ドラッグ可否", "Draggable"]],
1145
+ ["rotationStyle", ["回転方法", "Rotation style"]],
1146
+ ["tempo", ["テンポ", "Tempo"]],
1147
+ ["videoTransparency", ["ビデオの透明度", "Video transparency"]],
1148
+ ["videoState", ["ビデオの状態", "Video state"]],
1149
+ ["textToSpeechLanguage", ["音声合成の言語", "Text to speech language"]],
1150
+ ["monitors", ["モニター", "Monitors"]],
1151
+ ["extensions", ["拡張機能", "Extensions"]],
1152
+ ["meta", ["作品の情報", "Project metadata"]]
1153
+ ]);
1154
+ /** The Japanese or English name of a project or target property key; unknown keys are returned as they are. */
1155
+ function projectPropertyLabel(key, locale = "en") {
1156
+ var _propertyLabels$get, _propertyLabels$get2;
1157
+ return (_propertyLabels$get = (_propertyLabels$get2 = propertyLabels.get(key)) === null || _propertyLabels$get2 === void 0 ? void 0 : _propertyLabels$get2[locale.startsWith("ja") ? 0 : 1]) !== null && _propertyLabels$get !== void 0 ? _propertyLabels$get : key;
1158
+ }
1159
+ /** The asset file name of a costume or sound entry: `md5ext`, or `assetId.dataFormat`; `''` when it has neither. */
1160
+ function assetFileName(entry) {
1161
+ if (typeof entry.md5ext === "string") return entry.md5ext;
1162
+ return typeof entry.assetId === "string" && typeof entry.dataFormat === "string" ? entry.assetId + "." + entry.dataFormat : "";
1163
+ }
1164
+ function joinPath(...parts) {
1165
+ return parts.flatMap((part) => part.split("/")).filter((part) => part !== "" && part !== ".").join("/");
1166
+ }
1167
+ function statusOf(before, after) {
1168
+ return before === void 0 ? "added" : after === void 0 ? "removed" : "updated";
1169
+ }
1170
+ function entriesOf(target, kind) {
1171
+ const list = target === null || target === void 0 ? void 0 : target[kind];
1172
+ return Array.isArray(list) ? list.filter(isRecord) : [];
1173
+ }
1174
+ function nameOf(entry) {
1175
+ return typeof entry.name === "string" ? entry.name : "";
1176
+ }
1177
+ /** Pairs the entries of two versions by name (repeated names pair in order): after's order first, then removed ones. */
1178
+ function pairEntries(before, after) {
1179
+ var _remaining$get;
1180
+ const remaining = /* @__PURE__ */ new Map();
1181
+ for (const entry of before) {
1182
+ const queue = remaining.get(nameOf(entry));
1183
+ if (queue) queue.push(entry);
1184
+ else remaining.set(nameOf(entry), [entry]);
1185
+ }
1186
+ const pairs = [];
1187
+ for (const entry of after) pairs.push({
1188
+ name: nameOf(entry),
1189
+ before: (_remaining$get = remaining.get(nameOf(entry))) === null || _remaining$get === void 0 ? void 0 : _remaining$get.shift(),
1190
+ after: entry
1191
+ });
1192
+ for (const entry of before) {
1193
+ const queue = remaining.get(nameOf(entry));
1194
+ if ((queue === null || queue === void 0 ? void 0 : queue[0]) === entry) pairs.push({
1195
+ name: nameOf(entry),
1196
+ before: queue.shift()
1197
+ });
1198
+ }
1199
+ return pairs;
1200
+ }
1201
+ function changedKeys(before, after, skip) {
1202
+ return [.../* @__PURE__ */ new Set([...Object.keys(after), ...Object.keys(before)])].filter((key) => !skip(key) && !equal(before[key], after[key]));
1203
+ }
1204
+ /** Arranges the changed files of one project folder by stage, sprite and element. Pure; reads nothing. */
1205
+ function organizeProjectChanges(input) {
1206
+ const { folder, files, assetsDirectory } = input;
1207
+ const normalize = (project) => project === null ? { targets: [] } : _objectSpread2(_objectSpread2({}, project), {}, { targets: project.targets.map((target) => {
1208
+ var _target$blocks;
1209
+ return _objectSpread2(_objectSpread2({}, target), {}, { blocks: (_target$blocks = target.blocks) !== null && _target$blocks !== void 0 ? _target$blocks : {} });
1210
+ }) });
1211
+ const before = normalize(input.before), after = normalize(input.after);
1212
+ const projectPath = joinPath(folder, "project.source.json");
1213
+ const projectFiles = files.filter((file) => file.path === projectPath);
1214
+ const explained = /* @__PURE__ */ new Set();
1215
+ let projectUsed = false;
1216
+ const useProject = () => {
1217
+ projectUsed = true;
1218
+ return [...projectFiles];
1219
+ };
1220
+ const filesAt = (paths) => {
1221
+ const wanted = new Set(paths.filter((path) => path !== void 0));
1222
+ const found = files.filter((file) => wanted.has(file.path));
1223
+ for (const file of found) explained.add(file.path);
1224
+ return found;
1225
+ };
1226
+ const review = compareProjectForReview(before, after);
1227
+ const scripts = new Map(review.targets.map((target) => {
1228
+ var _target$after;
1229
+ return [targetKey((_target$after = target.after) !== null && _target$after !== void 0 ? _target$after : target.before), target.scripts.length];
1230
+ }));
1231
+ const oldTargets = new Map(before.targets.map((target) => [targetKey(target), target]));
1232
+ const newTargets = new Map(after.targets.map((target) => [targetKey(target), target]));
1233
+ const keys = [...new Set([...after.targets, ...before.targets].map(targetKey))];
1234
+ keys.sort((a, b) => Number(b === "stage") - Number(a === "stage"));
1235
+ const targets = [];
1236
+ for (const key of keys) {
1237
+ var _scripts$get;
1238
+ const a = oldTargets.get(key), b = newTargets.get(key);
1239
+ const status = statusOf(a, b);
1240
+ const elements = [];
1241
+ const count = (_scripts$get = scripts.get(key)) !== null && _scripts$get !== void 0 ? _scripts$get : 0;
1242
+ if (count) elements.push({
1243
+ key: JSON.stringify([
1244
+ key,
1245
+ "code",
1246
+ ""
1247
+ ]),
1248
+ kind: "code",
1249
+ status,
1250
+ scripts: count,
1251
+ files: useProject()
1252
+ });
1253
+ for (const kind of ["costumes", "sounds"]) {
1254
+ const seen = /* @__PURE__ */ new Map();
1255
+ for (const pair of pairEntries(entriesOf(a, kind), entriesOf(b, kind))) {
1256
+ var _seen$get;
1257
+ const md5ext = {
1258
+ before: pair.before && assetFileName(pair.before) || void 0,
1259
+ after: pair.after && assetFileName(pair.after) || void 0
1260
+ };
1261
+ const differs = !equal(pair.before, pair.after);
1262
+ const assets = filesAt([md5ext.before && joinPath(folder, assetsDirectory.before, md5ext.before), md5ext.after && joinPath(folder, assetsDirectory.after, md5ext.after)]);
1263
+ if (!differs && !assets.length) continue;
1264
+ const repeat = (_seen$get = seen.get(pair.name)) !== null && _seen$get !== void 0 ? _seen$get : 0;
1265
+ seen.set(pair.name, repeat + 1);
1266
+ elements.push({
1267
+ key: JSON.stringify(repeat ? [
1268
+ key,
1269
+ kind,
1270
+ pair.name,
1271
+ repeat
1272
+ ] : [
1273
+ key,
1274
+ kind,
1275
+ pair.name
1276
+ ]),
1277
+ kind,
1278
+ status: statusOf(pair.before, pair.after),
1279
+ name: pair.name,
1280
+ before: pair.before,
1281
+ after: pair.after,
1282
+ md5ext,
1283
+ files: [...differs ? useProject() : [], ...assets]
1284
+ });
1285
+ }
1286
+ }
1287
+ if (a && b) {
1288
+ const oldFields = a, newFields = b;
1289
+ const data = changedKeys(oldFields, newFields, (property) => !targetDataKeys.includes(property));
1290
+ if (data.length) elements.push({
1291
+ key: JSON.stringify([
1292
+ key,
1293
+ "data",
1294
+ ""
1295
+ ]),
1296
+ kind: "data",
1297
+ status,
1298
+ properties: data,
1299
+ files: useProject()
1300
+ });
1301
+ const settings = changedKeys(oldFields, newFields, (property) => nonSettingKeys.has(property));
1302
+ if (settings.length) elements.push({
1303
+ key: JSON.stringify([
1304
+ key,
1305
+ "settings",
1306
+ ""
1307
+ ]),
1308
+ kind: "settings",
1309
+ status,
1310
+ properties: settings,
1311
+ files: useProject()
1312
+ });
1313
+ } else projectUsed = true;
1314
+ if (status !== "updated" || elements.length) {
1315
+ const { name, isStage } = b !== null && b !== void 0 ? b : a;
1316
+ targets.push({
1317
+ key,
1318
+ name,
1319
+ isStage,
1320
+ status,
1321
+ before: a,
1322
+ after: b,
1323
+ elements
1324
+ });
1325
+ }
1326
+ }
1327
+ const project = changedKeys(before, after, (key) => key === "targets").map((property) => ({
1328
+ key: JSON.stringify([
1329
+ "project",
1330
+ "settings",
1331
+ property
1332
+ ]),
1333
+ kind: "settings",
1334
+ status: statusOf(before[property], after[property]),
1335
+ properties: [property],
1336
+ files: useProject()
1337
+ }));
1338
+ if (projectUsed) explained.add(projectPath);
1339
+ return {
1340
+ targets,
1341
+ project,
1342
+ other: files.filter((file) => !explained.has(file.path))
1343
+ };
1344
+ }
1345
+ // src/review-words.ts
1346
+ var pick = ([ja, en], locale) => locale.startsWith("ja") ? ja : en;
1347
+ var hats = /* @__PURE__ */ new Map([
1348
+ ["event_whenflagclicked", ["緑の旗が押されたとき", "When green flag clicked"]],
1349
+ ["event_whenthisspriteclicked", ["このスプライトが押されたとき", "When this sprite clicked"]],
1350
+ ["event_whenstageclicked", ["ステージが押されたとき", "When stage clicked"]],
1351
+ ["event_whenkeypressed", ["キーが押されたとき", "When key pressed"]],
1352
+ ["event_whenbackdropswitchesto", ["背景が切り替わったとき", "When backdrop switches"]],
1353
+ ["event_whengreaterthan", ["値が超えたとき", "When value exceeds"]],
1354
+ ["event_whenbroadcastreceived", ["メッセージを受け取ったとき", "When message received"]],
1355
+ ["control_start_as_clone", ["クローンされたとき", "When I start as a clone"]],
1356
+ ["procedures_definition", ["定義", "Define"]]
1357
+ ]);
1358
+ var blocks = /* @__PURE__ */ new Map([
1359
+ ["control_repeat", ["〜回繰り返す", "repeat"]],
1360
+ ["control_forever", ["ずっと", "forever"]],
1361
+ ["control_repeat_until", ["〜まで繰り返す", "repeat until"]],
1362
+ ["control_while", ["〜の間繰り返す", "while"]],
1363
+ ["control_if", ["もし〜なら", "if"]],
1364
+ ["control_if_else", ["もし〜なら、でなければ", "if else"]],
1365
+ ["control_wait", ["〜秒待つ", "wait"]],
1366
+ ["motion_movesteps", ["〜歩動かす", "move steps"]],
1367
+ ["motion_turnright", ["〜度回す", "turn"]],
1368
+ ["motion_turnleft", ["〜度回す", "turn"]],
1369
+ ["motion_gotoxy", ["x座標を〜、y座標を〜にする", "go to x y"]],
1370
+ ["motion_changeyby", ["y座標を〜ずつ変える", "change y by"]],
1371
+ ["motion_changexby", ["x座標を〜ずつ変える", "change x by"]],
1372
+ ["motion_glidesecstoxy", ["〜秒でx座標を〜に、y座標を〜に変える", "glide"]],
1373
+ ["looks_sayforsecs", ["〜と〜秒言う", "say for secs"]],
1374
+ ["looks_say", ["〜と言う", "say"]],
1375
+ ["looks_switchcostumeto", ["コスチュームを〜にする", "switch costume"]],
1376
+ ["looks_nextcostume", ["次のコスチュームにする", "next costume"]],
1377
+ ["data_setvariableto", ["〜を〜にする", "set variable"]],
1378
+ ["data_changevariableby", ["〜を〜ずつ変える", "change variable"]],
1379
+ ["event_broadcast", ["〜を送る", "broadcast"]],
1380
+ ["sound_play", ["〜の音を鳴らす", "play sound"]]
1381
+ ]);
1382
+ function hatName(opcode, locale) {
1383
+ const pair = hats.get(opcode);
1384
+ return pair && pick(pair, locale);
1385
+ }
1386
+ var blockName = (opcode, locale) => {
1387
+ var _blocks$get;
1388
+ const pair = (_blocks$get = blocks.get(opcode)) !== null && _blocks$get !== void 0 ? _blocks$get : hats.get(opcode);
1389
+ return pair ? pick(pair, locale) : locale.startsWith("ja") ? "ブロック" : "a block";
1390
+ };
1391
+ /** One phrase per summary item, in the reader's language. */
1392
+ function summaryPhrase(item, locale) {
1393
+ const ja = locale.startsWith("ja");
1394
+ switch (item.kind) {
1395
+ case "hatReplaced": return ja ? `「${blockName(item.before, locale)}」を「${blockName(item.after, locale)}」に替えた` : `replaced “${blockName(item.before, locale)}” with “${blockName(item.after, locale)}”`;
1396
+ case "wrapped": return ja ? `「${blockName(item.opcode, locale)}」で包んだ` : `wrapped in “${blockName(item.opcode, locale)}”`;
1397
+ case "branchAdded": return ja ? `分岐「${blockName(item.opcode, locale)}」を追加` : `added “${blockName(item.opcode, locale)}”`;
1398
+ case "valueChanged": return ja ? `「${blockName(item.opcode, locale)}」の ${item.name} を変更` : `changed ${item.name} of “${blockName(item.opcode, locale)}”`;
1399
+ case "blocksAdded": return ja ? `ブロックを ${item.count} 個追加` : `added ${item.count} ${item.count === 1 ? "block" : "blocks"}`;
1400
+ case "blocksRemoved": return ja ? `ブロックを ${item.count} 個削除` : `removed ${item.count} ${item.count === 1 ? "block" : "blocks"}`;
1401
+ case "reordered": return ja ? "つなぎ方だけを変更" : "only reconnected";
1402
+ case "moved": return ja ? "位置だけを変更" : "only moved";
1403
+ }
1404
+ }
1405
+ /** The colours of the previews, said once at the top of a review. */
1406
+ var legend = (locale) => locale.startsWith("ja") ? [
1407
+ ["added", "緑の縁: 追加したブロック"],
1408
+ ["removed", "赤の縁: 削除したブロック"],
1409
+ ["updated", "紫の縁: 中身を変えたブロック"],
1410
+ ["part", "オレンジの枠: 値を変えた欄"]
1411
+ ] : [
1412
+ ["added", "Green outline: added block"],
1413
+ ["removed", "Red outline: removed block"],
1414
+ ["updated", "Purple outline: block with changed contents"],
1415
+ ["part", "Orange frame: changed value"]
1416
+ ];
980
1417
  // src/project-source-review.tsx
981
1418
  init_objectSpread2();
982
1419
  var ja = {
@@ -994,6 +1431,14 @@ var ja = {
994
1431
  added: "追加",
995
1432
  removed: "削除",
996
1433
  reorganized: "再編",
1434
+ moved: "位置だけ",
1435
+ reordered: "並べ替え",
1436
+ stage: "ステージ",
1437
+ otherSettings: "そのほかの設定",
1438
+ legend: "色の見方",
1439
+ targetAdded: (name, stage) => stage ? "ステージを追加" : `スプライト『${name}』を追加`,
1440
+ targetRemoved: (name, stage) => stage ? "ステージを削除" : `スプライト『${name}』を削除`,
1441
+ assetOwner: (owner, kind, name) => `${owner} の${kind === "sound" ? "音" : kind === "backdrop" ? "背景" : "コスチューム"}『${name}』`,
997
1442
  absent: "この版にはこのコードがありません",
998
1443
  loading: "コードを描画中…",
999
1444
  unavailable: "このコードを描画できません。",
@@ -1028,6 +1473,14 @@ var en = {
1028
1473
  added: "Added",
1029
1474
  removed: "Removed",
1030
1475
  reorganized: "Reorganized",
1476
+ moved: "Moved only",
1477
+ reordered: "Reconnected",
1478
+ stage: "Stage",
1479
+ otherSettings: "Other settings",
1480
+ legend: "Colours",
1481
+ targetAdded: (name, stage) => stage ? "Added the stage" : `Added the sprite “${name}”`,
1482
+ targetRemoved: (name, stage) => stage ? "Removed the stage" : `Removed the sprite “${name}”`,
1483
+ assetOwner: (owner, kind, name) => `${kind === "sound" ? "Sound" : kind === "backdrop" ? "Backdrop" : "Costume"} “${name}” of ${owner}`,
1031
1484
  absent: "No code in this version",
1032
1485
  loading: "Rendering code…",
1033
1486
  unavailable: "This code could not be rendered.",
@@ -1066,15 +1519,12 @@ var properties = /* @__PURE__ */ new Map([
1066
1519
  function scriptTitle(side, labels) {
1067
1520
  const block = side.target.blocks[side.id];
1068
1521
  if (!isRecord(block)) return labels.script;
1069
- if (block.opcode === "event_whenflagclicked") return labels.flag;
1070
- if (block.opcode === "event_whenthisspriteclicked") return labels.click;
1071
- if (block.opcode === "event_whenkeypressed") return labels.key;
1072
1522
  if (block.opcode === "event_whenbroadcastreceived") {
1073
1523
  const field = isRecord(block.fields) ? block.fields.BROADCAST_OPTION : void 0;
1074
1524
  const message = Array.isArray(field) && typeof field[0] === "string" ? field[0] : "";
1075
1525
  if (message) return labels === ja ? `「${message}」${labels.broadcast}` : `“${message}” ${labels.broadcast}`;
1076
1526
  }
1077
- return typeof block.opcode === "string" ? block.opcode : labels.script;
1527
+ return typeof block.opcode === "string" && hatName(block.opcode, labels === ja ? "ja" : "en") || labels.script;
1078
1528
  }
1079
1529
  var sideKey = (side) => JSON.stringify([
1080
1530
  side.target.isStage,
@@ -1121,14 +1571,16 @@ function useBlockLocation(comparison, locateBlock) {
1121
1571
  function ScriptComparison({ script, labels, reviewed = false, setReviewed, renderBlock, loadAsset, location }) {
1122
1572
  const [open, setOpen] = useState(false);
1123
1573
  const { sync, toggle } = usePreviewSync(labels === ja ? "ja" : "en");
1124
- const status = !script.before.length ? "added" : !script.after.length ? "removed" : script.before.length !== 1 || script.after.length !== 1 ? "reorganized" : "updated";
1574
+ const status = script.kind;
1575
+ const locale = labels === ja ? "ja" : "en";
1576
+ const summary = script.summary.map((item) => summaryPhrase(item, locale)).join(labels === ja ? "、" : ", ");
1125
1577
  const beforeTitle = script.before[0] && scriptTitle(script.before[0], labels);
1126
1578
  const afterTitle = script.after[0] && scriptTitle(script.after[0], labels);
1127
1579
  const title = beforeTitle && afterTitle && beforeTitle !== afterTitle ? beforeTitle + " → " + afterTitle : afterTitle !== null && afterTitle !== void 0 ? afterTitle : beforeTitle;
1128
1580
  return /* @__PURE__ */ React.createElement("details", {
1129
1581
  className: "twgit-review-script",
1130
1582
  onToggle: (event) => setOpen(event.currentTarget.open)
1131
- }, /* @__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, {
1583
+ }, /* @__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), summary && /* @__PURE__ */ React.createElement("small", { className: "twgit-review-summary" }, summary), 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, {
1132
1584
  key: side.id,
1133
1585
  side,
1134
1586
  title: sides.length > 1 ? `${labels[version]} ${index + 1}` : labels[version],
@@ -1153,10 +1605,15 @@ function ScriptComparison({ script, labels, reviewed = false, setReviewed, rende
1153
1605
  }), " ", labels.reviewed)));
1154
1606
  }
1155
1607
  function propertyNames(keys, labels) {
1156
- return keys.map((key) => {
1608
+ const locale = labels === ja ? "ja" : "en";
1609
+ const named = /* @__PURE__ */ new Set(), other = [];
1610
+ for (const key of keys) {
1157
1611
  var _properties$get, _properties$get2;
1158
- 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;
1159
- }).join(" · ");
1612
+ const label = (_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 : projectPropertyLabel(key, locale);
1613
+ if (label === key) other.push(key);
1614
+ else named.add(label);
1615
+ }
1616
+ return /* @__PURE__ */ React.createElement("span", { title: keys.join(", ") }, [...named, ...other.length ? [labels.otherSettings] : []].join(" · "));
1160
1617
  }
1161
1618
  /**
1162
1619
  * The current costume (the backdrop for the stage) of each version of a target that has one. A changed costume shows
@@ -1193,7 +1650,7 @@ function ComparisonTargets({ comparison, labels, renderBlock, loadAsset, locateB
1193
1650
  target,
1194
1651
  labels,
1195
1652
  loadAsset
1196
- }), 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) => {
1653
+ }), target.isStage ? labels.stage : target.name, target.scripts.length > 0 && /* @__PURE__ */ React.createElement("small", null, labels.scriptCount(target.scripts.length))), target.status !== "updated" && /* @__PURE__ */ React.createElement("p", { className: "twgit-review-target-status" }, (target.status === "added" ? labels.targetAdded : labels.targetRemoved)(target.name, target.isStage)), target.status === "updated" && target.properties.length > 0 && /* @__PURE__ */ React.createElement("p", null, labels.properties, " · ", propertyNames(target.properties, labels)), target.scripts.map((script) => {
1197
1654
  const key = JSON.stringify([
1198
1655
  review === null || review === void 0 ? void 0 : review.file,
1199
1656
  target.isStage,
@@ -1212,6 +1669,54 @@ function ComparisonTargets({ comparison, labels, renderBlock, loadAsset, locateB
1212
1669
  });
1213
1670
  }))));
1214
1671
  }
1672
+ function Legend({ labels }) {
1673
+ return /* @__PURE__ */ React.createElement("p", {
1674
+ className: "twgit-review-legend",
1675
+ "aria-label": labels.legend
1676
+ }, legend(labels === ja ? "ja" : "en").map(([kind, text]) => /* @__PURE__ */ React.createElement("span", {
1677
+ key: kind,
1678
+ "data-legend": kind
1679
+ }, text)));
1680
+ }
1681
+ /** Finds who uses an asset file of the review: the costume or sound whose file name it is, in the project beside it. */
1682
+ function assetOwner(path, status, entries) {
1683
+ const parts = path.split("/");
1684
+ const name = parts.pop();
1685
+ const folder = parts.slice(0, -1).join("/");
1686
+ const version = status === "removed" ? "before" : "after";
1687
+ for (const { file, comparison } of entries) {
1688
+ if (!comparison || !("patch" in file) || file.path.split("/").slice(0, -1).join("/") !== folder) continue;
1689
+ for (const target of comparison.targets) {
1690
+ const owner = target[version];
1691
+ if (!owner) continue;
1692
+ for (const kind of ["costumes", "sounds"]) {
1693
+ const list = owner[kind];
1694
+ if (!Array.isArray(list)) continue;
1695
+ const entry = list.find((item) => isRecord(item) && assetFileName(item) === name);
1696
+ if (isRecord(entry)) return {
1697
+ target,
1698
+ version,
1699
+ md5ext: name,
1700
+ loadAsset: file.loadAsset,
1701
+ kind: kind === "sounds" ? "sound" : target.isStage ? "backdrop" : "costume",
1702
+ name: typeof entry.name === "string" ? entry.name : name
1703
+ };
1704
+ }
1705
+ }
1706
+ }
1707
+ return null;
1708
+ }
1709
+ function RelatedFile({ file, entries, labels }) {
1710
+ var _owner$version;
1711
+ const owner = assetOwner(file.path, file.status, entries);
1712
+ const image = useAssetImage(owner && owner.kind !== "sound" ? owner.md5ext : void 0, (_owner$version = owner === null || owner === void 0 ? void 0 : owner.version) !== null && _owner$version !== void 0 ? _owner$version : "after", owner === null || owner === void 0 ? void 0 : owner.loadAsset);
1713
+ if (!owner) return /* @__PURE__ */ React.createElement("p", null, labels.related, " · ", labels[file.status]);
1714
+ const ownerName = owner.target.isStage ? labels.stage : owner.target.name;
1715
+ return /* @__PURE__ */ React.createElement("p", { className: "twgit-review-asset" }, image && /* @__PURE__ */ React.createElement("img", {
1716
+ src: image,
1717
+ alt: ""
1718
+ }), /* @__PURE__ */ React.createElement("span", null, labels.assetOwner(ownerName, owner.kind, owner.name), " · ", labels[file.status]));
1719
+ }
1215
1720
  var countScripts = (comparison) => comparison.targets.reduce((count, target) => count + target.scripts.length, 0);
1216
1721
  /** Compares two versions of a project.source.json by stage, sprite and code, as the pull request review does. */
1217
1722
  function ProjectSourceComparison({ before, after, renderBlock, locale = "en", fallback, loadAsset, locateBlock, target }) {
@@ -1241,7 +1746,7 @@ function ProjectSourceComparison({ before, after, renderBlock, locale = "en", fa
1241
1746
  return /* @__PURE__ */ React.createElement("section", {
1242
1747
  className: "twgit-block-diff twgit-review twgit-review-comparison",
1243
1748
  "aria-label": labels.comparison
1244
- }, !only && /* @__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, {
1749
+ }, !only && /* @__PURE__ */ React.createElement(Legend, { labels }), !only && /* @__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, {
1245
1750
  comparison,
1246
1751
  labels,
1247
1752
  renderBlock,
@@ -1291,10 +1796,14 @@ function ProjectSourceReview({ title, description, verification, files, renderBl
1291
1796
  return /* @__PURE__ */ React.createElement("section", {
1292
1797
  className: "twgit-block-diff twgit-review",
1293
1798
  "aria-label": labels.review
1294
- }, /* @__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", {
1799
+ }, /* @__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(Legend, { labels }), /* @__PURE__ */ React.createElement("h3", { className: "twgit-review-files-title" }, labels.files), entries.map(({ file, comparison, invalid }) => /* @__PURE__ */ React.createElement("section", {
1295
1800
  key: file.path,
1296
1801
  className: "twgit-review-file"
1297
- }, /* @__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, {
1802
+ }, /* @__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(RelatedFile, {
1803
+ file,
1804
+ entries,
1805
+ labels
1806
+ }), (invalid || file.unavailable) && /* @__PURE__ */ React.createElement("p", { role: "alert" }, labels.invalid), comparison && /* @__PURE__ */ React.createElement(ComparisonTargets, {
1298
1807
  comparison,
1299
1808
  labels,
1300
1809
  renderBlock,
@@ -1307,236 +1816,6 @@ function ProjectSourceReview({ title, description, verification, files, renderBl
1307
1816
  }
1308
1817
  }))));
1309
1818
  }
1310
- // src/project-change-model.ts
1311
- init_objectSpread2();
1312
- var targetDataKeys = [
1313
- "variables",
1314
- "lists",
1315
- "broadcasts"
1316
- ];
1317
- var nonSettingKeys = /* @__PURE__ */ new Set([
1318
- "blocks",
1319
- "costumes",
1320
- "sounds",
1321
- ...targetDataKeys
1322
- ]);
1323
- var propertyLabels = /* @__PURE__ */ new Map([
1324
- ["variables", ["変数", "Variables"]],
1325
- ["lists", ["リスト", "Lists"]],
1326
- ["broadcasts", ["メッセージ", "Broadcast messages"]],
1327
- ["costumes", ["コスチューム・背景", "Costumes and backdrops"]],
1328
- ["sounds", ["音", "Sounds"]],
1329
- ["blocks", ["コード", "Code"]],
1330
- ["x", ["X 座標", "X position"]],
1331
- ["y", ["Y 座標", "Y position"]],
1332
- ["size", ["大きさ", "Size"]],
1333
- ["direction", ["向き", "Direction"]],
1334
- ["visible", ["表示状態", "Visibility"]],
1335
- ["currentCostume", ["表示するコスチューム", "Selected costume"]],
1336
- ["layerOrder", ["重なり順", "Layer order"]],
1337
- ["volume", ["音量", "Volume"]],
1338
- ["comments", ["コメント", "Comments"]],
1339
- ["draggable", ["ドラッグ可否", "Draggable"]],
1340
- ["rotationStyle", ["回転方法", "Rotation style"]],
1341
- ["tempo", ["テンポ", "Tempo"]],
1342
- ["videoTransparency", ["ビデオの透明度", "Video transparency"]],
1343
- ["videoState", ["ビデオの状態", "Video state"]],
1344
- ["textToSpeechLanguage", ["音声合成の言語", "Text to speech language"]],
1345
- ["monitors", ["モニター", "Monitors"]],
1346
- ["extensions", ["拡張機能", "Extensions"]],
1347
- ["meta", ["作品の情報", "Project metadata"]]
1348
- ]);
1349
- /** The Japanese or English name of a project or target property key; unknown keys are returned as they are. */
1350
- function projectPropertyLabel(key, locale = "en") {
1351
- var _propertyLabels$get, _propertyLabels$get2;
1352
- return (_propertyLabels$get = (_propertyLabels$get2 = propertyLabels.get(key)) === null || _propertyLabels$get2 === void 0 ? void 0 : _propertyLabels$get2[locale.startsWith("ja") ? 0 : 1]) !== null && _propertyLabels$get !== void 0 ? _propertyLabels$get : key;
1353
- }
1354
- /** The asset file name of a costume or sound entry: `md5ext`, or `assetId.dataFormat`; `''` when it has neither. */
1355
- function assetFileName(entry) {
1356
- if (typeof entry.md5ext === "string") return entry.md5ext;
1357
- return typeof entry.assetId === "string" && typeof entry.dataFormat === "string" ? entry.assetId + "." + entry.dataFormat : "";
1358
- }
1359
- function joinPath(...parts) {
1360
- return parts.flatMap((part) => part.split("/")).filter((part) => part !== "" && part !== ".").join("/");
1361
- }
1362
- function statusOf(before, after) {
1363
- return before === void 0 ? "added" : after === void 0 ? "removed" : "updated";
1364
- }
1365
- function entriesOf(target, kind) {
1366
- const list = target === null || target === void 0 ? void 0 : target[kind];
1367
- return Array.isArray(list) ? list.filter(isRecord) : [];
1368
- }
1369
- function nameOf(entry) {
1370
- return typeof entry.name === "string" ? entry.name : "";
1371
- }
1372
- /** Pairs the entries of two versions by name (repeated names pair in order): after's order first, then removed ones. */
1373
- function pairEntries(before, after) {
1374
- var _remaining$get;
1375
- const remaining = /* @__PURE__ */ new Map();
1376
- for (const entry of before) {
1377
- const queue = remaining.get(nameOf(entry));
1378
- if (queue) queue.push(entry);
1379
- else remaining.set(nameOf(entry), [entry]);
1380
- }
1381
- const pairs = [];
1382
- for (const entry of after) pairs.push({
1383
- name: nameOf(entry),
1384
- before: (_remaining$get = remaining.get(nameOf(entry))) === null || _remaining$get === void 0 ? void 0 : _remaining$get.shift(),
1385
- after: entry
1386
- });
1387
- for (const entry of before) {
1388
- const queue = remaining.get(nameOf(entry));
1389
- if ((queue === null || queue === void 0 ? void 0 : queue[0]) === entry) pairs.push({
1390
- name: nameOf(entry),
1391
- before: queue.shift()
1392
- });
1393
- }
1394
- return pairs;
1395
- }
1396
- function changedKeys(before, after, skip) {
1397
- return [.../* @__PURE__ */ new Set([...Object.keys(after), ...Object.keys(before)])].filter((key) => !skip(key) && !equal(before[key], after[key]));
1398
- }
1399
- /** Arranges the changed files of one project folder by stage, sprite and element. Pure; reads nothing. */
1400
- function organizeProjectChanges(input) {
1401
- const { folder, files, assetsDirectory } = input;
1402
- const normalize = (project) => project === null ? { targets: [] } : _objectSpread2(_objectSpread2({}, project), {}, { targets: project.targets.map((target) => {
1403
- var _target$blocks;
1404
- return _objectSpread2(_objectSpread2({}, target), {}, { blocks: (_target$blocks = target.blocks) !== null && _target$blocks !== void 0 ? _target$blocks : {} });
1405
- }) });
1406
- const before = normalize(input.before), after = normalize(input.after);
1407
- const projectPath = joinPath(folder, "project.source.json");
1408
- const projectFiles = files.filter((file) => file.path === projectPath);
1409
- const explained = /* @__PURE__ */ new Set();
1410
- let projectUsed = false;
1411
- const useProject = () => {
1412
- projectUsed = true;
1413
- return [...projectFiles];
1414
- };
1415
- const filesAt = (paths) => {
1416
- const wanted = new Set(paths.filter((path) => path !== void 0));
1417
- const found = files.filter((file) => wanted.has(file.path));
1418
- for (const file of found) explained.add(file.path);
1419
- return found;
1420
- };
1421
- const review = compareProjectForReview(before, after);
1422
- const scripts = new Map(review.targets.map((target) => {
1423
- var _target$after;
1424
- return [targetKey((_target$after = target.after) !== null && _target$after !== void 0 ? _target$after : target.before), target.scripts.length];
1425
- }));
1426
- const oldTargets = new Map(before.targets.map((target) => [targetKey(target), target]));
1427
- const newTargets = new Map(after.targets.map((target) => [targetKey(target), target]));
1428
- const keys = [...new Set([...after.targets, ...before.targets].map(targetKey))];
1429
- keys.sort((a, b) => Number(b === "stage") - Number(a === "stage"));
1430
- const targets = [];
1431
- for (const key of keys) {
1432
- var _scripts$get;
1433
- const a = oldTargets.get(key), b = newTargets.get(key);
1434
- const status = statusOf(a, b);
1435
- const elements = [];
1436
- const count = (_scripts$get = scripts.get(key)) !== null && _scripts$get !== void 0 ? _scripts$get : 0;
1437
- if (count) elements.push({
1438
- key: JSON.stringify([
1439
- key,
1440
- "code",
1441
- ""
1442
- ]),
1443
- kind: "code",
1444
- status,
1445
- scripts: count,
1446
- files: useProject()
1447
- });
1448
- for (const kind of ["costumes", "sounds"]) {
1449
- const seen = /* @__PURE__ */ new Map();
1450
- for (const pair of pairEntries(entriesOf(a, kind), entriesOf(b, kind))) {
1451
- var _seen$get;
1452
- const md5ext = {
1453
- before: pair.before && assetFileName(pair.before) || void 0,
1454
- after: pair.after && assetFileName(pair.after) || void 0
1455
- };
1456
- const differs = !equal(pair.before, pair.after);
1457
- const assets = filesAt([md5ext.before && joinPath(folder, assetsDirectory.before, md5ext.before), md5ext.after && joinPath(folder, assetsDirectory.after, md5ext.after)]);
1458
- if (!differs && !assets.length) continue;
1459
- const repeat = (_seen$get = seen.get(pair.name)) !== null && _seen$get !== void 0 ? _seen$get : 0;
1460
- seen.set(pair.name, repeat + 1);
1461
- elements.push({
1462
- key: JSON.stringify(repeat ? [
1463
- key,
1464
- kind,
1465
- pair.name,
1466
- repeat
1467
- ] : [
1468
- key,
1469
- kind,
1470
- pair.name
1471
- ]),
1472
- kind,
1473
- status: statusOf(pair.before, pair.after),
1474
- name: pair.name,
1475
- before: pair.before,
1476
- after: pair.after,
1477
- md5ext,
1478
- files: [...differs ? useProject() : [], ...assets]
1479
- });
1480
- }
1481
- }
1482
- if (a && b) {
1483
- const oldFields = a, newFields = b;
1484
- const data = changedKeys(oldFields, newFields, (property) => !targetDataKeys.includes(property));
1485
- if (data.length) elements.push({
1486
- key: JSON.stringify([
1487
- key,
1488
- "data",
1489
- ""
1490
- ]),
1491
- kind: "data",
1492
- status,
1493
- properties: data,
1494
- files: useProject()
1495
- });
1496
- const settings = changedKeys(oldFields, newFields, (property) => nonSettingKeys.has(property));
1497
- if (settings.length) elements.push({
1498
- key: JSON.stringify([
1499
- key,
1500
- "settings",
1501
- ""
1502
- ]),
1503
- kind: "settings",
1504
- status,
1505
- properties: settings,
1506
- files: useProject()
1507
- });
1508
- } else projectUsed = true;
1509
- if (status !== "updated" || elements.length) {
1510
- const { name, isStage } = b !== null && b !== void 0 ? b : a;
1511
- targets.push({
1512
- key,
1513
- name,
1514
- isStage,
1515
- status,
1516
- before: a,
1517
- after: b,
1518
- elements
1519
- });
1520
- }
1521
- }
1522
- const project = changedKeys(before, after, (key) => key === "targets").map((property) => ({
1523
- key: JSON.stringify([
1524
- "project",
1525
- "settings",
1526
- property
1527
- ]),
1528
- kind: "settings",
1529
- status: statusOf(before[property], after[property]),
1530
- properties: [property],
1531
- files: useProject()
1532
- }));
1533
- if (projectUsed) explained.add(projectPath);
1534
- return {
1535
- targets,
1536
- project,
1537
- other: files.filter((file) => !explained.has(file.path))
1538
- };
1539
- }
1540
1819
  // src/github-pull-requests.ts
1541
1820
  init_objectSpread2();
1542
1821
  var PullRequestError = class extends Error {
@@ -2135,4 +2414,4 @@ function createScratchBlocksDiffRenderer(options) {
2135
2414
  };
2136
2415
  };
2137
2416
  }
2138
- export { compareProjectBlocks as _, gitHubRepositoryFromOrigin as a, readProjectSourceDiff as b, projectPropertyLabel as c, BlockPreview as d, createPreviewSync as f, useCostumeImage as g, useAssetImage as h, createGitHubPullRequests as i, ProjectSourceComparison as l, currentCostume as m, createScratchBlocksDiffLocator as n, assetFileName as o, usePreviewSync as p, PullRequestError as r, organizeProjectChanges as s, createScratchBlocksDiffRenderer as t, ProjectSourceReview as u, createProjectSourceDiff as v, parseProjectSource as y };
2417
+ export { useCostumeImage as _, gitHubRepositoryFromOrigin as a, parseProjectSource as b, assetFileName as c, compareProjectForReview as d, BlockPreview as f, useAssetImage as g, currentCostume as h, createGitHubPullRequests as i, organizeProjectChanges as l, usePreviewSync as m, createScratchBlocksDiffLocator as n, ProjectSourceComparison as o, createPreviewSync as p, PullRequestError as r, ProjectSourceReview as s, createScratchBlocksDiffRenderer as t, projectPropertyLabel as u, compareProjectBlocks as v, readProjectSourceDiff as x, createProjectSourceDiff as y };