@hyperbook/markdown 0.50.0 → 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 =
|
|
28
|
-
|
|
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 =
|
|
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
|
-
|
|
87
|
-
const extractRelImagePaths = (src) => {
|
|
89
|
+
const extractRelFilePaths = (src) => {
|
|
88
90
|
const paths = new Set();
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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
|
-
})
|
|
131
|
-
|
|
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
|
|
136
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 +
|
|
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 (
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
270
|
-
errorOverlay.className =
|
|
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(
|
|
283
|
-
closeBtn.addEventListener(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
469
|
+
if (basePath && !basePath.startsWith("/")) {
|
|
470
|
+
basePath = "/" + basePath;
|
|
382
471
|
}
|
|
383
|
-
|
|
384
|
-
let sourceFiles = sourceFilesData
|
|
385
|
-
|
|
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 =
|
|
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 (
|
|
426
|
-
|
|
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 =
|
|
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 (
|
|
501
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 (
|
|
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 =
|
|
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(
|
|
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(
|
|
673
|
-
|
|
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(
|
|
681
|
-
|
|
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(
|
|
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(
|
|
842
|
+
if (url.startsWith("data:")) {
|
|
698
843
|
const response = await fetch(url);
|
|
699
844
|
arrayBuffer = await response.arrayBuffer();
|
|
700
|
-
} else if (url.startsWith(
|
|
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
|
|
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(
|
|
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
|
-
|
|
729
|
-
const
|
|
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 =
|
|
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(
|
|
737
|
-
|
|
884
|
+
if (/^(https?:|data:|blob:)/i.test(relPath)) continue;
|
|
885
|
+
|
|
738
886
|
try {
|
|
739
887
|
// Construct URL using basePath
|
|
740
|
-
const url = basePath
|
|
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
|
|
896
|
+
console.warn(`Failed to load asset: ${relPath} at ${url}`);
|
|
747
897
|
}
|
|
748
898
|
} catch (error) {
|
|
749
|
-
console.warn(`Error loading
|
|
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 (
|
|
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
|
}
|
|
@@ -30,6 +30,7 @@ dialog {
|
|
|
30
30
|
transform 0.25s ease-out
|
|
31
31
|
);
|
|
32
32
|
visibility: hidden;
|
|
33
|
+
opacity: 0;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
:host([right]) dialog {
|
|
@@ -67,6 +68,7 @@ dialog::backdrop {
|
|
|
67
68
|
|
|
68
69
|
dialog[open] {
|
|
69
70
|
visibility: visible;
|
|
71
|
+
opacity: 1;
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
:host([open]) dialog[open],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperbook/markdown",
|
|
3
|
-
"version": "0.
|
|
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/
|
|
97
|
-
"@hyperbook/
|
|
96
|
+
"@hyperbook/web-component-excalidraw": "0.3.2",
|
|
97
|
+
"@hyperbook/types": "0.20.0"
|
|
98
98
|
},
|
|
99
99
|
"scripts": {
|
|
100
100
|
"version": "pnpm build",
|