hyperbook 0.96.2 → 0.97.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.
@@ -19,6 +19,72 @@ hyperbook.openscad = (function () {
19
19
  };
20
20
 
21
21
  const i18nGet = (key, fallback = key) => hyperbook.i18n?.get(key) || fallback;
22
+ const ABSOLUTE_URL_PATTERN = /^(?:[a-z]+:)?\/\//i;
23
+
24
+ const normalizePath = (path) => {
25
+ if (!path) return "/";
26
+ return path.startsWith("/") ? path : `/${path}`;
27
+ };
28
+
29
+ const constructUrl = (path, basePath, pagePath) => {
30
+ if (path.startsWith("/")) {
31
+ return basePath ? `${basePath}${path}`.replace(/\/+/g, "/") : path;
32
+ }
33
+ return pagePath ? `${pagePath}/${path}`.replace(/\/+/g, "/") : path;
34
+ };
35
+
36
+ const resolveImportFsPath = (assetPath) => {
37
+ try {
38
+ return new URL(assetPath, "file:///tmp/model.scad").pathname || null;
39
+ } catch (_) {
40
+ return null;
41
+ }
42
+ };
43
+
44
+ const extractImportAssetPaths = (code) => {
45
+ const paths = new Set();
46
+ const pattern = /import\s*\(\s*(['"])([^'"]+)\1/g;
47
+ let match;
48
+ while ((match = pattern.exec(code || "")) !== null) {
49
+ const path = match[2];
50
+ if (!path) continue;
51
+ if (
52
+ ABSOLUTE_URL_PATTERN.test(path) ||
53
+ path.startsWith("data:") ||
54
+ path.startsWith("blob:")
55
+ ) {
56
+ continue;
57
+ }
58
+ paths.add(path);
59
+ }
60
+ return [...paths];
61
+ };
62
+
63
+ const buildAutoBinaryFiles = (code, basePath, pagePath) => {
64
+ return extractImportAssetPaths(code)
65
+ .map((assetPath) => {
66
+ const dest = resolveImportFsPath(assetPath);
67
+ if (!dest) return null;
68
+ return {
69
+ dest,
70
+ url: constructUrl(assetPath, basePath, pagePath),
71
+ };
72
+ })
73
+ .filter(Boolean);
74
+ };
75
+
76
+ const mergeBinaryFiles = (baseFiles = [], autoFiles = []) => {
77
+ const merged = new Map();
78
+ for (const file of autoFiles) {
79
+ if (!file?.dest || !file?.url) continue;
80
+ merged.set(file.dest, file);
81
+ }
82
+ for (const file of baseFiles) {
83
+ if (!file?.dest || !file?.url) continue;
84
+ merged.set(file.dest, file);
85
+ }
86
+ return [...merged.values()];
87
+ };
22
88
 
23
89
  const getWorker = async (slot) => {
24
90
  if (!window.Worker) {
@@ -93,11 +159,18 @@ hyperbook.openscad = (function () {
93
159
  .map((entry) => entry.stderr);
94
160
 
95
161
  // Build parameter UI metadata/markup in the worker to minimize main-thread work.
96
- const buildParamUiInWorker = async (code, libraryNames = [], currentOverrides = {}, id = "") => {
162
+ const buildParamUiInWorker = async (
163
+ code,
164
+ libraryNames = [],
165
+ binaryFiles = [],
166
+ currentOverrides = {},
167
+ id = "",
168
+ ) => {
97
169
  try {
98
170
  const result = await callWorker("param", "buildParamForm", {
99
171
  code,
100
172
  libraryNames,
173
+ binaryFiles,
101
174
  currentOverrides,
102
175
  id,
103
176
  });
@@ -658,6 +731,16 @@ hyperbook.openscad = (function () {
658
731
  const id = elem.getAttribute("data-id");
659
732
  const libraryNames = (elem.getAttribute("data-library") || "")
660
733
  .split(",").map(s => s.trim()).filter(Boolean);
734
+ const binaryFilesData = elem.getAttribute("data-binary-files");
735
+ const basePath = normalizePath(elem.getAttribute("data-base-path") || "/");
736
+ const pagePath = normalizePath(elem.getAttribute("data-page-path") || "");
737
+ const decodeBase64 = (str) => {
738
+ const binaryStr = atob(str);
739
+ const bytes = Uint8Array.from(binaryStr, (c) => c.charCodeAt(0));
740
+ return new TextDecoder("utf-8").decode(bytes);
741
+ };
742
+ const initialBinaryFiles = binaryFilesData ? JSON.parse(decodeBase64(binaryFilesData)) : [];
743
+ let userBinaryFiles = Array.isArray(initialBinaryFiles) ? [...initialBinaryFiles] : [];
661
744
 
662
745
  const previewContainer = elem.querySelector(".preview-container");
663
746
  const leftSide = elem.querySelector(".left-side");
@@ -682,6 +765,8 @@ hyperbook.openscad = (function () {
682
765
  const downloadBtn = elem.querySelector("button.download-stl");
683
766
  const resetBtn = elem.querySelector("button.reset");
684
767
  const fullscreenBtn = elem.querySelector("button.fullscreen");
768
+ const addBinaryFileBtn = elem.querySelector("button.add-binary-file");
769
+ const binaryFilesList = elem.querySelector(".binary-files-list");
685
770
  const bottomButtons = elem.querySelector(".buttons.bottom");
686
771
  let downloadFormatSelect = bottomButtons?.querySelector("select.download-format");
687
772
  if (!downloadFormatSelect && bottomButtons && downloadBtn) {
@@ -701,6 +786,97 @@ hyperbook.openscad = (function () {
701
786
  downloadBtn.textContent = i18nGet("openscad-download", "Download");
702
787
  }
703
788
 
789
+ const normalizeBinaryDest = (dest) => {
790
+ if (typeof dest !== "string") return null;
791
+ const trimmed = dest.trim();
792
+ if (!trimmed) return null;
793
+ return normalizePath(trimmed).replace(/\/+/g, "/");
794
+ };
795
+
796
+ const updateBinaryFilesList = () => {
797
+ if (!binaryFilesList) return;
798
+ binaryFilesList.innerHTML = "";
799
+
800
+ if (userBinaryFiles.length === 0) {
801
+ const emptyMsg = document.createElement("div");
802
+ emptyMsg.className = "binary-files-empty";
803
+ emptyMsg.textContent = i18nGet("openscad-no-binary-files", "No binary files");
804
+ binaryFilesList.appendChild(emptyMsg);
805
+ return;
806
+ }
807
+
808
+ userBinaryFiles.forEach((file) => {
809
+ const item = document.createElement("div");
810
+ item.className = "binary-file-item";
811
+
812
+ const icon = document.createElement("span");
813
+ icon.className = "binary-file-icon";
814
+ icon.textContent = "📎";
815
+ item.appendChild(icon);
816
+
817
+ const name = document.createElement("span");
818
+ name.className = "binary-file-name";
819
+ name.textContent = file.dest;
820
+ item.appendChild(name);
821
+
822
+ const deleteBtn = document.createElement("button");
823
+ deleteBtn.className = "binary-file-delete";
824
+ deleteBtn.textContent = "×";
825
+ deleteBtn.title = i18nGet("typst-delete-file", "Delete file");
826
+ deleteBtn.addEventListener("click", () => {
827
+ const confirmMsg = `${i18nGet("typst-delete-confirm", "Delete")} ${file.dest}?`;
828
+ if (!window.confirm(confirmMsg)) return;
829
+ userBinaryFiles = userBinaryFiles.filter((f) => f.dest !== file.dest);
830
+ updateBinaryFilesList();
831
+ scheduleSave();
832
+ void renderPreview();
833
+ });
834
+ item.appendChild(deleteBtn);
835
+
836
+ binaryFilesList.appendChild(item);
837
+ });
838
+ };
839
+
840
+ const handleAddBinaryFile = (event) => {
841
+ event.preventDefault();
842
+ event.stopPropagation();
843
+
844
+ const input = document.createElement("input");
845
+ input.type = "file";
846
+ input.accept = "*/*";
847
+
848
+ input.addEventListener("change", (changeEvent) => {
849
+ const file = changeEvent.target.files?.[0];
850
+ if (!file) return;
851
+
852
+ const dest = normalizeBinaryDest(`/${file.name}`);
853
+ if (!dest) return;
854
+
855
+ if (userBinaryFiles.some((f) => f.dest === dest)) {
856
+ if (!window.confirm(i18nGet("openscad-file-replace", `Replace existing ${dest}?`))) {
857
+ return;
858
+ }
859
+ userBinaryFiles = userBinaryFiles.filter((f) => f.dest !== dest);
860
+ }
861
+
862
+ const reader = new FileReader();
863
+ reader.onload = (loadEvent) => {
864
+ const url = loadEvent.target?.result;
865
+ if (typeof url !== "string") return;
866
+ userBinaryFiles.push({ dest, url });
867
+ updateBinaryFilesList();
868
+ scheduleSave();
869
+ void renderPreview();
870
+ };
871
+ reader.readAsDataURL(file);
872
+ });
873
+
874
+ input.click();
875
+ };
876
+
877
+ addBinaryFileBtn?.addEventListener("click", handleAddBinaryFile);
878
+ updateBinaryFilesList();
879
+
704
880
  // --- Canvas overlay ---
705
881
  let overlayDismissTimer = null;
706
882
 
@@ -800,6 +976,7 @@ hyperbook.openscad = (function () {
800
976
  id,
801
977
  code: cm?.getValue() || "",
802
978
  params: params?.value || "{}",
979
+ binaryFiles: userBinaryFiles,
803
980
  ...(Number.isFinite(splitHorizontal) && splitHorizontal > 0
804
981
  ? { splitHorizontal: Math.round(splitHorizontal) }
805
982
  : {}),
@@ -822,6 +999,15 @@ hyperbook.openscad = (function () {
822
999
  if (params && typeof result.params === "string") {
823
1000
  params.value = result.params;
824
1001
  }
1002
+ if (Array.isArray(result.binaryFiles)) {
1003
+ userBinaryFiles = result.binaryFiles
1004
+ .map((file) => ({
1005
+ dest: normalizeBinaryDest(file?.dest),
1006
+ url: typeof file?.url === "string" ? file.url : "",
1007
+ }))
1008
+ .filter((file) => file.dest && file.url);
1009
+ updateBinaryFilesList();
1010
+ }
825
1011
  if (Number.isFinite(result.splitHorizontal) && result.splitHorizontal > 0) {
826
1012
  elem.dataset.splitHorizontal = String(Math.round(result.splitHorizontal));
827
1013
  }
@@ -946,7 +1132,17 @@ hyperbook.openscad = (function () {
946
1132
 
947
1133
  // Code is the source of truth — param changes are always synced back to the
948
1134
  // code, so we never need stored overrides to win over the code's own values.
949
- const result = await buildParamUiInWorker(code, libraryNames, {}, id || "model");
1135
+ const resolvedBinaryFiles = mergeBinaryFiles(
1136
+ userBinaryFiles,
1137
+ buildAutoBinaryFiles(code, basePath, pagePath),
1138
+ );
1139
+ const result = await buildParamUiInWorker(
1140
+ code,
1141
+ libraryNames,
1142
+ resolvedBinaryFiles,
1143
+ {},
1144
+ id || "model",
1145
+ );
950
1146
  if (buildToken !== latestParamBuildToken) {
951
1147
  return false;
952
1148
  }
@@ -1035,10 +1231,15 @@ hyperbook.openscad = (function () {
1035
1231
  // itself is the single source of truth. Passing -D overrides is both
1036
1232
  // redundant and causes stale-value renders when the render fires before
1037
1233
  // the next param-build cycle completes.
1234
+ const resolvedBinaryFiles = mergeBinaryFiles(
1235
+ userBinaryFiles,
1236
+ buildAutoBinaryFiles(cm?.getValue() || "", basePath, pagePath),
1237
+ );
1038
1238
  const result = await callWorker("render", "render", {
1039
1239
  code: cm?.getValue() || "",
1040
1240
  format,
1041
1241
  libraryNames,
1242
+ binaryFiles: resolvedBinaryFiles,
1042
1243
  paramDefinitions: [],
1043
1244
  isPreview,
1044
1245
  });