@lofcz/tikz 0.0.0-stage → 0.2.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.
Files changed (118) hide show
  1. package/LICENSE +674 -0
  2. package/NOTICE.md +30 -0
  3. package/README.md +127 -2
  4. package/package.json +56 -4
  5. package/runtime/LICENSE +165 -0
  6. package/runtime/NOTICE.md +32 -0
  7. package/runtime/dist/auto.d.ts +68 -0
  8. package/runtime/dist/auto.js +564 -0
  9. package/runtime/dist/bibtex.mjs +2 -0
  10. package/runtime/dist/bibtex.wasm +0 -0
  11. package/runtime/dist/bundles/cm-tfm/files.br +0 -0
  12. package/runtime/dist/bundles/cm-tfm/files.index.json +1 -0
  13. package/runtime/dist/bundles/cm-tfm/manifest.json +1 -0
  14. package/runtime/dist/bundles/cm-type1/files.br +0 -0
  15. package/runtime/dist/bundles/cm-type1/files.index.json +1 -0
  16. package/runtime/dist/bundles/cm-type1/manifest.json +1 -0
  17. package/runtime/dist/bundles/core/files.br +0 -0
  18. package/runtime/dist/bundles/core/files.index.json +1 -0
  19. package/runtime/dist/bundles/core/manifest.json +1 -0
  20. package/runtime/dist/bundles/hot.json +1 -0
  21. package/runtime/dist/bundles/index.json +80 -0
  22. package/runtime/dist/bundles/latex-core/files.br +0 -0
  23. package/runtime/dist/bundles/latex-core/files.index.json +1 -0
  24. package/runtime/dist/bundles/latex-core/manifest.json +1 -0
  25. package/runtime/dist/bundles/latex-extra/files.br +0 -0
  26. package/runtime/dist/bundles/latex-extra/files.index.json +1 -0
  27. package/runtime/dist/bundles/latex-extra/manifest.json +1 -0
  28. package/runtime/dist/bundles/lm-fonts/files.br +0 -0
  29. package/runtime/dist/bundles/lm-fonts/files.index.json +1 -0
  30. package/runtime/dist/bundles/lm-fonts/manifest.json +1 -0
  31. package/runtime/dist/bundles/luatex/files.br +0 -0
  32. package/runtime/dist/bundles/luatex/files.index.json +1 -0
  33. package/runtime/dist/bundles/luatex/manifest.json +1 -0
  34. package/runtime/dist/bundles/ps-fonts/files.br +0 -0
  35. package/runtime/dist/bundles/ps-fonts/files.index.json +1 -0
  36. package/runtime/dist/bundles/ps-fonts/manifest.json +1 -0
  37. package/runtime/dist/bundles/tex-plain/files.br +0 -0
  38. package/runtime/dist/bundles/tex-plain/files.index.json +1 -0
  39. package/runtime/dist/bundles/tex-plain/manifest.json +1 -0
  40. package/runtime/dist/bundles/tikz-snapshot/files.br +0 -0
  41. package/runtime/dist/bundles/tikz-snapshot/files.index.json +1 -0
  42. package/runtime/dist/bundles/tikz-snapshot/manifest.json +1 -0
  43. package/runtime/dist/bundles-config.d.ts +3 -0
  44. package/runtime/dist/bundles-config.js +16 -0
  45. package/runtime/dist/cli.d.ts +2 -0
  46. package/runtime/dist/cli.js +372 -0
  47. package/runtime/dist/core.d.ts +124 -0
  48. package/runtime/dist/core.js +927 -0
  49. package/runtime/dist/diagnostics.d.ts +53 -0
  50. package/runtime/dist/diagnostics.js +184 -0
  51. package/runtime/dist/dvisvgm.d.ts +20 -0
  52. package/runtime/dist/dvisvgm.js +46 -0
  53. package/runtime/dist/dvisvgm.mjs +2 -0
  54. package/runtime/dist/dvisvgm.wasm +0 -0
  55. package/runtime/dist/figures.d.ts +92 -0
  56. package/runtime/dist/figures.js +250 -0
  57. package/runtime/dist/ghostscript.d.ts +92 -0
  58. package/runtime/dist/ghostscript.js +218 -0
  59. package/runtime/dist/index.d.ts +87 -0
  60. package/runtime/dist/index.js +364 -0
  61. package/runtime/dist/logger.d.ts +48 -0
  62. package/runtime/dist/logger.js +55 -0
  63. package/runtime/dist/luatex.mjs +2 -0
  64. package/runtime/dist/luatex.wasm +0 -0
  65. package/runtime/dist/makeindex.mjs +2 -0
  66. package/runtime/dist/makeindex.wasm +0 -0
  67. package/runtime/dist/mplib.d.ts +106 -0
  68. package/runtime/dist/mplib.js +85 -0
  69. package/runtime/dist/mplib.mjs +2 -0
  70. package/runtime/dist/mplib.wasm +0 -0
  71. package/runtime/dist/node.d.ts +4 -0
  72. package/runtime/dist/node.js +49 -0
  73. package/runtime/dist/postscript.d.ts +21 -0
  74. package/runtime/dist/postscript.js +130 -0
  75. package/runtime/dist/prerender.d.ts +55 -0
  76. package/runtime/dist/prerender.js +106 -0
  77. package/runtime/dist/render/svg.d.ts +20 -0
  78. package/runtime/dist/render/svg.js +96 -0
  79. package/runtime/dist/tex/bridge.d.ts +93 -0
  80. package/runtime/dist/tex/bridge.js +223 -0
  81. package/runtime/dist/tex/cache-key.d.ts +33 -0
  82. package/runtime/dist/tex/cache-key.js +150 -0
  83. package/runtime/dist/tex/mpto.d.ts +60 -0
  84. package/runtime/dist/tex/mpto.js +112 -0
  85. package/runtime/dist/tex/mpx.d.ts +25 -0
  86. package/runtime/dist/tex/mpx.js +54 -0
  87. package/runtime/dist/tex/scanner.d.ts +78 -0
  88. package/runtime/dist/tex/scanner.js +326 -0
  89. package/runtime/dist/tex.mjs +2 -0
  90. package/runtime/dist/tex.wasm +0 -0
  91. package/runtime/dist/texengine.d.ts +35 -0
  92. package/runtime/dist/texengine.js +57 -0
  93. package/runtime/dist/texutils.d.ts +19 -0
  94. package/runtime/dist/texutils.js +60 -0
  95. package/runtime/dist/types.d.ts +376 -0
  96. package/runtime/dist/types.js +6 -0
  97. package/runtime/dist/vfs/bundle.d.ts +88 -0
  98. package/runtime/dist/vfs/bundle.js +315 -0
  99. package/runtime/dist/vfs/lazyfs.d.ts +62 -0
  100. package/runtime/dist/vfs/lazyfs.js +133 -0
  101. package/runtime/dist/worker.d.ts +15 -0
  102. package/runtime/dist/worker.js +143 -0
  103. package/runtime/licenses/COPYING.AFPL +230 -0
  104. package/runtime/licenses/COPYING.makeindex +42 -0
  105. package/runtime/licenses/GPL-3.0.txt +674 -0
  106. package/runtime/licenses/LGPL-3.0.txt +165 -0
  107. package/runtime/licenses/OFL-1.1.txt +93 -0
  108. package/runtime/package.json +1 -0
  109. package/runtime/upstream.json +27 -0
  110. package/src/browser-worker.js +26 -0
  111. package/src/browser.d.ts +18 -0
  112. package/src/browser.js +68 -0
  113. package/src/deploy.d.ts +5 -0
  114. package/src/deploy.js +48 -0
  115. package/src/index.d.ts +28 -0
  116. package/src/index.js +103 -0
  117. package/src/worker.js +15 -0
  118. package/upstream.json +27 -0
