@kubohiroya/turbowarp-plus-editor-git 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,52 +1,6 @@
1
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";
2
+ import { a as createTwoFilesPatch, i as isAssetName, n as assetsDirectoryParts, o as hasOnlyUnixLineEndings, r as folderSettingsLimit, s as hasOnlyWinLineEndings, t as assetLimit } from "./project-asset-paths-KV715791.js";
3
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
4
  // node_modules/diff/libesm/patch/line-endings.js
51
5
  function unixToWin(patch) {
52
6
  if (Array.isArray(patch)) return patch.map((p) => unixToWin(p));
@@ -560,6 +514,7 @@ function applyStructuredPatch(source, patch, options = {}) {
560
514
  return resultLines.join("\n");
561
515
  }
562
516
  // \0@oxc-project+runtime@0.156.0/helpers/esm/objectWithoutPropertiesLoose.js
517
+ init_objectSpread2();
563
518
  function _objectWithoutPropertiesLoose(r, e) {
564
519
  if (null == r) return {};
565
520
  var t = {};
@@ -580,7 +535,6 @@ function _objectWithoutProperties(e, t) {
580
535
  return i;
581
536
  }
582
537
  // src/project-source-patch.ts
583
- init_objectSpread2();
584
538
  var _excluded = ["blocks"];
585
539
  var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
586
540
  function readProject(source) {
@@ -698,6 +652,201 @@ function compareProjectBlocks(before, after) {
698
652
  otherChanges: !equal(withoutBlocks(before), withoutBlocks(after))
699
653
  };
700
654
  }
655
+ // src/project-assets.ts
656
+ var imageTypes = {
657
+ svg: "image/svg+xml",
658
+ png: "image/png",
659
+ jpg: "image/jpeg",
660
+ jpeg: "image/jpeg",
661
+ gif: "image/gif",
662
+ bmp: "image/bmp",
663
+ webp: "image/webp"
664
+ };
665
+ /** The costume a sprite shows, or the backdrop the stage shows, when the project data names an image file for it. */
666
+ function currentCostume(target) {
667
+ const { costumes, currentCostume: index } = target;
668
+ if (!Array.isArray(costumes)) return null;
669
+ const costume = costumes[typeof index === "number" && Number.isInteger(index) && index >= 0 ? index : 0];
670
+ if (!isRecord(costume)) return null;
671
+ const md5ext = typeof costume.md5ext === "string" ? costume.md5ext : typeof costume.assetId === "string" && typeof costume.dataFormat === "string" ? costume.assetId + "." + costume.dataFormat : "";
672
+ const match = /^[0-9A-Za-z_-]{1,128}\.([0-9A-Za-z]{1,8})$/.exec(md5ext);
673
+ const type = match ? imageTypes[match[1].toLowerCase()] : void 0;
674
+ if (!type) return null;
675
+ return {
676
+ name: typeof costume.name === "string" ? costume.name : "",
677
+ md5ext,
678
+ type
679
+ };
680
+ }
681
+ var reads = /* @__PURE__ */ new WeakMap();
682
+ function readAsset(loadAsset, version, md5ext) {
683
+ let cache = reads.get(loadAsset);
684
+ if (!cache) reads.set(loadAsset, cache = /* @__PURE__ */ new Map());
685
+ const key = version + ":" + md5ext;
686
+ let read = cache.get(key);
687
+ if (!read) {
688
+ read = loadAsset({
689
+ version,
690
+ md5ext,
691
+ signal: new AbortController().signal
692
+ }).catch(() => null);
693
+ cache.set(key, read);
694
+ }
695
+ return read;
696
+ }
697
+ /** Loads the current costume of one version of a target as an object URL for an image, or `null` while it is unavailable. */
698
+ function useCostumeImage(target, version, loadAsset) {
699
+ const costume = target && loadAsset ? currentCostume(target) : null;
700
+ const key = costume ? version + ":" + costume.md5ext : "";
701
+ const [image, setImage] = useState();
702
+ useEffect(() => {
703
+ if (!costume || !loadAsset) return;
704
+ let active = true;
705
+ let url;
706
+ readAsset(loadAsset, version, costume.md5ext).then((blob) => {
707
+ if (!blob || !active) return;
708
+ url = URL.createObjectURL(new Blob([blob], { type: costume.type }));
709
+ setImage({
710
+ key,
711
+ url
712
+ });
713
+ });
714
+ return () => {
715
+ active = false;
716
+ if (url) URL.revokeObjectURL(url);
717
+ };
718
+ }, [key, loadAsset]);
719
+ return (image === null || image === void 0 ? void 0 : image.key) === key ? image.url : null;
720
+ }
721
+ // src/block-diff-preview.tsx
722
+ init_objectSpread2();
723
+ function createPreviewSync() {
724
+ const members = /* @__PURE__ */ new Map();
725
+ let linked = true;
726
+ let moving = false;
727
+ const align = () => {
728
+ if (!linked || !members.size) return;
729
+ const views = [...members.keys()];
730
+ const fit = {
731
+ scale: Math.min(...views.map((view) => view.fit.scale)),
732
+ height: Math.max(...views.map((view) => view.fit.height))
733
+ };
734
+ for (const view of views) view.layout(fit);
735
+ };
736
+ return {
737
+ add(view) {
738
+ const stop = view.onChange(() => {
739
+ if (!linked || moving) return;
740
+ moving = true;
741
+ try {
742
+ const viewport = view.viewport();
743
+ for (const other of members.keys()) if (other !== view) other.setViewport(viewport);
744
+ } finally {
745
+ moving = false;
746
+ }
747
+ });
748
+ members.set(view, stop);
749
+ align();
750
+ return () => {
751
+ stop();
752
+ members.delete(view);
753
+ };
754
+ },
755
+ setLinked(value) {
756
+ if (linked === value) return;
757
+ linked = value;
758
+ align();
759
+ }
760
+ };
761
+ }
762
+ var linkLabels = {
763
+ en: "Move both versions together",
764
+ ja: "変更前と変更後を連動して動かす"
765
+ };
766
+ /** One sync group per comparison, with the checkbox that links or unlinks its previews. */
767
+ function usePreviewSync(locale) {
768
+ const sync = useMemo(createPreviewSync, []);
769
+ const [linked, setLinked] = useState(true);
770
+ useEffect(() => sync.setLinked(linked), [sync, linked]);
771
+ return {
772
+ sync,
773
+ toggle: /* @__PURE__ */ React.createElement("label", { className: "twgit-block-diff-link" }, /* @__PURE__ */ React.createElement("input", {
774
+ type: "checkbox",
775
+ checked: linked,
776
+ onChange: (event) => setLinked(event.currentTarget.checked)
777
+ }), " ", locale.startsWith("ja") ? linkLabels.ja : linkLabels.en)
778
+ };
779
+ }
780
+ var rendering = (result) => typeof result === "function" ? { release: result } : {
781
+ release: result && (() => result.dispose()),
782
+ view: result === null || result === void 0 ? void 0 : result.view
783
+ };
784
+ function BlockPreview({ side, title, parts, marks, renderBlock, withContext, labels, onLocate, locationStatus, version, loadAsset, sync }) {
785
+ const host = useRef(null);
786
+ const watermark = useRef(null);
787
+ const [status, setStatus] = useState("loading");
788
+ const thumbnail = !!(side && version && loadAsset && currentCostume(side.target));
789
+ const image = useCostumeImage(thumbnail ? side.target : void 0, version !== null && version !== void 0 ? version : "after", loadAsset);
790
+ useEffect(() => {
791
+ if (!side || !host.current) return;
792
+ const container = host.current;
793
+ const abort = new AbortController();
794
+ let dispose;
795
+ setStatus("loading");
796
+ Promise.resolve().then(() => {
797
+ var _watermark$current;
798
+ return renderBlock(_objectSpread2(_objectSpread2({}, side), {}, {
799
+ container,
800
+ withContext,
801
+ parts,
802
+ marks,
803
+ signal: abort.signal,
804
+ watermark: thumbnail ? (_watermark$current = watermark.current) !== null && _watermark$current !== void 0 ? _watermark$current : void 0 : void 0
805
+ }));
806
+ }).then((result) => {
807
+ const { release, view } = rendering(result);
808
+ if (abort.signal.aborted) {
809
+ release === null || release === void 0 || release();
810
+ return;
811
+ }
812
+ const leave = view && (sync === null || sync === void 0 ? void 0 : sync.add(view));
813
+ dispose = () => {
814
+ leave === null || leave === void 0 || leave();
815
+ release === null || release === void 0 || release();
816
+ };
817
+ setStatus("ready");
818
+ }, () => {
819
+ if (!abort.signal.aborted) setStatus("failed");
820
+ });
821
+ return () => {
822
+ abort.abort();
823
+ dispose === null || dispose === void 0 || dispose();
824
+ };
825
+ }, [
826
+ side,
827
+ renderBlock,
828
+ withContext,
829
+ parts,
830
+ marks,
831
+ sync,
832
+ thumbnail
833
+ ]);
834
+ return /* @__PURE__ */ React.createElement("div", { className: "twgit-block-diff-side" }, /* @__PURE__ */ React.createElement("h4", null, title), !side && /* @__PURE__ */ React.createElement("p", null, labels.absent), side && status === "loading" && /* @__PURE__ */ React.createElement("p", { role: "status" }, labels.loading), side && status === "failed" && /* @__PURE__ */ React.createElement("p", { role: "status" }, labels.unavailable), side && /* @__PURE__ */ React.createElement("div", {
835
+ ref: host,
836
+ className: "twgit-block-diff-preview"
837
+ }, thumbnail && /* @__PURE__ */ React.createElement("div", {
838
+ ref: watermark,
839
+ className: "twgit-block-diff-watermark",
840
+ "data-dockview-costume-thumbnail": ""
841
+ }, image && /* @__PURE__ */ React.createElement("img", {
842
+ src: image,
843
+ alt: ""
844
+ }))), side && onLocate && /* @__PURE__ */ React.createElement("button", {
845
+ type: "button",
846
+ "aria-label": `${title} · ${labels.locate}`,
847
+ onClick: onLocate
848
+ }, labels.locate), locationStatus && /* @__PURE__ */ React.createElement("p", { role: "status" }, labels[locationStatus]));
849
+ }
701
850
  // src/project-source-review-model.ts
702
851
  init_objectSpread2();
703
852
  function scriptRoots(target) {
@@ -807,7 +956,9 @@ function compareProjectForReview(before, after) {
807
956
  name,
808
957
  isStage,
809
958
  scripts,
810
- properties
959
+ properties,
960
+ before: a,
961
+ after: b
811
962
  });
812
963
  }
813
964
  }
