@hyperbook/markdown 0.52.0 → 0.52.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,7 +15,7 @@ hyperbook.typst = (function () {
15
15
  };
16
16
 
17
17
  const REGEX_PATTERNS = {
18
- READ: /#read\s*\(\s*(['"])([^'"]+)\1[^)]*\)/gi,
18
+ READ: /read\s*\(\s*(['"])([^'"]+)\1[^)]*\)/gi,
19
19
  CSV: /csv\s*\(\s*(['"])([^'"]+)\1[^)]*\)/gi,
20
20
  JSON: /json\s*\(\s*(['"])([^'"]+)\1[^)]*\)/gi,
21
21
  YAML: /yaml\s*\(\s*(['"])([^'"]+)\1[^)]*\)/gi,
@@ -25,6 +25,9 @@ hyperbook.typst = (function () {
25
25
  ERROR_MESSAGE: /message:\s*"([^"]+)"/,
26
26
  };
27
27
 
28
+ // Text file patterns that need UTF-8 encoding
29
+ const TEXT_PATTERNS = ['READ', 'CSV', 'JSON', 'YAML', 'XML'];
30
+
28
31
  // ============================================================================
29
32
  // UTILITY FUNCTIONS
30
33
  // ============================================================================
@@ -229,15 +232,15 @@ hyperbook.typst = (function () {
229
232
  /**
230
233
  * Extract relative file paths from Typst source code
231
234
  * @param {string} src - Typst source code
232
- * @returns {Array<string>} Array of file paths
235
+ * @returns {Array<{path: string, isText: boolean}>} Array of file paths with type info
233
236
  */
234
237
  extractFilePaths(src) {
235
- const paths = new Set();
236
- const patterns = Object.values(REGEX_PATTERNS).filter(
237
- p => p !== REGEX_PATTERNS.ABSOLUTE_URL && p !== REGEX_PATTERNS.ERROR_MESSAGE
238
- );
238
+ const paths = new Map(); // path -> isText
239
239
 
240
- for (const pattern of patterns) {
240
+ for (const [name, pattern] of Object.entries(REGEX_PATTERNS)) {
241
+ if (name === 'ABSOLUTE_URL' || name === 'ERROR_MESSAGE') continue;
242
+
243
+ const isText = TEXT_PATTERNS.includes(name);
241
244
  let match;
242
245
  // Reset regex lastIndex
243
246
  pattern.lastIndex = 0;
@@ -246,11 +249,11 @@ hyperbook.typst = (function () {
246
249
  const path = match[2];
247
250
  // Skip absolute URLs, data URLs, blob URLs
248
251
  if (REGEX_PATTERNS.ABSOLUTE_URL.test(path)) continue;
249
- paths.add(path);
252
+ paths.set(path, isText);
250
253
  }
251
254
  }
252
255
 
253
- return Array.from(paths);
256
+ return Array.from(paths.entries()).map(([path, isText]) => ({ path, isText }));
254
257
  }
255
258
 
256
259
  /**
@@ -258,9 +261,10 @@ hyperbook.typst = (function () {
258
261
  * @param {string} path - Asset path
259
262
  * @param {string} basePath - Base path
260
263
  * @param {string} pagePath - Page path
264
+ * @param {boolean} isText - Whether this is a text file
261
265
  * @returns {Promise<Uint8Array|null>}
262
266
  */
263
- async fetchAsset(path, basePath, pagePath) {
267
+ async fetchAsset(path, basePath, pagePath, isText = false) {
264
268
  try {
265
269
  const url = constructUrl(path, basePath, pagePath);
266
270
  const response = await fetch(url);
@@ -270,8 +274,15 @@ hyperbook.typst = (function () {
270
274
  return null;
271
275
  }
272
276
 
273
- const arrayBuffer = await response.arrayBuffer();
274
- return new Uint8Array(arrayBuffer);
277
+ if (isText) {
278
+ // For text files, decode as text and re-encode as UTF-8
279
+ const text = await response.text();
280
+ return new TextEncoder().encode(text);
281
+ } else {
282
+ // For binary files, use arrayBuffer directly
283
+ const arrayBuffer = await response.arrayBuffer();
284
+ return new Uint8Array(arrayBuffer);
285
+ }
275
286
  } catch (error) {
276
287
  console.warn(`Error loading asset ${path}:`, error);
277
288
  return null;
@@ -280,36 +291,124 @@ hyperbook.typst = (function () {
280
291
 
281
292
  /**
282
293
  * Fetch multiple assets and cache them
283
- * @param {Array<string>} paths - Array of asset paths
294
+ * @param {Array<{path: string, isText: boolean}>} pathInfos - Array of path info objects
284
295
  * @param {string} basePath - Base path
285
296
  * @param {string} pagePath - Page path
286
297
  * @returns {Promise<void>}
287
298
  */
288
- async fetchAssets(paths, basePath, pagePath) {
289
- const missingPaths = paths.filter((p) => !this.cache.has(p));
299
+ async fetchAssets(pathInfos, basePath, pagePath) {
300
+ const missingPaths = pathInfos.filter(({ path }) => !this.cache.has(path));
290
301
 
291
302
  await Promise.all(
292
- missingPaths.map(async (path) => {
293
- const data = await this.fetchAsset(path, basePath, pagePath);
303
+ missingPaths.map(async ({ path, isText }) => {
304
+ const data = await this.fetchAsset(path, basePath, pagePath, isText);
294
305
  this.cache.set(path, data);
295
306
  })
296
307
  );
297
308
  }
298
309
 
299
310
  /**
300
- * Map cached assets to Typst virtual filesystem
311
+ * Build Typst preamble with inlined assets as bytes
312
+ * @returns {string} Typst preamble code
301
313
  */
302
- mapToShadow() {
303
- for (const [path, data] of this.cache.entries()) {
304
- if (data !== null) {
305
- const normalizedPath = normalizePath(path);
306
- window.$typst.mapShadow(normalizedPath, data);
314
+ buildAssetsPreamble() {
315
+ if (this.cache.size === 0) return "";
316
+ const entries = [...this.cache.entries()]
317
+ .filter(([name, u8]) => u8 !== null)
318
+ .map(([name, u8]) => {
319
+ const nums = Array.from(u8).join(",");
320
+ return ` "${name}": bytes((${nums}))`;
321
+ })
322
+ .join(",\n");
323
+ if (!entries) return "";
324
+ return `#let __assets = (\n${entries}\n)\n\n`;
325
+ }
326
+
327
+ /**
328
+ * Rewrite file calls (image, read, csv, json, yaml, xml) to use inlined assets
329
+ * @param {string} src - Typst source code
330
+ * @returns {string} Rewritten source code
331
+ */
332
+ rewriteAssetCalls(src) {
333
+ if (this.cache.size === 0) return src;
334
+
335
+ // Rewrite image() calls
336
+ src = src.replace(/image\s*\(\s*(['"])([^'"]+)\1/g, (m, q, fname) => {
337
+ if (this.cache.has(fname)) {
338
+ const asset = this.cache.get(fname);
339
+ if (asset === null) {
340
+ return `[File not found: _${fname}_]`;
341
+ }
342
+ return `image(__assets.at("${fname}")`;
307
343
  }
308
- }
344
+ return m;
345
+ });
346
+
347
+ // Rewrite read() calls
348
+ src = src.replace(/read\s*\(\s*(['"])([^'"]+)\1/g, (m, q, fname) => {
349
+ if (this.cache.has(fname)) {
350
+ const asset = this.cache.get(fname);
351
+ if (asset === null) {
352
+ return `[File not found: _${fname}_]`;
353
+ }
354
+ return `read(__assets.at("${fname}")`;
355
+ }
356
+ return m;
357
+ });
358
+
359
+ // Rewrite csv() calls
360
+ src = src.replace(/csv\s*\(\s*(['"])([^'"]+)\1/g, (m, q, fname) => {
361
+ if (this.cache.has(fname)) {
362
+ const asset = this.cache.get(fname);
363
+ if (asset === null) {
364
+ return `[File not found: _${fname}_]`;
365
+ }
366
+ return `csv(__assets.at("${fname}")`;
367
+ }
368
+ return m;
369
+ });
370
+
371
+ // Rewrite json() calls
372
+ src = src.replace(/json\s*\(\s*(['"])([^'"]+)\1/g, (m, q, fname) => {
373
+ if (this.cache.has(fname)) {
374
+ const asset = this.cache.get(fname);
375
+ if (asset === null) {
376
+ return `[File not found: _${fname}_]`;
377
+ }
378
+ return `json(__assets.at("${fname}")`;
379
+ }
380
+ return m;
381
+ });
382
+
383
+ // Rewrite yaml() calls
384
+ src = src.replace(/yaml\s*\(\s*(['"])([^'"]+)\1/g, (m, q, fname) => {
385
+ if (this.cache.has(fname)) {
386
+ const asset = this.cache.get(fname);
387
+ if (asset === null) {
388
+ return `[File not found: _${fname}_]`;
389
+ }
390
+ return `yaml(__assets.at("${fname}")`;
391
+ }
392
+ return m;
393
+ });
394
+
395
+ // Rewrite xml() calls
396
+ src = src.replace(/xml\s*\(\s*(['"])([^'"]+)\1/g, (m, q, fname) => {
397
+ if (this.cache.has(fname)) {
398
+ const asset = this.cache.get(fname);
399
+ if (asset === null) {
400
+ return `[File not found: _${fname}_]`;
401
+ }
402
+ return `xml(__assets.at("${fname}")`;
403
+ }
404
+ return m;
405
+ });
406
+
407
+ return src;
309
408
  }
310
409
 
311
410
  /**
312
- * Prepare assets for rendering (extract, fetch, and map)
411
+ * Prepare assets for rendering (extract and fetch)
313
412
  * @param {string} mainSrc - Main Typst source
314
413
  * @param {Array} sourceFiles - Source file objects
315
414
  * @param {string} basePath - Base path
@@ -317,21 +416,24 @@ hyperbook.typst = (function () {
317
416
  * @returns {Promise<void>}
318
417
  */
319
418
  async prepare(mainSrc, sourceFiles, basePath, pagePath) {
320
- const allPaths = new Set();
419
+ const allPaths = new Map(); // path -> isText
321
420
 
322
421
  // Extract from main source
323
- this.extractFilePaths(mainSrc).forEach((p) => allPaths.add(p));
422
+ for (const { path, isText } of this.extractFilePaths(mainSrc)) {
423
+ allPaths.set(path, isText);
424
+ }
324
425
 
325
426
  // Extract from all source files
326
427
  for (const { content } of sourceFiles) {
327
- this.extractFilePaths(content).forEach((p) => allPaths.add(p));
428
+ for (const { path, isText } of this.extractFilePaths(content)) {
429
+ allPaths.set(path, isText);
430
+ }
328
431
  }
329
432
 
330
- const paths = Array.from(allPaths);
433
+ const pathInfos = Array.from(allPaths.entries()).map(([path, isText]) => ({ path, isText }));
331
434
 
332
- if (paths.length > 0) {
333
- await this.fetchAssets(paths, basePath, pagePath);
334
- this.mapToShadow();
435
+ if (pathInfos.length > 0) {
436
+ await this.fetchAssets(pathInfos, basePath, pagePath);
335
437
  }
336
438
  }
337
439
  }
@@ -516,14 +618,23 @@ hyperbook.typst = (function () {
516
618
  // Prepare assets
517
619
  await this.assetManager.prepare(code, sourceFiles, basePath, pagePath);
518
620
 
519
- // Add source files
520
- await this.addSourceFiles(sourceFiles);
621
+ // Build assets preamble and rewrite source files
622
+ const assetsPreamble = this.assetManager.buildAssetsPreamble();
623
+ const rewrittenCode = this.assetManager.rewriteAssetCalls(code);
624
+ const rewrittenSourceFiles = sourceFiles.map(({ filename, content }) => ({
625
+ filename,
626
+ content: assetsPreamble + this.assetManager.rewriteAssetCalls(content),
627
+ }));
628
+
629
+ // Add source files with rewritten content (includes preamble)
630
+ await this.addSourceFiles(rewrittenSourceFiles);
521
631
 
522
632
  // Add binary files
523
633
  await BinaryFileHandler.addToShadow(binaryFiles);
524
634
 
525
- // Render to SVG
526
- const svg = await window.$typst.svg({ mainContent: code });
635
+ // Render to SVG with preamble prepended
636
+ const mainContent = assetsPreamble + rewrittenCode;
637
+ const svg = await window.$typst.svg({ mainContent });
527
638
 
528
639
  // Clear any existing errors
529
640
  if (previewContainer) {
@@ -584,14 +695,23 @@ hyperbook.typst = (function () {
584
695
  // Prepare assets
585
696
  await this.assetManager.prepare(code, sourceFiles, basePath, pagePath);
586
697
 
587
- // Add source files
588
- await this.addSourceFiles(sourceFiles);
698
+ // Build assets preamble and rewrite source files
699
+ const assetsPreamble = this.assetManager.buildAssetsPreamble();
700
+ const rewrittenCode = this.assetManager.rewriteAssetCalls(code);
701
+ const rewrittenSourceFiles = sourceFiles.map(({ filename, content }) => ({
702
+ filename,
703
+ content: assetsPreamble + this.assetManager.rewriteAssetCalls(content),
704
+ }));
705
+
706
+ // Add source files with rewritten content (includes preamble)
707
+ await this.addSourceFiles(rewrittenSourceFiles);
589
708
 
590
709
  // Add binary files
591
710
  await BinaryFileHandler.addToShadow(binaryFiles);
592
711
 
593
- // Generate PDF
594
- const pdfData = await window.$typst.pdf({ mainContent: code });
712
+ // Generate PDF with preamble prepended
713
+ const mainContent = assetsPreamble + rewrittenCode;
714
+ const pdfData = await window.$typst.pdf({ mainContent });
595
715
  const pdfBlob = new Blob([pdfData], { type: 'application/pdf' });
596
716
 
597
717
  // Download PDF
@@ -803,9 +923,9 @@ hyperbook.typst = (function () {
803
923
  * @returns {Promise<void>}
804
924
  */
805
925
  async addAssets(zipFiles, code, basePath, pagePath) {
806
- const relPaths = this.assetManager.extractFilePaths(code);
926
+ const pathInfos = this.assetManager.extractFilePaths(code);
807
927
 
808
- for (const relPath of relPaths) {
928
+ for (const { path: relPath, isText } of pathInfos) {
809
929
  const normalizedPath = relPath.startsWith('/')
810
930
  ? relPath.substring(1)
811
931
  : relPath;
@@ -821,8 +941,13 @@ hyperbook.typst = (function () {
821
941
  const response = await fetch(url);
822
942
 
823
943
  if (response.ok) {
824
- const arrayBuffer = await response.arrayBuffer();
825
- zipFiles[normalizedPath] = new Uint8Array(arrayBuffer);
944
+ if (isText) {
945
+ const text = await response.text();
946
+ zipFiles[normalizedPath] = new TextEncoder().encode(text);
947
+ } else {
948
+ const arrayBuffer = await response.arrayBuffer();
949
+ zipFiles[normalizedPath] = new Uint8Array(arrayBuffer);
950
+ }
826
951
  } else {
827
952
  console.warn(`Failed to load asset: ${relPath} at ${url}`);
828
953
  }
@@ -1189,7 +1314,9 @@ hyperbook.typst = (function () {
1189
1314
  this.fileManager.updateCurrentContent(this.editor.value);
1190
1315
 
1191
1316
  const mainFile = this.fileManager.findMainFile();
1192
- const mainCode = mainFile ? mainFile.content : '';
1317
+ const mainCode = mainFile
1318
+ ? this.fileManager.contents.get(mainFile.filename) || mainFile.content
1319
+ : '';
1193
1320
 
1194
1321
  this.renderer.render({
1195
1322
  code: mainCode,
@@ -1388,9 +1515,16 @@ hyperbook.typst = (function () {
1388
1515
  const basePath = elem.getAttribute('data-base-path') || '';
1389
1516
  const pagePath = elem.getAttribute('data-page-path') || '';
1390
1517
 
1391
- const sourceFiles = sourceFilesData ? JSON.parse(atob(sourceFilesData)) : [];
1392
- const binaryFiles = binaryFilesData ? JSON.parse(atob(binaryFilesData)) : [];
1393
- const fontFiles = fontFilesData ? JSON.parse(atob(fontFilesData)) : [];
1518
+ // Decode base64 with proper UTF-8 handling
1519
+ const decodeBase64 = (str) => {
1520
+ const binaryStr = atob(str);
1521
+ const bytes = Uint8Array.from(binaryStr, (c) => c.charCodeAt(0));
1522
+ return new TextDecoder('utf-8').decode(bytes);
1523
+ };
1524
+
1525
+ const sourceFiles = sourceFilesData ? JSON.parse(decodeBase64(sourceFilesData)) : [];
1526
+ const binaryFiles = binaryFilesData ? JSON.parse(decodeBase64(binaryFilesData)) : [];
1527
+ const fontFiles = fontFilesData ? JSON.parse(decodeBase64(fontFilesData)) : [];
1394
1528
 
1395
1529
  new TypstEditor({
1396
1530
  elem,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperbook/markdown",
3
- "version": "0.52.0",
3
+ "version": "0.52.1",
4
4
  "author": "Mike Barkmin",
5
5
  "homepage": "https://github.com/openpatch/hyperbook#readme",
6
6
  "license": "MIT",
@@ -92,8 +92,8 @@
92
92
  "vfile": "^6.0.3",
93
93
  "vitest": "^4.0.18",
94
94
  "wavesurfer.js": "^7.12.1",
95
- "@hyperbook/types": "0.20.0",
96
95
  "@hyperbook/fs": "0.24.2",
96
+ "@hyperbook/types": "0.20.0",
97
97
  "@hyperbook/web-component-excalidraw": "0.3.2"
98
98
  },
99
99
  "scripts": {