@hyperbook/markdown 0.50.1 → 0.51.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.
@@ -24,8 +24,10 @@ hyperbook.typst = (function () {
24
24
  const elems = document.getElementsByClassName("directive-typst");
25
25
 
26
26
  // Typst WASM module URLs
27
- const TYPST_COMPILER_URL = "https://cdn.jsdelivr.net/npm/@myriaddreamin/typst-ts-web-compiler/pkg/typst_ts_web_compiler_bg.wasm";
28
- const TYPST_RENDERER_URL = "https://cdn.jsdelivr.net/npm/@myriaddreamin/typst-ts-renderer/pkg/typst_ts_renderer_bg.wasm";
27
+ const TYPST_COMPILER_URL =
28
+ "https://cdn.jsdelivr.net/npm/@myriaddreamin/typst-ts-web-compiler/pkg/typst_ts_web_compiler_bg.wasm";
29
+ const TYPST_RENDERER_URL =
30
+ "https://cdn.jsdelivr.net/npm/@myriaddreamin/typst-ts-renderer/pkg/typst_ts_renderer_bg.wasm";
29
31
 
30
32
  // Load typst all-in-one bundle
31
33
  let typstLoaded = false;
@@ -51,7 +53,8 @@ hyperbook.typst = (function () {
51
53
 
52
54
  typstLoadPromise = new Promise((resolve, reject) => {
53
55
  const script = document.createElement("script");
54
- script.src = "https://cdn.jsdelivr.net/npm/@myriaddreamin/typst.ts/dist/esm/contrib/all-in-one-lite.bundle.js";
56
+ script.src =
57
+ "https://cdn.jsdelivr.net/npm/@myriaddreamin/typst.ts/dist/esm/contrib/all-in-one-lite.bundle.js";
55
58
  script.type = "module";
56
59
  script.id = "typst-loader";
57
60
  script.onload = () => {
@@ -83,78 +86,147 @@ hyperbook.typst = (function () {
83
86
  // Asset cache for server-loaded images
84
87
  const assetCache = new Map(); // filepath -> Uint8Array
85
88
 
86
- // Extract relative image paths from typst source
87
- const extractRelImagePaths = (src) => {
89
+ const extractRelFilePaths = (src) => {
88
90
  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);
91
+
92
+ // Pattern for read() function
93
+ // Matches: read("path"), read('path'), read("path", encoding: ...)
94
+ const readRe = /#read\s*\(\s*(['"])([^'"]+)\1/gi;
95
+
96
+ // Pattern for csv() function
97
+ // Matches: csv("path"), csv('path'), csv("path", delimiter: ...)
98
+ const csvRe = /csv\s*\(\s*(['"])([^'"]+)\1/gi;
99
+
100
+ // Pattern for json() function
101
+ // Matches: json("path"), json('path')
102
+ const jsonRe = /json\s*\(\s*(['"])([^'"]+)\1/gi;
103
+
104
+ // Pattern for yaml() function
105
+ const yamlRe = /yaml\s*\(\s*(['"])([^'"]+)\1/gi;
106
+ //
107
+ // Pattern for xml() function
108
+ const xmlRe = /xml\s*\(\s*(['"])([^'"]+)\1/gi;
109
+
110
+ const imageRe = /image\s*\(\s*(['"])([^'"]+)\1/gi;
111
+
112
+ // Process all patterns
113
+ const patterns = [imageRe, readRe, csvRe, jsonRe, yamlRe, xmlRe];
114
+
115
+ for (const re of patterns) {
116
+ let m;
117
+ while ((m = re.exec(src))) {
118
+ const p = m[2];
119
+ // Skip absolute URLs, data URLs, blob URLs, and paths starting with "/"
120
+ if (/^(https?:|data:|blob:|\/)/i.test(p)) continue;
121
+ paths.add(p);
122
+ }
96
123
  }
124
+
97
125
  return [...paths];
98
126
  };
99
127
 
100
128
  // Fetch assets from server using base path
101
129
  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})`);
130
+ const misses = paths.filter((p) => !assetCache.has(p));
131
+ await Promise.all(
132
+ misses.map(async (p) => {
133
+ try {
134
+ // Construct URL using base path
135
+ const url = basePath ? `${basePath}/${p}`.replace(/\/+/g, "/") : p;
136
+ const res = await fetch(url);
137
+ if (!res.ok) {
138
+ console.warn(
139
+ `Asset not found: ${p} at ${url} (HTTP ${res.status})`,
140
+ );
141
+ assetCache.set(p, null); // Mark as failed
142
+ return;
143
+ }
144
+ const buf = await res.arrayBuffer();
145
+ assetCache.set(p, new Uint8Array(buf));
146
+ } catch (error) {
147
+ console.warn(`Error loading asset ${p}:`, error);
110
148
  assetCache.set(p, null); // Mark as failed
111
- return;
112
149
  }
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
- }));
150
+ }),
151
+ );
120
152
  };
121
153
 
122
154
  // Build typst preamble with inlined assets
123
155
  const buildAssetsPreamble = () => {
124
- if (assetCache.size === 0) return '';
156
+ if (assetCache.size === 0) return "";
125
157
  const entries = [...assetCache.entries()]
126
158
  .filter(([name, u8]) => u8 !== null) // Skip failed images
127
159
  .map(([name, u8]) => {
128
- const nums = Array.from(u8).join(',');
160
+ const nums = Array.from(u8).join(",");
129
161
  return ` "${name}": bytes((${nums}))`;
130
- }).join(',\n');
131
- if (!entries) return '';
162
+ })
163
+ .join(",\n");
164
+ if (!entries) return "";
132
165
  return `#let __assets = (\n${entries}\n)\n\n`;
133
166
  };
134
167
 
135
- // Rewrite image() calls to use inlined assets
136
- const rewriteImageCalls = (src) => {
168
+ // Rewrite file calls (image, read, csv, json) to use inlined assets
169
+ const rewriteAssetCalls = (src) => {
137
170
  if (assetCache.size === 0) return src;
138
- return src.replace(/image\s*\(\s*(['"])([^'"]+)\1/g, (m, q, fname) => {
171
+
172
+ // Rewrite image() calls
173
+ src = src.replace(/image\s*\(\s*(['"])([^'"]+)\1/g, (m, q, fname) => {
139
174
  if (assetCache.has(fname)) {
140
175
  const asset = assetCache.get(fname);
141
176
  if (asset === null) {
142
- // Image not found – replace with error text
143
- return `[Image not found: _${fname}_]`;
177
+ return `[File not found: _${fname}_]`;
144
178
  }
145
179
  return `image(__assets.at("${fname}")`;
146
180
  }
147
181
  return m;
148
182
  });
183
+
184
+ // Rewrite read() calls
185
+ src = src.replace(/#read\s*\(\s*(['"])([^'"]+)\1/g, (m, q, fname) => {
186
+ if (assetCache.has(fname)) {
187
+ const asset = assetCache.get(fname);
188
+ if (asset === null) {
189
+ return `[File not found: _${fname}_]`;
190
+ }
191
+ return `#read(__assets.at("${fname}")`;
192
+ }
193
+ return m;
194
+ });
195
+
196
+ // Rewrite csv() calls
197
+ src = src.replace(/csv\s*\(\s*(['"])([^'"]+)\1/g, (m, q, fname) => {
198
+ if (assetCache.has(fname)) {
199
+ const asset = assetCache.get(fname);
200
+ if (asset === null) {
201
+ return `[File not found: _${fname}_]`;
202
+ }
203
+ return `csv(__assets.at("${fname}")`;
204
+ }
205
+ return m;
206
+ });
207
+
208
+ // Rewrite json() calls
209
+ src = src.replace(/json\s*\(\s*(['"])([^'"]+)\1/g, (m, q, fname) => {
210
+ if (assetCache.has(fname)) {
211
+ const asset = assetCache.get(fname);
212
+ if (asset === null) {
213
+ return `[File not found: _${fname}_]`;
214
+ }
215
+ return `json(__assets.at("${fname}")`;
216
+ }
217
+ return m;
218
+ });
219
+
220
+ return src;
149
221
  };
150
222
 
151
223
  // Prepare typst source with server-loaded assets
152
224
  const prepareTypstSourceWithAssets = async (src, basePath) => {
153
- const relPaths = extractRelImagePaths(src);
225
+ const relPaths = extractRelFilePaths(src);
154
226
  if (relPaths.length > 0) {
155
227
  await fetchAssets(relPaths, basePath);
156
228
  const preamble = buildAssetsPreamble();
157
- return preamble + rewriteImageCalls(src);
229
+ return preamble + rewriteAssetCalls(src);
158
230
  }
159
231
  return src;
160
232
  };
@@ -174,7 +246,16 @@ hyperbook.typst = (function () {
174
246
  };
175
247
 
176
248
  // Render typst code to SVG
177
- const renderTypst = async (code, container, loadingIndicator, sourceFiles, binaryFiles, id, previewContainer, basePath) => {
249
+ const renderTypst = async (
250
+ code,
251
+ container,
252
+ loadingIndicator,
253
+ sourceFiles,
254
+ binaryFiles,
255
+ id,
256
+ previewContainer,
257
+ basePath,
258
+ ) => {
178
259
  // Queue this render to ensure only one compilation runs at a time
179
260
  return queueRender(async () => {
180
261
  // Show loading indicator
@@ -183,7 +264,7 @@ hyperbook.typst = (function () {
183
264
  }
184
265
 
185
266
  await loadTypst();
186
-
267
+
187
268
  try {
188
269
  // Reset shadow files for this render
189
270
  $typst.resetShadow();
@@ -193,7 +274,9 @@ hyperbook.typst = (function () {
193
274
 
194
275
  // Add source files
195
276
  for (const { filename, content } of sourceFiles) {
196
- const path = filename.startsWith('/') ? filename.substring(1) : filename;
277
+ const path = filename.startsWith("/")
278
+ ? filename.substring(1)
279
+ : filename;
197
280
  await $typst.addSource(`/${path}`, content);
198
281
  }
199
282
 
@@ -201,9 +284,9 @@ hyperbook.typst = (function () {
201
284
  for (const { dest, url } of binaryFiles) {
202
285
  try {
203
286
  let arrayBuffer;
204
-
287
+
205
288
  // Check if URL is a data URL (user-uploaded file)
206
- if (url.startsWith('data:')) {
289
+ if (url.startsWith("data:")) {
207
290
  const response = await fetch(url);
208
291
  arrayBuffer = await response.arrayBuffer();
209
292
  } else {
@@ -215,8 +298,8 @@ hyperbook.typst = (function () {
215
298
  }
216
299
  arrayBuffer = await response.arrayBuffer();
217
300
  }
218
-
219
- const path = dest.startsWith('/') ? dest.substring(1) : dest;
301
+
302
+ const path = dest.startsWith("/") ? dest.substring(1) : dest;
220
303
  $typst.mapShadow(`/${path}`, new Uint8Array(arrayBuffer));
221
304
  } catch (error) {
222
305
  console.warn(`Error loading binary file ${url}:`, error);
@@ -224,15 +307,17 @@ hyperbook.typst = (function () {
224
307
  }
225
308
 
226
309
  const svg = await $typst.svg({ mainContent: preparedCode });
227
-
310
+
228
311
  // Remove any existing error overlay from preview-container
229
312
  if (previewContainer) {
230
- const existingError = previewContainer.querySelector('.typst-error-overlay');
313
+ const existingError = previewContainer.querySelector(
314
+ ".typst-error-overlay",
315
+ );
231
316
  if (existingError) {
232
317
  existingError.remove();
233
318
  }
234
319
  }
235
-
320
+
236
321
  container.innerHTML = svg;
237
322
 
238
323
  // Scale SVG to fit container
@@ -248,26 +333,28 @@ hyperbook.typst = (function () {
248
333
  }
249
334
  } catch (error) {
250
335
  const errorText = parseTypstError(error || "Error rendering Typst");
251
-
336
+
252
337
  // Check if we have existing content (previous successful render)
253
- const hasExistingContent = container.querySelector('svg') !== null;
254
-
338
+ const hasExistingContent = container.querySelector("svg") !== null;
339
+
255
340
  // Always use error overlay in preview-container if available
256
341
  if (previewContainer) {
257
342
  // Remove any existing error overlay
258
- const existingError = previewContainer.querySelector('.typst-error-overlay');
343
+ const existingError = previewContainer.querySelector(
344
+ ".typst-error-overlay",
345
+ );
259
346
  if (existingError) {
260
347
  existingError.remove();
261
348
  }
262
-
349
+
263
350
  // Clear preview if no existing content
264
351
  if (!hasExistingContent) {
265
- container.innerHTML = '';
352
+ container.innerHTML = "";
266
353
  }
267
-
354
+
268
355
  // Create floating error overlay in preview-container
269
- const errorOverlay = document.createElement('div');
270
- errorOverlay.className = 'typst-error-overlay';
356
+ const errorOverlay = document.createElement("div");
357
+ errorOverlay.className = "typst-error-overlay";
271
358
  errorOverlay.innerHTML = `
272
359
  <div class="typst-error-content">
273
360
  <div class="typst-error-header">
@@ -277,13 +364,13 @@ hyperbook.typst = (function () {
277
364
  <div class="typst-error-message">${errorText}</div>
278
365
  </div>
279
366
  `;
280
-
367
+
281
368
  // Add close button functionality
282
- const closeBtn = errorOverlay.querySelector('.typst-error-close');
283
- closeBtn.addEventListener('click', () => {
369
+ const closeBtn = errorOverlay.querySelector(".typst-error-close");
370
+ closeBtn.addEventListener("click", () => {
284
371
  errorOverlay.remove();
285
372
  });
286
-
373
+
287
374
  previewContainer.appendChild(errorOverlay);
288
375
  } else {
289
376
  // Fallback: show error in preview container directly
@@ -303,7 +390,7 @@ hyperbook.typst = (function () {
303
390
  // Queue this export to ensure only one compilation runs at a time
304
391
  return queueRender(async () => {
305
392
  await loadTypst();
306
-
393
+
307
394
  try {
308
395
  // Reset shadow files for this export
309
396
  $typst.resetShadow();
@@ -313,7 +400,9 @@ hyperbook.typst = (function () {
313
400
 
314
401
  // Add source files
315
402
  for (const { filename, content } of sourceFiles) {
316
- const path = filename.startsWith('/') ? filename.substring(1) : filename;
403
+ const path = filename.startsWith("/")
404
+ ? filename.substring(1)
405
+ : filename;
317
406
  await $typst.addSource(`/${path}`, content);
318
407
  }
319
408
 
@@ -321,9 +410,9 @@ hyperbook.typst = (function () {
321
410
  for (const { dest, url } of binaryFiles) {
322
411
  try {
323
412
  let arrayBuffer;
324
-
413
+
325
414
  // Check if URL is a data URL (user-uploaded file)
326
- if (url.startsWith('data:')) {
415
+ if (url.startsWith("data:")) {
327
416
  const response = await fetch(url);
328
417
  arrayBuffer = await response.arrayBuffer();
329
418
  } else {
@@ -334,8 +423,8 @@ hyperbook.typst = (function () {
334
423
  }
335
424
  arrayBuffer = await response.arrayBuffer();
336
425
  }
337
-
338
- const path = dest.startsWith('/') ? dest.substring(1) : dest;
426
+
427
+ const path = dest.startsWith("/") ? dest.substring(1) : dest;
339
428
  $typst.mapShadow(`/${path}`, new Uint8Array(arrayBuffer));
340
429
  } catch (error) {
341
430
  console.warn(`Error loading binary file ${url}:`, error);
@@ -375,45 +464,44 @@ hyperbook.typst = (function () {
375
464
  const sourceFilesData = elem.getAttribute("data-source-files");
376
465
  const binaryFilesData = elem.getAttribute("data-binary-files");
377
466
  let basePath = elem.getAttribute("data-base-path") || "";
378
-
467
+
379
468
  // Ensure basePath starts with / for absolute paths
380
- if (basePath && !basePath.startsWith('/')) {
381
- basePath = '/' + basePath;
469
+ if (basePath && !basePath.startsWith("/")) {
470
+ basePath = "/" + basePath;
382
471
  }
383
-
384
- let sourceFiles = sourceFilesData
385
- ? JSON.parse(atob(sourceFilesData))
386
- : [];
387
- let binaryFiles = binaryFilesData
388
- ? JSON.parse(atob(binaryFilesData))
389
- : [];
472
+
473
+ let sourceFiles = sourceFilesData ? JSON.parse(atob(sourceFilesData)) : [];
474
+ let binaryFiles = binaryFilesData ? JSON.parse(atob(binaryFilesData)) : [];
390
475
 
391
476
  // Track current active file
392
- let currentFile = sourceFiles.find(f => f.filename === "main.typ" || f.filename === "main.typst") || sourceFiles[0];
393
-
477
+ let currentFile =
478
+ sourceFiles.find(
479
+ (f) => f.filename === "main.typ" || f.filename === "main.typst",
480
+ ) || sourceFiles[0];
481
+
394
482
  // Store file contents in memory
395
483
  const fileContents = new Map();
396
- sourceFiles.forEach(f => fileContents.set(f.filename, f.content));
484
+ sourceFiles.forEach((f) => fileContents.set(f.filename, f.content));
397
485
 
398
486
  // Function to update tabs UI
399
487
  const updateTabs = () => {
400
488
  if (!tabsList) return;
401
-
489
+
402
490
  tabsList.innerHTML = "";
403
-
491
+
404
492
  // Add source file tabs
405
- sourceFiles.forEach(file => {
493
+ sourceFiles.forEach((file) => {
406
494
  const tab = document.createElement("div");
407
495
  tab.className = "file-tab";
408
496
  if (file.filename === currentFile.filename) {
409
497
  tab.classList.add("active");
410
498
  }
411
-
499
+
412
500
  const tabName = document.createElement("span");
413
501
  tabName.className = "tab-name";
414
502
  tabName.textContent = file.filename;
415
503
  tab.appendChild(tabName);
416
-
504
+
417
505
  // Add delete button (except for main file)
418
506
  if (file.filename !== "main.typ" && file.filename !== "main.typst") {
419
507
  const deleteBtn = document.createElement("button");
@@ -422,10 +510,16 @@ hyperbook.typst = (function () {
422
510
  deleteBtn.title = i18n.get("typst-delete-file") || "Delete file";
423
511
  deleteBtn.addEventListener("click", (e) => {
424
512
  e.stopPropagation();
425
- if (confirm(`${i18n.get("typst-delete-confirm") || "Delete"} ${file.filename}?`)) {
426
- sourceFiles = sourceFiles.filter(f => f.filename !== file.filename);
513
+ if (
514
+ confirm(
515
+ `${i18n.get("typst-delete-confirm") || "Delete"} ${file.filename}?`,
516
+ )
517
+ ) {
518
+ sourceFiles = sourceFiles.filter(
519
+ (f) => f.filename !== file.filename,
520
+ );
427
521
  fileContents.delete(file.filename);
428
-
522
+
429
523
  // Switch to main file if we deleted the current file
430
524
  if (currentFile.filename === file.filename) {
431
525
  currentFile = sourceFiles[0];
@@ -433,7 +527,7 @@ hyperbook.typst = (function () {
433
527
  editor.value = fileContents.get(currentFile.filename) || "";
434
528
  }
435
529
  }
436
-
530
+
437
531
  updateTabs();
438
532
  saveState();
439
533
  rerenderTypst();
@@ -441,25 +535,25 @@ hyperbook.typst = (function () {
441
535
  });
442
536
  tab.appendChild(deleteBtn);
443
537
  }
444
-
538
+
445
539
  tab.addEventListener("click", () => {
446
540
  if (currentFile.filename !== file.filename) {
447
541
  // Save current file content
448
542
  if (editor) {
449
543
  fileContents.set(currentFile.filename, editor.value);
450
544
  }
451
-
545
+
452
546
  // Switch to new file
453
547
  currentFile = file;
454
548
  if (editor) {
455
549
  editor.value = fileContents.get(currentFile.filename) || "";
456
550
  }
457
-
551
+
458
552
  updateTabs();
459
553
  saveState();
460
554
  }
461
555
  });
462
-
556
+
463
557
  tabsList.appendChild(tab);
464
558
  });
465
559
  };
@@ -467,45 +561,50 @@ hyperbook.typst = (function () {
467
561
  // Function to update binary files list
468
562
  const updateBinaryFilesList = () => {
469
563
  if (!binaryFilesList) return;
470
-
564
+
471
565
  binaryFilesList.innerHTML = "";
472
-
566
+
473
567
  if (binaryFiles.length === 0) {
474
568
  const emptyMsg = document.createElement("div");
475
569
  emptyMsg.className = "binary-files-empty";
476
- emptyMsg.textContent = i18n.get("typst-no-binary-files") || "No binary files";
570
+ emptyMsg.textContent =
571
+ i18n.get("typst-no-binary-files") || "No binary files";
477
572
  binaryFilesList.appendChild(emptyMsg);
478
573
  return;
479
574
  }
480
-
481
- binaryFiles.forEach(file => {
575
+
576
+ binaryFiles.forEach((file) => {
482
577
  const item = document.createElement("div");
483
578
  item.className = "binary-file-item";
484
-
579
+
485
580
  const icon = document.createElement("span");
486
581
  icon.className = "binary-file-icon";
487
582
  icon.textContent = "📎";
488
583
  item.appendChild(icon);
489
-
584
+
490
585
  const name = document.createElement("span");
491
586
  name.className = "binary-file-name";
492
587
  name.textContent = file.dest;
493
588
  item.appendChild(name);
494
-
589
+
495
590
  const deleteBtn = document.createElement("button");
496
591
  deleteBtn.className = "binary-file-delete";
497
592
  deleteBtn.textContent = "×";
498
593
  deleteBtn.title = i18n.get("typst-delete-file") || "Delete file";
499
594
  deleteBtn.addEventListener("click", () => {
500
- if (confirm(`${i18n.get("typst-delete-confirm") || "Delete"} ${file.dest}?`)) {
501
- binaryFiles = binaryFiles.filter(f => f.dest !== file.dest);
595
+ if (
596
+ confirm(
597
+ `${i18n.get("typst-delete-confirm") || "Delete"} ${file.dest}?`,
598
+ )
599
+ ) {
600
+ binaryFiles = binaryFiles.filter((f) => f.dest !== file.dest);
502
601
  updateBinaryFilesList();
503
602
  saveState();
504
603
  rerenderTypst();
505
604
  }
506
605
  });
507
606
  item.appendChild(deleteBtn);
508
-
607
+
509
608
  binaryFilesList.appendChild(item);
510
609
  });
511
610
  };
@@ -513,22 +612,22 @@ hyperbook.typst = (function () {
513
612
  // Function to save state to store
514
613
  const saveState = async () => {
515
614
  if (!editor) return;
516
-
615
+
517
616
  // Update current file content
518
617
  fileContents.set(currentFile.filename, editor.value);
519
-
618
+
520
619
  // Update sourceFiles array with latest content
521
- sourceFiles = sourceFiles.map(f => ({
620
+ sourceFiles = sourceFiles.map((f) => ({
522
621
  filename: f.filename,
523
- content: fileContents.get(f.filename) || f.content
622
+ content: fileContents.get(f.filename) || f.content,
524
623
  }));
525
-
624
+
526
625
  await store.typst?.put({
527
626
  id,
528
627
  code: editor.value,
529
628
  sourceFiles,
530
629
  binaryFiles,
531
- currentFile: currentFile.filename
630
+ currentFile: currentFile.filename,
532
631
  });
533
632
  };
534
633
 
@@ -537,14 +636,25 @@ hyperbook.typst = (function () {
537
636
  if (editor) {
538
637
  // Update sourceFiles with current editor content
539
638
  fileContents.set(currentFile.filename, editor.value);
540
- sourceFiles = sourceFiles.map(f => ({
639
+ sourceFiles = sourceFiles.map((f) => ({
541
640
  filename: f.filename,
542
- content: fileContents.get(f.filename) || f.content
641
+ content: fileContents.get(f.filename) || f.content,
543
642
  }));
544
-
545
- const mainFile = sourceFiles.find(f => f.filename === "main.typ" || f.filename === "main.typst");
643
+
644
+ const mainFile = sourceFiles.find(
645
+ (f) => f.filename === "main.typ" || f.filename === "main.typst",
646
+ );
546
647
  const mainCode = mainFile ? mainFile.content : "";
547
- renderTypst(mainCode, preview, loadingIndicator, sourceFiles, binaryFiles, id, previewContainer, basePath);
648
+ renderTypst(
649
+ mainCode,
650
+ preview,
651
+ loadingIndicator,
652
+ sourceFiles,
653
+ binaryFiles,
654
+ id,
655
+ previewContainer,
656
+ basePath,
657
+ );
548
658
  }
549
659
  };
550
660
 
@@ -553,24 +663,30 @@ hyperbook.typst = (function () {
553
663
 
554
664
  // Add source file button
555
665
  addSourceFileBtn?.addEventListener("click", () => {
556
- const filename = prompt(i18n.get("typst-filename-prompt") || "Enter filename (e.g., helper.typ):");
666
+ const filename = prompt(
667
+ i18n.get("typst-filename-prompt") ||
668
+ "Enter filename (e.g., helper.typ):",
669
+ );
557
670
  if (filename) {
558
671
  // Validate filename
559
672
  if (!filename.endsWith(".typ") && !filename.endsWith(".typst")) {
560
- alert(i18n.get("typst-filename-error") || "Filename must end with .typ or .typst");
673
+ alert(
674
+ i18n.get("typst-filename-error") ||
675
+ "Filename must end with .typ or .typst",
676
+ );
561
677
  return;
562
678
  }
563
-
564
- if (sourceFiles.some(f => f.filename === filename)) {
679
+
680
+ if (sourceFiles.some((f) => f.filename === filename)) {
565
681
  alert(i18n.get("typst-filename-exists") || "File already exists");
566
682
  return;
567
683
  }
568
-
684
+
569
685
  // Add new file
570
686
  const newFile = { filename, content: `// ${filename}\n` };
571
687
  sourceFiles.push(newFile);
572
688
  fileContents.set(filename, newFile.content);
573
-
689
+
574
690
  // Switch to new file
575
691
  if (editor) {
576
692
  fileContents.set(currentFile.filename, editor.value);
@@ -579,7 +695,7 @@ hyperbook.typst = (function () {
579
695
  if (editor) {
580
696
  editor.value = newFile.content;
581
697
  }
582
-
698
+
583
699
  updateTabs();
584
700
  saveState();
585
701
  rerenderTypst();
@@ -590,7 +706,7 @@ hyperbook.typst = (function () {
590
706
  addBinaryFileBtn?.addEventListener("click", (e) => {
591
707
  e.preventDefault();
592
708
  e.stopPropagation();
593
-
709
+
594
710
  const input = document.createElement("input");
595
711
  input.type = "file";
596
712
  input.accept = "image/*,.pdf";
@@ -598,15 +714,19 @@ hyperbook.typst = (function () {
598
714
  const file = e.target.files[0];
599
715
  if (file) {
600
716
  const dest = `/${file.name}`;
601
-
717
+
602
718
  // Check if file already exists
603
- if (binaryFiles.some(f => f.dest === dest)) {
604
- if (!confirm(i18n.get("typst-file-replace") || `Replace existing ${dest}?`)) {
719
+ if (binaryFiles.some((f) => f.dest === dest)) {
720
+ if (
721
+ !confirm(
722
+ i18n.get("typst-file-replace") || `Replace existing ${dest}?`,
723
+ )
724
+ ) {
605
725
  return;
606
726
  }
607
- binaryFiles = binaryFiles.filter(f => f.dest !== dest);
727
+ binaryFiles = binaryFiles.filter((f) => f.dest !== dest);
608
728
  }
609
-
729
+
610
730
  // Read file as data URL
611
731
  const reader = new FileReader();
612
732
  reader.onload = async (e) => {
@@ -632,22 +752,24 @@ hyperbook.typst = (function () {
632
752
  const result = await store.typst?.get(id);
633
753
  if (result) {
634
754
  editor.value = result.code;
635
-
755
+
636
756
  // Restore sourceFiles and binaryFiles if available
637
757
  if (result.sourceFiles) {
638
758
  sourceFiles = result.sourceFiles;
639
- sourceFiles.forEach(f => fileContents.set(f.filename, f.content));
759
+ sourceFiles.forEach((f) => fileContents.set(f.filename, f.content));
640
760
  }
641
761
  if (result.binaryFiles) {
642
762
  binaryFiles = result.binaryFiles;
643
763
  }
644
764
  if (result.currentFile) {
645
- currentFile = sourceFiles.find(f => f.filename === result.currentFile) || sourceFiles[0];
765
+ currentFile =
766
+ sourceFiles.find((f) => f.filename === result.currentFile) ||
767
+ sourceFiles[0];
646
768
  editor.value = fileContents.get(currentFile.filename) || "";
647
769
  }
648
770
  }
649
771
  initialCode = editor.value;
650
-
772
+
651
773
  updateTabs();
652
774
  updateBinaryFilesList();
653
775
  rerenderTypst();
@@ -662,29 +784,52 @@ hyperbook.typst = (function () {
662
784
  // Preview mode - code is in hidden textarea
663
785
  initialCode = sourceTextarea.value;
664
786
  loadTypst().then(() => {
665
- renderTypst(initialCode, preview, loadingIndicator, sourceFiles, binaryFiles, id, previewContainer, basePath);
787
+ renderTypst(
788
+ initialCode,
789
+ preview,
790
+ loadingIndicator,
791
+ sourceFiles,
792
+ binaryFiles,
793
+ id,
794
+ previewContainer,
795
+ basePath,
796
+ );
666
797
  });
667
798
  }
668
799
 
669
800
  // Download PDF button
670
801
  downloadBtn?.addEventListener("click", async () => {
671
802
  // Get the main file content
672
- const mainFile = sourceFiles.find(f => f.filename === "main.typ" || f.filename === "main.typst");
673
- const code = mainFile ? mainFile.content : (editor ? editor.value : initialCode);
803
+ const mainFile = sourceFiles.find(
804
+ (f) => f.filename === "main.typ" || f.filename === "main.typst",
805
+ );
806
+ const code = mainFile
807
+ ? mainFile.content
808
+ : editor
809
+ ? editor.value
810
+ : initialCode;
674
811
  await exportPdf(code, id, sourceFiles, binaryFiles, basePath);
675
812
  });
676
813
 
677
814
  // Download Project button (ZIP with all files)
678
815
  downloadProjectBtn?.addEventListener("click", async () => {
679
816
  // Get the main file content
680
- const mainFile = sourceFiles.find(f => f.filename === "main.typ" || f.filename === "main.typst");
681
- const code = mainFile ? mainFile.content : (editor ? editor.value : initialCode);
817
+ const mainFile = sourceFiles.find(
818
+ (f) => f.filename === "main.typ" || f.filename === "main.typst",
819
+ );
820
+ const code = mainFile
821
+ ? mainFile.content
822
+ : editor
823
+ ? editor.value
824
+ : initialCode;
682
825
  const encoder = new TextEncoder();
683
826
  const zipFiles = {};
684
827
 
685
828
  // Add all source files
686
829
  for (const { filename, content } of sourceFiles) {
687
- const path = filename.startsWith('/') ? filename.substring(1) : filename;
830
+ const path = filename.startsWith("/")
831
+ ? filename.substring(1)
832
+ : filename;
688
833
  zipFiles[path] = encoder.encode(content);
689
834
  }
690
835
 
@@ -692,12 +837,12 @@ hyperbook.typst = (function () {
692
837
  for (const { dest, url } of binaryFiles) {
693
838
  try {
694
839
  let arrayBuffer;
695
-
840
+
696
841
  // Check if URL is a data URL (user-uploaded file)
697
- if (url.startsWith('data:')) {
842
+ if (url.startsWith("data:")) {
698
843
  const response = await fetch(url);
699
844
  arrayBuffer = await response.arrayBuffer();
700
- } else if (url.startsWith('http://') || url.startsWith('https://')) {
845
+ } else if (url.startsWith("http://") || url.startsWith("https://")) {
701
846
  // External URL
702
847
  const response = await fetch(url);
703
848
  if (response.ok) {
@@ -708,7 +853,9 @@ hyperbook.typst = (function () {
708
853
  }
709
854
  } else {
710
855
  // Relative URL - use basePath to construct full URL
711
- const fullUrl = basePath ? `${basePath}/${url}`.replace(/\/+/g, '/') : url;
856
+ const fullUrl = basePath
857
+ ? `${basePath}/${url}`.replace(/\/+/g, "/")
858
+ : url;
712
859
  const response = await fetch(fullUrl);
713
860
  if (response.ok) {
714
861
  arrayBuffer = await response.arrayBuffer();
@@ -717,36 +864,39 @@ hyperbook.typst = (function () {
717
864
  continue;
718
865
  }
719
866
  }
720
-
721
- const path = dest.startsWith('/') ? dest.substring(1) : dest;
867
+
868
+ const path = dest.startsWith("/") ? dest.substring(1) : dest;
722
869
  zipFiles[path] = new Uint8Array(arrayBuffer);
723
870
  } catch (error) {
724
871
  console.warn(`Error loading binary file ${url}:`, error);
725
872
  }
726
873
  }
727
874
 
728
- // Also include assets loaded from image() calls in the code
729
- const relImagePaths = extractRelImagePaths(code);
730
- for (const imagePath of relImagePaths) {
875
+ const relPaths = extractRelFilePaths(code);
876
+ for (const relPath of relPaths) {
731
877
  // Skip if already in zipFiles or already handled as binary file
732
- const normalizedPath = imagePath.startsWith('/') ? imagePath.substring(1) : imagePath;
878
+ const normalizedPath = relPath.startsWith("/")
879
+ ? relPath.substring(1)
880
+ : relPath;
733
881
  if (zipFiles[normalizedPath]) continue;
734
-
882
+
735
883
  // Skip absolute URLs, data URLs, and blob URLs
736
- if (/^(https?:|data:|blob:)/i.test(imagePath)) continue;
737
-
884
+ if (/^(https?:|data:|blob:)/i.test(relPath)) continue;
885
+
738
886
  try {
739
887
  // Construct URL using basePath
740
- const url = basePath ? `${basePath}/${imagePath}`.replace(/\/+/g, '/') : imagePath;
888
+ const url = basePath
889
+ ? `${basePath}/${relPath}`.replace(/\/+/g, "/")
890
+ : imagePath;
741
891
  const response = await fetch(url);
742
892
  if (response.ok) {
743
893
  const arrayBuffer = await response.arrayBuffer();
744
894
  zipFiles[normalizedPath] = new Uint8Array(arrayBuffer);
745
895
  } else {
746
- console.warn(`Failed to load image asset: ${imagePath} at ${url}`);
896
+ console.warn(`Failed to load asset: ${relPath} at ${url}`);
747
897
  }
748
898
  } catch (error) {
749
- console.warn(`Error loading image asset ${imagePath}:`, error);
899
+ console.warn(`Error loading asset ${relPath}:`, error);
750
900
  }
751
901
  }
752
902
 
@@ -762,7 +912,12 @@ hyperbook.typst = (function () {
762
912
 
763
913
  // Reset button (edit mode only)
764
914
  resetBtn?.addEventListener("click", async () => {
765
- if (window.confirm(i18n.get("typst-reset-prompt") || "Are you sure you want to reset the code?")) {
915
+ if (
916
+ window.confirm(
917
+ i18n.get("typst-reset-prompt") ||
918
+ "Are you sure you want to reset the code?",
919
+ )
920
+ ) {
766
921
  store.typst?.delete(id);
767
922
  window.location.reload();
768
923
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperbook/markdown",
3
- "version": "0.50.1",
3
+ "version": "0.51.0",
4
4
  "author": "Mike Barkmin",
5
5
  "homepage": "https://github.com/openpatch/hyperbook#readme",
6
6
  "license": "MIT",
@@ -93,8 +93,8 @@
93
93
  "vitest": "^4.0.18",
94
94
  "wavesurfer.js": "^7.12.1",
95
95
  "@hyperbook/fs": "0.24.2",
96
- "@hyperbook/types": "0.20.0",
97
- "@hyperbook/web-component-excalidraw": "0.3.2"
96
+ "@hyperbook/web-component-excalidraw": "0.3.2",
97
+ "@hyperbook/types": "0.20.0"
98
98
  },
99
99
  "scripts": {
100
100
  "version": "pnpm build",