@@ -823,18 +974,18 @@ var ja = {
823
974
  checks: "確認してほしいこと",
824
975
  files: "変更ファイル",
825
976
  targets: "影響するステージ・スプライト",
826
- scripts: "スクリプトの比較",
977
+ scripts: "コードの変更",
978
+ scriptCount: (count) => `コードの変更 · ${count} か所`,
827
979
  reviewed: "確認済み",
828
- reviewedScripts: "スクリプト確認済み",
829
980
  before: "変更前",
830
981
  after: "変更後",
831
982
  updated: "変更",
832
983
  added: "追加",
833
984
  removed: "削除",
834
985
  reorganized: "再編",
835
- absent: "この版にはスクリプトがありません",
836
- loading: "スクリプトを描画中…",
837
- unavailable: "このスクリプトを描画できません。",
986
+ absent: "この版にはこのコードがありません",
987
+ loading: "コードを描画中…",
988
+ unavailable: "このコードを描画できません。",
838
989
  related: "関連ファイル",
839
990
  properties: "コード以外の変更",
840
991
  invalid: "この作品の差分を読み込めません。",
@@ -842,7 +993,7 @@ var ja = {
842
993
  broadcast: "を受け取ったとき",
843
994
  key: "キーが押されたとき",
844
995
  click: "このスプライトが押されたとき",
845
- script: "スクリプト",
996
+ script: "コード",
846
997
  noChanges: "作品に内容の変更はありません。",
847
998
  comparison: "作品の変更",
848
999
  unreadable: "作品データとして読み込めないため、テキストの差分を表示します。"
@@ -853,18 +1004,18 @@ var en = {
853
1004
  checks: "Review checklist",
854
1005
  files: "Changed files",
855
1006
  targets: "Affected stages and sprites",
856
- scripts: "Script comparisons",
1007
+ scripts: "Code changes",
1008
+ scriptCount: (count) => `Code changes · ${count}`,
857
1009
  reviewed: "Reviewed",
858
- reviewedScripts: "Reviewed scripts",
859
1010
  before: "Before",
860
1011
  after: "After",
861
1012
  updated: "Changed",
862
1013
  added: "Added",
863
1014
  removed: "Removed",
864
1015
  reorganized: "Reorganized",
865
- absent: "No script in this version",
866
- loading: "Rendering script…",
867
- unavailable: "This script could not be rendered.",
1016
+ absent: "No code in this version",
1017
+ loading: "Rendering code…",
1018
+ unavailable: "This code could not be rendered.",
868
1019
  related: "Related file",
869
1020
  properties: "Changes outside the code",
870
1021
  invalid: "This project diff could not be read.",
@@ -872,7 +1023,7 @@ var en = {
872
1023
  broadcast: "received",
873
1024
  key: "When key pressed",
874
1025
  click: "When this sprite clicked",
875
- script: "Script",
1026
+ script: "Code",
876
1027
  noChanges: "No changes to the project contents.",
877
1028
  comparison: "Project changes",
878
1029
  unreadable: "This file could not be read as project data, so its text diff is shown."
@@ -906,8 +1057,9 @@ function scriptTitle(side, labels) {
906
1057
  }
907
1058
  return typeof block.opcode === "string" ? block.opcode : labels.script;
908
1059
  }
909
- function ScriptComparison({ script, labels, reviewed = false, setReviewed, renderBlock }) {
1060
+ function ScriptComparison({ script, labels, reviewed = false, setReviewed, renderBlock, loadAsset }) {
910
1061
  const [open, setOpen] = useState(false);
1062
+ const { sync, toggle } = usePreviewSync(labels === ja ? "ja" : "en");
911
1063
  const status = !script.before.length ? "added" : !script.after.length ? "removed" : script.before.length !== 1 || script.after.length !== 1 ? "reorganized" : "updated";
912
1064
  const beforeTitle = script.before[0] && scriptTitle(script.before[0], labels);
913
1065
  const afterTitle = script.after[0] && scriptTitle(script.after[0], labels);
@@ -922,13 +1074,16 @@ function ScriptComparison({ script, labels, reviewed = false, setReviewed, rende
922
1074
  marks: side.marks,
923
1075
  renderBlock,
924
1076
  withContext: true,
925
- labels
1077
+ labels,
1078
+ version,
1079
+ loadAsset,
1080
+ sync
926
1081
  })) : /* @__PURE__ */ React.createElement(BlockPreview, {
927
1082
  title: labels[version],
928
1083
  renderBlock,
929
1084
  withContext: true,
930
1085
  labels
931
- })))), setReviewed && /* @__PURE__ */ React.createElement("label", { className: "twgit-review-checked" }, /* @__PURE__ */ React.createElement("input", {
1086
+ })))), toggle, setReviewed && /* @__PURE__ */ React.createElement("label", { className: "twgit-review-checked" }, /* @__PURE__ */ React.createElement("input", {
932
1087
  type: "checkbox",
933
1088
  checked: reviewed,
934
1089
  onChange: (event) => setReviewed(event.currentTarget.checked)
@@ -940,12 +1095,41 @@ function propertyNames(keys, labels) {
940
1095
  return (_properties$get = (_properties$get2 = properties.get(key)) === null || _properties$get2 === void 0 ? void 0 : _properties$get2[labels === ja ? 0 : 1]) !== null && _properties$get !== void 0 ? _properties$get : key;
941
1096
  }).join(" · ");
942
1097
  }
943
- /** Lists the changed stages and sprites of one project file, each with its script comparisons. */
944
- function ComparisonTargets({ comparison, labels, renderBlock, review }) {
1098
+ /**
1099
+ * The current costume (the backdrop for the stage) of each version of a target that has one. A changed costume shows
1100
+ * before → after. Nothing is shown until an image is read, so the heading falls back to the name alone.
1101
+ */
1102
+ function TargetThumbnails({ target, labels, loadAsset }) {
1103
+ const images = {
1104
+ before: useCostumeImage(target.before, "before", loadAsset),
1105
+ after: useCostumeImage(target.after, "after", loadAsset)
1106
+ };
1107
+ const costumes = {
1108
+ before: target.before && currentCostume(target.before),
1109
+ after: target.after && currentCostume(target.after)
1110
+ };
1111
+ const changed = !!costumes.before && !!costumes.after && costumes.before.md5ext !== costumes.after.md5ext;
1112
+ const shown = (changed ? ["before", "after"] : [images.after ? "after" : "before"]).filter((version) => images[version]);
1113
+ if (!shown.length) return null;
1114
+ return /* @__PURE__ */ React.createElement("span", { className: "twgit-review-thumbnails" }, shown.map((version, index) => {
1115
+ var _costumes$version, _costumes$version$nam, _costumes$version2;
1116
+ return /* @__PURE__ */ React.createElement(React.Fragment, { key: version }, index > 0 && /* @__PURE__ */ React.createElement("span", { "aria-hidden": "true" }, "→"), /* @__PURE__ */ React.createElement("img", {
1117
+ src: images[version],
1118
+ title: ((_costumes$version = costumes[version]) === null || _costumes$version === void 0 ? void 0 : _costumes$version.name) || void 0,
1119
+ alt: changed ? `${labels[version]}: ${(_costumes$version$nam = (_costumes$version2 = costumes[version]) === null || _costumes$version2 === void 0 ? void 0 : _costumes$version2.name) !== null && _costumes$version$nam !== void 0 ? _costumes$version$nam : ""}` : ""
1120
+ }));
1121
+ }));
1122
+ }
1123
+ /** Lists the changed stages and sprites of one project file, each with its code comparisons. */
1124
+ function ComparisonTargets({ comparison, labels, renderBlock, loadAsset, review }) {
945
1125
  return /* @__PURE__ */ React.createElement(React.Fragment, null, comparison.properties.length > 0 && /* @__PURE__ */ React.createElement("p", null, labels.properties, " · ", propertyNames(comparison.properties, labels)), !comparison.targets.length && !comparison.properties.length && /* @__PURE__ */ React.createElement("p", null, labels.noChanges), comparison.targets.map((target) => /* @__PURE__ */ React.createElement("section", {
946
1126
  className: "twgit-review-target",
947
1127
  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) => {
1128
+ }, /* @__PURE__ */ React.createElement("h5", null, /* @__PURE__ */ React.createElement(TargetThumbnails, {
1129
+ target,
1130
+ labels,
1131
+ loadAsset
1132
+ }), target.name, target.scripts.length > 0 && /* @__PURE__ */ React.createElement("small", null, labels.scriptCount(target.scripts.length))), target.properties.length > 0 && /* @__PURE__ */ React.createElement("p", null, labels.properties, " · ", propertyNames(target.properties, labels)), target.scripts.map((script) => {
949
1133
  const key = JSON.stringify([
950
1134
  review === null || review === void 0 ? void 0 : review.file,
951
1135
  target.isStage,
@@ -957,14 +1141,15 @@ function ComparisonTargets({ comparison, labels, renderBlock, review }) {
957
1141
  script,
958
1142
  labels,
959
1143
  renderBlock,
1144
+ loadAsset,
960
1145
  reviewed: review === null || review === void 0 ? void 0 : review.keys.has(key),
961
1146
  setReviewed: review && ((value) => review.set(key, value))
962
1147
  });
963
1148
  }))));
964
1149
  }
965
1150
  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 }) {
1151
+ /** Compares two versions of a project.source.json by stage, sprite and code, as the pull request review does. */
1152
+ function ProjectSourceComparison({ before, after, renderBlock, locale = "en", fallback, loadAsset }) {
968
1153
  const labels = locale.startsWith("ja") ? ja : en;
969
1154
  const comparison = useMemo(() => {
970
1155
  try {
@@ -983,7 +1168,8 @@ function ProjectSourceComparison({ before, after, renderBlock, locale = "en", fa
983
1168
  }, /* @__PURE__ */ React.createElement("header", { className: "twgit-review-overview" }, /* @__PURE__ */ React.createElement("dl", null, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("dt", null, labels.targets), /* @__PURE__ */ React.createElement("dd", null, comparison.targets.length)), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("dt", null, labels.scripts), /* @__PURE__ */ React.createElement("dd", null, countScripts(comparison))))), /* @__PURE__ */ React.createElement(ComparisonTargets, {
984
1169
  comparison,
985
1170
  labels,
986
- renderBlock
1171
+ renderBlock,
1172
+ loadAsset
987
1173
  }));
988
1174
  }
989
1175
  function ProjectSourceReview({ title, description, verification, files, renderBlock, locale = "en" }) {
@@ -1028,13 +1214,14 @@ function ProjectSourceReview({ title, description, verification, files, renderBl
1028
1214
  return /* @__PURE__ */ React.createElement("section", {
1029
1215
  className: "twgit-block-diff twgit-review",
1030
1216
  "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", {
1217
+ }, /* @__PURE__ */ React.createElement("header", { className: "twgit-review-overview" }, /* @__PURE__ */ React.createElement("p", null, labels.review), /* @__PURE__ */ React.createElement("h2", null, title), excerpt && /* @__PURE__ */ React.createElement("p", { className: "twgit-review-description" }, excerpt), /* @__PURE__ */ React.createElement("dl", null, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("dt", null, labels.files), /* @__PURE__ */ React.createElement("dd", null, entries.length)), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("dt", null, labels.targets), /* @__PURE__ */ React.createElement("dd", null, targets)), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("dt", null, labels.scripts), /* @__PURE__ */ React.createElement("dd", null, scripts)), scripts > 0 && /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("dt", null, labels.reviewed), /* @__PURE__ */ React.createElement("dd", null, reviewed.size, " / ", scripts))), longBody && /* @__PURE__ */ React.createElement("details", { className: "twgit-review-body" }, /* @__PURE__ */ React.createElement("summary", null, labels.fullDescription), /* @__PURE__ */ React.createElement("p", { className: "twgit-review-description" }, body)), !!(verification === null || verification === void 0 ? void 0 : verification.length) && /* @__PURE__ */ React.createElement("div", { className: "twgit-review-checklist" }, /* @__PURE__ */ React.createElement("h3", null, labels.checks), /* @__PURE__ */ React.createElement("ul", null, verification.map((item, index) => /* @__PURE__ */ React.createElement("li", { key: index }, item))))), /* @__PURE__ */ React.createElement("h3", { className: "twgit-review-files-title" }, labels.files), entries.map(({ file, comparison, invalid }) => /* @__PURE__ */ React.createElement("section", {
1032
1218
  key: file.path,
1033
1219
  className: "twgit-review-file"
1034
1220
  }, /* @__PURE__ */ React.createElement("header", null, /* @__PURE__ */ React.createElement("h4", null, file.previousPath ? file.previousPath + " → " + file.path : file.path), file.summary && /* @__PURE__ */ React.createElement("p", null, file.summary)), "status" in file && /* @__PURE__ */ React.createElement("p", null, labels.related, " · ", labels[file.status]), (invalid || file.unavailable) && /* @__PURE__ */ React.createElement("p", { role: "alert" }, labels.invalid), comparison && /* @__PURE__ */ React.createElement(ComparisonTargets, {
1035
1221
  comparison,
1036
1222
  labels,
1037
1223
  renderBlock,
1224
+ loadAsset: "loadAsset" in file ? file.loadAsset : void 0,
1038
1225
  review: {
1039
1226
  file: file.path,
1040
1227
  keys: reviewed,
@@ -1065,25 +1252,29 @@ function repoPath(repository) {
1065
1252
  var invalidResponse = () => new PullRequestError("failed");
1066
1253
  var emptyProject = "{\"targets\":[]}\n";
1067
1254
  var sourceLimit = 1048576;
1068
- async function readText(response, limit) {
1255
+ async function readBody(response, limit, chunk) {
1069
1256
  if (Number(response.headers.get("Content-Length")) > limit || !response.body) throw invalidResponse();
1070
1257
  const reader = response.body.getReader();
1071
- const decoder = new TextDecoder("utf-8", { fatal: true });
1072
- const chunks = [];
1073
1258
  let length = 0;
1074
1259
  try {
1075
1260
  for (;;) {
1076
1261
  const { done, value } = await reader.read();
1077
- if (done) return chunks.join("") + decoder.decode();
1262
+ if (done) return;
1078
1263
  length += value.byteLength;
1079
1264
  if (length > limit) throw invalidResponse();
1080
- chunks.push(decoder.decode(value, { stream: true }));
1265
+ chunk(value);
1081
1266
  }
1082
1267
  } catch (error) {
1083
1268
  reader.cancel().catch(() => {});
1084
1269
  throw error;
1085
1270
  }
1086
1271
  }
1272
+ async function readText(response, limit) {
1273
+ const decoder = new TextDecoder("utf-8", { fatal: true });
1274
+ const chunks = [];
1275
+ await readBody(response, limit, (value) => chunks.push(decoder.decode(value, { stream: true })));
1276
+ return chunks.join("") + decoder.decode();
1277
+ }
1087
1278
  function readSummary(value) {
1088
1279
  if (!isRecord(value) || !Number.isSafeInteger(value.number) || Number(value.number) < 1 || typeof value.title !== "string") throw invalidResponse();
1089
1280
  return {
@@ -1129,6 +1320,63 @@ function contentPath(path) {
1129
1320
  if (parts.some((part) => !part || part === "." || part === "..")) throw invalidResponse();
1130
1321
  return parts.map(encodeURIComponent).join("/");
1131
1322
  }
1323
+ /**
1324
+ * Reads the assets of both versions of a project file from raw.githubusercontent.com, which does not count against the
1325
+ * API rate limit. Each version's sb3-source.json names its assets directory. Every read is capped, honors both its own
1326
+ * signal and the pull request's, and resolves `null` on any failure.
1327
+ */
1328
+ function rawAssetLoader(versions, send, review) {
1329
+ const directories = /* @__PURE__ */ new Map();
1330
+ const get = (url, signal) => send(url, {
1331
+ credentials: "omit",
1332
+ redirect: "error",
1333
+ signal
1334
+ });
1335
+ const folderUrl = ({ repository, sha, path }) => "https://raw.githubusercontent.com/" + repository.split("/").map(encodeURIComponent).join("/") + "/" + sha + "/" + contentPath(path).split("/").slice(0, -1).map((part) => part + "/").join("");
1336
+ const readDirectory = async (folder, signal) => {
1337
+ let settings = null;
1338
+ try {
1339
+ var _response$body;
1340
+ const response = await get(folder + "sb3-source.json", signal);
1341
+ if (response.ok) settings = await readText(response, folderSettingsLimit);
1342
+ else (_response$body = response.body) === null || _response$body === void 0 || _response$body.cancel().catch(() => {});
1343
+ } catch (error) {
1344
+ if (signal.aborted) throw error;
1345
+ }
1346
+ return assetsDirectoryParts(settings);
1347
+ };
1348
+ return async ({ version, md5ext, signal: own }) => {
1349
+ const location = versions[version];
1350
+ if (!location || !isAssetName(md5ext)) return null;
1351
+ try {
1352
+ const signal = AbortSignal.any([own, review]);
1353
+ if (signal.aborted) return null;
1354
+ const folder = folderUrl(location);
1355
+ let directory = directories.get(version);
1356
+ if (!directory) {
1357
+ directory = readDirectory(folder, signal);
1358
+ directories.set(version, directory);
1359
+ const pending = directory;
1360
+ pending.catch(() => {
1361
+ if (directories.get(version) === pending) directories.delete(version);
1362
+ });
1363
+ }
1364
+ const parts = await directory;
1365
+ if (!parts || signal.aborted) return null;
1366
+ const response = await get(folder + parts.map(encodeURIComponent).join("/") + "/" + md5ext, signal);
1367
+ if (!response.ok) {
1368
+ var _response$body2;
1369
+ (_response$body2 = response.body) === null || _response$body2 === void 0 || _response$body2.cancel().catch(() => {});
1370
+ return null;
1371
+ }
1372
+ const chunks = [];
1373
+ await readBody(response, assetLimit, (value) => chunks.push(value));
1374
+ return signal.aborted ? null : new Blob(chunks);
1375
+ } catch (_unused) {
1376
+ return null;
1377
+ }
1378
+ };
1379
+ }
1132
1380
  function createGitHubPullRequests(repository, send = fetch) {
1133
1381
  if (!validRepository(repository)) throw invalidResponse();
1134
1382
  const api = "https://api.github.com";
@@ -1193,13 +1441,26 @@ function createGitHubPullRequests(repository, send = fetch) {
1193
1441
  continue;
1194
1442
  }
1195
1443
  try {
1196
- var _file$previous, _pr$head$repository;
1444
+ var _file$previous, _pr$head$repository, _file$previous2, _pr$head$repository2;
1197
1445
  if (status !== "added" && !base) throw invalidResponse();
1198
1446
  const [before, after] = await Promise.all([status === "added" ? emptyProject : source(repository, (_file$previous = file.previous) !== null && _file$previous !== void 0 ? _file$previous : file.path, base, signal), status === "removed" ? emptyProject : source((_pr$head$repository = pr.head.repository) !== null && _pr$head$repository !== void 0 ? _pr$head$repository : repository, file.path, pr.head.sha, signal)]);
1447
+ const loadAsset = rawAssetLoader({
1448
+ before: status === "added" ? null : {
1449
+ repository,
1450
+ sha: base,
1451
+ path: (_file$previous2 = file.previous) !== null && _file$previous2 !== void 0 ? _file$previous2 : file.path
1452
+ },
1453
+ after: status === "removed" ? null : {
1454
+ repository: (_pr$head$repository2 = pr.head.repository) !== null && _pr$head$repository2 !== void 0 ? _pr$head$repository2 : repository,
1455
+ sha: pr.head.sha,
1456
+ path: file.path
1457
+ }
1458
+ }, send, signal);
1199
1459
  files.push({
1200
1460
  path: file.path,
1201
1461
  previousPath: file.previous,
1202
- patch: createProjectSourceDiff(before, after)
1462
+ patch: createProjectSourceDiff(before, after),
1463
+ loadAsset
1203
1464
  });
1204
1465
  } catch (error) {
1205
1466
  if (signal.aborted) throw error;
@@ -1372,9 +1633,93 @@ function placeholderDefinition(element, opcode, type, blockly, locale) {
1372
1633
  }
1373
1634
  return definition;
1374
1635
  }
1636
+ var defaultScale = .675;
1637
+ var padding = 16;
1638
+ var minHeight = 160;
1639
+ var maxHeight = 420;
1640
+ /**
1641
+ * Sizes a preview frame to its blocks and reports scrolling and zooming, so both versions can move together.
1642
+ * The frame keeps a gutter on the right for the zoom buttons, and for the minimap and thumbnail when present.
1643
+ */
1644
+ function createView(workspace, blockly, surface, frame, startScale, navigation) {
1645
+ var _bounds$x, _bounds$y, _workspace$translate, _surface$ownerDocumen;
1646
+ const bounds = workspace.getBlocksBoundingBox();
1647
+ const left = (_bounds$x = bounds.x) !== null && _bounds$x !== void 0 ? _bounds$x : 0, top = (_bounds$y = bounds.y) !== null && _bounds$y !== void 0 ? _bounds$y : 0;
1648
+ const gutter = navigation ? 150 : 56;
1649
+ const width = frame.clientWidth || 400;
1650
+ const scale = Math.max(.25, Math.min(startScale, (width - padding - gutter) / Math.max(1, bounds.width), 388 / Math.max(1, bounds.height)));
1651
+ const fit = {
1652
+ scale,
1653
+ height: Math.min(maxHeight, Math.max(minHeight, Math.ceil(bounds.height * scale + 32)))
1654
+ };
1655
+ const listeners = /* @__PURE__ */ new Set();
1656
+ const translate = (_workspace$translate = workspace.translate) === null || _workspace$translate === void 0 ? void 0 : _workspace$translate.bind(workspace);
1657
+ let quiet = false;
1658
+ if (translate) workspace.translate = (x, y) => {
1659
+ translate(x, y);
1660
+ if (!quiet) for (const listener of listeners) listener();
1661
+ };
1662
+ const setViewport = ({ scale, x, y }) => {
1663
+ if (!translate) return;
1664
+ quiet = true;
1665
+ try {
1666
+ var _workspace$getMetrics, _metrics$absoluteLeft, _metrics$absoluteTop, _workspace$scrollbar;
1667
+ workspace.scale = scale;
1668
+ workspace.scrollX = x;
1669
+ workspace.scrollY = y;
1670
+ const metrics = (_workspace$getMetrics = workspace.getMetrics) === null || _workspace$getMetrics === void 0 ? void 0 : _workspace$getMetrics.call(workspace);
1671
+ translate(x + ((_metrics$absoluteLeft = metrics === null || metrics === void 0 ? void 0 : metrics.absoluteLeft) !== null && _metrics$absoluteLeft !== void 0 ? _metrics$absoluteLeft : 0), y + ((_metrics$absoluteTop = metrics === null || metrics === void 0 ? void 0 : metrics.absoluteTop) !== null && _metrics$absoluteTop !== void 0 ? _metrics$absoluteTop : 0));
1672
+ (_workspace$scrollbar = workspace.scrollbar) === null || _workspace$scrollbar === void 0 || _workspace$scrollbar.resize();
1673
+ } finally {
1674
+ quiet = false;
1675
+ }
1676
+ };
1677
+ const layout = ({ scale, height }) => {
1678
+ frame.style.height = height + "px";
1679
+ blockly.svgResize(workspace);
1680
+ setViewport({
1681
+ scale,
1682
+ x: padding - left * scale,
1683
+ y: padding - top * scale
1684
+ });
1685
+ };
1686
+ layout(fit);
1687
+ const resize = new ((_surface$ownerDocumen = surface.ownerDocument.defaultView) !== null && _surface$ownerDocumen !== void 0 ? _surface$ownerDocumen : globalThis).ResizeObserver(() => blockly.svgResize(workspace));
1688
+ resize.observe(surface);
1689
+ const wheel = (event) => {
1690
+ if (!event.ctrlKey) event.stopPropagation();
1691
+ };
1692
+ surface.addEventListener("wheel", wheel, { capture: true });
1693
+ return {
1694
+ api: {
1695
+ fit,
1696
+ layout,
1697
+ setViewport,
1698
+ viewport: () => {
1699
+ var _workspace$scale, _workspace$scrollX, _workspace$scrollY;
1700
+ return {
1701
+ scale: (_workspace$scale = workspace.scale) !== null && _workspace$scale !== void 0 ? _workspace$scale : fit.scale,
1702
+ x: (_workspace$scrollX = workspace.scrollX) !== null && _workspace$scrollX !== void 0 ? _workspace$scrollX : 0,
1703
+ y: (_workspace$scrollY = workspace.scrollY) !== null && _workspace$scrollY !== void 0 ? _workspace$scrollY : 0
1704
+ };
1705
+ },
1706
+ onChange(listener) {
1707
+ listeners.add(listener);
1708
+ return () => listeners.delete(listener);
1709
+ }
1710
+ },
1711
+ dispose() {
1712
+ frame.style.removeProperty("height");
1713
+ listeners.clear();
1714
+ resize.disconnect();
1715
+ surface.removeEventListener("wheel", wheel, { capture: true });
1716
+ if (translate) workspace.translate = translate;
1717
+ }
1718
+ };
1719
+ }
1375
1720
  /** Use the host's Scratch VM serializer and Scratch Blocks definitions in an isolated workspace. */
1376
1721
  function createScratchBlocksDiffRenderer(options) {
1377
- return async ({ container, target, id, withContext, parts = [], marks, signal }) => {
1722
+ return async ({ container, target, id, withContext, parts = [], marks, signal, watermark }) => {
1378
1723
  var _Object$values$find;
1379
1724
  const blockly = await options.getBlockly();
1380
1725
  if (signal.aborted) return;
@@ -1400,11 +1745,16 @@ function createScratchBlocksDiffRenderer(options) {
1400
1745
  surface.className = "twgit-block-diff-surface";
1401
1746
  container.append(surface);
1402
1747
  let workspace;
1748
+ let view;
1749
+ let navigation;
1750
+ const cleanups = [];
1403
1751
  const placeholders = [];
1404
1752
  const dispose = () => {
1405
1753
  const active = blockly.getMainWorkspace();
1754
+ for (const cleanup of cleanups.splice(0)) cleanup();
1406
1755
  blockly.Events.disable();
1407
1756
  try {
1757
+ navigation === null || navigation === void 0 || navigation.dispose();
1408
1758
  workspace === null || workspace === void 0 || workspace.dispose();
1409
1759
  } finally {
1410
1760
  if (active === workspace) blockly.mainWorkspace = main;
@@ -1415,7 +1765,7 @@ function createScratchBlocksDiffRenderer(options) {
1415
1765
  };
1416
1766
  blockly.Events.disable();
1417
1767
  try {
1418
- var _main$options;
1768
+ var _main$options$zoomOpt, _main$options, _main$options2, _options$attachNaviga;
1419
1769
  for (const element of Array.from(dom.querySelectorAll("block,shadow"))) {
1420
1770
  var _element$getAttribute2, _options$locale;
1421
1771
  const opcode = (_element$getAttribute2 = element.getAttribute("type")) !== null && _element$getAttribute2 !== void 0 ? _element$getAttribute2 : "";
@@ -1429,10 +1779,20 @@ function createScratchBlocksDiffRenderer(options) {
1429
1779
  blockly.defineBlocksWithJsonArray([placeholderDefinition(element, opcode, alias, blockly, (_options$locale = options.locale) !== null && _options$locale !== void 0 ? _options$locale : container.ownerDocument.documentElement.lang)]);
1430
1780
  element.setAttribute("type", alias);
1431
1781
  }
1782
+ const startScale = (_main$options$zoomOpt = main === null || main === void 0 || (_main$options = main.options) === null || _main$options === void 0 || (_main$options = _main$options.zoomOptions) === null || _main$options === void 0 ? void 0 : _main$options.startScale) !== null && _main$options$zoomOpt !== void 0 ? _main$options$zoomOpt : defaultScale;
1432
1783
  workspace = blockly.inject(surface, {
1433
1784
  readOnly: true,
1434
1785
  sounds: false,
1435
- media: main === null || main === void 0 || (_main$options = main.options) === null || _main$options === void 0 ? void 0 : _main$options.pathToMedia
1786
+ media: main === null || main === void 0 || (_main$options2 = main.options) === null || _main$options2 === void 0 ? void 0 : _main$options2.pathToMedia,
1787
+ scrollbars: true,
1788
+ zoom: {
1789
+ controls: true,
1790
+ wheel: true,
1791
+ startScale,
1792
+ maxScale: 4,
1793
+ minScale: .25,
1794
+ scaleSpeed: 1.2
1795
+ }
1436
1796
  });
1437
1797
  workspace.markFocused = () => {};
1438
1798
  blockly.Xml.domToWorkspace(dom, workspace);
@@ -1447,10 +1807,13 @@ function createScratchBlocksDiffRenderer(options) {
1447
1807
  const changed = workspace.getBlockById(id === root ? serializedRoot : id);
1448
1808
  if (changed) markBlockLocation(changed, parts);
1449
1809
  }
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);
1810
+ view = createView(workspace, blockly, surface, container, startScale, !!options.attachNavigation);
1811
+ cleanups.push(view.dispose);
1812
+ navigation = (_options$attachNaviga = options.attachNavigation) === null || _options$attachNaviga === void 0 ? void 0 : _options$attachNaviga.call(options, workspace, blockly, watermark);
1813
+ if (navigation && watermark) {
1814
+ watermark.dataset.navigation = "";
1815
+ cleanups.push(() => delete watermark.dataset.navigation);
1816
+ }
1454
1817
  } catch (error) {
1455
1818
  dispose();
1456
1819
  throw error;
@@ -1458,7 +1821,10 @@ function createScratchBlocksDiffRenderer(options) {
1458
1821
  blockly.mainWorkspace = main;
1459
1822
  blockly.Events.enable();
1460
1823
  }
1461
- return dispose;
1824
+ return {
1825
+ dispose,
1826
+ view: view.api
1827
+ };
1462
1828
  };
1463
1829
  }
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 };
1830
+ export { gitHubRepositoryFromOrigin as a, BlockPreview as c, currentCostume as d, compareProjectBlocks as f, createGitHubPullRequests as i, createPreviewSync as l, readProjectSourceDiff as m, createScratchBlocksDiffLocator as n, ProjectSourceComparison as o, createProjectSourceDiff as p, PullRequestError as r, ProjectSourceReview as s, createScratchBlocksDiffRenderer as t, usePreviewSync as u };