@@ -0,0 +1,315 @@
1
+ import { createLazyFile, mkdirp } from './lazyfs.js';
2
+ import { silentLogger, plural } from '../logger.js';
3
+ export const TEXMF_ROOT = '/texmf';
4
+ const inWorker = typeof globalThis.WorkerGlobalScope !== 'undefined' && typeof globalThis.importScripts === 'function';
5
+ /** Inside a Worker, post a `fetching` progress event; the main thread's stall watchdog and status text listen for them. */
6
+ function fetchingEvent(name, current, total) {
7
+ if (!inWorker)
8
+ return;
9
+ try {
10
+ globalThis.postMessage({ event: 'progress', data: { phase: 'fetching', detail: name, current, total } });
11
+ }
12
+ catch { /* ignore */ }
13
+ }
14
+ function joinUrl(base, rel) {
15
+ if (!base.endsWith('/'))
16
+ base += '/';
17
+ return base + rel;
18
+ }
19
+ export class BundleSet {
20
+ io;
21
+ files = new Map(); // relative texmf path -> file
22
+ byName = new Map(); // basename -> files
23
+ manifests = [];
24
+ cache = new Map(); // in-memory copy of fetched files
25
+ negative = new Set();
26
+ /** The files a first run of each kind touches (`bundles/hot.json`, written by scripts/make-hotlists.mjs). */
27
+ hot = {};
28
+ /** Where fetches are reported: prefetch summaries at info, on-demand loads at debug, every file at trace. */
29
+ logger = silentLogger;
30
+ constructor(io) {
31
+ this.io = io;
32
+ }
33
+ /** Load a manifest; `spec.manifestUrl` is the bundle directory/URL (containing manifest.json). */
34
+ async add(spec) {
35
+ const base = spec.manifestUrl.replace(/\/manifest\.json$/, '');
36
+ const manifest = (await this.io.fetchJson(joinUrl(base, 'manifest.json')));
37
+ this.addLoaded(spec, base, manifest);
38
+ }
39
+ addLoaded(spec, base, manifest) {
40
+ this.manifests.push({ spec, manifest, base });
41
+ const blobBase = spec.blobBaseUrl ?? joinUrl(base, 'files');
42
+ for (const [path, info] of Object.entries(manifest.files)) {
43
+ if (this.files.has(path))
44
+ continue; // first bundle wins
45
+ // The file's hash in its URL: a rebuilt file gets a new URL, so an HTTP cache can
46
+ // never hand back a stale copy (a server that sends no Cache-Control lets the
47
+ // browser reuse files heuristically; a new font with an old font map then drew
48
+ // nothing), and an unchanged one may be cached for good.
49
+ const url = joinUrl(blobBase, path) + (info.sha ? `?v=${info.sha}` : '');
50
+ const f = { bundle: manifest.name, path, size: info.size, sha: info.sha, url };
51
+ this.files.set(path, f);
52
+ const name = path.slice(path.lastIndexOf('/') + 1);
53
+ const list = this.byName.get(name);
54
+ if (list)
55
+ list.push(f);
56
+ else
57
+ this.byName.set(name, [f]);
58
+ }
59
+ }
60
+ /** Add several bundles: the manifests are fetched in parallel, then merged in the given order (first bundle wins). */
61
+ async addAll(specs) {
62
+ const manifests = await Promise.all(specs.map(async (spec) => {
63
+ const base = spec.manifestUrl.replace(/\/manifest\.json$/, '');
64
+ return { spec, base, manifest: (await this.io.fetchJson(joinUrl(base, 'manifest.json'))) };
65
+ }));
66
+ for (const m of manifests)
67
+ this.addLoaded(m.spec, m.base, m.manifest);
68
+ }
69
+ /** Load the hot lists; optional, so a missing file is not an error. */
70
+ async loadHot(url) {
71
+ try {
72
+ const h = (await this.io.fetchJson(url));
73
+ if (h && h.kinds)
74
+ this.hot = h.kinds;
75
+ }
76
+ catch { /* no hot.json: prefetch() is a no-op */ }
77
+ }
78
+ /** Fetch, in parallel, every file the hot lists name for these kinds that is not in memory yet. Returns how many. */
79
+ async prefetchHot(kinds, concurrency = 16) {
80
+ const paths = new Set();
81
+ for (const k of kinds)
82
+ for (const p of this.hot[k] ?? [])
83
+ paths.add(p);
84
+ const wanted = [];
85
+ for (const p of paths) {
86
+ const f = this.files.get(p);
87
+ if (f && !this.cache.has(f.path))
88
+ wanted.push(f);
89
+ }
90
+ let i = 0, done = 0;
91
+ await Promise.all(Array.from({ length: Math.min(concurrency, wanted.length) }, async () => {
92
+ while (i < wanted.length) {
93
+ const f = wanted[i++];
94
+ try {
95
+ await this.fetchAsync(f);
96
+ }
97
+ catch (e) {
98
+ this.logger.warn('bundle', `prefetch ${f.path}: ${e?.message ?? e}`);
99
+ } // the run reports it too
100
+ fetchingEvent(f.path.slice(f.path.lastIndexOf('/') + 1), ++done, wanted.length);
101
+ }
102
+ }));
103
+ this.logger.info('bundle', `prefetch ${kinds.join(', ')}: ${wanted.length ? `${plural(wanted.length, 'file')} fetched` : 'nothing to fetch'}`);
104
+ return wanted.length;
105
+ }
106
+ /** Every manifest's `eager` list, fetched into memory. */
107
+ async prefetchEager() {
108
+ const wanted = [];
109
+ for (const { manifest } of this.manifests)
110
+ for (const p of manifest.eager ?? []) {
111
+ const f = this.files.get(p);
112
+ if (f)
113
+ wanted.push(f);
114
+ }
115
+ await Promise.all(wanted.map((f) => this.fetchAsync(f)));
116
+ }
117
+ /** Fetch every file in the set (for environments without sync I/O). */
118
+ async prefetchAll(filter) {
119
+ const wanted = [...this.files.values()].filter((f) => !this.cache.has(f.path) && (!filter || filter(f)));
120
+ // modest concurrency
121
+ let i = 0;
122
+ const workers = Array.from({ length: 8 }, async () => {
123
+ while (i < wanted.length) {
124
+ const f = wanted[i++];
125
+ await this.fetchAsync(f);
126
+ }
127
+ });
128
+ await Promise.all(workers);
129
+ }
130
+ async fetchAsync(f) {
131
+ const hit = this.cache.get(f.path);
132
+ if (hit)
133
+ return hit;
134
+ const data = await this.io.fetch(f.url);
135
+ this.cache.set(f.path, data);
136
+ this.logger.trace('bundle', `fetched ${f.path} (${data.length} bytes)`);
137
+ return data;
138
+ }
139
+ /** Synchronous bytes for a file: from memory, else via fetchSync. Throws if impossible. */
140
+ fetchSyncFile(f) {
141
+ const hit = this.cache.get(f.path);
142
+ if (hit)
143
+ return hit;
144
+ if (!this.io.fetchSync)
145
+ throw new Error(`mp-tikz-wasm: ${f.path} was not prefetched and this environment cannot load files synchronously (run in a Worker, or call preload())`);
146
+ let data;
147
+ try {
148
+ data = this.io.fetchSync(f.url);
149
+ }
150
+ catch (e) {
151
+ this.logger.error('bundle', `${f.path}: ${e?.message ?? e}`);
152
+ throw e;
153
+ }
154
+ this.cache.set(f.path, data);
155
+ this.logger.debug('bundle', `loaded ${f.path} (${data.length} bytes)`); // a sync XHR in the Worker, a disk read in Node
156
+ return data;
157
+ }
158
+ get canFetchSync() { return !!this.io.fetchSync; }
159
+ has(path) { return this.cache.has(path); }
160
+ /**
161
+ * Materialise the tree in the Emscripten FS under /texmf, plus an ls-R
162
+ * database so kpathsea never has to walk directories. Directories are
163
+ * created on demand: the root gets a lookup hook that creates a child
164
+ * directory or a lazy file the first time anything asks for it, so a run
165
+ * pays for the handful of files it touches, not for the whole tree
166
+ * (installing every node eagerly cost ~60 ms per TeX run).
167
+ */
168
+ install(FS, root = TEXMF_ROOT) {
169
+ mkdirp(FS, root);
170
+ const index = this.index();
171
+ const lazyDir = (dirPath, rel) => {
172
+ const node = FS.lookupPath(dirPath).node;
173
+ const origOps = node.node_ops;
174
+ const children = index.dirs.get(rel);
175
+ const materialised = new Set();
176
+ const create = (name) => {
177
+ if (materialised.has(name))
178
+ return false;
179
+ materialised.add(name);
180
+ const childRel = rel ? `${rel}/${name}` : name;
181
+ const full = `${dirPath}/${name}`;
182
+ if (index.dirs.has(childRel)) {
183
+ FS.mkdir(full);
184
+ lazyDir(full, childRel);
185
+ return true;
186
+ }
187
+ const f = this.files.get(childRel);
188
+ if (!f)
189
+ return false;
190
+ const data = this.cache.get(childRel);
191
+ if (data)
192
+ FS.writeFile(full, data);
193
+ else
194
+ createLazyFile(FS, full, f.size, () => this.fetchSyncFile(f));
195
+ return true;
196
+ };
197
+ node.node_ops = {
198
+ ...origOps,
199
+ lookup: (parent, name) => {
200
+ if (children?.has(name) && create(name))
201
+ return FS.lookupNode(parent, name);
202
+ return origOps.lookup(parent, name);
203
+ },
204
+ readdir: (n) => { if (children)
205
+ for (const name of children)
206
+ create(name); return origOps.readdir(n); },
207
+ };
208
+ };
209
+ lazyDir(root, '');
210
+ FS.writeFile(`${root}/ls-R`, this.lsRText ??= this.lsR());
211
+ }
212
+ lsRText;
213
+ indexCache;
214
+ /** directory -> immediate children, for the on-demand tree */
215
+ index() {
216
+ if (this.indexCache)
217
+ return this.indexCache;
218
+ const dirs = new Map();
219
+ const ensure = (d) => { let s = dirs.get(d); if (!s) {
220
+ s = new Set();
221
+ dirs.set(d, s);
222
+ } return s; };
223
+ ensure('');
224
+ for (const path of this.files.keys()) {
225
+ const parts = path.split('/');
226
+ let d = '';
227
+ for (let i = 0; i < parts.length - 1; i++) {
228
+ ensure(d).add(parts[i]);
229
+ d = d ? `${d}/${parts[i]}` : parts[i];
230
+ ensure(d);
231
+ }
232
+ ensure(d).add(parts[parts.length - 1]);
233
+ }
234
+ for (const { manifest } of this.manifests)
235
+ for (const d of manifest.dirs ?? []) {
236
+ const parts = d.split('/');
237
+ let p = '';
238
+ for (const x of parts) {
239
+ ensure(p).add(x);
240
+ p = p ? `${p}/${x}` : x;
241
+ ensure(p);
242
+ }
243
+ }
244
+ return (this.indexCache = { dirs });
245
+ }
246
+ /** kpathsea ls-R database text for the merged tree. */
247
+ lsR() {
248
+ const dirs = new Map();
249
+ const ensure = (d) => { let l = dirs.get(d); if (!l) {
250
+ l = [];
251
+ dirs.set(d, l);
252
+ } return l; };
253
+ ensure('.');
254
+ for (const path of this.files.keys()) {
255
+ const parts = path.split('/');
256
+ let d = '.';
257
+ for (let i = 0; i < parts.length - 1; i++) {
258
+ const sub = parts[i];
259
+ const list = ensure(d);
260
+ if (!list.includes(sub))
261
+ list.push(sub);
262
+ d = d + '/' + sub;
263
+ ensure(d);
264
+ }
265
+ ensure(d).push(parts[parts.length - 1]);
266
+ }
267
+ let out = '% ls-R -- filename database for kpathsea; do not change this line.\n';
268
+ for (const d of [...dirs.keys()].sort()) {
269
+ out += `\n${d}:\n`;
270
+ for (const e of dirs.get(d).sort())
271
+ out += e + '\n';
272
+ }
273
+ return out;
274
+ }
275
+ /** Find bundle files by bare name (e.g. "cmr10.tfm"), optionally restricted to a directory prefix. */
276
+ lookup(name, prefixes) {
277
+ const list = this.byName.get(name);
278
+ if (!list)
279
+ return undefined;
280
+ if (!prefixes)
281
+ return list[0];
282
+ for (const p of prefixes) {
283
+ const f = list.find((x) => x.path.startsWith(p));
284
+ if (f)
285
+ return f;
286
+ }
287
+ return undefined;
288
+ }
289
+ noteMiss(key) { this.negative.add(key); }
290
+ isMiss(key) { return this.negative.has(key); }
291
+ }
292
+ /** Browser I/O: fetch() plus a synchronous XHR that only works inside a Worker. */
293
+ export function browserIO() {
294
+ const io = {
295
+ async fetch(url) { const r = await fetch(url); if (!r.ok)
296
+ throw new Error(`fetch ${url}: ${r.status}`); return new Uint8Array(await r.arrayBuffer()); },
297
+ // manifests and hot lists name the current files: always revalidated (a 304 when unchanged)
298
+ async fetchJson(url) { const r = await fetch(url, { cache: 'no-cache' }); if (!r.ok)
299
+ throw new Error(`fetch ${url}: ${r.status}`); return r.json(); },
300
+ };
301
+ if (inWorker && typeof XMLHttpRequest !== 'undefined') {
302
+ io.fetchSync = (url) => {
303
+ fetchingEvent(url.slice(url.lastIndexOf('/') + 1)); // the stall watchdog sees a slow host at work
304
+ const xhr = new XMLHttpRequest();
305
+ xhr.open('GET', url, false);
306
+ xhr.responseType = 'arraybuffer';
307
+ xhr.send(null);
308
+ if (xhr.status < 200 || xhr.status >= 300)
309
+ throw new Error(`sync fetch ${url}: ${xhr.status}`);
310
+ return new Uint8Array(xhr.response);
311
+ };
312
+ }
313
+ return io;
314
+ }
315
+ //# sourceMappingURL=bundle.js.map
@@ -0,0 +1,62 @@
1
+ /**
2
+ * lazyfs.ts — lazily-loaded files in the Emscripten MEMFS (docs/06 §4.3).
3
+ *
4
+ * A lazy file is a real MEMFS node whose size is known up front (so that
5
+ * stat(), readdir() and kpathsea's ls-R walks work) and whose bytes are
6
+ * fetched synchronously the first time anything reads it. The loader must be
7
+ * synchronous: a sync XMLHttpRequest inside a Web Worker, or fs.readFileSync
8
+ * in Node. On the browser main thread there is no synchronous loader, so the
9
+ * caller must prefetch (see bundle.ts).
10
+ */
11
+ export interface EmscriptenFS {
12
+ mkdir(path: string, mode?: number): void;
13
+ writeFile(path: string, data: Uint8Array | string, opts?: {
14
+ encoding?: string;
15
+ }): void;
16
+ readFile(path: string, opts?: {
17
+ encoding?: 'binary' | 'utf8';
18
+ }): Uint8Array | string;
19
+ analyzePath(path: string): {
20
+ exists: boolean;
21
+ object?: any;
22
+ isRoot?: boolean;
23
+ };
24
+ createFile(parent: string, name: string, properties: object, canRead: boolean, canWrite: boolean): any;
25
+ lookupPath(path: string, opts?: object): {
26
+ node: any;
27
+ };
28
+ unlink(path: string): void;
29
+ readdir(path: string): string[];
30
+ stat(path: string): {
31
+ size: number;
32
+ mode: number;
33
+ mtime: Date;
34
+ };
35
+ isDir(mode: number): boolean;
36
+ isFile(mode: number): boolean;
37
+ chdir(path: string): void;
38
+ cwd(): string;
39
+ mount(type: unknown, opts: object, mountpoint: string): void;
40
+ utime?(path: string, atime: number, mtime: number): void;
41
+ [k: string]: any;
42
+ }
43
+ export type LazyLoader = (path: string) => Uint8Array;
44
+ /** mkdir -p */
45
+ export declare function mkdirp(FS: EmscriptenFS, dir: string): void;
46
+ /** Create a lazy node at `path` with the given size. Returns false if a file already exists there. */
47
+ export declare function createLazyFile(FS: EmscriptenFS, path: string, size: number, load: LazyLoader): boolean;
48
+ /** True if `path` is a lazy node that has not been fetched yet. */
49
+ export declare function isUnloadedLazy(FS: EmscriptenFS, path: string): boolean;
50
+ /** Force-load a lazy node (no-op for ordinary files). */
51
+ export declare function ensureLoaded(FS: EmscriptenFS, path: string): void;
52
+ /** Write a text or binary file, creating directories as needed. */
53
+ export declare function writeFileDeep(FS: EmscriptenFS, path: string, data: Uint8Array | string): void;
54
+ /**
55
+ * Record every file opened for writing from now on (absolute paths, into the
56
+ * returned set). The engine's open syscall reaches the filesystem through
57
+ * FS.open, so wrapping it sees TeX's \openout and LuaTeX's io.open alike;
58
+ * install it after the host's own setup writes, or they count too.
59
+ */
60
+ export declare function trackWrites(FS: EmscriptenFS): Set<string>;
61
+ /** Recursively collect regular files under `dir` (used to harvest artifacts). */
62
+ export declare function listFiles(FS: EmscriptenFS, dir: string): string[];
@@ -0,0 +1,133 @@
1
+ /**
2
+ * lazyfs.ts — lazily-loaded files in the Emscripten MEMFS (docs/06 §4.3).
3
+ *
4
+ * A lazy file is a real MEMFS node whose size is known up front (so that
5
+ * stat(), readdir() and kpathsea's ls-R walks work) and whose bytes are
6
+ * fetched synchronously the first time anything reads it. The loader must be
7
+ * synchronous: a sync XMLHttpRequest inside a Web Worker, or fs.readFileSync
8
+ * in Node. On the browser main thread there is no synchronous loader, so the
9
+ * caller must prefetch (see bundle.ts).
10
+ */
11
+ const LAZY = Symbol('mpwasm.lazy');
12
+ /** mkdir -p */
13
+ export function mkdirp(FS, dir) {
14
+ const parts = dir.split('/').filter(Boolean);
15
+ let cur = '';
16
+ for (const p of parts) {
17
+ cur += '/' + p;
18
+ const a = FS.analyzePath(cur);
19
+ if (!a.exists)
20
+ FS.mkdir(cur);
21
+ }
22
+ }
23
+ /** Create a lazy node at `path` with the given size. Returns false if a file already exists there. */
24
+ export function createLazyFile(FS, path, size, load) {
25
+ const i = path.lastIndexOf('/');
26
+ const dir = i > 0 ? path.slice(0, i) : '/';
27
+ const name = path.slice(i + 1);
28
+ if (FS.analyzePath(path).exists)
29
+ return false;
30
+ mkdirp(FS, dir);
31
+ const node = FS.createFile(dir, name, {}, true, true);
32
+ const state = { loaded: false, data: null };
33
+ node[LAZY] = state;
34
+ const origOps = node.stream_ops;
35
+ const origUsedBytes = Object.getOwnPropertyDescriptor(node, 'usedBytes');
36
+ Object.defineProperty(node, 'usedBytes', {
37
+ configurable: true, enumerable: true,
38
+ get: () => (state.loaded ? state.data.length : size),
39
+ set: () => { },
40
+ });
41
+ const force = () => {
42
+ if (state.loaded)
43
+ return;
44
+ const data = load(path);
45
+ state.loaded = true;
46
+ state.data = data;
47
+ // become an ordinary MEMFS file again
48
+ delete node.usedBytes;
49
+ if (origUsedBytes)
50
+ Object.defineProperty(node, 'usedBytes', origUsedBytes);
51
+ node.contents = data;
52
+ node.usedBytes = data.length;
53
+ node.stream_ops = origOps;
54
+ };
55
+ const ops = {};
56
+ for (const k of Object.keys(origOps)) {
57
+ ops[k] = (...args) => { force(); return origOps[k](...args); };
58
+ }
59
+ node.stream_ops = ops;
60
+ return true;
61
+ }
62
+ /** True if `path` is a lazy node that has not been fetched yet. */
63
+ export function isUnloadedLazy(FS, path) {
64
+ const a = FS.analyzePath(path);
65
+ if (!a.exists || !a.object)
66
+ return false;
67
+ const st = a.object[LAZY];
68
+ return !!st && !st.loaded;
69
+ }
70
+ /** Force-load a lazy node (no-op for ordinary files). */
71
+ export function ensureLoaded(FS, path) {
72
+ const a = FS.analyzePath(path);
73
+ if (!a.exists || !a.object)
74
+ return;
75
+ const st = a.object[LAZY];
76
+ if (st && !st.loaded) {
77
+ // trigger through the stream ops
78
+ const fd = FS.open(path, 'r');
79
+ const buf = new Uint8Array(1);
80
+ FS.read(fd, buf, 0, 1, 0);
81
+ FS.close(fd);
82
+ }
83
+ }
84
+ /** Write a text or binary file, creating directories as needed. */
85
+ export function writeFileDeep(FS, path, data) {
86
+ const i = path.lastIndexOf('/');
87
+ if (i > 0)
88
+ mkdirp(FS, path.slice(0, i));
89
+ if (FS.analyzePath(path).exists) {
90
+ try {
91
+ FS.unlink(path);
92
+ }
93
+ catch { /* ignore */ }
94
+ }
95
+ FS.writeFile(path, data);
96
+ }
97
+ /**
98
+ * Record every file opened for writing from now on (absolute paths, into the
99
+ * returned set). The engine's open syscall reaches the filesystem through
100
+ * FS.open, so wrapping it sees TeX's \openout and LuaTeX's io.open alike;
101
+ * install it after the host's own setup writes, or they count too.
102
+ */
103
+ export function trackWrites(FS) {
104
+ const written = new Set();
105
+ const open = FS.open;
106
+ FS.open = function (path, flags, mode) {
107
+ const stream = open.call(this, path, flags, mode);
108
+ // O_WRONLY or O_RDWR (numeric), or any mode string but plain 'r'
109
+ if (typeof flags === 'string' ? flags !== 'r' : (flags & 3) !== 0)
110
+ written.add(stream.path);
111
+ return stream;
112
+ };
113
+ return written;
114
+ }
115
+ /** Recursively collect regular files under `dir` (used to harvest artifacts). */
116
+ export function listFiles(FS, dir) {
117
+ const out = [];
118
+ const walk = (d) => {
119
+ for (const name of FS.readdir(d)) {
120
+ if (name === '.' || name === '..')
121
+ continue;
122
+ const p = d === '/' ? '/' + name : d + '/' + name;
123
+ const st = FS.stat(p);
124
+ if (FS.isDir(st.mode))
125
+ walk(p);
126
+ else
127
+ out.push(p);
128
+ }
129
+ };
130
+ walk(dir);
131
+ return out;
132
+ }
133
+ //# sourceMappingURL=lazyfs.js.map
@@ -0,0 +1,15 @@
1
+ export interface WorkerRequest {
2
+ id: number;
3
+ op: 'init' | 'run' | 'latex' | 'bibtex' | 'makeindex' | 'addFiles' | 'removeFiles' | 'clearCache' | 'preload' | 'prefetch' | 'setLogLevel' | 'dispose';
4
+ [k: string]: unknown;
5
+ }
6
+ export interface WorkerResponse {
7
+ id: number;
8
+ ok: boolean;
9
+ result?: unknown;
10
+ error?: string;
11
+ }
12
+ export interface WorkerEvent {
13
+ event: 'progress' | 'log' | 'record';
14
+ data: unknown;
15
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * worker.ts — the Web Worker entry point (docs/01 §2). Hosts one MetaPostCore
3
+ * and speaks a tiny request/response protocol with index.ts. The same file runs
4
+ * as a Node worker_threads worker (MetaPost.create({ worker: true }) in Node),
5
+ * so that a runaway engine can be terminated there too: a synchronous wasm loop
6
+ * cannot be interrupted from the thread it runs on.
7
+ */
8
+ import { MetaPostCore } from './core.js';
9
+ import { BundleSet, browserIO } from './vfs/bundle.js';
10
+ import { resolveBundleSpecs, DEFAULT_BUNDLES } from './bundles-config.js';
11
+ import { Logger, DEFAULT_LOG_LEVEL } from './logger.js';
12
+ import { isNode, nodeIO } from './node.js';
13
+ import { ghostscriptLoader, ghostscriptWanted } from './ghostscript.js';
14
+ // Node worker_threads: give the rest of this file the Web Worker surface it uses
15
+ // (self.onmessage, postMessage, close), backed by parentPort.
16
+ if (isNode) {
17
+ const { parentPort } = await import('node:worker_threads');
18
+ if (parentPort) {
19
+ const g = globalThis;
20
+ g.self = g;
21
+ g.postMessage = (m, transfer) => parentPort.postMessage(m, transfer);
22
+ g.close = () => parentPort.close();
23
+ parentPort.on('message', (data) => g.onmessage?.({ data }));
24
+ }
25
+ }
26
+ let core = null;
27
+ let bundles = null;
28
+ let logger = null;
29
+ let baseUrl = '';
30
+ const post = (m, transfer) => self.postMessage(m, transfer ?? []);
31
+ self.onmessage = async (ev) => {
32
+ const req = ev.data;
33
+ try {
34
+ switch (req.op) {
35
+ case 'init': {
36
+ const options = req.options;
37
+ baseUrl = String(req.baseUrl);
38
+ const io = isNode ? await nodeIO() : browserIO();
39
+ bundles = new BundleSet(io);
40
+ // records cross to the main thread, which writes them to the console (or the `logger` option)
41
+ logger = new Logger(options.logLevel ?? DEFAULT_LOG_LEVEL, (r) => post({ event: 'record', data: r }));
42
+ bundles.logger = logger;
43
+ // Node can mount a real texmf tree instead of loading bundles
44
+ const texmfDir = isNode ? options.texmfDir : undefined;
45
+ // manifests, hot lists and engine glue all at once: a slow host charges per round trip
46
+ const bundleBase = options.bundleBaseUrl ?? new URL('./bundles/', baseUrl).href;
47
+ const wantTex = (options.tex ?? 'auto') !== 'none';
48
+ const glue = (name) => import(/* @vite-ignore */ new URL(name, baseUrl).href).then((m) => m.default, () => undefined);
49
+ const [mplibFactory, texFactory, dvisvgmFactory, luatexFactory] = await Promise.all([
50
+ import(/* @vite-ignore */ new URL('./mplib.mjs', baseUrl).href).then((m) => m.default),
51
+ wantTex ? glue('./tex.mjs') : undefined,
52
+ wantTex ? glue('./dvisvgm.mjs') : undefined,
53
+ wantTex ? glue('./luatex.mjs') : undefined,
54
+ texmfDir ? undefined : bundles.addAll(resolveBundleSpecs(options.bundles ?? DEFAULT_BUNDLES, bundleBase)),
55
+ texmfDir ? undefined : bundles.loadHot((bundleBase.endsWith('/') ? bundleBase : bundleBase + '/') + 'hot.json'),
56
+ ]);
57
+ if (!texmfDir) {
58
+ await bundles.prefetchEager();
59
+ if (!bundles.canFetchSync)
60
+ await bundles.prefetchAll();
61
+ }
62
+ const gsBase = options.ghostscriptBaseUrl ?? new URL('./ghostscript/', baseUrl).href;
63
+ const ghostscript = dvisvgmFactory && ghostscriptWanted(options.ghostscript, options.bundles ?? DEFAULT_BUNDLES)
64
+ ? ghostscriptLoader({ glueUrl: new URL('gs.mjs', gsBase).href, wasmUrl: new URL('gs.wasm', gsBase).href, fetch: (u) => io.fetch(u), log: (m) => logger?.info('host', m) })
65
+ : undefined;
66
+ // bibtex.wasm and makeindex.wasm: imported on first use
67
+ const toolFactories = new Map();
68
+ const tools = (name) => { let f = toolFactories.get(name); if (!f)
69
+ toolFactories.set(name, f = glue(`./${name}.mjs`)); return f; };
70
+ core = new MetaPostCore({
71
+ mplibFactory, texFactory, luatexFactory, dvisvgmFactory, ghostscript, tools, bundles: texmfDir ? undefined : bundles, texmfDir, options, logger,
72
+ onProgress: (e) => post({ event: 'progress', data: e }),
73
+ onLog: (line) => post({ event: 'log', data: line }),
74
+ });
75
+ await core.init();
76
+ post({ id: req.id, ok: true, result: { version: core.version } });
77
+ break;
78
+ }
79
+ case 'run': {
80
+ const r = await core.run(req.source, req.options);
81
+ post({ id: req.id, ok: true, result: r });
82
+ break;
83
+ }
84
+ case 'latex': {
85
+ const r = await core.latex(req.source, req.options);
86
+ post({ id: req.id, ok: true, result: r });
87
+ break;
88
+ }
89
+ case 'bibtex':
90
+ post({ id: req.id, ok: true, result: await core.bibtex(req.options) });
91
+ break;
92
+ case 'makeindex':
93
+ post({ id: req.id, ok: true, result: await core.makeindex(req.options) });
94
+ break;
95
+ case 'addFiles':
96
+ core.addFiles(req.files);
97
+ post({ id: req.id, ok: true });
98
+ break;
99
+ case 'removeFiles':
100
+ core.removeFiles(req.names);
101
+ post({ id: req.id, ok: true });
102
+ break;
103
+ case 'clearCache':
104
+ core.clearCache();
105
+ post({ id: req.id, ok: true });
106
+ break;
107
+ case 'preload': {
108
+ const names = req.bundles;
109
+ for (const spec of resolveBundleSpecs(names, new URL('./bundles/', baseUrl).href)) {
110
+ if (!bundles.manifests.some((m) => m.spec.name === spec.name))
111
+ await bundles.add(spec);
112
+ }
113
+ await bundles.prefetchAll((f) => names.includes(f.bundle));
114
+ post({ id: req.id, ok: true });
115
+ break;
116
+ }
117
+ case 'prefetch': {
118
+ const n = await bundles.prefetchHot(req.kinds);
119
+ post({ id: req.id, ok: true, result: n });
120
+ break;
121
+ }
122
+ case 'setLogLevel':
123
+ if (logger)
124
+ logger.level = req.level;
125
+ post({ id: req.id, ok: true });
126
+ break;
127
+ case 'dispose':
128
+ post({ id: req.id, ok: true });
129
+ self.close();
130
+ break;
131
+ }
132
+ }
133
+ catch (e) {
134
+ const msg = typeof e === 'string' ? e : e?.stack ?? e?.message ?? (() => { try {
135
+ return JSON.stringify(e);
136
+ }
137
+ catch {
138
+ return String(e);
139
+ } })();
140
+ post({ id: req.id, ok: false, error: msg });
141
+ }
142
+ };
143
+ //# sourceMappingURL=worker.js.map