hyperbook 0.80.0 → 0.81.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.
- package/dist/assets/directive-typst/client.js +1255 -774
- package/dist/index.js +66 -28
- package/package.json +2 -2
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
hyperbook.typst = (function () {
|
|
2
|
-
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// CONSTANTS AND CONFIGURATION
|
|
6
|
+
// ============================================================================
|
|
7
|
+
|
|
8
|
+
const CONFIG = {
|
|
9
|
+
TYPST_COMPILER_URL: "https://cdn.jsdelivr.net/npm/@myriaddreamin/typst-ts-web-compiler/pkg/typst_ts_web_compiler_bg.wasm",
|
|
10
|
+
TYPST_RENDERER_URL: "https://cdn.jsdelivr.net/npm/@myriaddreamin/typst-ts-renderer/pkg/typst_ts_renderer_bg.wasm",
|
|
11
|
+
TYPST_BUNDLE_URL: "https://cdn.jsdelivr.net/npm/@myriaddreamin/typst.ts/dist/esm/contrib/all-in-one-lite.bundle.js",
|
|
12
|
+
DEBOUNCE_DELAY: 500,
|
|
13
|
+
TYPST_CHECK_INTERVAL: 50,
|
|
14
|
+
CONTAINER_PADDING: 20,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const REGEX_PATTERNS = {
|
|
18
|
+
READ: /#read\s*\(\s*(['"])([^'"]+)\1[^)]*\)/gi,
|
|
19
|
+
CSV: /csv\s*\(\s*(['"])([^'"]+)\1[^)]*\)/gi,
|
|
20
|
+
JSON: /json\s*\(\s*(['"])([^'"]+)\1[^)]*\)/gi,
|
|
21
|
+
YAML: /yaml\s*\(\s*(['"])([^'"]+)\1[^)]*\)/gi,
|
|
22
|
+
XML: /xml\s*\(\s*(['"])([^'"]+)\1[^)]*\)/gi,
|
|
23
|
+
IMAGE: /image\s*\(\s*(['"])([^'"]+)\1[^)]*\)/gi,
|
|
24
|
+
ABSOLUTE_URL: /^(https?:|data:|blob:)/i,
|
|
25
|
+
ERROR_MESSAGE: /message:\s*"([^"]+)"/,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// UTILITY FUNCTIONS
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Debounce utility function
|
|
34
|
+
* @param {Function} func - Function to debounce
|
|
35
|
+
* @param {number} wait - Wait time in milliseconds
|
|
36
|
+
* @returns {Function} Debounced function
|
|
37
|
+
*/
|
|
3
38
|
const debounce = (func, wait) => {
|
|
4
39
|
let timeout;
|
|
5
40
|
return function executedFunction(...args) {
|
|
@@ -12,883 +47,779 @@ hyperbook.typst = (function () {
|
|
|
12
47
|
};
|
|
13
48
|
};
|
|
14
49
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Normalize path to ensure it starts with /
|
|
52
|
+
* @param {string} path - Path to normalize
|
|
53
|
+
* @returns {string} Normalized path
|
|
54
|
+
*/
|
|
55
|
+
const normalizePath = (path) => {
|
|
56
|
+
if (!path) return '/';
|
|
57
|
+
return path.startsWith('/') ? path : `/${path}`;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Construct full URL from base path and relative path
|
|
62
|
+
* @param {string} path - Relative or absolute path
|
|
63
|
+
* @param {string} basePath - Base path for absolute paths
|
|
64
|
+
* @param {string} pagePath - Page path for relative paths
|
|
65
|
+
* @returns {string} Full URL
|
|
66
|
+
*/
|
|
67
|
+
const constructUrl = (path, basePath, pagePath) => {
|
|
68
|
+
if (path.startsWith('/')) {
|
|
69
|
+
return basePath ? `${basePath}${path}`.replace(/\/+/g, '/') : path;
|
|
70
|
+
}
|
|
71
|
+
return pagePath ? `${pagePath}/${path}`.replace(/\/+/g, '/') : path;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get localized string with fallback
|
|
76
|
+
* @param {string} key - Translation key
|
|
77
|
+
* @param {string} fallback - Fallback text
|
|
78
|
+
* @returns {string} Localized or fallback text
|
|
79
|
+
*/
|
|
80
|
+
const i18nGet = (key, fallback = '') => {
|
|
81
|
+
return window.i18n?.get(key) || fallback;
|
|
44
82
|
};
|
|
45
83
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// INITIALIZATION
|
|
86
|
+
// ============================================================================
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Initialize code-input template for Typst syntax highlighting
|
|
90
|
+
*/
|
|
91
|
+
const initializeCodeInput = () => {
|
|
92
|
+
if (!window.codeInput) return;
|
|
93
|
+
|
|
94
|
+
window.codeInput.registerTemplate(
|
|
95
|
+
"typst-highlighted",
|
|
96
|
+
window.codeInput.templates.prism(window.Prism, [
|
|
97
|
+
new window.codeInput.plugins.AutoCloseBrackets(),
|
|
98
|
+
new window.codeInput.plugins.Indent(true, 2),
|
|
99
|
+
])
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// ============================================================================
|
|
104
|
+
// TYPST LOADER
|
|
105
|
+
// ============================================================================
|
|
106
|
+
|
|
107
|
+
class TypstLoader {
|
|
108
|
+
constructor() {
|
|
109
|
+
this.loaded = false;
|
|
110
|
+
this.loadPromise = null;
|
|
111
|
+
this.loadedFonts = new Set();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Load Typst compiler and renderer
|
|
116
|
+
* @param {Array} fontFiles - Array of font file objects
|
|
117
|
+
* @returns {Promise<void>}
|
|
118
|
+
*/
|
|
119
|
+
async load({ fontFiles = [] } = {}) {
|
|
120
|
+
if (this.loaded) {
|
|
121
|
+
return Promise.resolve();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (this.loadPromise) {
|
|
125
|
+
return this.loadPromise;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this.loadPromise = new Promise((resolve, reject) => {
|
|
129
|
+
const script = document.createElement('script');
|
|
130
|
+
script.src = CONFIG.TYPST_BUNDLE_URL;
|
|
131
|
+
script.type = 'module';
|
|
132
|
+
script.id = 'typst-loader';
|
|
133
|
+
|
|
134
|
+
script.onload = () => {
|
|
135
|
+
this.waitForTypst(fontFiles)
|
|
136
|
+
.then(resolve)
|
|
137
|
+
.catch(reject);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
script.onerror = () => {
|
|
141
|
+
reject(new Error('Failed to load Typst bundle'));
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
document.head.appendChild(script);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return this.loadPromise;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Wait for Typst module to initialize
|
|
152
|
+
* @param {Array} fontFiles - Array of font file objects
|
|
153
|
+
* @returns {Promise<void>}
|
|
154
|
+
*/
|
|
155
|
+
async waitForTypst(fontFiles) {
|
|
156
|
+
return new Promise((resolve, reject) => {
|
|
157
|
+
const checkTypst = async () => {
|
|
158
|
+
if (typeof window.$typst !== 'undefined') {
|
|
159
|
+
try {
|
|
160
|
+
await this.initializeTypst(fontFiles);
|
|
161
|
+
this.loaded = true;
|
|
162
|
+
resolve();
|
|
163
|
+
} catch (error) {
|
|
164
|
+
reject(error);
|
|
165
|
+
}
|
|
73
166
|
} else {
|
|
74
|
-
setTimeout(checkTypst,
|
|
167
|
+
setTimeout(checkTypst, CONFIG.TYPST_CHECK_INTERVAL);
|
|
75
168
|
}
|
|
76
169
|
};
|
|
77
170
|
checkTypst();
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
document.head.appendChild(script);
|
|
81
|
-
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
82
173
|
|
|
83
|
-
|
|
84
|
-
|
|
174
|
+
/**
|
|
175
|
+
* Initialize Typst compiler and renderer with fonts
|
|
176
|
+
* @param {Array} fontFiles - Array of font file objects
|
|
177
|
+
* @returns {Promise<void>}
|
|
178
|
+
*/
|
|
179
|
+
async initializeTypst(fontFiles) {
|
|
180
|
+
const fonts = window.TypstCompileModule.loadFonts(
|
|
181
|
+
fontFiles.map((f) => f.url)
|
|
182
|
+
);
|
|
85
183
|
|
|
86
|
-
|
|
87
|
-
|
|
184
|
+
window.$typst.setCompilerInitOptions({
|
|
185
|
+
beforeBuild: [fonts],
|
|
186
|
+
getModule: () => CONFIG.TYPST_COMPILER_URL,
|
|
187
|
+
});
|
|
88
188
|
|
|
89
|
-
|
|
90
|
-
|
|
189
|
+
window.$typst.setRendererInitOptions({
|
|
190
|
+
beforeBuild: [fonts],
|
|
191
|
+
getModule: () => CONFIG.TYPST_RENDERER_URL,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
91
195
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
196
|
+
// ============================================================================
|
|
197
|
+
// RENDER QUEUE
|
|
198
|
+
// ============================================================================
|
|
95
199
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
200
|
+
class RenderQueue {
|
|
201
|
+
constructor() {
|
|
202
|
+
this.queue = Promise.resolve();
|
|
203
|
+
}
|
|
99
204
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
205
|
+
/**
|
|
206
|
+
* Add render function to queue
|
|
207
|
+
* @param {Function} renderFn - Async render function
|
|
208
|
+
* @returns {Promise}
|
|
209
|
+
*/
|
|
210
|
+
add(renderFn) {
|
|
211
|
+
this.queue = this.queue
|
|
212
|
+
.then(renderFn)
|
|
213
|
+
.catch((error) => {
|
|
214
|
+
console.error('Queued render error:', error);
|
|
215
|
+
});
|
|
216
|
+
return this.queue;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
103
219
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// Pattern for xml() function
|
|
108
|
-
const xmlRe = /xml\s*\(\s*(['"])([^'"]+)\1/gi;
|
|
220
|
+
// ============================================================================
|
|
221
|
+
// ASSET MANAGEMENT
|
|
222
|
+
// ============================================================================
|
|
109
223
|
|
|
110
|
-
|
|
224
|
+
class AssetManager {
|
|
225
|
+
constructor() {
|
|
226
|
+
this.cache = new Map(); // filepath -> Uint8Array | null
|
|
227
|
+
}
|
|
111
228
|
|
|
112
|
-
|
|
113
|
-
|
|
229
|
+
/**
|
|
230
|
+
* Extract relative file paths from Typst source code
|
|
231
|
+
* @param {string} src - Typst source code
|
|
232
|
+
* @returns {Array<string>} Array of file paths
|
|
233
|
+
*/
|
|
234
|
+
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
|
+
);
|
|
114
239
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
240
|
+
for (const pattern of patterns) {
|
|
241
|
+
let match;
|
|
242
|
+
// Reset regex lastIndex
|
|
243
|
+
pattern.lastIndex = 0;
|
|
244
|
+
|
|
245
|
+
while ((match = pattern.exec(src)) !== null) {
|
|
246
|
+
const path = match[2];
|
|
247
|
+
// Skip absolute URLs, data URLs, blob URLs
|
|
248
|
+
if (REGEX_PATTERNS.ABSOLUTE_URL.test(path)) continue;
|
|
249
|
+
paths.add(path);
|
|
250
|
+
}
|
|
122
251
|
}
|
|
252
|
+
|
|
253
|
+
return Array.from(paths);
|
|
123
254
|
}
|
|
124
255
|
|
|
125
|
-
|
|
126
|
-
|
|
256
|
+
/**
|
|
257
|
+
* Fetch single asset from server
|
|
258
|
+
* @param {string} path - Asset path
|
|
259
|
+
* @param {string} basePath - Base path
|
|
260
|
+
* @param {string} pagePath - Page path
|
|
261
|
+
* @returns {Promise<Uint8Array|null>}
|
|
262
|
+
*/
|
|
263
|
+
async fetchAsset(path, basePath, pagePath) {
|
|
264
|
+
try {
|
|
265
|
+
const url = constructUrl(path, basePath, pagePath);
|
|
266
|
+
const response = await fetch(url);
|
|
127
267
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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);
|
|
148
|
-
assetCache.set(p, null); // Mark as failed
|
|
268
|
+
if (!response.ok) {
|
|
269
|
+
console.warn(`Asset not found: ${path} at ${url} (HTTP ${response.status})`);
|
|
270
|
+
return null;
|
|
149
271
|
}
|
|
150
|
-
}),
|
|
151
|
-
);
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
// Build typst preamble with inlined assets
|
|
155
|
-
const buildAssetsPreamble = () => {
|
|
156
|
-
if (assetCache.size === 0) return "";
|
|
157
|
-
const entries = [...assetCache.entries()]
|
|
158
|
-
.filter(([name, u8]) => u8 !== null) // Skip failed images
|
|
159
|
-
.map(([name, u8]) => {
|
|
160
|
-
const nums = Array.from(u8).join(",");
|
|
161
|
-
return ` "${name}": bytes((${nums}))`;
|
|
162
|
-
})
|
|
163
|
-
.join(",\n");
|
|
164
|
-
if (!entries) return "";
|
|
165
|
-
return `#let __assets = (\n${entries}\n)\n\n`;
|
|
166
|
-
};
|
|
167
272
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
src = src.replace(/image\s*\(\s*(['"])([^'"]+)\1/g, (m, q, fname) => {
|
|
174
|
-
if (assetCache.has(fname)) {
|
|
175
|
-
const asset = assetCache.get(fname);
|
|
176
|
-
if (asset === null) {
|
|
177
|
-
return `[File not found: _${fname}_]`;
|
|
178
|
-
}
|
|
179
|
-
return `image(__assets.at("${fname}")`;
|
|
180
|
-
}
|
|
181
|
-
return m;
|
|
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}")`;
|
|
273
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
274
|
+
return new Uint8Array(arrayBuffer);
|
|
275
|
+
} catch (error) {
|
|
276
|
+
console.warn(`Error loading asset ${path}:`, error);
|
|
277
|
+
return null;
|
|
192
278
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Fetch multiple assets and cache them
|
|
283
|
+
* @param {Array<string>} paths - Array of asset paths
|
|
284
|
+
* @param {string} basePath - Base path
|
|
285
|
+
* @param {string} pagePath - Page path
|
|
286
|
+
* @returns {Promise<void>}
|
|
287
|
+
*/
|
|
288
|
+
async fetchAssets(paths, basePath, pagePath) {
|
|
289
|
+
const missingPaths = paths.filter((p) => !this.cache.has(p));
|
|
290
|
+
|
|
291
|
+
await Promise.all(
|
|
292
|
+
missingPaths.map(async (path) => {
|
|
293
|
+
const data = await this.fetchAsset(path, basePath, pagePath);
|
|
294
|
+
this.cache.set(path, data);
|
|
295
|
+
})
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Map cached assets to Typst virtual filesystem
|
|
301
|
+
*/
|
|
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);
|
|
202
307
|
}
|
|
203
|
-
return `csv(__assets.at("${fname}")`;
|
|
204
308
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Prepare assets for rendering (extract, fetch, and map)
|
|
313
|
+
* @param {string} mainSrc - Main Typst source
|
|
314
|
+
* @param {Array} sourceFiles - Source file objects
|
|
315
|
+
* @param {string} basePath - Base path
|
|
316
|
+
* @param {string} pagePath - Page path
|
|
317
|
+
* @returns {Promise<void>}
|
|
318
|
+
*/
|
|
319
|
+
async prepare(mainSrc, sourceFiles, basePath, pagePath) {
|
|
320
|
+
const allPaths = new Set();
|
|
321
|
+
|
|
322
|
+
// Extract from main source
|
|
323
|
+
this.extractFilePaths(mainSrc).forEach((p) => allPaths.add(p));
|
|
324
|
+
|
|
325
|
+
// Extract from all source files
|
|
326
|
+
for (const { content } of sourceFiles) {
|
|
327
|
+
this.extractFilePaths(content).forEach((p) => allPaths.add(p));
|
|
216
328
|
}
|
|
217
|
-
return m;
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
return src;
|
|
221
|
-
};
|
|
222
329
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
return preamble + rewriteAssetCalls(src);
|
|
330
|
+
const paths = Array.from(allPaths);
|
|
331
|
+
|
|
332
|
+
if (paths.length > 0) {
|
|
333
|
+
await this.fetchAssets(paths, basePath, pagePath);
|
|
334
|
+
this.mapToShadow();
|
|
335
|
+
}
|
|
230
336
|
}
|
|
231
|
-
|
|
232
|
-
};
|
|
337
|
+
}
|
|
233
338
|
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
339
|
+
// ============================================================================
|
|
340
|
+
// ERROR HANDLING
|
|
341
|
+
// ============================================================================
|
|
342
|
+
|
|
343
|
+
class ErrorHandler {
|
|
344
|
+
/**
|
|
345
|
+
* Parse error message from SourceDiagnostic format
|
|
346
|
+
* @param {string|Error} error - Error object or message
|
|
347
|
+
* @returns {string} Parsed error message
|
|
348
|
+
*/
|
|
349
|
+
static parse(error) {
|
|
350
|
+
const errorMessage = error?.toString() || 'Unknown error';
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
const match = errorMessage.match(REGEX_PATTERNS.ERROR_MESSAGE);
|
|
354
|
+
if (match) {
|
|
355
|
+
return match[1];
|
|
356
|
+
}
|
|
357
|
+
} catch (e) {
|
|
358
|
+
// Fallback to original message
|
|
241
359
|
}
|
|
242
|
-
|
|
243
|
-
|
|
360
|
+
|
|
361
|
+
return errorMessage;
|
|
244
362
|
}
|
|
245
|
-
return errorMessage;
|
|
246
|
-
};
|
|
247
363
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
// Queue this render to ensure only one compilation runs at a time
|
|
260
|
-
return queueRender(async () => {
|
|
261
|
-
// Show loading indicator
|
|
262
|
-
if (loadingIndicator) {
|
|
263
|
-
loadingIndicator.style.display = "flex";
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
await loadTypst();
|
|
364
|
+
/**
|
|
365
|
+
* Display error overlay in preview container
|
|
366
|
+
* @param {HTMLElement} previewContainer - Preview container element
|
|
367
|
+
* @param {string} errorText - Error message
|
|
368
|
+
*/
|
|
369
|
+
static showOverlay(previewContainer, errorText) {
|
|
370
|
+
// Remove any existing error overlay
|
|
371
|
+
const existingError = previewContainer.querySelector('.typst-error-overlay');
|
|
372
|
+
if (existingError) {
|
|
373
|
+
existingError.remove();
|
|
374
|
+
}
|
|
267
375
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
376
|
+
// Create floating error overlay
|
|
377
|
+
const errorOverlay = document.createElement('div');
|
|
378
|
+
errorOverlay.className = 'typst-error-overlay';
|
|
379
|
+
errorOverlay.innerHTML = `
|
|
380
|
+
<div class="typst-error-content">
|
|
381
|
+
<div class="typst-error-header">
|
|
382
|
+
<span class="typst-error-title">⚠️ Typst Error</span>
|
|
383
|
+
<button class="typst-error-close" title="Dismiss error">×</button>
|
|
384
|
+
</div>
|
|
385
|
+
<div class="typst-error-message">${errorText}</div>
|
|
386
|
+
</div>
|
|
387
|
+
`;
|
|
388
|
+
|
|
389
|
+
// Add close button functionality
|
|
390
|
+
const closeBtn = errorOverlay.querySelector('.typst-error-close');
|
|
391
|
+
closeBtn.addEventListener('click', () => errorOverlay.remove());
|
|
392
|
+
|
|
393
|
+
previewContainer.appendChild(errorOverlay);
|
|
394
|
+
}
|
|
271
395
|
|
|
272
|
-
|
|
273
|
-
|
|
396
|
+
/**
|
|
397
|
+
* Display error inline
|
|
398
|
+
* @param {HTMLElement} container - Container element
|
|
399
|
+
* @param {string} errorText - Error message
|
|
400
|
+
*/
|
|
401
|
+
static showInline(container, errorText) {
|
|
402
|
+
container.innerHTML = `<div class="typst-error">${errorText}</div>`;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
274
405
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
406
|
+
// ============================================================================
|
|
407
|
+
// BINARY FILE HANDLER
|
|
408
|
+
// ============================================================================
|
|
409
|
+
|
|
410
|
+
class BinaryFileHandler {
|
|
411
|
+
/**
|
|
412
|
+
* Load binary file from URL
|
|
413
|
+
* @param {string} url - File URL (data URL or HTTP URL)
|
|
414
|
+
* @returns {Promise<ArrayBuffer>}
|
|
415
|
+
*/
|
|
416
|
+
static async load(url) {
|
|
417
|
+
const response = await fetch(url);
|
|
418
|
+
|
|
419
|
+
if (!response.ok && !url.startsWith('data:')) {
|
|
420
|
+
throw new Error(`Failed to load binary file: ${url}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return response.arrayBuffer();
|
|
424
|
+
}
|
|
282
425
|
|
|
283
|
-
|
|
284
|
-
|
|
426
|
+
/**
|
|
427
|
+
* Add binary files to Typst shadow filesystem
|
|
428
|
+
* @param {Array} binaryFiles - Array of binary file objects
|
|
429
|
+
* @returns {Promise<void>}
|
|
430
|
+
*/
|
|
431
|
+
static async addToShadow(binaryFiles) {
|
|
432
|
+
await Promise.all(
|
|
433
|
+
binaryFiles.map(async ({ dest, url }) => {
|
|
285
434
|
try {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (url.startsWith("data:")) {
|
|
290
|
-
const response = await fetch(url);
|
|
291
|
-
arrayBuffer = await response.arrayBuffer();
|
|
292
|
-
} else {
|
|
293
|
-
// External URL
|
|
294
|
-
const response = await fetch(url);
|
|
295
|
-
if (!response.ok) {
|
|
296
|
-
console.warn(`Failed to load binary file: ${url}`);
|
|
297
|
-
continue;
|
|
298
|
-
}
|
|
299
|
-
arrayBuffer = await response.arrayBuffer();
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const path = dest.startsWith("/") ? dest.substring(1) : dest;
|
|
303
|
-
$typst.mapShadow(`/${path}`, new Uint8Array(arrayBuffer));
|
|
435
|
+
const arrayBuffer = await BinaryFileHandler.load(url);
|
|
436
|
+
const path = dest.startsWith('/') ? dest.substring(1) : dest;
|
|
437
|
+
window.$typst.mapShadow(`/${path}`, new Uint8Array(arrayBuffer));
|
|
304
438
|
} catch (error) {
|
|
305
439
|
console.warn(`Error loading binary file ${url}:`, error);
|
|
306
440
|
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
|
|
441
|
+
})
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
310
445
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
".typst-error-overlay",
|
|
315
|
-
);
|
|
316
|
-
if (existingError) {
|
|
317
|
-
existingError.remove();
|
|
318
|
-
}
|
|
319
|
-
}
|
|
446
|
+
// ============================================================================
|
|
447
|
+
// TYPST RENDERER
|
|
448
|
+
// ============================================================================
|
|
320
449
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const height = Number.parseFloat(svgElem.getAttribute("height"));
|
|
328
|
-
const containerWidth = container.clientWidth - 20;
|
|
329
|
-
if (width > 0 && containerWidth > 0) {
|
|
330
|
-
svgElem.setAttribute("width", containerWidth);
|
|
331
|
-
svgElem.setAttribute("height", (height * containerWidth) / width);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
} catch (error) {
|
|
335
|
-
const errorText = parseTypstError(error || "Error rendering Typst");
|
|
450
|
+
class TypstRenderer {
|
|
451
|
+
constructor(loader, assetManager, renderQueue) {
|
|
452
|
+
this.loader = loader;
|
|
453
|
+
this.assetManager = assetManager;
|
|
454
|
+
this.renderQueue = renderQueue;
|
|
455
|
+
}
|
|
336
456
|
|
|
337
|
-
|
|
338
|
-
|
|
457
|
+
/**
|
|
458
|
+
* Add source files to Typst
|
|
459
|
+
* @param {Array} sourceFiles - Source file objects
|
|
460
|
+
* @returns {Promise<void>}
|
|
461
|
+
*/
|
|
462
|
+
async addSourceFiles(sourceFiles) {
|
|
463
|
+
for (const { filename, content } of sourceFiles) {
|
|
464
|
+
const path = filename.startsWith('/') ? filename.substring(1) : filename;
|
|
465
|
+
await window.$typst.addSource(`/${path}`, content);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
339
468
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
469
|
+
/**
|
|
470
|
+
* Scale SVG to fit container
|
|
471
|
+
* @param {HTMLElement} container - Container element
|
|
472
|
+
*/
|
|
473
|
+
scaleSvg(container) {
|
|
474
|
+
const svgElem = container.firstElementChild;
|
|
475
|
+
if (!svgElem) return;
|
|
476
|
+
|
|
477
|
+
const width = Number.parseFloat(svgElem.getAttribute('width'));
|
|
478
|
+
const height = Number.parseFloat(svgElem.getAttribute('height'));
|
|
479
|
+
const containerWidth = container.clientWidth - CONFIG.CONTAINER_PADDING;
|
|
480
|
+
|
|
481
|
+
if (width > 0 && containerWidth > 0) {
|
|
482
|
+
svgElem.setAttribute('width', containerWidth);
|
|
483
|
+
svgElem.setAttribute('height', (height * containerWidth) / width);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
349
486
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
487
|
+
/**
|
|
488
|
+
* Render Typst code to SVG
|
|
489
|
+
* @param {Object} params - Render parameters
|
|
490
|
+
* @returns {Promise<void>}
|
|
491
|
+
*/
|
|
492
|
+
async render({
|
|
493
|
+
code,
|
|
494
|
+
container,
|
|
495
|
+
loadingIndicator,
|
|
496
|
+
sourceFiles,
|
|
497
|
+
binaryFiles,
|
|
498
|
+
fontFiles,
|
|
499
|
+
id,
|
|
500
|
+
previewContainer,
|
|
501
|
+
basePath,
|
|
502
|
+
pagePath,
|
|
503
|
+
}) {
|
|
504
|
+
return this.renderQueue.add(async () => {
|
|
505
|
+
try {
|
|
506
|
+
// Show loading indicator
|
|
507
|
+
if (loadingIndicator) {
|
|
508
|
+
loadingIndicator.style.display = 'flex';
|
|
353
509
|
}
|
|
354
510
|
|
|
355
|
-
|
|
356
|
-
const errorOverlay = document.createElement("div");
|
|
357
|
-
errorOverlay.className = "typst-error-overlay";
|
|
358
|
-
errorOverlay.innerHTML = `
|
|
359
|
-
<div class="typst-error-content">
|
|
360
|
-
<div class="typst-error-header">
|
|
361
|
-
<span class="typst-error-title">⚠️ Typst Error</span>
|
|
362
|
-
<button class="typst-error-close" title="Dismiss error">×</button>
|
|
363
|
-
</div>
|
|
364
|
-
<div class="typst-error-message">${errorText}</div>
|
|
365
|
-
</div>
|
|
366
|
-
`;
|
|
367
|
-
|
|
368
|
-
// Add close button functionality
|
|
369
|
-
const closeBtn = errorOverlay.querySelector(".typst-error-close");
|
|
370
|
-
closeBtn.addEventListener("click", () => {
|
|
371
|
-
errorOverlay.remove();
|
|
372
|
-
});
|
|
511
|
+
await this.loader.load({ fontFiles });
|
|
373
512
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
// Fallback: show error in preview container directly
|
|
377
|
-
container.innerHTML = `<div class="typst-error">${errorText}</div>`;
|
|
378
|
-
}
|
|
379
|
-
} finally {
|
|
380
|
-
// Hide loading indicator
|
|
381
|
-
if (loadingIndicator) {
|
|
382
|
-
loadingIndicator.style.display = "none";
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
});
|
|
386
|
-
};
|
|
513
|
+
// Reset shadow files
|
|
514
|
+
window.$typst.resetShadow();
|
|
387
515
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
// Queue this export to ensure only one compilation runs at a time
|
|
391
|
-
return queueRender(async () => {
|
|
392
|
-
await loadTypst();
|
|
516
|
+
// Prepare assets
|
|
517
|
+
await this.assetManager.prepare(code, sourceFiles, basePath, pagePath);
|
|
393
518
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
$typst.resetShadow();
|
|
519
|
+
// Add source files
|
|
520
|
+
await this.addSourceFiles(sourceFiles);
|
|
397
521
|
|
|
398
|
-
|
|
399
|
-
|
|
522
|
+
// Add binary files
|
|
523
|
+
await BinaryFileHandler.addToShadow(binaryFiles);
|
|
400
524
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const path = filename.startsWith("/")
|
|
404
|
-
? filename.substring(1)
|
|
405
|
-
: filename;
|
|
406
|
-
await $typst.addSource(`/${path}`, content);
|
|
407
|
-
}
|
|
525
|
+
// Render to SVG
|
|
526
|
+
const svg = await window.$typst.svg({ mainContent: code });
|
|
408
527
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
// Check if URL is a data URL (user-uploaded file)
|
|
415
|
-
if (url.startsWith("data:")) {
|
|
416
|
-
const response = await fetch(url);
|
|
417
|
-
arrayBuffer = await response.arrayBuffer();
|
|
418
|
-
} else {
|
|
419
|
-
// External URL
|
|
420
|
-
const response = await fetch(url);
|
|
421
|
-
if (!response.ok) {
|
|
422
|
-
continue;
|
|
423
|
-
}
|
|
424
|
-
arrayBuffer = await response.arrayBuffer();
|
|
528
|
+
// Clear any existing errors
|
|
529
|
+
if (previewContainer) {
|
|
530
|
+
const existingError = previewContainer.querySelector('.typst-error-overlay');
|
|
531
|
+
if (existingError) {
|
|
532
|
+
existingError.remove();
|
|
425
533
|
}
|
|
426
|
-
|
|
427
|
-
const path = dest.startsWith("/") ? dest.substring(1) : dest;
|
|
428
|
-
$typst.mapShadow(`/${path}`, new Uint8Array(arrayBuffer));
|
|
429
|
-
} catch (error) {
|
|
430
|
-
console.warn(`Error loading binary file ${url}:`, error);
|
|
431
534
|
}
|
|
432
|
-
}
|
|
433
535
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
link.href = URL.createObjectURL(pdfFile);
|
|
438
|
-
link.download = `typst-${id}.pdf`;
|
|
439
|
-
link.click();
|
|
440
|
-
URL.revokeObjectURL(link.href);
|
|
441
|
-
} catch (error) {
|
|
442
|
-
console.error("PDF export error:", error);
|
|
443
|
-
alert(i18n.get("typst-pdf-error") || "Error exporting PDF");
|
|
444
|
-
}
|
|
445
|
-
});
|
|
446
|
-
};
|
|
447
|
-
|
|
448
|
-
for (let elem of elems) {
|
|
449
|
-
const id = elem.getAttribute("data-id");
|
|
450
|
-
const previewContainer = elem.querySelector(".preview-container");
|
|
451
|
-
const preview = elem.querySelector(".typst-preview");
|
|
452
|
-
const loadingIndicator = elem.querySelector(".typst-loading");
|
|
453
|
-
const editor = elem.querySelector(".editor.typst");
|
|
454
|
-
const downloadBtn = elem.querySelector(".download-pdf");
|
|
455
|
-
const downloadProjectBtn = elem.querySelector(".download-project");
|
|
456
|
-
const resetBtn = elem.querySelector(".reset");
|
|
457
|
-
const sourceTextarea = elem.querySelector(".typst-source");
|
|
458
|
-
const tabsList = elem.querySelector(".tabs-list");
|
|
459
|
-
const binaryFilesList = elem.querySelector(".binary-files-list");
|
|
460
|
-
const addSourceFileBtn = elem.querySelector(".add-source-file");
|
|
461
|
-
const addBinaryFileBtn = elem.querySelector(".add-binary-file");
|
|
462
|
-
|
|
463
|
-
// Parse source files and binary files from data attributes
|
|
464
|
-
const sourceFilesData = elem.getAttribute("data-source-files");
|
|
465
|
-
const binaryFilesData = elem.getAttribute("data-binary-files");
|
|
466
|
-
let basePath = elem.getAttribute("data-base-path") || "";
|
|
467
|
-
|
|
468
|
-
// Ensure basePath starts with / for absolute paths
|
|
469
|
-
if (basePath && !basePath.startsWith("/")) {
|
|
470
|
-
basePath = "/" + basePath;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
let sourceFiles = sourceFilesData ? JSON.parse(atob(sourceFilesData)) : [];
|
|
474
|
-
let binaryFiles = binaryFilesData ? JSON.parse(atob(binaryFilesData)) : [];
|
|
475
|
-
|
|
476
|
-
// Track current active file
|
|
477
|
-
let currentFile =
|
|
478
|
-
sourceFiles.find(
|
|
479
|
-
(f) => f.filename === "main.typ" || f.filename === "main.typst",
|
|
480
|
-
) || sourceFiles[0];
|
|
481
|
-
|
|
482
|
-
// Store file contents in memory
|
|
483
|
-
const fileContents = new Map();
|
|
484
|
-
sourceFiles.forEach((f) => fileContents.set(f.filename, f.content));
|
|
485
|
-
|
|
486
|
-
// Function to update tabs UI
|
|
487
|
-
const updateTabs = () => {
|
|
488
|
-
if (!tabsList) return;
|
|
489
|
-
|
|
490
|
-
tabsList.innerHTML = "";
|
|
491
|
-
|
|
492
|
-
// Add source file tabs
|
|
493
|
-
sourceFiles.forEach((file) => {
|
|
494
|
-
const tab = document.createElement("div");
|
|
495
|
-
tab.className = "file-tab";
|
|
496
|
-
if (file.filename === currentFile.filename) {
|
|
497
|
-
tab.classList.add("active");
|
|
498
|
-
}
|
|
536
|
+
// Update container with SVG
|
|
537
|
+
container.innerHTML = svg;
|
|
538
|
+
this.scaleSvg(container);
|
|
499
539
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
tab.appendChild(tabName);
|
|
504
|
-
|
|
505
|
-
// Add delete button (except for main file)
|
|
506
|
-
if (file.filename !== "main.typ" && file.filename !== "main.typst") {
|
|
507
|
-
const deleteBtn = document.createElement("button");
|
|
508
|
-
deleteBtn.className = "tab-delete";
|
|
509
|
-
deleteBtn.textContent = "×";
|
|
510
|
-
deleteBtn.title = i18n.get("typst-delete-file") || "Delete file";
|
|
511
|
-
deleteBtn.addEventListener("click", (e) => {
|
|
512
|
-
e.stopPropagation();
|
|
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
|
-
);
|
|
521
|
-
fileContents.delete(file.filename);
|
|
522
|
-
|
|
523
|
-
// Switch to main file if we deleted the current file
|
|
524
|
-
if (currentFile.filename === file.filename) {
|
|
525
|
-
currentFile = sourceFiles[0];
|
|
526
|
-
if (editor) {
|
|
527
|
-
editor.value = fileContents.get(currentFile.filename) || "";
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
updateTabs();
|
|
532
|
-
saveState();
|
|
533
|
-
rerenderTypst();
|
|
534
|
-
}
|
|
535
|
-
});
|
|
536
|
-
tab.appendChild(deleteBtn);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
tab.addEventListener("click", () => {
|
|
540
|
-
if (currentFile.filename !== file.filename) {
|
|
541
|
-
// Save current file content
|
|
542
|
-
if (editor) {
|
|
543
|
-
fileContents.set(currentFile.filename, editor.value);
|
|
544
|
-
}
|
|
540
|
+
} catch (error) {
|
|
541
|
+
const errorText = ErrorHandler.parse(error);
|
|
542
|
+
const hasExistingContent = container.querySelector('svg') !== null;
|
|
545
543
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
if (
|
|
549
|
-
|
|
544
|
+
if (previewContainer) {
|
|
545
|
+
// Don't clear existing content on error
|
|
546
|
+
if (!hasExistingContent) {
|
|
547
|
+
container.innerHTML = '';
|
|
550
548
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
549
|
+
ErrorHandler.showOverlay(previewContainer, errorText);
|
|
550
|
+
} else {
|
|
551
|
+
ErrorHandler.showInline(container, errorText);
|
|
554
552
|
}
|
|
555
|
-
});
|
|
556
553
|
|
|
557
|
-
|
|
554
|
+
} finally {
|
|
555
|
+
// Hide loading indicator
|
|
556
|
+
if (loadingIndicator) {
|
|
557
|
+
loadingIndicator.style.display = 'none';
|
|
558
|
+
}
|
|
559
|
+
}
|
|
558
560
|
});
|
|
559
|
-
}
|
|
561
|
+
}
|
|
560
562
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
563
|
+
/**
|
|
564
|
+
* Export Typst code to PDF
|
|
565
|
+
* @param {Object} params - Export parameters
|
|
566
|
+
* @returns {Promise<void>}
|
|
567
|
+
*/
|
|
568
|
+
async exportPdf({
|
|
569
|
+
code,
|
|
570
|
+
id,
|
|
571
|
+
sourceFiles,
|
|
572
|
+
binaryFiles,
|
|
573
|
+
fontFiles,
|
|
574
|
+
basePath,
|
|
575
|
+
pagePath,
|
|
576
|
+
}) {
|
|
577
|
+
return this.renderQueue.add(async () => {
|
|
578
|
+
try {
|
|
579
|
+
await this.loader.load({ fontFiles });
|
|
564
580
|
|
|
565
|
-
|
|
581
|
+
// Reset shadow files
|
|
582
|
+
window.$typst.resetShadow();
|
|
566
583
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
emptyMsg.className = "binary-files-empty";
|
|
570
|
-
emptyMsg.textContent =
|
|
571
|
-
i18n.get("typst-no-binary-files") || "No binary files";
|
|
572
|
-
binaryFilesList.appendChild(emptyMsg);
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
584
|
+
// Prepare assets
|
|
585
|
+
await this.assetManager.prepare(code, sourceFiles, basePath, pagePath);
|
|
575
586
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
icon.className = "binary-file-icon";
|
|
582
|
-
icon.textContent = "📎";
|
|
583
|
-
item.appendChild(icon);
|
|
584
|
-
|
|
585
|
-
const name = document.createElement("span");
|
|
586
|
-
name.className = "binary-file-name";
|
|
587
|
-
name.textContent = file.dest;
|
|
588
|
-
item.appendChild(name);
|
|
589
|
-
|
|
590
|
-
const deleteBtn = document.createElement("button");
|
|
591
|
-
deleteBtn.className = "binary-file-delete";
|
|
592
|
-
deleteBtn.textContent = "×";
|
|
593
|
-
deleteBtn.title = i18n.get("typst-delete-file") || "Delete file";
|
|
594
|
-
deleteBtn.addEventListener("click", () => {
|
|
595
|
-
if (
|
|
596
|
-
confirm(
|
|
597
|
-
`${i18n.get("typst-delete-confirm") || "Delete"} ${file.dest}?`,
|
|
598
|
-
)
|
|
599
|
-
) {
|
|
600
|
-
binaryFiles = binaryFiles.filter((f) => f.dest !== file.dest);
|
|
601
|
-
updateBinaryFilesList();
|
|
602
|
-
saveState();
|
|
603
|
-
rerenderTypst();
|
|
604
|
-
}
|
|
605
|
-
});
|
|
606
|
-
item.appendChild(deleteBtn);
|
|
587
|
+
// Add source files
|
|
588
|
+
await this.addSourceFiles(sourceFiles);
|
|
589
|
+
|
|
590
|
+
// Add binary files
|
|
591
|
+
await BinaryFileHandler.addToShadow(binaryFiles);
|
|
607
592
|
|
|
608
|
-
|
|
593
|
+
// Generate PDF
|
|
594
|
+
const pdfData = await window.$typst.pdf({ mainContent: code });
|
|
595
|
+
const pdfBlob = new Blob([pdfData], { type: 'application/pdf' });
|
|
596
|
+
|
|
597
|
+
// Download PDF
|
|
598
|
+
const link = document.createElement('a');
|
|
599
|
+
link.href = URL.createObjectURL(pdfBlob);
|
|
600
|
+
link.download = `typst-${id}.pdf`;
|
|
601
|
+
link.click();
|
|
602
|
+
URL.revokeObjectURL(link.href);
|
|
603
|
+
|
|
604
|
+
} catch (error) {
|
|
605
|
+
console.error('PDF export error:', error);
|
|
606
|
+
alert(i18nGet('typst-pdf-error', 'Error exporting PDF'));
|
|
607
|
+
}
|
|
609
608
|
});
|
|
610
|
-
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
611
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
612
|
+
// ============================================================================
|
|
613
|
+
// FILE MANAGER
|
|
614
|
+
// ============================================================================
|
|
615
|
+
|
|
616
|
+
class FileManager {
|
|
617
|
+
constructor(sourceFiles) {
|
|
618
|
+
this.sourceFiles = sourceFiles;
|
|
619
|
+
this.contents = new Map();
|
|
620
|
+
this.currentFile = this.findMainFile() || sourceFiles[0];
|
|
621
|
+
|
|
622
|
+
// Initialize contents map
|
|
623
|
+
sourceFiles.forEach((f) => this.contents.set(f.filename, f.content));
|
|
624
|
+
}
|
|
615
625
|
|
|
616
|
-
|
|
617
|
-
|
|
626
|
+
/**
|
|
627
|
+
* Find main Typst file
|
|
628
|
+
* @returns {Object|null} Main file object
|
|
629
|
+
*/
|
|
630
|
+
findMainFile() {
|
|
631
|
+
return this.sourceFiles.find(
|
|
632
|
+
(f) => f.filename === 'main.typ' || f.filename === 'main.typst'
|
|
633
|
+
);
|
|
634
|
+
}
|
|
618
635
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
636
|
+
/**
|
|
637
|
+
* Get current file content
|
|
638
|
+
* @returns {string} File content
|
|
639
|
+
*/
|
|
640
|
+
getCurrentContent() {
|
|
641
|
+
return this.contents.get(this.currentFile.filename) || '';
|
|
642
|
+
}
|
|
624
643
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
};
|
|
644
|
+
/**
|
|
645
|
+
* Update current file content
|
|
646
|
+
* @param {string} content - New content
|
|
647
|
+
*/
|
|
648
|
+
updateCurrentContent(content) {
|
|
649
|
+
this.contents.set(this.currentFile.filename, content);
|
|
650
|
+
}
|
|
633
651
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
const mainFile = sourceFiles.find(
|
|
645
|
-
(f) => f.filename === "main.typ" || f.filename === "main.typst",
|
|
646
|
-
);
|
|
647
|
-
const mainCode = mainFile ? mainFile.content : "";
|
|
648
|
-
renderTypst(
|
|
649
|
-
mainCode,
|
|
650
|
-
preview,
|
|
651
|
-
loadingIndicator,
|
|
652
|
-
sourceFiles,
|
|
653
|
-
binaryFiles,
|
|
654
|
-
id,
|
|
655
|
-
previewContainer,
|
|
656
|
-
basePath,
|
|
657
|
-
);
|
|
652
|
+
/**
|
|
653
|
+
* Switch to different file
|
|
654
|
+
* @param {string} filename - Target filename
|
|
655
|
+
* @returns {string} New file content
|
|
656
|
+
*/
|
|
657
|
+
switchTo(filename) {
|
|
658
|
+
const file = this.sourceFiles.find((f) => f.filename === filename);
|
|
659
|
+
if (file) {
|
|
660
|
+
this.currentFile = file;
|
|
661
|
+
return this.getCurrentContent();
|
|
658
662
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
// Create debounced version of rerenderTypst for input events (500ms delay)
|
|
662
|
-
const debouncedRerenderTypst = debounce(rerenderTypst, 500);
|
|
663
|
+
return '';
|
|
664
|
+
}
|
|
663
665
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
i18n.get("typst-filename-error") ||
|
|
675
|
-
"Filename must end with .typ or .typst",
|
|
676
|
-
);
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
666
|
+
/**
|
|
667
|
+
* Add new source file
|
|
668
|
+
* @param {string} filename - New filename
|
|
669
|
+
* @param {string} content - File content
|
|
670
|
+
* @returns {boolean} Success status
|
|
671
|
+
*/
|
|
672
|
+
addFile(filename, content = `// ${filename}\n`) {
|
|
673
|
+
if (this.sourceFiles.some((f) => f.filename === filename)) {
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
679
676
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
677
|
+
const newFile = { filename, content };
|
|
678
|
+
this.sourceFiles.push(newFile);
|
|
679
|
+
this.contents.set(filename, content);
|
|
680
|
+
this.currentFile = newFile;
|
|
681
|
+
|
|
682
|
+
return true;
|
|
683
|
+
}
|
|
684
684
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
685
|
+
/**
|
|
686
|
+
* Delete source file
|
|
687
|
+
* @param {string} filename - Filename to delete
|
|
688
|
+
* @returns {boolean} Success status
|
|
689
|
+
*/
|
|
690
|
+
deleteFile(filename) {
|
|
691
|
+
// Don't delete main file
|
|
692
|
+
if (filename === 'main.typ' || filename === 'main.typst') {
|
|
693
|
+
return false;
|
|
694
|
+
}
|
|
689
695
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
fileContents.set(currentFile.filename, editor.value);
|
|
693
|
-
}
|
|
694
|
-
currentFile = newFile;
|
|
695
|
-
if (editor) {
|
|
696
|
-
editor.value = newFile.content;
|
|
697
|
-
}
|
|
696
|
+
this.sourceFiles = this.sourceFiles.filter((f) => f.filename !== filename);
|
|
697
|
+
this.contents.delete(filename);
|
|
698
698
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
699
|
+
// Switch to main file if we deleted current file
|
|
700
|
+
if (this.currentFile.filename === filename) {
|
|
701
|
+
this.currentFile = this.sourceFiles[0];
|
|
702
702
|
}
|
|
703
|
-
});
|
|
704
703
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
e.preventDefault();
|
|
708
|
-
e.stopPropagation();
|
|
704
|
+
return true;
|
|
705
|
+
}
|
|
709
706
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
!confirm(
|
|
722
|
-
i18n.get("typst-file-replace") || `Replace existing ${dest}?`,
|
|
723
|
-
)
|
|
724
|
-
) {
|
|
725
|
-
return;
|
|
726
|
-
}
|
|
727
|
-
binaryFiles = binaryFiles.filter((f) => f.dest !== dest);
|
|
728
|
-
}
|
|
707
|
+
/**
|
|
708
|
+
* Get all source files with current content
|
|
709
|
+
* @returns {Array} Updated source files
|
|
710
|
+
*/
|
|
711
|
+
getSourceFiles() {
|
|
712
|
+
return this.sourceFiles.map((f) => ({
|
|
713
|
+
filename: f.filename,
|
|
714
|
+
content: this.contents.get(f.filename) || f.content,
|
|
715
|
+
}));
|
|
716
|
+
}
|
|
717
|
+
}
|
|
729
718
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
const url = e.target.result;
|
|
734
|
-
binaryFiles.push({ dest, url });
|
|
735
|
-
updateBinaryFilesList();
|
|
736
|
-
saveState();
|
|
737
|
-
rerenderTypst();
|
|
738
|
-
};
|
|
739
|
-
reader.readAsDataURL(file);
|
|
740
|
-
}
|
|
741
|
-
});
|
|
742
|
-
input.click();
|
|
743
|
-
});
|
|
719
|
+
// ============================================================================
|
|
720
|
+
// PROJECT EXPORTER
|
|
721
|
+
// ============================================================================
|
|
744
722
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
if (result.currentFile) {
|
|
765
|
-
currentFile =
|
|
766
|
-
sourceFiles.find((f) => f.filename === result.currentFile) ||
|
|
767
|
-
sourceFiles[0];
|
|
768
|
-
editor.value = fileContents.get(currentFile.filename) || "";
|
|
769
|
-
}
|
|
723
|
+
class ProjectExporter {
|
|
724
|
+
constructor(assetManager) {
|
|
725
|
+
this.assetManager = assetManager;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Export project as ZIP file
|
|
730
|
+
* @param {Object} params - Export parameters
|
|
731
|
+
* @returns {Promise<void>}
|
|
732
|
+
*/
|
|
733
|
+
async export({ code, id, sourceFiles, binaryFiles, basePath, pagePath }) {
|
|
734
|
+
try {
|
|
735
|
+
const encoder = new TextEncoder();
|
|
736
|
+
const zipFiles = {};
|
|
737
|
+
|
|
738
|
+
// Add all source files
|
|
739
|
+
for (const { filename, content } of sourceFiles) {
|
|
740
|
+
const path = filename.startsWith('/') ? filename.substring(1) : filename;
|
|
741
|
+
zipFiles[path] = encoder.encode(content);
|
|
770
742
|
}
|
|
771
|
-
initialCode = editor.value;
|
|
772
743
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
rerenderTypst();
|
|
744
|
+
// Add binary files
|
|
745
|
+
await this.addBinaryFiles(zipFiles, binaryFiles, basePath, pagePath);
|
|
776
746
|
|
|
777
|
-
//
|
|
778
|
-
|
|
779
|
-
saveState();
|
|
780
|
-
debouncedRerenderTypst();
|
|
781
|
-
});
|
|
782
|
-
});
|
|
783
|
-
} else if (sourceTextarea) {
|
|
784
|
-
// Preview mode - code is in hidden textarea
|
|
785
|
-
initialCode = sourceTextarea.value;
|
|
786
|
-
loadTypst().then(() => {
|
|
787
|
-
renderTypst(
|
|
788
|
-
initialCode,
|
|
789
|
-
preview,
|
|
790
|
-
loadingIndicator,
|
|
791
|
-
sourceFiles,
|
|
792
|
-
binaryFiles,
|
|
793
|
-
id,
|
|
794
|
-
previewContainer,
|
|
795
|
-
basePath,
|
|
796
|
-
);
|
|
797
|
-
});
|
|
798
|
-
}
|
|
747
|
+
// Add referenced assets
|
|
748
|
+
await this.addAssets(zipFiles, code, basePath, pagePath);
|
|
799
749
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
// Get the main file content
|
|
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;
|
|
811
|
-
await exportPdf(code, id, sourceFiles, binaryFiles, basePath);
|
|
812
|
-
});
|
|
750
|
+
// Create and download ZIP
|
|
751
|
+
this.downloadZip(zipFiles, id);
|
|
813
752
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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;
|
|
825
|
-
const encoder = new TextEncoder();
|
|
826
|
-
const zipFiles = {};
|
|
827
|
-
|
|
828
|
-
// Add all source files
|
|
829
|
-
for (const { filename, content } of sourceFiles) {
|
|
830
|
-
const path = filename.startsWith("/")
|
|
831
|
-
? filename.substring(1)
|
|
832
|
-
: filename;
|
|
833
|
-
zipFiles[path] = encoder.encode(content);
|
|
753
|
+
} catch (error) {
|
|
754
|
+
console.error('Project export error:', error);
|
|
755
|
+
alert(i18nGet('typst-export-error', 'Error exporting project'));
|
|
834
756
|
}
|
|
757
|
+
}
|
|
835
758
|
|
|
836
|
-
|
|
759
|
+
/**
|
|
760
|
+
* Add binary files to ZIP
|
|
761
|
+
* @param {Object} zipFiles - ZIP files object
|
|
762
|
+
* @param {Array} binaryFiles - Binary files array
|
|
763
|
+
* @param {string} basePath - Base path
|
|
764
|
+
* @param {string} pagePath - Page path
|
|
765
|
+
* @returns {Promise<void>}
|
|
766
|
+
*/
|
|
767
|
+
async addBinaryFiles(zipFiles, binaryFiles, basePath, pagePath) {
|
|
837
768
|
for (const { dest, url } of binaryFiles) {
|
|
838
769
|
try {
|
|
839
770
|
let arrayBuffer;
|
|
840
771
|
|
|
841
|
-
|
|
842
|
-
if (url.startsWith("data:")) {
|
|
772
|
+
if (url.startsWith('data:')) {
|
|
843
773
|
const response = await fetch(url);
|
|
844
774
|
arrayBuffer = await response.arrayBuffer();
|
|
845
|
-
} else if (url.startsWith(
|
|
846
|
-
// External URL
|
|
775
|
+
} else if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
847
776
|
const response = await fetch(url);
|
|
848
|
-
if (response.ok)
|
|
849
|
-
|
|
850
|
-
} else {
|
|
851
|
-
console.warn(`Failed to load binary file: ${url}`);
|
|
852
|
-
continue;
|
|
853
|
-
}
|
|
777
|
+
if (!response.ok) continue;
|
|
778
|
+
arrayBuffer = await response.arrayBuffer();
|
|
854
779
|
} else {
|
|
855
|
-
|
|
856
|
-
const fullUrl = basePath
|
|
857
|
-
? `${basePath}/${url}`.replace(/\/+/g, "/")
|
|
858
|
-
: url;
|
|
780
|
+
const fullUrl = constructUrl(url, basePath, pagePath);
|
|
859
781
|
const response = await fetch(fullUrl);
|
|
860
|
-
if (response.ok) {
|
|
861
|
-
arrayBuffer = await response.arrayBuffer();
|
|
862
|
-
} else {
|
|
782
|
+
if (!response.ok) {
|
|
863
783
|
console.warn(`Failed to load binary file: ${url} at ${fullUrl}`);
|
|
864
784
|
continue;
|
|
865
785
|
}
|
|
786
|
+
arrayBuffer = await response.arrayBuffer();
|
|
866
787
|
}
|
|
867
788
|
|
|
868
|
-
const path = dest.startsWith(
|
|
789
|
+
const path = dest.startsWith('/') ? dest.substring(1) : dest;
|
|
869
790
|
zipFiles[path] = new Uint8Array(arrayBuffer);
|
|
870
791
|
} catch (error) {
|
|
871
792
|
console.warn(`Error loading binary file ${url}:`, error);
|
|
872
793
|
}
|
|
873
794
|
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Add referenced assets to ZIP
|
|
799
|
+
* @param {Object} zipFiles - ZIP files object
|
|
800
|
+
* @param {string} code - Typst source code
|
|
801
|
+
* @param {string} basePath - Base path
|
|
802
|
+
* @param {string} pagePath - Page path
|
|
803
|
+
* @returns {Promise<void>}
|
|
804
|
+
*/
|
|
805
|
+
async addAssets(zipFiles, code, basePath, pagePath) {
|
|
806
|
+
const relPaths = this.assetManager.extractFilePaths(code);
|
|
874
807
|
|
|
875
|
-
const relPaths = extractRelFilePaths(code);
|
|
876
808
|
for (const relPath of relPaths) {
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
? relPath.substring(1)
|
|
809
|
+
const normalizedPath = relPath.startsWith('/')
|
|
810
|
+
? relPath.substring(1)
|
|
880
811
|
: relPath;
|
|
812
|
+
|
|
813
|
+
// Skip if already in zipFiles
|
|
881
814
|
if (zipFiles[normalizedPath]) continue;
|
|
882
815
|
|
|
883
|
-
// Skip absolute URLs
|
|
884
|
-
if (
|
|
816
|
+
// Skip absolute URLs
|
|
817
|
+
if (REGEX_PATTERNS.ABSOLUTE_URL.test(relPath)) continue;
|
|
885
818
|
|
|
886
819
|
try {
|
|
887
|
-
|
|
888
|
-
const url = basePath
|
|
889
|
-
? `${basePath}/${relPath}`.replace(/\/+/g, "/")
|
|
890
|
-
: imagePath;
|
|
820
|
+
const url = constructUrl(relPath, basePath, pagePath);
|
|
891
821
|
const response = await fetch(url);
|
|
822
|
+
|
|
892
823
|
if (response.ok) {
|
|
893
824
|
const arrayBuffer = await response.arrayBuffer();
|
|
894
825
|
zipFiles[normalizedPath] = new Uint8Array(arrayBuffer);
|
|
@@ -899,28 +830,578 @@ hyperbook.typst = (function () {
|
|
|
899
830
|
console.warn(`Error loading asset ${relPath}:`, error);
|
|
900
831
|
}
|
|
901
832
|
}
|
|
833
|
+
}
|
|
902
834
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
835
|
+
/**
|
|
836
|
+
* Create and download ZIP file
|
|
837
|
+
* @param {Object} zipFiles - ZIP files object
|
|
838
|
+
* @param {string} id - Project ID
|
|
839
|
+
*/
|
|
840
|
+
downloadZip(zipFiles, id) {
|
|
841
|
+
if (typeof window.UZIP === 'undefined') {
|
|
842
|
+
throw new Error('UZIP library not loaded');
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const zipData = window.UZIP.encode(zipFiles);
|
|
846
|
+
const zipBlob = new Blob([zipData], { type: 'application/zip' });
|
|
847
|
+
const link = document.createElement('a');
|
|
907
848
|
link.href = URL.createObjectURL(zipBlob);
|
|
908
849
|
link.download = `typst-project-${id}.zip`;
|
|
909
850
|
link.click();
|
|
910
851
|
URL.revokeObjectURL(link.href);
|
|
911
|
-
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// ============================================================================
|
|
856
|
+
// UI MANAGER
|
|
857
|
+
// ============================================================================
|
|
858
|
+
|
|
859
|
+
class UIManager {
|
|
860
|
+
constructor(elem, fileManager, binaryFiles) {
|
|
861
|
+
this.elem = elem;
|
|
862
|
+
this.fileManager = fileManager;
|
|
863
|
+
this.binaryFiles = binaryFiles;
|
|
864
|
+
this.tabsList = elem.querySelector('.tabs-list');
|
|
865
|
+
this.binaryFilesList = elem.querySelector('.binary-files-list');
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Update tabs UI
|
|
870
|
+
*/
|
|
871
|
+
updateTabs() {
|
|
872
|
+
if (!this.tabsList) return;
|
|
873
|
+
|
|
874
|
+
this.tabsList.innerHTML = '';
|
|
875
|
+
|
|
876
|
+
this.fileManager.sourceFiles.forEach((file) => {
|
|
877
|
+
const tab = this.createTab(file);
|
|
878
|
+
this.tabsList.appendChild(tab);
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Create tab element for file
|
|
884
|
+
* @param {Object} file - File object
|
|
885
|
+
* @returns {HTMLElement} Tab element
|
|
886
|
+
*/
|
|
887
|
+
createTab(file) {
|
|
888
|
+
const tab = document.createElement('div');
|
|
889
|
+
tab.className = 'file-tab';
|
|
890
|
+
|
|
891
|
+
if (file.filename === this.fileManager.currentFile.filename) {
|
|
892
|
+
tab.classList.add('active');
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// Tab name
|
|
896
|
+
const tabName = document.createElement('span');
|
|
897
|
+
tabName.className = 'tab-name';
|
|
898
|
+
tabName.textContent = file.filename;
|
|
899
|
+
tab.appendChild(tabName);
|
|
900
|
+
|
|
901
|
+
// Delete button (except for main file)
|
|
902
|
+
if (file.filename !== 'main.typ' && file.filename !== 'main.typst') {
|
|
903
|
+
const deleteBtn = this.createDeleteButton(file);
|
|
904
|
+
tab.appendChild(deleteBtn);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Click handler
|
|
908
|
+
tab.addEventListener('click', () => this.handleTabClick(file));
|
|
909
|
+
|
|
910
|
+
return tab;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Create delete button for tab
|
|
915
|
+
* @param {Object} file - File object
|
|
916
|
+
* @returns {HTMLElement} Delete button
|
|
917
|
+
*/
|
|
918
|
+
createDeleteButton(file) {
|
|
919
|
+
const deleteBtn = document.createElement('button');
|
|
920
|
+
deleteBtn.className = 'tab-delete';
|
|
921
|
+
deleteBtn.textContent = '×';
|
|
922
|
+
deleteBtn.title = i18nGet('typst-delete-file', 'Delete file');
|
|
923
|
+
|
|
924
|
+
deleteBtn.addEventListener('click', (e) => {
|
|
925
|
+
e.stopPropagation();
|
|
926
|
+
this.handleDeleteFile(file.filename);
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
return deleteBtn;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Handle tab click
|
|
934
|
+
* @param {Object} file - File object
|
|
935
|
+
*/
|
|
936
|
+
handleTabClick(file) {
|
|
937
|
+
if (this.fileManager.currentFile.filename !== file.filename) {
|
|
938
|
+
this.onFileSwitch?.(file.filename);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Handle file deletion
|
|
944
|
+
* @param {string} filename - Filename to delete
|
|
945
|
+
*/
|
|
946
|
+
handleDeleteFile(filename) {
|
|
947
|
+
const confirmMsg = `${i18nGet('typst-delete-confirm', 'Delete')} ${filename}?`;
|
|
948
|
+
|
|
949
|
+
if (confirm(confirmMsg)) {
|
|
950
|
+
this.fileManager.deleteFile(filename);
|
|
951
|
+
this.updateTabs();
|
|
952
|
+
this.onFilesChange?.();
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* Update binary files list UI
|
|
958
|
+
*/
|
|
959
|
+
updateBinaryFilesList() {
|
|
960
|
+
if (!this.binaryFilesList) return;
|
|
961
|
+
|
|
962
|
+
this.binaryFilesList.innerHTML = '';
|
|
963
|
+
|
|
964
|
+
if (this.binaryFiles.length === 0) {
|
|
965
|
+
const emptyMsg = document.createElement('div');
|
|
966
|
+
emptyMsg.className = 'binary-files-empty';
|
|
967
|
+
emptyMsg.textContent = i18nGet('typst-no-binary-files', 'No binary files');
|
|
968
|
+
this.binaryFilesList.appendChild(emptyMsg);
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
this.binaryFiles.forEach((file) => {
|
|
973
|
+
const item = this.createBinaryFileItem(file);
|
|
974
|
+
this.binaryFilesList.appendChild(item);
|
|
975
|
+
});
|
|
976
|
+
}
|
|
912
977
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
978
|
+
/**
|
|
979
|
+
* Create binary file list item
|
|
980
|
+
* @param {Object} file - Binary file object
|
|
981
|
+
* @returns {HTMLElement} List item
|
|
982
|
+
*/
|
|
983
|
+
createBinaryFileItem(file) {
|
|
984
|
+
const item = document.createElement('div');
|
|
985
|
+
item.className = 'binary-file-item';
|
|
986
|
+
|
|
987
|
+
const icon = document.createElement('span');
|
|
988
|
+
icon.className = 'binary-file-icon';
|
|
989
|
+
icon.textContent = '📎';
|
|
990
|
+
item.appendChild(icon);
|
|
991
|
+
|
|
992
|
+
const name = document.createElement('span');
|
|
993
|
+
name.className = 'binary-file-name';
|
|
994
|
+
name.textContent = file.dest;
|
|
995
|
+
item.appendChild(name);
|
|
996
|
+
|
|
997
|
+
const deleteBtn = document.createElement('button');
|
|
998
|
+
deleteBtn.className = 'binary-file-delete';
|
|
999
|
+
deleteBtn.textContent = '×';
|
|
1000
|
+
deleteBtn.title = i18nGet('typst-delete-file', 'Delete file');
|
|
1001
|
+
deleteBtn.addEventListener('click', () => this.handleDeleteBinaryFile(file.dest));
|
|
1002
|
+
item.appendChild(deleteBtn);
|
|
1003
|
+
|
|
1004
|
+
return item;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Handle binary file deletion
|
|
1009
|
+
* @param {string} dest - File destination path
|
|
1010
|
+
*/
|
|
1011
|
+
handleDeleteBinaryFile(dest) {
|
|
1012
|
+
const confirmMsg = `${i18nGet('typst-delete-confirm', 'Delete')} ${dest}?`;
|
|
1013
|
+
|
|
1014
|
+
if (confirm(confirmMsg)) {
|
|
1015
|
+
this.binaryFiles = this.binaryFiles.filter((f) => f.dest !== dest);
|
|
1016
|
+
this.updateBinaryFilesList();
|
|
1017
|
+
this.onBinaryFilesChange?.(this.binaryFiles);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// ============================================================================
|
|
1023
|
+
// TYPST EDITOR
|
|
1024
|
+
// ============================================================================
|
|
1025
|
+
|
|
1026
|
+
class TypstEditor {
|
|
1027
|
+
constructor({
|
|
1028
|
+
elem,
|
|
1029
|
+
id,
|
|
1030
|
+
sourceFiles,
|
|
1031
|
+
binaryFiles,
|
|
1032
|
+
fontFiles,
|
|
1033
|
+
basePath,
|
|
1034
|
+
pagePath,
|
|
1035
|
+
renderer,
|
|
1036
|
+
exporter,
|
|
1037
|
+
}) {
|
|
1038
|
+
this.elem = elem;
|
|
1039
|
+
this.id = id;
|
|
1040
|
+
this.fontFiles = fontFiles;
|
|
1041
|
+
this.basePath = normalizePath(basePath);
|
|
1042
|
+
this.pagePath = normalizePath(pagePath);
|
|
1043
|
+
this.renderer = renderer;
|
|
1044
|
+
this.exporter = exporter;
|
|
1045
|
+
|
|
1046
|
+
// Initialize managers
|
|
1047
|
+
this.fileManager = new FileManager(sourceFiles);
|
|
1048
|
+
this.binaryFiles = binaryFiles;
|
|
1049
|
+
this.uiManager = new UIManager(elem, this.fileManager, this.binaryFiles);
|
|
1050
|
+
|
|
1051
|
+
// Get DOM elements
|
|
1052
|
+
this.previewContainer = elem.querySelector('.preview-container');
|
|
1053
|
+
this.preview = elem.querySelector('.typst-preview');
|
|
1054
|
+
this.loadingIndicator = elem.querySelector('.typst-loading');
|
|
1055
|
+
this.editor = elem.querySelector('.editor.typst');
|
|
1056
|
+
this.sourceTextarea = elem.querySelector('.typst-source');
|
|
1057
|
+
|
|
1058
|
+
// Setup UI callbacks
|
|
1059
|
+
this.setupUICallbacks();
|
|
1060
|
+
|
|
1061
|
+
// Setup event handlers
|
|
1062
|
+
this.setupEventHandlers();
|
|
1063
|
+
|
|
1064
|
+
// Initialize
|
|
1065
|
+
this.initialize();
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* Setup UI manager callbacks
|
|
1070
|
+
*/
|
|
1071
|
+
setupUICallbacks() {
|
|
1072
|
+
this.uiManager.onFileSwitch = (filename) => this.handleFileSwitch(filename);
|
|
1073
|
+
this.uiManager.onFilesChange = () => this.handleFilesChange();
|
|
1074
|
+
this.uiManager.onBinaryFilesChange = (binaryFiles) => this.handleBinaryFilesChange(binaryFiles);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/**
|
|
1078
|
+
* Setup event handlers
|
|
1079
|
+
*/
|
|
1080
|
+
setupEventHandlers() {
|
|
1081
|
+
const downloadBtn = this.elem.querySelector('.download-pdf');
|
|
1082
|
+
const downloadProjectBtn = this.elem.querySelector('.download-project');
|
|
1083
|
+
const resetBtn = this.elem.querySelector('.reset');
|
|
1084
|
+
const addSourceFileBtn = this.elem.querySelector('.add-source-file');
|
|
1085
|
+
const addBinaryFileBtn = this.elem.querySelector('.add-binary-file');
|
|
1086
|
+
|
|
1087
|
+
downloadBtn?.addEventListener('click', () => this.handleExportPdf());
|
|
1088
|
+
downloadProjectBtn?.addEventListener('click', () => this.handleExportProject());
|
|
1089
|
+
resetBtn?.addEventListener('click', () => this.handleReset());
|
|
1090
|
+
addSourceFileBtn?.addEventListener('click', () => this.handleAddSourceFile());
|
|
1091
|
+
addBinaryFileBtn?.addEventListener('click', (e) => this.handleAddBinaryFile(e));
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Initialize editor
|
|
1096
|
+
*/
|
|
1097
|
+
async initialize() {
|
|
1098
|
+
if (this.editor) {
|
|
1099
|
+
// Edit mode - wait for code-input to load
|
|
1100
|
+
this.editor.addEventListener('code-input_load', async () => {
|
|
1101
|
+
await this.restoreState();
|
|
1102
|
+
this.uiManager.updateTabs();
|
|
1103
|
+
this.uiManager.updateBinaryFilesList();
|
|
1104
|
+
this.rerender();
|
|
1105
|
+
|
|
1106
|
+
// Create debounced rerender for input events
|
|
1107
|
+
const debouncedRerender = debounce(() => this.rerender(), CONFIG.DEBOUNCE_DELAY);
|
|
1108
|
+
|
|
1109
|
+
this.editor.addEventListener('input', () => {
|
|
1110
|
+
this.saveState();
|
|
1111
|
+
debouncedRerender();
|
|
1112
|
+
});
|
|
1113
|
+
});
|
|
1114
|
+
} else if (this.sourceTextarea) {
|
|
1115
|
+
// Preview mode
|
|
1116
|
+
const initialCode = this.sourceTextarea.value;
|
|
1117
|
+
await this.renderer.render({
|
|
1118
|
+
code: initialCode,
|
|
1119
|
+
container: this.preview,
|
|
1120
|
+
loadingIndicator: this.loadingIndicator,
|
|
1121
|
+
sourceFiles: this.fileManager.sourceFiles,
|
|
1122
|
+
binaryFiles: this.binaryFiles,
|
|
1123
|
+
fontFiles: this.fontFiles,
|
|
1124
|
+
id: this.id,
|
|
1125
|
+
previewContainer: this.previewContainer,
|
|
1126
|
+
basePath: this.basePath,
|
|
1127
|
+
pagePath: this.pagePath,
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* Restore state from storage
|
|
1134
|
+
*/
|
|
1135
|
+
async restoreState() {
|
|
1136
|
+
const result = await window.store?.typst?.get(this.id);
|
|
1137
|
+
|
|
1138
|
+
if (result) {
|
|
1139
|
+
this.editor.value = result.code;
|
|
1140
|
+
|
|
1141
|
+
if (result.sourceFiles) {
|
|
1142
|
+
this.fileManager.sourceFiles = result.sourceFiles;
|
|
1143
|
+
result.sourceFiles.forEach((f) =>
|
|
1144
|
+
this.fileManager.contents.set(f.filename, f.content)
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
if (result.binaryFiles) {
|
|
1149
|
+
this.binaryFiles = result.binaryFiles;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
if (result.currentFile) {
|
|
1153
|
+
const file = this.fileManager.sourceFiles.find(
|
|
1154
|
+
(f) => f.filename === result.currentFile
|
|
1155
|
+
);
|
|
1156
|
+
if (file) {
|
|
1157
|
+
this.fileManager.currentFile = file;
|
|
1158
|
+
this.editor.value = this.fileManager.getCurrentContent();
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
} else {
|
|
1162
|
+
this.editor.value = this.fileManager.getCurrentContent();
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
/**
|
|
1167
|
+
* Save state to storage
|
|
1168
|
+
*/
|
|
1169
|
+
async saveState() {
|
|
1170
|
+
if (!this.editor) return;
|
|
1171
|
+
|
|
1172
|
+
this.fileManager.updateCurrentContent(this.editor.value);
|
|
1173
|
+
|
|
1174
|
+
await window.store?.typst?.put({
|
|
1175
|
+
id: this.id,
|
|
1176
|
+
code: this.editor.value,
|
|
1177
|
+
sourceFiles: this.fileManager.getSourceFiles(),
|
|
1178
|
+
binaryFiles: this.binaryFiles,
|
|
1179
|
+
currentFile: this.fileManager.currentFile.filename,
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
/**
|
|
1184
|
+
* Rerender Typst preview
|
|
1185
|
+
*/
|
|
1186
|
+
rerender() {
|
|
1187
|
+
if (!this.editor) return;
|
|
1188
|
+
|
|
1189
|
+
this.fileManager.updateCurrentContent(this.editor.value);
|
|
1190
|
+
|
|
1191
|
+
const mainFile = this.fileManager.findMainFile();
|
|
1192
|
+
const mainCode = mainFile ? mainFile.content : '';
|
|
1193
|
+
|
|
1194
|
+
this.renderer.render({
|
|
1195
|
+
code: mainCode,
|
|
1196
|
+
container: this.preview,
|
|
1197
|
+
loadingIndicator: this.loadingIndicator,
|
|
1198
|
+
sourceFiles: this.fileManager.getSourceFiles(),
|
|
1199
|
+
binaryFiles: this.binaryFiles,
|
|
1200
|
+
fontFiles: this.fontFiles,
|
|
1201
|
+
id: this.id,
|
|
1202
|
+
previewContainer: this.previewContainer,
|
|
1203
|
+
basePath: this.basePath,
|
|
1204
|
+
pagePath: this.pagePath,
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
/**
|
|
1209
|
+
* Handle file switch
|
|
1210
|
+
* @param {string} filename - Target filename
|
|
1211
|
+
*/
|
|
1212
|
+
handleFileSwitch(filename) {
|
|
1213
|
+
if (this.editor) {
|
|
1214
|
+
this.fileManager.updateCurrentContent(this.editor.value);
|
|
1215
|
+
const content = this.fileManager.switchTo(filename);
|
|
1216
|
+
this.editor.value = content;
|
|
1217
|
+
this.uiManager.updateTabs();
|
|
1218
|
+
this.saveState();
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* Handle files change
|
|
1224
|
+
*/
|
|
1225
|
+
handleFilesChange() {
|
|
1226
|
+
if (this.editor) {
|
|
1227
|
+
this.editor.value = this.fileManager.getCurrentContent();
|
|
1228
|
+
}
|
|
1229
|
+
this.saveState();
|
|
1230
|
+
this.rerender();
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* Handle binary files change
|
|
1235
|
+
* @param {Array} binaryFiles - Updated binary files array
|
|
1236
|
+
*/
|
|
1237
|
+
handleBinaryFilesChange(binaryFiles) {
|
|
1238
|
+
this.binaryFiles = binaryFiles;
|
|
1239
|
+
this.saveState();
|
|
1240
|
+
this.rerender();
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/**
|
|
1244
|
+
* Handle add source file
|
|
1245
|
+
*/
|
|
1246
|
+
handleAddSourceFile() {
|
|
1247
|
+
const filename = prompt(
|
|
1248
|
+
i18nGet('typst-filename-prompt', 'Enter filename (e.g., helper.typ):')
|
|
1249
|
+
);
|
|
1250
|
+
|
|
1251
|
+
if (!filename) return;
|
|
1252
|
+
|
|
1253
|
+
// Validate filename
|
|
1254
|
+
if (!filename.endsWith('.typ') && !filename.endsWith('.typst')) {
|
|
1255
|
+
alert(i18nGet('typst-filename-error', 'Filename must end with .typ or .typst'));
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
if (!this.fileManager.addFile(filename)) {
|
|
1260
|
+
alert(i18nGet('typst-filename-exists', 'File already exists'));
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
if (this.editor) {
|
|
1265
|
+
this.editor.value = this.fileManager.getCurrentContent();
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
this.uiManager.updateTabs();
|
|
1269
|
+
this.saveState();
|
|
1270
|
+
this.rerender();
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/**
|
|
1274
|
+
* Handle add binary file
|
|
1275
|
+
* @param {Event} e - Click event
|
|
1276
|
+
*/
|
|
1277
|
+
handleAddBinaryFile(e) {
|
|
1278
|
+
e.preventDefault();
|
|
1279
|
+
e.stopPropagation();
|
|
1280
|
+
|
|
1281
|
+
const input = document.createElement('input');
|
|
1282
|
+
input.type = 'file';
|
|
1283
|
+
input.accept = 'image/*,.pdf';
|
|
1284
|
+
|
|
1285
|
+
input.addEventListener('change', async (e) => {
|
|
1286
|
+
const file = e.target.files[0];
|
|
1287
|
+
if (!file) return;
|
|
1288
|
+
|
|
1289
|
+
const dest = `/${file.name}`;
|
|
1290
|
+
|
|
1291
|
+
// Check if file already exists
|
|
1292
|
+
if (this.binaryFiles.some((f) => f.dest === dest)) {
|
|
1293
|
+
if (!confirm(i18nGet('typst-file-replace', `Replace existing ${dest}?`))) {
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
this.binaryFiles = this.binaryFiles.filter((f) => f.dest !== dest);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// Read file as data URL
|
|
1300
|
+
const reader = new FileReader();
|
|
1301
|
+
reader.onload = async (e) => {
|
|
1302
|
+
const url = e.target.result;
|
|
1303
|
+
this.binaryFiles.push({ dest, url });
|
|
1304
|
+
this.uiManager.updateBinaryFilesList();
|
|
1305
|
+
this.saveState();
|
|
1306
|
+
this.rerender();
|
|
1307
|
+
};
|
|
1308
|
+
reader.readAsDataURL(file);
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
input.click();
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
/**
|
|
1315
|
+
* Handle PDF export
|
|
1316
|
+
*/
|
|
1317
|
+
async handleExportPdf() {
|
|
1318
|
+
const mainFile = this.fileManager.findMainFile();
|
|
1319
|
+
const code = mainFile ? mainFile.content : (this.editor ? this.editor.value : '');
|
|
1320
|
+
|
|
1321
|
+
await this.renderer.exportPdf({
|
|
1322
|
+
code,
|
|
1323
|
+
id: this.id,
|
|
1324
|
+
sourceFiles: this.fileManager.getSourceFiles(),
|
|
1325
|
+
binaryFiles: this.binaryFiles,
|
|
1326
|
+
fontFiles: this.fontFiles,
|
|
1327
|
+
basePath: this.basePath,
|
|
1328
|
+
pagePath: this.pagePath,
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
/**
|
|
1333
|
+
* Handle project export
|
|
1334
|
+
*/
|
|
1335
|
+
async handleExportProject() {
|
|
1336
|
+
const mainFile = this.fileManager.findMainFile();
|
|
1337
|
+
const code = mainFile ? mainFile.content : (this.editor ? this.editor.value : '');
|
|
1338
|
+
|
|
1339
|
+
await this.exporter.export({
|
|
1340
|
+
code,
|
|
1341
|
+
id: this.id,
|
|
1342
|
+
sourceFiles: this.fileManager.getSourceFiles(),
|
|
1343
|
+
binaryFiles: this.binaryFiles,
|
|
1344
|
+
basePath: this.basePath,
|
|
1345
|
+
pagePath: this.pagePath,
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
/**
|
|
1350
|
+
* Handle reset
|
|
1351
|
+
*/
|
|
1352
|
+
async handleReset() {
|
|
1353
|
+
const confirmMsg = i18nGet(
|
|
1354
|
+
'typst-reset-prompt',
|
|
1355
|
+
'Are you sure you want to reset the code?'
|
|
1356
|
+
);
|
|
1357
|
+
|
|
1358
|
+
if (confirm(confirmMsg)) {
|
|
1359
|
+
await window.store?.typst?.delete(this.id);
|
|
922
1360
|
window.location.reload();
|
|
923
1361
|
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// ============================================================================
|
|
1366
|
+
// MAIN INITIALIZATION
|
|
1367
|
+
// ============================================================================
|
|
1368
|
+
|
|
1369
|
+
// Initialize code-input
|
|
1370
|
+
initializeCodeInput();
|
|
1371
|
+
|
|
1372
|
+
// Get all Typst directive elements
|
|
1373
|
+
const elements = document.getElementsByClassName('directive-typst');
|
|
1374
|
+
|
|
1375
|
+
// Create shared instances
|
|
1376
|
+
const typstLoader = new TypstLoader();
|
|
1377
|
+
const renderQueue = new RenderQueue();
|
|
1378
|
+
const assetManager = new AssetManager();
|
|
1379
|
+
const renderer = new TypstRenderer(typstLoader, assetManager, renderQueue);
|
|
1380
|
+
const exporter = new ProjectExporter(assetManager);
|
|
1381
|
+
|
|
1382
|
+
// Initialize each Typst element
|
|
1383
|
+
for (const elem of elements) {
|
|
1384
|
+
const id = elem.getAttribute('data-id');
|
|
1385
|
+
const sourceFilesData = elem.getAttribute('data-source-files');
|
|
1386
|
+
const binaryFilesData = elem.getAttribute('data-binary-files');
|
|
1387
|
+
const fontFilesData = elem.getAttribute('data-font-files');
|
|
1388
|
+
const basePath = elem.getAttribute('data-base-path') || '';
|
|
1389
|
+
const pagePath = elem.getAttribute('data-page-path') || '';
|
|
1390
|
+
|
|
1391
|
+
const sourceFiles = sourceFilesData ? JSON.parse(atob(sourceFilesData)) : [];
|
|
1392
|
+
const binaryFiles = binaryFilesData ? JSON.parse(atob(binaryFilesData)) : [];
|
|
1393
|
+
const fontFiles = fontFilesData ? JSON.parse(atob(fontFilesData)) : [];
|
|
1394
|
+
|
|
1395
|
+
new TypstEditor({
|
|
1396
|
+
elem,
|
|
1397
|
+
id,
|
|
1398
|
+
sourceFiles,
|
|
1399
|
+
binaryFiles,
|
|
1400
|
+
fontFiles,
|
|
1401
|
+
basePath,
|
|
1402
|
+
pagePath,
|
|
1403
|
+
renderer,
|
|
1404
|
+
exporter,
|
|
924
1405
|
});
|
|
925
1406
|
}
|
|
926
1407
|
|