hyperbook 0.78.0 → 0.79.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,4 +1,17 @@
1
1
  hyperbook.typst = (function () {
2
+ // Debounce utility function
3
+ const debounce = (func, wait) => {
4
+ let timeout;
5
+ return function executedFunction(...args) {
6
+ const later = () => {
7
+ clearTimeout(timeout);
8
+ func(...args);
9
+ };
10
+ clearTimeout(timeout);
11
+ timeout = setTimeout(later, wait);
12
+ };
13
+ };
14
+
2
15
  // Register code-input template for typst syntax highlighting
3
16
  window.codeInput?.registerTemplate(
4
17
  "typst-highlighted",
@@ -67,6 +80,85 @@ hyperbook.typst = (function () {
67
80
  return typstLoadPromise;
68
81
  };
69
82
 
83
+ // Asset cache for server-loaded images
84
+ const assetCache = new Map(); // filepath -> Uint8Array
85
+
86
+ // Extract relative image paths from typst source
87
+ const extractRelImagePaths = (src) => {
88
+ const paths = new Set();
89
+ const re = /image\s*\(\s*(['"])([^'"]+)\1/gi;
90
+ let m;
91
+ while ((m = re.exec(src))) {
92
+ const p = m[2];
93
+ // Skip absolute URLs, data URLs, blob URLs, and paths starting with "/"
94
+ if (/^(https?:|data:|blob:|\/)/i.test(p)) continue;
95
+ paths.add(p);
96
+ }
97
+ return [...paths];
98
+ };
99
+
100
+ // Fetch assets from server using base path
101
+ const fetchAssets = async (paths, basePath) => {
102
+ const misses = paths.filter(p => !assetCache.has(p));
103
+ await Promise.all(misses.map(async (p) => {
104
+ try {
105
+ // Construct URL using base path
106
+ const url = basePath ? `${basePath}/${p}`.replace(/\/+/g, '/') : p;
107
+ const res = await fetch(url);
108
+ if (!res.ok) {
109
+ console.warn(`Image not found: ${p} at ${url} (HTTP ${res.status})`);
110
+ assetCache.set(p, null); // Mark as failed
111
+ return;
112
+ }
113
+ const buf = await res.arrayBuffer();
114
+ assetCache.set(p, new Uint8Array(buf));
115
+ } catch (error) {
116
+ console.warn(`Error loading image ${p}:`, error);
117
+ assetCache.set(p, null); // Mark as failed
118
+ }
119
+ }));
120
+ };
121
+
122
+ // Build typst preamble with inlined assets
123
+ const buildAssetsPreamble = () => {
124
+ if (assetCache.size === 0) return '';
125
+ const entries = [...assetCache.entries()]
126
+ .filter(([name, u8]) => u8 !== null) // Skip failed images
127
+ .map(([name, u8]) => {
128
+ const nums = Array.from(u8).join(',');
129
+ return ` "${name}": bytes((${nums}))`;
130
+ }).join(',\n');
131
+ if (!entries) return '';
132
+ return `#let __assets = (\n${entries}\n)\n\n`;
133
+ };
134
+
135
+ // Rewrite image() calls to use inlined assets
136
+ const rewriteImageCalls = (src) => {
137
+ if (assetCache.size === 0) return src;
138
+ return src.replace(/image\s*\(\s*(['"])([^'"]+)\1/g, (m, q, fname) => {
139
+ if (assetCache.has(fname)) {
140
+ const asset = assetCache.get(fname);
141
+ if (asset === null) {
142
+ // Image not found – replace with error text
143
+ return `[Image not found: _${fname}_]`;
144
+ }
145
+ return `image(__assets.at("${fname}")`;
146
+ }
147
+ return m;
148
+ });
149
+ };
150
+
151
+ // Prepare typst source with server-loaded assets
152
+ const prepareTypstSourceWithAssets = async (src, basePath) => {
153
+ const relPaths = extractRelImagePaths(src);
154
+ if (relPaths.length > 0) {
155
+ await fetchAssets(relPaths, basePath);
156
+ const preamble = buildAssetsPreamble();
157
+ return preamble + rewriteImageCalls(src);
158
+ }
159
+ return src;
160
+ };
161
+
70
162
  // Parse error message from SourceDiagnostic format
71
163
  const parseTypstError = (errorMessage) => {
72
164
  try {
@@ -82,7 +174,7 @@ hyperbook.typst = (function () {
82
174
  };
83
175
 
84
176
  // Render typst code to SVG
85
- const renderTypst = async (code, container, loadingIndicator, sourceFiles, binaryFiles, id, previewContainer) => {
177
+ const renderTypst = async (code, container, loadingIndicator, sourceFiles, binaryFiles, id, previewContainer, basePath) => {
86
178
  // Queue this render to ensure only one compilation runs at a time
87
179
  return queueRender(async () => {
88
180
  // Show loading indicator
@@ -96,6 +188,9 @@ hyperbook.typst = (function () {
96
188
  // Reset shadow files for this render
97
189
  $typst.resetShadow();
98
190
 
191
+ // Prepare code with server-loaded assets
192
+ const preparedCode = await prepareTypstSourceWithAssets(code, basePath);
193
+
99
194
  // Add source files
100
195
  for (const { filename, content } of sourceFiles) {
101
196
  const path = filename.startsWith('/') ? filename.substring(1) : filename;
@@ -128,7 +223,7 @@ hyperbook.typst = (function () {
128
223
  }
129
224
  }
130
225
 
131
- const svg = await $typst.svg({ mainContent: code });
226
+ const svg = await $typst.svg({ mainContent: preparedCode });
132
227
 
133
228
  // Remove any existing error overlay from preview-container
134
229
  if (previewContainer) {
@@ -204,7 +299,7 @@ hyperbook.typst = (function () {
204
299
  };
205
300
 
206
301
  // Export to PDF
207
- const exportPdf = async (code, id, sourceFiles, binaryFiles) => {
302
+ const exportPdf = async (code, id, sourceFiles, binaryFiles, basePath) => {
208
303
  // Queue this export to ensure only one compilation runs at a time
209
304
  return queueRender(async () => {
210
305
  await loadTypst();
@@ -213,6 +308,9 @@ hyperbook.typst = (function () {
213
308
  // Reset shadow files for this export
214
309
  $typst.resetShadow();
215
310
 
311
+ // Prepare code with server-loaded assets
312
+ const preparedCode = await prepareTypstSourceWithAssets(code, basePath);
313
+
216
314
  // Add source files
217
315
  for (const { filename, content } of sourceFiles) {
218
316
  const path = filename.startsWith('/') ? filename.substring(1) : filename;
@@ -244,7 +342,7 @@ hyperbook.typst = (function () {
244
342
  }
245
343
  }
246
344
 
247
- const pdfData = await $typst.pdf({ mainContent: code });
345
+ const pdfData = await $typst.pdf({ mainContent: preparedCode });
248
346
  const pdfFile = new Blob([pdfData], { type: "application/pdf" });
249
347
  const link = document.createElement("a");
250
348
  link.href = URL.createObjectURL(pdfFile);
@@ -276,6 +374,12 @@ hyperbook.typst = (function () {
276
374
  // Parse source files and binary files from data attributes
277
375
  const sourceFilesData = elem.getAttribute("data-source-files");
278
376
  const binaryFilesData = elem.getAttribute("data-binary-files");
377
+ let basePath = elem.getAttribute("data-base-path") || "";
378
+
379
+ // Ensure basePath starts with / for absolute paths
380
+ if (basePath && !basePath.startsWith('/')) {
381
+ basePath = '/' + basePath;
382
+ }
279
383
 
280
384
  let sourceFiles = sourceFilesData
281
385
  ? JSON.parse(atob(sourceFilesData))
@@ -440,10 +544,13 @@ hyperbook.typst = (function () {
440
544
 
441
545
  const mainFile = sourceFiles.find(f => f.filename === "main.typ" || f.filename === "main.typst");
442
546
  const mainCode = mainFile ? mainFile.content : "";
443
- renderTypst(mainCode, preview, loadingIndicator, sourceFiles, binaryFiles, id, previewContainer);
547
+ renderTypst(mainCode, preview, loadingIndicator, sourceFiles, binaryFiles, id, previewContainer, basePath);
444
548
  }
445
549
  };
446
550
 
551
+ // Create debounced version of rerenderTypst for input events (500ms delay)
552
+ const debouncedRerenderTypst = debounce(rerenderTypst, 500);
553
+
447
554
  // Add source file button
448
555
  addSourceFileBtn?.addEventListener("click", () => {
449
556
  const filename = prompt(i18n.get("typst-filename-prompt") || "Enter filename (e.g., helper.typ):");
@@ -548,14 +655,14 @@ hyperbook.typst = (function () {
548
655
  // Listen for input changes
549
656
  editor.addEventListener("input", () => {
550
657
  saveState();
551
- rerenderTypst();
658
+ debouncedRerenderTypst();
552
659
  });
553
660
  });
554
661
  } else if (sourceTextarea) {
555
662
  // Preview mode - code is in hidden textarea
556
663
  initialCode = sourceTextarea.value;
557
664
  loadTypst().then(() => {
558
- renderTypst(initialCode, preview, loadingIndicator, sourceFiles, binaryFiles, id, previewContainer);
665
+ renderTypst(initialCode, preview, loadingIndicator, sourceFiles, binaryFiles, id, previewContainer, basePath);
559
666
  });
560
667
  }
561
668
 
@@ -564,7 +671,7 @@ hyperbook.typst = (function () {
564
671
  // Get the main file content
565
672
  const mainFile = sourceFiles.find(f => f.filename === "main.typ" || f.filename === "main.typst");
566
673
  const code = mainFile ? mainFile.content : (editor ? editor.value : initialCode);
567
- await exportPdf(code, id, sourceFiles, binaryFiles);
674
+ await exportPdf(code, id, sourceFiles, binaryFiles, basePath);
568
675
  });
569
676
 
570
677
  // Download Project button (ZIP with all files)
@@ -590,7 +697,7 @@ hyperbook.typst = (function () {
590
697
  if (url.startsWith('data:')) {
591
698
  const response = await fetch(url);
592
699
  arrayBuffer = await response.arrayBuffer();
593
- } else {
700
+ } else if (url.startsWith('http://') || url.startsWith('https://')) {
594
701
  // External URL
595
702
  const response = await fetch(url);
596
703
  if (response.ok) {
@@ -599,6 +706,16 @@ hyperbook.typst = (function () {
599
706
  console.warn(`Failed to load binary file: ${url}`);
600
707
  continue;
601
708
  }
709
+ } else {
710
+ // Relative URL - use basePath to construct full URL
711
+ const fullUrl = basePath ? `${basePath}/${url}`.replace(/\/+/g, '/') : url;
712
+ const response = await fetch(fullUrl);
713
+ if (response.ok) {
714
+ arrayBuffer = await response.arrayBuffer();
715
+ } else {
716
+ console.warn(`Failed to load binary file: ${url} at ${fullUrl}`);
717
+ continue;
718
+ }
602
719
  }
603
720
 
604
721
  const path = dest.startsWith('/') ? dest.substring(1) : dest;
@@ -608,6 +725,31 @@ hyperbook.typst = (function () {
608
725
  }
609
726
  }
610
727
 
728
+ // Also include assets loaded from image() calls in the code
729
+ const relImagePaths = extractRelImagePaths(code);
730
+ for (const imagePath of relImagePaths) {
731
+ // Skip if already in zipFiles or already handled as binary file
732
+ const normalizedPath = imagePath.startsWith('/') ? imagePath.substring(1) : imagePath;
733
+ if (zipFiles[normalizedPath]) continue;
734
+
735
+ // Skip absolute URLs, data URLs, and blob URLs
736
+ if (/^(https?:|data:|blob:)/i.test(imagePath)) continue;
737
+
738
+ try {
739
+ // Construct URL using basePath
740
+ const url = basePath ? `${basePath}/${imagePath}`.replace(/\/+/g, '/') : imagePath;
741
+ const response = await fetch(url);
742
+ if (response.ok) {
743
+ const arrayBuffer = await response.arrayBuffer();
744
+ zipFiles[normalizedPath] = new Uint8Array(arrayBuffer);
745
+ } else {
746
+ console.warn(`Failed to load image asset: ${imagePath} at ${url}`);
747
+ }
748
+ } catch (error) {
749
+ console.warn(`Error loading image asset ${imagePath}:`, error);
750
+ }
751
+ }
752
+
611
753
  // Create ZIP using UZIP
612
754
  const zipData = UZIP.encode(zipFiles);
613
755
  const zipBlob = new Blob([zipData], { type: "application/zip" });
package/dist/index.js CHANGED
@@ -202070,6 +202070,7 @@ var remarkDirectiveTypst_default = (ctx) => () => {
202070
202070
  }
202071
202071
  const isEditMode = mode === "edit";
202072
202072
  const isPreviewMode = mode === "preview" || !isEditMode;
202073
+ const basePath = ctx.navigation.current?.path?.directory || "";
202073
202074
  data.hName = "div";
202074
202075
  data.hProperties = {
202075
202076
  class: ["directive-typst", isPreviewMode ? "preview-only" : ""].join(" ").trim(),
@@ -202079,7 +202080,8 @@ var remarkDirectiveTypst_default = (ctx) => () => {
202079
202080
  ).toString("base64"),
202080
202081
  "data-binary-files": Buffer.from(
202081
202082
  JSON.stringify(binaryFiles)
202082
- ).toString("base64")
202083
+ ).toString("base64"),
202084
+ "data-base-path": basePath
202083
202085
  };
202084
202086
  const previewContainer = {
202085
202087
  type: "element",
@@ -202500,7 +202502,7 @@ module.exports = /*#__PURE__*/JSON.parse('{"application/1d-interleaved-parityfec
202500
202502
  /***/ ((module) => {
202501
202503
 
202502
202504
  "use strict";
202503
- module.exports = /*#__PURE__*/JSON.parse('{"name":"hyperbook","version":"0.78.0","author":"Mike Barkmin","homepage":"https://github.com/openpatch/hyperbook#readme","license":"MIT","bin":{"hyperbook":"./dist/index.js"},"files":["dist"],"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/openpatch/hyperbook.git","directory":"packages/hyperbook"},"bugs":{"url":"https://github.com/openpatch/hyperbook/issues"},"engines":{"node":">=18"},"scripts":{"version":"pnpm build","lint":"tsc --noEmit","dev":"ncc build ./index.ts -w -o dist/","build":"rimraf dist && ncc build ./index.ts -o ./dist/ --no-cache --no-source-map-register --external favicons --external sharp && node postbuild.mjs"},"dependencies":{"favicons":"^7.2.0"},"devDependencies":{"create-hyperbook":"workspace:*","@hyperbook/fs":"workspace:*","@hyperbook/markdown":"workspace:*","@hyperbook/types":"workspace:*","@pnpm/exportable-manifest":"1000.0.6","@types/archiver":"6.0.3","@types/async-retry":"1.4.9","@types/cross-spawn":"6.0.6","@types/lunr":"^2.3.7","@types/prompts":"2.4.9","@types/tar":"6.1.13","@types/ws":"^8.5.14","@vercel/ncc":"0.38.3","archiver":"7.0.1","async-retry":"1.3.3","chalk":"5.4.1","chokidar":"4.0.3","commander":"12.1.0","cpy":"11.1.0","cross-spawn":"7.0.6","domutils":"^3.2.2","extract-zip":"^2.0.1","got":"12.6.0","htmlparser2":"^10.0.0","lunr":"^2.3.9","lunr-languages":"^1.14.0","mime":"^4.0.6","prompts":"2.4.2","rimraf":"6.0.1","tar":"7.4.3","update-check":"1.5.4","ws":"^8.18.0"}}');
202505
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"hyperbook","version":"0.79.0","author":"Mike Barkmin","homepage":"https://github.com/openpatch/hyperbook#readme","license":"MIT","bin":{"hyperbook":"./dist/index.js"},"files":["dist"],"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/openpatch/hyperbook.git","directory":"packages/hyperbook"},"bugs":{"url":"https://github.com/openpatch/hyperbook/issues"},"engines":{"node":">=18"},"scripts":{"version":"pnpm build","lint":"tsc --noEmit","dev":"ncc build ./index.ts -w -o dist/","build":"rimraf dist && ncc build ./index.ts -o ./dist/ --no-cache --no-source-map-register --external favicons --external sharp && node postbuild.mjs"},"dependencies":{"favicons":"^7.2.0"},"devDependencies":{"create-hyperbook":"workspace:*","@hyperbook/fs":"workspace:*","@hyperbook/markdown":"workspace:*","@hyperbook/types":"workspace:*","@pnpm/exportable-manifest":"1000.0.6","@types/archiver":"6.0.3","@types/async-retry":"1.4.9","@types/cross-spawn":"6.0.6","@types/lunr":"^2.3.7","@types/prompts":"2.4.9","@types/tar":"6.1.13","@types/ws":"^8.5.14","@vercel/ncc":"0.38.3","archiver":"7.0.1","async-retry":"1.3.3","chalk":"5.4.1","chokidar":"4.0.3","commander":"12.1.0","cpy":"11.1.0","cross-spawn":"7.0.6","domutils":"^3.2.2","extract-zip":"^2.0.1","got":"12.6.0","htmlparser2":"^10.0.0","lunr":"^2.3.9","lunr-languages":"^1.14.0","mime":"^4.0.6","prompts":"2.4.2","rimraf":"6.0.1","tar":"7.4.3","update-check":"1.5.4","ws":"^8.18.0"}}');
202504
202506
 
202505
202507
  /***/ })
202506
202508
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperbook",
3
- "version": "0.78.0",
3
+ "version": "0.79.0",
4
4
  "author": "Mike Barkmin",
5
5
  "homepage": "https://github.com/openpatch/hyperbook#readme",
6
6
  "license": "MIT",
@@ -58,8 +58,8 @@
58
58
  "ws": "^8.18.0",
59
59
  "create-hyperbook": "0.3.2",
60
60
  "@hyperbook/fs": "0.24.2",
61
- "@hyperbook/types": "0.20.0",
62
- "@hyperbook/markdown": "0.49.0"
61
+ "@hyperbook/markdown": "0.50.0",
62
+ "@hyperbook/types": "0.20.0"
63
63
  },
64
64
  "scripts": {
65
65
  "version": "pnpm build",