@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.
- package/LICENSE +674 -0
- package/NOTICE.md +30 -0
- package/README.md +127 -2
- package/package.json +56 -4
- package/runtime/LICENSE +165 -0
- package/runtime/NOTICE.md +32 -0
- package/runtime/dist/auto.d.ts +68 -0
- package/runtime/dist/auto.js +564 -0
- package/runtime/dist/bibtex.mjs +2 -0
- package/runtime/dist/bibtex.wasm +0 -0
- package/runtime/dist/bundles/cm-tfm/files.br +0 -0
- package/runtime/dist/bundles/cm-tfm/files.index.json +1 -0
- package/runtime/dist/bundles/cm-tfm/manifest.json +1 -0
- package/runtime/dist/bundles/cm-type1/files.br +0 -0
- package/runtime/dist/bundles/cm-type1/files.index.json +1 -0
- package/runtime/dist/bundles/cm-type1/manifest.json +1 -0
- package/runtime/dist/bundles/core/files.br +0 -0
- package/runtime/dist/bundles/core/files.index.json +1 -0
- package/runtime/dist/bundles/core/manifest.json +1 -0
- package/runtime/dist/bundles/hot.json +1 -0
- package/runtime/dist/bundles/index.json +80 -0
- package/runtime/dist/bundles/latex-core/files.br +0 -0
- package/runtime/dist/bundles/latex-core/files.index.json +1 -0
- package/runtime/dist/bundles/latex-core/manifest.json +1 -0
- package/runtime/dist/bundles/latex-extra/files.br +0 -0
- package/runtime/dist/bundles/latex-extra/files.index.json +1 -0
- package/runtime/dist/bundles/latex-extra/manifest.json +1 -0
- package/runtime/dist/bundles/lm-fonts/files.br +0 -0
- package/runtime/dist/bundles/lm-fonts/files.index.json +1 -0
- package/runtime/dist/bundles/lm-fonts/manifest.json +1 -0
- package/runtime/dist/bundles/luatex/files.br +0 -0
- package/runtime/dist/bundles/luatex/files.index.json +1 -0
- package/runtime/dist/bundles/luatex/manifest.json +1 -0
- package/runtime/dist/bundles/ps-fonts/files.br +0 -0
- package/runtime/dist/bundles/ps-fonts/files.index.json +1 -0
- package/runtime/dist/bundles/ps-fonts/manifest.json +1 -0
- package/runtime/dist/bundles/tex-plain/files.br +0 -0
- package/runtime/dist/bundles/tex-plain/files.index.json +1 -0
- package/runtime/dist/bundles/tex-plain/manifest.json +1 -0
- package/runtime/dist/bundles/tikz-snapshot/files.br +0 -0
- package/runtime/dist/bundles/tikz-snapshot/files.index.json +1 -0
- package/runtime/dist/bundles/tikz-snapshot/manifest.json +1 -0
- package/runtime/dist/bundles-config.d.ts +3 -0
- package/runtime/dist/bundles-config.js +16 -0
- package/runtime/dist/cli.d.ts +2 -0
- package/runtime/dist/cli.js +372 -0
- package/runtime/dist/core.d.ts +124 -0
- package/runtime/dist/core.js +927 -0
- package/runtime/dist/diagnostics.d.ts +53 -0
- package/runtime/dist/diagnostics.js +184 -0
- package/runtime/dist/dvisvgm.d.ts +20 -0
- package/runtime/dist/dvisvgm.js +46 -0
- package/runtime/dist/dvisvgm.mjs +2 -0
- package/runtime/dist/dvisvgm.wasm +0 -0
- package/runtime/dist/figures.d.ts +92 -0
- package/runtime/dist/figures.js +250 -0
- package/runtime/dist/ghostscript.d.ts +92 -0
- package/runtime/dist/ghostscript.js +218 -0
- package/runtime/dist/index.d.ts +87 -0
- package/runtime/dist/index.js +364 -0
- package/runtime/dist/logger.d.ts +48 -0
- package/runtime/dist/logger.js +55 -0
- package/runtime/dist/luatex.mjs +2 -0
- package/runtime/dist/luatex.wasm +0 -0
- package/runtime/dist/makeindex.mjs +2 -0
- package/runtime/dist/makeindex.wasm +0 -0
- package/runtime/dist/mplib.d.ts +106 -0
- package/runtime/dist/mplib.js +85 -0
- package/runtime/dist/mplib.mjs +2 -0
- package/runtime/dist/mplib.wasm +0 -0
- package/runtime/dist/node.d.ts +4 -0
- package/runtime/dist/node.js +49 -0
- package/runtime/dist/postscript.d.ts +21 -0
- package/runtime/dist/postscript.js +130 -0
- package/runtime/dist/prerender.d.ts +55 -0
- package/runtime/dist/prerender.js +106 -0
- package/runtime/dist/render/svg.d.ts +20 -0
- package/runtime/dist/render/svg.js +96 -0
- package/runtime/dist/tex/bridge.d.ts +93 -0
- package/runtime/dist/tex/bridge.js +223 -0
- package/runtime/dist/tex/cache-key.d.ts +33 -0
- package/runtime/dist/tex/cache-key.js +150 -0
- package/runtime/dist/tex/mpto.d.ts +60 -0
- package/runtime/dist/tex/mpto.js +112 -0
- package/runtime/dist/tex/mpx.d.ts +25 -0
- package/runtime/dist/tex/mpx.js +54 -0
- package/runtime/dist/tex/scanner.d.ts +78 -0
- package/runtime/dist/tex/scanner.js +326 -0
- package/runtime/dist/tex.mjs +2 -0
- package/runtime/dist/tex.wasm +0 -0
- package/runtime/dist/texengine.d.ts +35 -0
- package/runtime/dist/texengine.js +57 -0
- package/runtime/dist/texutils.d.ts +19 -0
- package/runtime/dist/texutils.js +60 -0
- package/runtime/dist/types.d.ts +376 -0
- package/runtime/dist/types.js +6 -0
- package/runtime/dist/vfs/bundle.d.ts +88 -0
- package/runtime/dist/vfs/bundle.js +315 -0
- package/runtime/dist/vfs/lazyfs.d.ts +62 -0
- package/runtime/dist/vfs/lazyfs.js +133 -0
- package/runtime/dist/worker.d.ts +15 -0
- package/runtime/dist/worker.js +143 -0
- package/runtime/licenses/COPYING.AFPL +230 -0
- package/runtime/licenses/COPYING.makeindex +42 -0
- package/runtime/licenses/GPL-3.0.txt +674 -0
- package/runtime/licenses/LGPL-3.0.txt +165 -0
- package/runtime/licenses/OFL-1.1.txt +93 -0
- package/runtime/package.json +1 -0
- package/runtime/upstream.json +27 -0
- package/src/browser-worker.js +26 -0
- package/src/browser.d.ts +18 -0
- package/src/browser.js +68 -0
- package/src/deploy.d.ts +5 -0
- package/src/deploy.js +48 -0
- package/src/index.d.ts +28 -0
- package/src/index.js +103 -0
- package/src/worker.js +15 -0
- package/upstream.json +27 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
import { Logger, consoleSink, DEFAULT_LOG_LEVEL, LOG_LEVELS, plural } from './logger.js';
|
|
2
|
+
import { DEFAULT_BUNDLES } from './bundles-config.js';
|
|
3
|
+
import { figureHash, figureName, renderFigure, isSvg, makeZip, wrapTikz, wrapMetaPost, bundleList } from './figures.js';
|
|
4
|
+
export { wrapTikz, wrapMetaPost, figureHash, figureName };
|
|
5
|
+
// ---------------------------------------------------------------- the result cache
|
|
6
|
+
// Version 2: since the saved-figure work the key is the six-character figure
|
|
7
|
+
// hash (the same identity as the file name); the store is recreated on upgrade.
|
|
8
|
+
// Version 3: a wrapped TikZ figure's SVG is the standalone page, border
|
|
9
|
+
// included (renderFigure); the hash is the source, so the old crops must go.
|
|
10
|
+
const DB_NAME = 'mp-tikz-wasm-cache', DB_VERSION = 3, STORE = 'svg';
|
|
11
|
+
function openDb() {
|
|
12
|
+
if (typeof indexedDB === 'undefined')
|
|
13
|
+
return Promise.resolve(null);
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
const req = indexedDB.open(DB_NAME, DB_VERSION);
|
|
16
|
+
req.onupgradeneeded = () => {
|
|
17
|
+
const db = req.result;
|
|
18
|
+
if (db.objectStoreNames.contains(STORE))
|
|
19
|
+
db.deleteObjectStore(STORE);
|
|
20
|
+
db.createObjectStore(STORE);
|
|
21
|
+
};
|
|
22
|
+
req.onsuccess = () => resolve(req.result);
|
|
23
|
+
req.onerror = () => resolve(null);
|
|
24
|
+
req.onblocked = () => resolve(null);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
// each call opens its own connection and closes it when done, so a page of figures does not hold dozens open
|
|
28
|
+
async function cacheGet(key) {
|
|
29
|
+
const db = await openDb();
|
|
30
|
+
if (!db)
|
|
31
|
+
return undefined;
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
const r = db.transaction(STORE).objectStore(STORE).get(key);
|
|
34
|
+
r.onsuccess = () => { resolve(r.result); db.close(); };
|
|
35
|
+
r.onerror = () => { resolve(undefined); db.close(); };
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
async function cachePut(key, svg) {
|
|
39
|
+
const db = await openDb();
|
|
40
|
+
if (!db)
|
|
41
|
+
return;
|
|
42
|
+
await new Promise((resolve) => {
|
|
43
|
+
const t = db.transaction(STORE, 'readwrite');
|
|
44
|
+
t.objectStore(STORE).put(svg, key);
|
|
45
|
+
t.oncomplete = () => { db.close(); resolve(); };
|
|
46
|
+
t.onerror = () => { db.close(); resolve(); };
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/** A saved figure, or undefined if the server has none by that name (or answers with something that is not SVG). */
|
|
50
|
+
async function fetchSavedFigure(url) {
|
|
51
|
+
try {
|
|
52
|
+
const res = await fetch(url);
|
|
53
|
+
if (!res.ok)
|
|
54
|
+
return undefined;
|
|
55
|
+
const text = await res.text();
|
|
56
|
+
return isSvg(text) ? text : undefined;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Status spans of figures waiting for the engine; progress events update them (a slow host shows what it is fetching). */
|
|
63
|
+
const statusSpans = new Set();
|
|
64
|
+
export class AutoRenderer {
|
|
65
|
+
options;
|
|
66
|
+
engine = null;
|
|
67
|
+
log;
|
|
68
|
+
rendered = new Map();
|
|
69
|
+
inFlight = new Set();
|
|
70
|
+
/** Files handed over before the engine existed; applied when it starts (see addFiles). */
|
|
71
|
+
pendingFiles = {};
|
|
72
|
+
constructor(options = {}) {
|
|
73
|
+
this.options = options;
|
|
74
|
+
this.log = new Logger(options.logLevel ?? DEFAULT_LOG_LEVEL, options.logger ?? consoleSink);
|
|
75
|
+
}
|
|
76
|
+
/** Has an engine been started? A page whose figures all came from the cache or the saved files never starts one. */
|
|
77
|
+
get started() { return this.engine !== null; }
|
|
78
|
+
/**
|
|
79
|
+
* Put files where this page's TeX runs will find them: a font the host wants to
|
|
80
|
+
* typeset in (`\setmainfont{X.ttf}[Path=./]` — the run's working directory leads
|
|
81
|
+
* OPENTYPEFONTS and TTFONTS), an image, a .sty. They stay for the life of the page.
|
|
82
|
+
*
|
|
83
|
+
* Deliberately does NOT start the engine: files handed over before the first
|
|
84
|
+
* render are held and applied when one is created, so a page whose figures all
|
|
85
|
+
* come from the cache or from saved files still starts nothing. Call it before
|
|
86
|
+
* the figures that need the font, and with the `opentype` bundle loaded
|
|
87
|
+
* (`data-bundles="+opentype"`) if the font is for fontspec.
|
|
88
|
+
*/
|
|
89
|
+
addFiles(files) {
|
|
90
|
+
Object.assign(this.pendingFiles, files);
|
|
91
|
+
return this.engine ? this.engine.then((m) => m.addFiles(files)) : Promise.resolve();
|
|
92
|
+
}
|
|
93
|
+
get mp() {
|
|
94
|
+
if (!this.engine) {
|
|
95
|
+
// prefetch the files the page's diagrams will need, in parallel, before the first render;
|
|
96
|
+
// the progress listener goes on first so the placeholders show it happening
|
|
97
|
+
const { prefetch, cacheResults: _c, figuresBaseUrl: _f, ...rest } = this.options;
|
|
98
|
+
const kinds = prefetch ?? (typeof document !== 'undefined' ? prefetchKinds() : []);
|
|
99
|
+
this.engine = import('./index.js').then(({ MetaPost }) => MetaPost.create(rest)).then(async (m) => {
|
|
100
|
+
m.on('progress', (e) => {
|
|
101
|
+
const text = e.phase === 'fetching'
|
|
102
|
+
? (e.total ? `fetching files (${e.current} of ${e.total})…` : `fetching ${e.detail ?? ''}…`)
|
|
103
|
+
: e.phase === 'typesetting' ? `typesetting${e.detail ? ` (${e.detail})` : ''}…` : `${e.phase}…`;
|
|
104
|
+
for (const s of statusSpans)
|
|
105
|
+
s.textContent = text;
|
|
106
|
+
});
|
|
107
|
+
if (Object.keys(this.pendingFiles).length)
|
|
108
|
+
await m.addFiles(this.pendingFiles);
|
|
109
|
+
if (kinds.length)
|
|
110
|
+
await m.prefetch(kinds);
|
|
111
|
+
return m;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return this.engine;
|
|
115
|
+
}
|
|
116
|
+
/** How much reaches the console (see `MetaPostOptions.logLevel`), for the engine now and later. */
|
|
117
|
+
setLogLevel(level) {
|
|
118
|
+
this.options.logLevel = level;
|
|
119
|
+
this.log.level = level;
|
|
120
|
+
void this.engine?.then((m) => { m.logLevel = level; });
|
|
121
|
+
}
|
|
122
|
+
render(req) {
|
|
123
|
+
const p = this.renderFrom(req);
|
|
124
|
+
this.inFlight.add(p);
|
|
125
|
+
p.catch(() => { }).finally(() => this.inFlight.delete(p));
|
|
126
|
+
return p;
|
|
127
|
+
}
|
|
128
|
+
async renderFrom(req) {
|
|
129
|
+
const hash = figureHash(req), name = figureName(hash);
|
|
130
|
+
const useCache = this.options.cacheResults !== false && req.attrs.cache !== 'off';
|
|
131
|
+
const found = (svg, from) => ({ svg, log: '', diagnostics: [], ok: true, ms: 0, cached: true, from, hash, name });
|
|
132
|
+
if (useCache) {
|
|
133
|
+
const hit = await cacheGet(hash);
|
|
134
|
+
if (hit) {
|
|
135
|
+
this.log.debug('host', `${name}: from the result cache`);
|
|
136
|
+
return this.done(req, found(hit, 'cache'));
|
|
137
|
+
}
|
|
138
|
+
const base = this.options.figuresBaseUrl;
|
|
139
|
+
if (base) {
|
|
140
|
+
const svg = await fetchSavedFigure(new URL(name, base).href);
|
|
141
|
+
if (svg) {
|
|
142
|
+
this.log.debug('host', `${name}: from ${base}`);
|
|
143
|
+
void cachePut(hash, svg);
|
|
144
|
+
return this.done(req, found(svg, 'file'));
|
|
145
|
+
}
|
|
146
|
+
this.log.debug('host', `${name}: not saved under ${base}; rendering`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const mp = await this.mp;
|
|
150
|
+
const r = await renderFigure(mp, req, hash);
|
|
151
|
+
if (useCache && r.ok)
|
|
152
|
+
void cachePut(hash, r.svg);
|
|
153
|
+
return this.done(req, { ...r, cached: false, from: 'engine', hash, name });
|
|
154
|
+
}
|
|
155
|
+
done(req, out) {
|
|
156
|
+
if (out.ok)
|
|
157
|
+
this.rendered.set(out.hash, { name: out.name, hash: out.hash, kind: req.kind, source: req.source, svg: out.svg });
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
/** Drop a figure from figures() (and so from saveFigures()): a live element has replaced it. */
|
|
161
|
+
forget(nameOrHash) {
|
|
162
|
+
for (const [hash, f] of this.rendered)
|
|
163
|
+
if (hash === nameOrHash || f.name === nameOrHash)
|
|
164
|
+
this.rendered.delete(hash);
|
|
165
|
+
}
|
|
166
|
+
/** Resolves when no render is in flight. */
|
|
167
|
+
async idle() { while (this.inFlight.size)
|
|
168
|
+
await Promise.allSettled([...this.inFlight]); }
|
|
169
|
+
/** Every figure that has rendered successfully so far (from any source), in the order it finished. */
|
|
170
|
+
figures() { return [...this.rendered.values()]; }
|
|
171
|
+
/**
|
|
172
|
+
* Write every figure on the page as figure-HASH.svg: into a folder chosen with the
|
|
173
|
+
* directory picker where the browser has one (Chrome, Edge), otherwise as a zip
|
|
174
|
+
* download. Waits for figures still rendering. Returns the file names written.
|
|
175
|
+
*/
|
|
176
|
+
async saveFigures(opts = {}) {
|
|
177
|
+
await this.idle();
|
|
178
|
+
const figs = this.figures();
|
|
179
|
+
if (!figs.length) {
|
|
180
|
+
this.log.warn('host', 'saveFigures: no figure has rendered on this page');
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
const names = figs.map((f) => f.name);
|
|
184
|
+
const w = globalThis;
|
|
185
|
+
if (!opts.zip && typeof w.showDirectoryPicker === 'function') {
|
|
186
|
+
let dir = null;
|
|
187
|
+
try {
|
|
188
|
+
dir = await w.showDirectoryPicker({ mode: 'readwrite', id: 'mp-tikz-wasm-figures' });
|
|
189
|
+
}
|
|
190
|
+
catch (e) {
|
|
191
|
+
if (e?.name === 'AbortError') {
|
|
192
|
+
this.log.info('host', 'saveFigures: cancelled');
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
this.log.info('host', `saveFigures: no directory picker here (${e?.message ?? e}); downloading a zip instead`);
|
|
196
|
+
}
|
|
197
|
+
if (dir) {
|
|
198
|
+
for (const f of figs) {
|
|
199
|
+
const h = await dir.getFileHandle(f.name, { create: true });
|
|
200
|
+
const ws = await h.createWritable();
|
|
201
|
+
await ws.write(f.svg);
|
|
202
|
+
await ws.close();
|
|
203
|
+
}
|
|
204
|
+
this.log.info('host', `saveFigures: wrote ${plural(figs.length, 'figure')} to ${dir.name}/`);
|
|
205
|
+
return names;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const zip = makeZip(figs.map((f) => ({ name: f.name, data: f.svg })));
|
|
209
|
+
const url = URL.createObjectURL(new Blob([zip], { type: 'application/zip' }));
|
|
210
|
+
const a = document.createElement('a');
|
|
211
|
+
a.href = url;
|
|
212
|
+
a.download = opts.name ?? 'figures.zip';
|
|
213
|
+
document.body.appendChild(a);
|
|
214
|
+
a.click();
|
|
215
|
+
a.remove();
|
|
216
|
+
setTimeout(() => URL.revokeObjectURL(url), 60_000);
|
|
217
|
+
this.log.info('host', `saveFigures: ${a.download} with ${plural(figs.length, 'figure')}`);
|
|
218
|
+
return names;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// ---------------------------------------------------------------- the DOM side
|
|
222
|
+
const SELECTOR = 'script[type="text/tikz"], script[type="text/metapost"], tikz-diagram, metapost-diagram';
|
|
223
|
+
let renderer = null;
|
|
224
|
+
const pending = new WeakSet();
|
|
225
|
+
function attrsOf(el) {
|
|
226
|
+
const a = {};
|
|
227
|
+
for (const { name, value } of Array.from(el.attributes))
|
|
228
|
+
a[name.replace(/^data-/, '')] = value;
|
|
229
|
+
return a;
|
|
230
|
+
}
|
|
231
|
+
function kindOf(el) {
|
|
232
|
+
const t = el.tagName.toLowerCase();
|
|
233
|
+
if (t === 'metapost-diagram' || (t === 'script' && el.getAttribute('type') === 'text/metapost'))
|
|
234
|
+
return 'metapost';
|
|
235
|
+
return 'tikz';
|
|
236
|
+
}
|
|
237
|
+
function sourceOf(el) {
|
|
238
|
+
// script bodies are raw; custom elements hold HTML text, so entities are decoded by the parser already
|
|
239
|
+
return (el.textContent ?? '').replace(/^\s*\n/, '').replace(/\s+$/, '');
|
|
240
|
+
}
|
|
241
|
+
/** Which kinds of run the page's diagrams will need, for the engine's parallel prefetch. */
|
|
242
|
+
function prefetchKinds() {
|
|
243
|
+
const kinds = new Set();
|
|
244
|
+
const add = (kind, a, src) => {
|
|
245
|
+
if (kind === 'tikz') {
|
|
246
|
+
const engine = a.engine ?? 'auto';
|
|
247
|
+
if (engine === 'plain')
|
|
248
|
+
kinds.add('plain');
|
|
249
|
+
else if (engine === 'lualatex' || engine === 'luatex' || a.gdlibraries || /graphdrawing|\\directlua|luacode/.test(src))
|
|
250
|
+
kinds.add('lualatex');
|
|
251
|
+
else
|
|
252
|
+
kinds.add('latex');
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
kinds.add('metapost');
|
|
256
|
+
if (a.tex === 'latex' || /documentclass/.test(src))
|
|
257
|
+
kinds.add('latex');
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
for (const el of Array.from(document.querySelectorAll(SELECTOR)))
|
|
261
|
+
add(kindOf(el), attrsOf(el), liveSource(el));
|
|
262
|
+
// script tags are replaced by their figure as soon as they are seen, before the
|
|
263
|
+
// engine (and so this scan) exists: they are remembered in replacedScripts
|
|
264
|
+
for (const r of replacedScripts)
|
|
265
|
+
add(r.kind, r.attrs, r.source);
|
|
266
|
+
return [...kinds];
|
|
267
|
+
}
|
|
268
|
+
/** The requests of script tags already replaced by their figure, for prefetchKinds(). */
|
|
269
|
+
const replacedScripts = [];
|
|
270
|
+
/** A figure host: the <figure> a rendered diagram lives in. */
|
|
271
|
+
function makeHost(kind, attrs) {
|
|
272
|
+
const host = document.createElement('figure');
|
|
273
|
+
host.className = `mpw-figure mpw-${kind} mpw-pending`;
|
|
274
|
+
host.setAttribute('role', 'img');
|
|
275
|
+
if (attrs.alt)
|
|
276
|
+
host.setAttribute('aria-label', attrs.alt);
|
|
277
|
+
host.innerHTML = `<span class="mpw-status">rendering ${kind === 'tikz' ? 'TikZ' : 'MetaPost'}…</span>`;
|
|
278
|
+
return host;
|
|
279
|
+
}
|
|
280
|
+
/** Drop a figure from figures() once nothing on the page shows it (a live element replaced, emptied or overtook it). */
|
|
281
|
+
function forgetIfUnshown(name) {
|
|
282
|
+
if (name && renderer && !document.querySelector(`[data-figure="${name}"]`))
|
|
283
|
+
renderer.forget(name);
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Swap `el` for the <svg> root(s) now in `host` (data-replace): the diagram becomes plain
|
|
287
|
+
* SVG in the page, so that e.g. reveal.js sees its `class="fragment"` groups as ordinary
|
|
288
|
+
* elements of the slide. The element's id, class and style go to the (first) SVG root.
|
|
289
|
+
* Returns the root that now stands for the figure.
|
|
290
|
+
*/
|
|
291
|
+
function replaceWithSvg(el, host, name, attrs) {
|
|
292
|
+
const roots = Array.from(host.children).filter((n) => n.localName === 'svg');
|
|
293
|
+
if (!roots.length)
|
|
294
|
+
return null;
|
|
295
|
+
const first = roots[0];
|
|
296
|
+
const cls = el.getAttribute('class');
|
|
297
|
+
if (cls)
|
|
298
|
+
first.setAttribute('class', `${first.getAttribute('class') ?? ''} ${cls}`.trim());
|
|
299
|
+
const style = el.getAttribute('style');
|
|
300
|
+
if (style)
|
|
301
|
+
first.setAttribute('style', `${first.getAttribute('style') ?? ''};${style}`.replace(/^;/, ''));
|
|
302
|
+
if (el.id)
|
|
303
|
+
first.id = el.id;
|
|
304
|
+
first.setAttribute('role', 'img');
|
|
305
|
+
if (attrs.alt)
|
|
306
|
+
first.setAttribute('aria-label', attrs.alt);
|
|
307
|
+
for (const r of roots)
|
|
308
|
+
r.setAttribute('data-figure', name); // what forgetIfUnshown() looks for
|
|
309
|
+
el.replaceWith(...roots);
|
|
310
|
+
return first;
|
|
311
|
+
}
|
|
312
|
+
/** Is data-replace on (present, and not "false" or "off")? */
|
|
313
|
+
function wantsReplace(el) {
|
|
314
|
+
const v = el.getAttribute('data-replace') ?? el.getAttribute('replace');
|
|
315
|
+
return v !== null && !/^(false|off|no)$/i.test(v.trim());
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Typeset one request. `stale()` is asked before painting, so a result overtaken by a newer
|
|
319
|
+
* edit is dropped. With `replace`, a successful result replaces that element (see
|
|
320
|
+
* replaceWithSvg); returns true when it did.
|
|
321
|
+
*/
|
|
322
|
+
async function typesetInto(host, kind, source, attrs, update, stale = () => false, replace) {
|
|
323
|
+
const status = host.querySelector('.mpw-status');
|
|
324
|
+
if (status)
|
|
325
|
+
statusSpans.add(status);
|
|
326
|
+
try {
|
|
327
|
+
renderer ??= new AutoRenderer(loaderOptions());
|
|
328
|
+
const r = await renderer.render({ kind, source, attrs });
|
|
329
|
+
if (stale()) {
|
|
330
|
+
forgetIfUnshown(r.name);
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
const previous = host.dataset.figure;
|
|
334
|
+
host.className = `mpw-figure mpw-${kind} ${r.ok ? 'mpw-ok' : 'mpw-error'}`;
|
|
335
|
+
host.dataset.figure = r.name;
|
|
336
|
+
host.innerHTML = r.svg || '';
|
|
337
|
+
if (!r.ok || 'show-console' in attrs) {
|
|
338
|
+
const pre = document.createElement('pre');
|
|
339
|
+
pre.className = 'mpw-console';
|
|
340
|
+
pre.textContent = (r.diagnostics.map((d) => `${d.severity}: ${d.message}${d.line ? ` (line ${d.line})` : ''}`).join('\n') + '\n' + r.log).trim();
|
|
341
|
+
host.appendChild(pre);
|
|
342
|
+
}
|
|
343
|
+
// replace before announcing, so a listener (Reveal.sync(), say) sees the final DOM
|
|
344
|
+
const root = replace && r.ok ? replaceWithSvg(replace, host, r.name, attrs) : null;
|
|
345
|
+
// a figure no longer shown anywhere on the page is not one saveFigures() should write
|
|
346
|
+
if (previous !== r.name)
|
|
347
|
+
forgetIfUnshown(previous);
|
|
348
|
+
(root ?? host).dispatchEvent(new CustomEvent('mp-tikz-wasm:rendered', { bubbles: true, detail: { kind, ok: r.ok, ms: r.ms, cached: r.cached, from: r.from, hash: r.hash, name: r.name, update, replaced: !!root } }));
|
|
349
|
+
return !!root;
|
|
350
|
+
}
|
|
351
|
+
catch (e) {
|
|
352
|
+
if (stale())
|
|
353
|
+
return false;
|
|
354
|
+
host.className = `mpw-figure mpw-${kind} mpw-error`;
|
|
355
|
+
const msg = typeof e === 'string' ? e : e?.message ?? e?.error ?? (() => { try {
|
|
356
|
+
return JSON.stringify(e);
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
return String(e);
|
|
360
|
+
} })();
|
|
361
|
+
host.innerHTML = `<pre class="mpw-console">${String(msg).replace(/[<>&]/g, (c) => ({ '<': '<', '>': '>', '&': '&' }[c]))}</pre>`;
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
finally {
|
|
365
|
+
if (status)
|
|
366
|
+
statusSpans.delete(status);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// ---- live custom elements
|
|
370
|
+
// <tikz-diagram> and <metapost-diagram> typeset again when they change: new text
|
|
371
|
+
// content (el.textContent = …, el.innerHTML = …), the `source` property, or an
|
|
372
|
+
// attribute that is not purely presentational. The source is kept here because
|
|
373
|
+
// the element's children become the figure. Changes are debounced (data-debounce,
|
|
374
|
+
// in ms, default 200), the old figure stays up until the new one is ready, and a
|
|
375
|
+
// result overtaken by a later change is dropped. Script tags render once.
|
|
376
|
+
/** Attributes that never change the typeset result. */
|
|
377
|
+
const PRESENTATIONAL = /^(class|style|id|hidden|title|role|tabindex|slot|lang|dir|aria-.*|data-figure)$/;
|
|
378
|
+
const DEFAULT_DEBOUNCE_MS = 200;
|
|
379
|
+
const live = new WeakMap();
|
|
380
|
+
function requestKey(kind, source, attrs) {
|
|
381
|
+
return `${figureHash({ kind, source, attrs })}${'show-console' in attrs ? '+console' : ''}`;
|
|
382
|
+
}
|
|
383
|
+
/** Take new source text out of the element's children (anything but the figure), leaving just the figure. */
|
|
384
|
+
function absorbContent(el, st) {
|
|
385
|
+
const others = Array.from(el.childNodes).filter((n) => n !== st.host);
|
|
386
|
+
if (!others.length && st.host?.parentNode === el)
|
|
387
|
+
return false;
|
|
388
|
+
st.source = others.map((n) => n.textContent ?? '').join('').replace(/^\s*\n/, '').replace(/\s+$/, '');
|
|
389
|
+
for (const n of others)
|
|
390
|
+
n.remove();
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
function scheduleLive(el, st, immediate = false) {
|
|
394
|
+
clearTimeout(st.timer);
|
|
395
|
+
const d = Number(el.getAttribute('data-debounce'));
|
|
396
|
+
const delay = immediate ? 0 : Number.isFinite(d) && d >= 0 ? d : DEFAULT_DEBOUNCE_MS;
|
|
397
|
+
const run = () => { void renderLive(el, st); };
|
|
398
|
+
if (delay === 0)
|
|
399
|
+
run();
|
|
400
|
+
else
|
|
401
|
+
st.timer = setTimeout(run, delay);
|
|
402
|
+
}
|
|
403
|
+
async function renderLive(el, st) {
|
|
404
|
+
const kind = kindOf(el), attrs = attrsOf(el);
|
|
405
|
+
if (!st.source.trim()) { // nothing to typeset (yet): an empty element stays empty
|
|
406
|
+
st.key = '';
|
|
407
|
+
st.generation++;
|
|
408
|
+
if (st.host) {
|
|
409
|
+
const shown = st.host.dataset.figure;
|
|
410
|
+
st.host.remove();
|
|
411
|
+
st.host = null;
|
|
412
|
+
forgetIfUnshown(shown);
|
|
413
|
+
}
|
|
414
|
+
st.observer.takeRecords();
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const key = requestKey(kind, st.source, attrs);
|
|
418
|
+
if (key === st.key && st.host?.parentNode === el)
|
|
419
|
+
return;
|
|
420
|
+
st.key = key;
|
|
421
|
+
const gen = ++st.generation;
|
|
422
|
+
const update = !!st.host;
|
|
423
|
+
if (!st.host)
|
|
424
|
+
st.host = makeHost(kind, attrs);
|
|
425
|
+
else
|
|
426
|
+
st.host.classList.add('mpw-pending'); // keep the old figure up, dimmed, until the new one is ready
|
|
427
|
+
if (st.host.parentNode !== el)
|
|
428
|
+
el.appendChild(st.host);
|
|
429
|
+
st.observer.takeRecords(); // our own writes are not edits
|
|
430
|
+
const replaced = await typesetInto(st.host, kind, st.source, attrs, update, () => gen !== st.generation, wantsReplace(el) ? el : undefined);
|
|
431
|
+
if (replaced) { // the element is gone: the SVG is static from here on
|
|
432
|
+
st.observer.disconnect();
|
|
433
|
+
clearTimeout(st.timer);
|
|
434
|
+
st.host = null;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function onLiveMutations(el, st, records) {
|
|
438
|
+
let changed = false;
|
|
439
|
+
for (const m of records) {
|
|
440
|
+
if (st.host?.contains(m.target))
|
|
441
|
+
continue; // the figure and everything in it: our own writes
|
|
442
|
+
if (m.type === 'attributes' && PRESENTATIONAL.test(m.attributeName ?? ''))
|
|
443
|
+
continue;
|
|
444
|
+
changed = true;
|
|
445
|
+
}
|
|
446
|
+
if (!changed)
|
|
447
|
+
return;
|
|
448
|
+
absorbContent(el, st);
|
|
449
|
+
st.observer.takeRecords();
|
|
450
|
+
scheduleLive(el, st);
|
|
451
|
+
}
|
|
452
|
+
/** First sight of a custom element: read its source, start watching it, typeset. Later calls do nothing. */
|
|
453
|
+
function startLive(el) {
|
|
454
|
+
if (live.has(el))
|
|
455
|
+
return;
|
|
456
|
+
const st = { source: '', key: '', host: null, generation: 0, observer: new MutationObserver((recs) => onLiveMutations(el, st, recs)) };
|
|
457
|
+
live.set(el, st);
|
|
458
|
+
absorbContent(el, st);
|
|
459
|
+
st.observer.observe(el, { childList: true, characterData: true, subtree: true, attributes: true });
|
|
460
|
+
scheduleLive(el, st, true);
|
|
461
|
+
}
|
|
462
|
+
/** The `source` property of the two custom elements: read the diagram's source, or set it to typeset again. */
|
|
463
|
+
function liveSource(el) { return live.get(el)?.source ?? sourceOf(el); }
|
|
464
|
+
function setLiveSource(el, value) {
|
|
465
|
+
const st = live.get(el);
|
|
466
|
+
if (!st) {
|
|
467
|
+
el.textContent = value;
|
|
468
|
+
return;
|
|
469
|
+
} // not rendered yet: connecting it will read this
|
|
470
|
+
st.source = String(value).replace(/^\s*\n/, '').replace(/\s+$/, '');
|
|
471
|
+
scheduleLive(el, st);
|
|
472
|
+
}
|
|
473
|
+
export async function renderElement(el) {
|
|
474
|
+
if (el.tagName.toLowerCase() !== 'script') {
|
|
475
|
+
startLive(el);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (pending.has(el))
|
|
479
|
+
return;
|
|
480
|
+
pending.add(el);
|
|
481
|
+
const kind = kindOf(el), attrs = attrsOf(el), source = sourceOf(el);
|
|
482
|
+
replacedScripts.push({ kind, attrs, source });
|
|
483
|
+
const host = makeHost(kind, attrs);
|
|
484
|
+
el.replaceWith(host);
|
|
485
|
+
await typesetInto(host, kind, source, attrs, false);
|
|
486
|
+
}
|
|
487
|
+
function loaderOptions() {
|
|
488
|
+
const me = document.currentScript ?? document.querySelector('script[src*="auto.js"]');
|
|
489
|
+
const ds = me?.dataset ?? {};
|
|
490
|
+
const o = {};
|
|
491
|
+
if (ds.base)
|
|
492
|
+
o.bundleBaseUrl = new URL('bundles/', new URL(ds.base, location.href)).href;
|
|
493
|
+
if (ds.worker === 'off')
|
|
494
|
+
o.worker = false;
|
|
495
|
+
if (ds.cache === 'off')
|
|
496
|
+
o.cacheResults = false;
|
|
497
|
+
// `+opentype` adds to the defaults, a bare list replaces them (bundleList, shared with --prerender)
|
|
498
|
+
const bundles = bundleList(ds.bundles, DEFAULT_BUNDLES);
|
|
499
|
+
if (bundles)
|
|
500
|
+
o.bundles = bundles;
|
|
501
|
+
if (ds.snapshot === 'on' || ds.snapshot === 'auto')
|
|
502
|
+
o.snapshot = 'auto'; // opt in: 5.8 MB format, faster after the first figure
|
|
503
|
+
if (ds.prefetch === 'off')
|
|
504
|
+
o.prefetch = []; // default: the kinds the page contains
|
|
505
|
+
if (ds.figures && ds.figures !== 'off')
|
|
506
|
+
o.figuresBaseUrl = new URL(ds.figures.replace(/\/?$/, '/'), location.href).href;
|
|
507
|
+
if (ds.log && LOG_LEVELS.includes(ds.log))
|
|
508
|
+
o.logLevel = ds.log;
|
|
509
|
+
return o;
|
|
510
|
+
}
|
|
511
|
+
/** Render every diagram element currently in the document (and later ones, unless observe is off). */
|
|
512
|
+
export function autoRender(root = document) {
|
|
513
|
+
for (const el of Array.from(root.querySelectorAll(SELECTOR)))
|
|
514
|
+
void renderElement(el);
|
|
515
|
+
}
|
|
516
|
+
if (typeof document !== 'undefined' && typeof customElements !== 'undefined') {
|
|
517
|
+
for (const tag of ['tikz-diagram', 'metapost-diagram']) {
|
|
518
|
+
if (!customElements.get(tag))
|
|
519
|
+
customElements.define(tag, class extends HTMLElement {
|
|
520
|
+
connectedCallback() { queueMicrotask(() => { if (this.isConnected)
|
|
521
|
+
void renderElement(this); }); }
|
|
522
|
+
/** The diagram's source; setting it typesets the diagram again. */
|
|
523
|
+
get source() { return liveSource(this); }
|
|
524
|
+
set source(value) { setLiveSource(this, value); }
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
const style = document.createElement('style');
|
|
528
|
+
style.textContent = `.mpw-figure{display:inline-block;margin:0;vertical-align:middle;max-width:100%}.mpw-figure svg{max-width:100%;height:auto}.mpw-figure.mpw-pending>svg{opacity:.55;transition:opacity .2s}.mpw-status{font:13px system-ui,sans-serif;color:#777}.mpw-console{font:12px/1.4 ui-monospace,Menlo,monospace;white-space:pre-wrap;color:#a33;background:#fff5f5;border:1px solid #f0c0c0;padding:6px 8px;margin:4px 0 0;max-width:60em}`;
|
|
529
|
+
document.head.appendChild(style);
|
|
530
|
+
const start = () => {
|
|
531
|
+
autoRender();
|
|
532
|
+
const me = document.currentScript ?? document.querySelector('script[src*="auto.js"]');
|
|
533
|
+
if (me?.dataset?.observe !== 'off') {
|
|
534
|
+
new MutationObserver((muts) => {
|
|
535
|
+
for (const m of muts)
|
|
536
|
+
for (const n of Array.from(m.addedNodes)) {
|
|
537
|
+
if (!(n instanceof Element))
|
|
538
|
+
continue;
|
|
539
|
+
if (n.matches(SELECTOR))
|
|
540
|
+
void renderElement(n);
|
|
541
|
+
else
|
|
542
|
+
autoRender(n);
|
|
543
|
+
}
|
|
544
|
+
}).observe(document.documentElement, { childList: true, subtree: true });
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
if (document.readyState === 'loading')
|
|
548
|
+
document.addEventListener('DOMContentLoaded', start);
|
|
549
|
+
else
|
|
550
|
+
start();
|
|
551
|
+
const shared = () => (renderer ??= new AutoRenderer(loaderOptions()));
|
|
552
|
+
globalThis.mpTikzWasm = {
|
|
553
|
+
render: (req) => shared().render(req),
|
|
554
|
+
setLogLevel: (level) => shared().setLogLevel(level),
|
|
555
|
+
/** The figures rendered so far, each with its file name, hash, kind, source and SVG. */
|
|
556
|
+
figures: () => renderer?.figures() ?? [],
|
|
557
|
+
/** Save every figure as figure-HASH.svg (a folder you pick, or a zip); `{ zip: true }` forces the zip. */
|
|
558
|
+
saveFigures: (opts) => shared().saveFigures(opts),
|
|
559
|
+
/** Fonts (or images, .sty files) for this page's TeX runs; does not start the engine by itself. */
|
|
560
|
+
addFiles: (files) => shared().addFiles(files),
|
|
561
|
+
figureHash, figureName, autoRender, wrapTikz, wrapMetaPost,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
//# sourceMappingURL=auto.js.map
|