@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,927 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core.ts — the orchestrator (docs/01 §3). Runs in a Worker (browser) or on
|
|
3
|
+
* the main thread (Node). Owns one mplib.wasm instance for its lifetime and
|
|
4
|
+
* creates one tex.wasm instance per TeX invocation.
|
|
5
|
+
*/
|
|
6
|
+
import { MpJob, MP_FTYPE_NAMES, MPX_FTYPE_TFM, MPX_FTYPE_VF, mpxAddPath, mplibVersion, mplibBuildId } from './mplib.js';
|
|
7
|
+
import { runTex } from './texengine.js';
|
|
8
|
+
import { runDvisvgm } from './dvisvgm.js';
|
|
9
|
+
import { dviSpecials, describeLostPostScript } from './postscript.js';
|
|
10
|
+
import { TEXMF_ROOT } from './vfs/bundle.js';
|
|
11
|
+
import { listFiles, mkdirp, writeFileDeep, isUnloadedLazy, ensureLoaded, trackWrites } from './vfs/lazyfs.js';
|
|
12
|
+
import { scanTexBlocks, scanInputs } from './tex/scanner.js';
|
|
13
|
+
import { TexBridge, MemorySnippetCache, resolveEngine } from './tex/bridge.js';
|
|
14
|
+
import { parseMetaPostLog } from './diagnostics.js';
|
|
15
|
+
import { parseBibtexLog, parseMakeindexLog } from './texutils.js';
|
|
16
|
+
import { postProcessSvg } from './render/svg.js';
|
|
17
|
+
import { silentLogger, ms, plural } from './logger.js';
|
|
18
|
+
/** TEXMFVAR in the bundled texmf.cnf — where luaotfload keeps its font cache. */
|
|
19
|
+
const TEXMF_VAR = '/texmf-var';
|
|
20
|
+
/**
|
|
21
|
+
* luaotfload's font-name database, prebuilt against the bundled tree by
|
|
22
|
+
* scripts/make-fontdb.mjs and shipped in the `opentype` bundle under
|
|
23
|
+
* FONT_DB_SEED. Without it every fresh engine builds the database on its
|
|
24
|
+
* first font request by opening every face it can see (72 of them).
|
|
25
|
+
*/
|
|
26
|
+
const FONT_DB = 'luaotfload-names.lua.gz';
|
|
27
|
+
const FONT_DB_SEED = `${TEXMF_ROOT}/luaotfload/${FONT_DB}`;
|
|
28
|
+
const FONT_DB_PATH = `${TEXMF_VAR}/luatex-cache/generic/names/${FONT_DB}`;
|
|
29
|
+
/** The local references (href="#id") in an SVG that name no element in it. */
|
|
30
|
+
export function danglingRefs(svg) {
|
|
31
|
+
const ids = new Set();
|
|
32
|
+
for (const m of svg.matchAll(/\bid=(["'])([^"']+)\1/g))
|
|
33
|
+
ids.add(m[2]);
|
|
34
|
+
const missing = new Set();
|
|
35
|
+
for (const m of svg.matchAll(/href=(["'])#([^"']+)\1/g))
|
|
36
|
+
if (!ids.has(m[2]))
|
|
37
|
+
missing.add(m[2]);
|
|
38
|
+
return [...missing];
|
|
39
|
+
}
|
|
40
|
+
/** Where MetaPost looks for each file type (docs/06 §2). */
|
|
41
|
+
export const SEARCH_PATHS = {
|
|
42
|
+
[2 /* MpFtype.program */]: ['/work', `${TEXMF_ROOT}/metapost/base`, `${TEXMF_ROOT}/metapost`],
|
|
43
|
+
[6 /* MpFtype.memfile */]: [`${TEXMF_ROOT}/metapost/base`],
|
|
44
|
+
[7 /* MpFtype.metrics */]: [`${TEXMF_ROOT}/fonts/tfm`],
|
|
45
|
+
[9 /* MpFtype.font */]: [`${TEXMF_ROOT}/fonts/type1`],
|
|
46
|
+
[8 /* MpFtype.fontmap */]: [`${TEXMF_ROOT}/fonts/map`],
|
|
47
|
+
[10 /* MpFtype.encoding */]: [`${TEXMF_ROOT}/fonts/enc`],
|
|
48
|
+
[11 /* MpFtype.text */]: ['/work', TEXMF_ROOT],
|
|
49
|
+
};
|
|
50
|
+
/** bundle path prefixes to consult on a miss, per file type */
|
|
51
|
+
const BUNDLE_PREFIX = {
|
|
52
|
+
[2 /* MpFtype.program */]: ['metapost/'],
|
|
53
|
+
[6 /* MpFtype.memfile */]: ['metapost/'],
|
|
54
|
+
[7 /* MpFtype.metrics */]: ['fonts/tfm/'],
|
|
55
|
+
[9 /* MpFtype.font */]: ['fonts/type1/'],
|
|
56
|
+
[8 /* MpFtype.fontmap */]: ['fonts/map/'],
|
|
57
|
+
[10 /* MpFtype.encoding */]: ['fonts/enc/'],
|
|
58
|
+
[11 /* MpFtype.text */]: [''],
|
|
59
|
+
[MPX_FTYPE_TFM]: ['fonts/tfm/'],
|
|
60
|
+
[MPX_FTYPE_VF]: ['fonts/vf/'],
|
|
61
|
+
};
|
|
62
|
+
const DEFAULT_EXT = {
|
|
63
|
+
[2 /* MpFtype.program */]: '.mp', [6 /* MpFtype.memfile */]: '.mp', [7 /* MpFtype.metrics */]: '.tfm', [9 /* MpFtype.font */]: '.pfb',
|
|
64
|
+
[8 /* MpFtype.fontmap */]: '.map', [10 /* MpFtype.encoding */]: '.enc', [MPX_FTYPE_TFM]: '.tfm', [MPX_FTYPE_VF]: '.vf',
|
|
65
|
+
};
|
|
66
|
+
export class MetaPostCore {
|
|
67
|
+
env;
|
|
68
|
+
M;
|
|
69
|
+
cache = new MemorySnippetCache();
|
|
70
|
+
bridge;
|
|
71
|
+
version = { metapost: '', tex: 'pdfTeX 1.40.27 (DVI)', build: '' };
|
|
72
|
+
userFiles = new Set();
|
|
73
|
+
/**
|
|
74
|
+
* luaotfload's font cache (TEXMF_VAR), carried from one LuaTeX run to the
|
|
75
|
+
* next. Parsing an OpenType face costs about a second, and every TeX run
|
|
76
|
+
* gets a fresh filesystem, so without this each render pays that again.
|
|
77
|
+
*/
|
|
78
|
+
texmfVar = new Map();
|
|
79
|
+
get logger() { return this.env.logger ?? silentLogger; }
|
|
80
|
+
constructor(env) {
|
|
81
|
+
this.env = env;
|
|
82
|
+
}
|
|
83
|
+
async init() {
|
|
84
|
+
const o = this.env.options;
|
|
85
|
+
this.progress({ phase: 'loading', detail: 'mplib.wasm' });
|
|
86
|
+
// mplib's own stdout/stderr (dvitomp, the runtime) and, through the termLine hook, MetaPost's terminal
|
|
87
|
+
// as it is written: the raw 'log' event, the last lines for an abort message, and the debug log
|
|
88
|
+
const note = (s) => { this.recentOutput.push(s); if (this.recentOutput.length > 20)
|
|
89
|
+
this.recentOutput.shift(); this.env.onLog?.(s); this.logger.debug('metapost', s); };
|
|
90
|
+
this.M = await this.env.mplibFactory({
|
|
91
|
+
print: note,
|
|
92
|
+
printErr: note,
|
|
93
|
+
...(o.wasmUrls?.mplib ? { locateFile: (p) => (p.endsWith('.wasm') ? o.wasmUrls.mplib : p) } : {}),
|
|
94
|
+
});
|
|
95
|
+
const FS = this.M.FS;
|
|
96
|
+
mkdirp(FS, '/work');
|
|
97
|
+
this.installTexmf(FS, this.M.NODEFS);
|
|
98
|
+
FS.chdir('/work');
|
|
99
|
+
mpxAddPath(this.M, `${TEXMF_ROOT}/fonts/tfm`);
|
|
100
|
+
mpxAddPath(this.M, `${TEXMF_ROOT}/fonts/vf`);
|
|
101
|
+
this.M.mpwasmHooks = {
|
|
102
|
+
findFile: (name, ftype, mode) => this.findFile(name, ftype, mode),
|
|
103
|
+
makeText: (text, mode) => this.makeText(text, mode),
|
|
104
|
+
runScript: (script) => this.runScript(script),
|
|
105
|
+
termLine: note,
|
|
106
|
+
};
|
|
107
|
+
this.version.metapost = mplibVersion(this.M);
|
|
108
|
+
this.version.build = mplibBuildId(this.M);
|
|
109
|
+
if (this.env.texFactory && (o.tex ?? 'auto') !== 'none') {
|
|
110
|
+
this.bridge = new TexBridge({
|
|
111
|
+
texFactory: this.env.texFactory,
|
|
112
|
+
mplib: this.M,
|
|
113
|
+
setupTexFS: (T) => { mkdirp(T.FS, '/work'); this.installTexmf(T.FS, T.NODEFS); this.copyUserFiles(T); },
|
|
114
|
+
cache: this.cache,
|
|
115
|
+
texPreamble: o.texPreamble,
|
|
116
|
+
onLine: (l) => { this.env.onLog?.(l); this.logger.debug('tex', l); },
|
|
117
|
+
engineId: this.version.build,
|
|
118
|
+
formatIds: { plain: this.formatId('plain'), etex: this.formatId('etex'), latex: this.formatId('latex') },
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const b = this.env.bundles;
|
|
122
|
+
this.logger.info('host', `ready: MetaPost ${this.version.metapost}, ${this.bridge ? this.version.tex : 'no TeX'}${this.env.luatexFactory ? ', LuaTeX' : ''}${this.env.dvisvgmFactory ? ', dvisvgm' : ''}; ${b ? `${plural(b.manifests.length, 'bundle')}, ${plural(b.files.size, 'file')}` : this.env.texmfDir ? `texmf ${this.env.texmfDir}` : 'no texmf tree'}`);
|
|
123
|
+
}
|
|
124
|
+
progress(e) {
|
|
125
|
+
this.env.onProgress?.(e);
|
|
126
|
+
this.logger.debug('host', `${e.phase}${e.detail ? ` ${e.detail}` : ''}${e.current !== undefined ? ` ${e.current}` : ''}${e.total !== undefined ? ` (${e.total})` : ''}`);
|
|
127
|
+
}
|
|
128
|
+
/** The errors and warnings of a finished run, one record each; a MetaPost help paragraph is indented under its error. */
|
|
129
|
+
report(diagnostics) {
|
|
130
|
+
for (const d of diagnostics) {
|
|
131
|
+
const where = d.file ? `${d.file}${d.line ? `:${d.line}` : ''}: ` : d.line ? `line ${d.line}: ` : '';
|
|
132
|
+
const help = d.help?.length ? '\n' + d.help.map((h) => ` ${h}`).join('\n') : '';
|
|
133
|
+
this.logger.log(d.severity === 'error' ? 'error' : 'warn', d.source, `${where}${d.message}${help}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
formatId(name) {
|
|
137
|
+
const f = this.env.bundles?.files.get(`web2c/${name}.fmt`);
|
|
138
|
+
return f ? `${name}:${f.sha ?? f.size}` : `${name}:${this.env.texmfDir ?? 'local'}`;
|
|
139
|
+
}
|
|
140
|
+
installTexmf(FS, NODEFS) {
|
|
141
|
+
if (this.env.texmfDir) {
|
|
142
|
+
mkdirp(FS, TEXMF_ROOT);
|
|
143
|
+
FS.mount(NODEFS, { root: this.env.texmfDir }, TEXMF_ROOT);
|
|
144
|
+
}
|
|
145
|
+
else if (this.env.bundles) {
|
|
146
|
+
this.env.bundles.install(FS, TEXMF_ROOT);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
mkdirp(FS, TEXMF_ROOT);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** Restore the font cache this instance has accumulated (LuaTeX runs only). */
|
|
153
|
+
installTexmfVar(FS) {
|
|
154
|
+
mkdirp(FS, TEXMF_VAR);
|
|
155
|
+
if (!this.texmfVar.has(FONT_DB_PATH))
|
|
156
|
+
this.seedFontDb(FS);
|
|
157
|
+
for (const [p, bytes] of this.texmfVar)
|
|
158
|
+
writeFileDeep(FS, p, bytes);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Start the font cache from the prebuilt name database, if the tree has one
|
|
162
|
+
* (the `opentype` bundle, or a mounted texmfDir built by make-fontdb). Its
|
|
163
|
+
* paths are the /texmf ones every instance sees; luaotfload checks only the
|
|
164
|
+
* index version, and rescans by itself when a lookup misses.
|
|
165
|
+
*/
|
|
166
|
+
seedFontDb(FS) {
|
|
167
|
+
if (!FS.analyzePath(FONT_DB_SEED).exists)
|
|
168
|
+
return;
|
|
169
|
+
try {
|
|
170
|
+
ensureLoaded(FS, FONT_DB_SEED);
|
|
171
|
+
this.texmfVar.set(FONT_DB_PATH, FS.readFile(FONT_DB_SEED, { encoding: 'binary' }));
|
|
172
|
+
this.logger.trace('host', `font cache: seeded from ${FONT_DB_SEED}`);
|
|
173
|
+
}
|
|
174
|
+
catch (e) {
|
|
175
|
+
this.logger.warn('host', `font cache: cannot read ${FONT_DB_SEED}: ${e?.message ?? e}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** Keep whatever luaotfload wrote, so the next run starts warm. */
|
|
179
|
+
collectTexmfVar(FS) {
|
|
180
|
+
const kept = new Map();
|
|
181
|
+
try {
|
|
182
|
+
for (const p of listFiles(FS, TEXMF_VAR))
|
|
183
|
+
kept.set(p, FS.readFile(p, { encoding: 'binary' }));
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return;
|
|
187
|
+
} // nothing was written; keep what we had
|
|
188
|
+
let bytes = 0;
|
|
189
|
+
for (const v of kept.values())
|
|
190
|
+
bytes += v.length;
|
|
191
|
+
this.logger.trace('host', `font cache: ${plural(kept.size, 'file')}, ${(bytes / 1024).toFixed(0)} KB`);
|
|
192
|
+
this.texmfVar = kept;
|
|
193
|
+
}
|
|
194
|
+
copyUserFiles(T) {
|
|
195
|
+
// TeX may \input files the user supplied next to the job
|
|
196
|
+
for (const p of this.userFiles) {
|
|
197
|
+
try {
|
|
198
|
+
writeFileDeep(T.FS, p, this.M.FS.readFile(p, { encoding: 'binary' }));
|
|
199
|
+
}
|
|
200
|
+
catch { /* ignore */ }
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// ---- host hooks ---------------------------------------------------------
|
|
204
|
+
/** Last-chance file resolution when the C search failed: consult bundles, then the user hook. */
|
|
205
|
+
findFile(name, ftype, mode) {
|
|
206
|
+
if (mode[0] !== 'r')
|
|
207
|
+
return null;
|
|
208
|
+
const r = this.findFileImpl(name, ftype);
|
|
209
|
+
this.logger.trace('host', `find_file ${name} (${ftypeName(ftype)}): ${r ?? 'not found'}`);
|
|
210
|
+
return r;
|
|
211
|
+
}
|
|
212
|
+
findFileImpl(name, ftype) {
|
|
213
|
+
const base = name.slice(name.lastIndexOf('/') + 1);
|
|
214
|
+
const ext = DEFAULT_EXT[ftype];
|
|
215
|
+
const candidates = ext && !base.endsWith(ext) ? [base, base + ext] : [base];
|
|
216
|
+
const b = this.env.bundles;
|
|
217
|
+
if (b) {
|
|
218
|
+
for (const c of candidates) {
|
|
219
|
+
const f = b.lookup(c, BUNDLE_PREFIX[ftype] ?? ['']);
|
|
220
|
+
if (f) {
|
|
221
|
+
const full = `${TEXMF_ROOT}/${f.path}`;
|
|
222
|
+
if (isUnloadedLazy(this.M.FS, full)) {
|
|
223
|
+
try {
|
|
224
|
+
ensureLoaded(this.M.FS, full);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return full;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const hook = this.env.options.onFindFile;
|
|
235
|
+
if (hook) {
|
|
236
|
+
const r = hook(name, ['terminal', 'error', 'program', 'log', 'postscript', 'bitmap', 'memfile', 'metrics', 'fontmap', 'font', 'encoding', 'text'][Math.min(ftype, 11)], 'r');
|
|
237
|
+
if (r)
|
|
238
|
+
return r;
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
// per-run state
|
|
243
|
+
chain = [];
|
|
244
|
+
misses = [];
|
|
245
|
+
stats;
|
|
246
|
+
engine = 'plain';
|
|
247
|
+
texDiagnostics = [];
|
|
248
|
+
makeText(text, mode) {
|
|
249
|
+
const user = this.env.options.makeText;
|
|
250
|
+
if (user) {
|
|
251
|
+
const r = user(text, mode === 1);
|
|
252
|
+
if (r !== undefined)
|
|
253
|
+
return r;
|
|
254
|
+
}
|
|
255
|
+
if (mode === 1) {
|
|
256
|
+
this.chain.push(text);
|
|
257
|
+
return '';
|
|
258
|
+
}
|
|
259
|
+
if (this.engine === 'none' || !this.bridge) {
|
|
260
|
+
this.misses.push({ key: '', chain: [...this.chain], body: text });
|
|
261
|
+
return 'nullpicture';
|
|
262
|
+
}
|
|
263
|
+
const key = this.bridge.key(this.engine, this.chain, text);
|
|
264
|
+
const hit = this.cache.get(key);
|
|
265
|
+
this.logger.trace('host', `btex ${hit !== undefined ? 'cached' : 'miss'}: ${JSON.stringify(text.length > 60 ? text.slice(0, 57) + '...' : text)}`);
|
|
266
|
+
// mplib injects the returned string as a ONE-line pseudo-file, so an .mpx
|
|
267
|
+
// chunk (several lines, no comments) is joined with spaces (docs/04 §5.1).
|
|
268
|
+
if (hit !== undefined) {
|
|
269
|
+
this.stats.snippetCacheHits++;
|
|
270
|
+
return hit.replace(/\r?\n/g, ' ');
|
|
271
|
+
}
|
|
272
|
+
this.stats.snippetCacheMisses++;
|
|
273
|
+
this.misses.push({ key, chain: [...this.chain], body: text });
|
|
274
|
+
return 'nullpicture';
|
|
275
|
+
}
|
|
276
|
+
runScript(script) {
|
|
277
|
+
const f = this.env.options.runScript;
|
|
278
|
+
if (!f)
|
|
279
|
+
return ''; // runscript is opt-in (docs/10 §3)
|
|
280
|
+
try {
|
|
281
|
+
return String(f(script) ?? '');
|
|
282
|
+
}
|
|
283
|
+
catch (e) {
|
|
284
|
+
return `errmessage("runscript: ${String(e).replace(/"/g, "'")}")`;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// ---- public operations --------------------------------------------------
|
|
288
|
+
addFiles(files) {
|
|
289
|
+
for (const [name, data] of Object.entries(files)) {
|
|
290
|
+
const p = name.startsWith('/') ? name : `/work/${name}`;
|
|
291
|
+
writeFileDeep(this.M.FS, p, data);
|
|
292
|
+
this.userFiles.add(p);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Take files off this instance that addFiles (or a run's `files`) put there, named as they were
|
|
297
|
+
* given; with no names, all of them. They otherwise stay for the instance's lifetime, so a host
|
|
298
|
+
* typesetting one document after another would hand the first one's .aux to the next.
|
|
299
|
+
*/
|
|
300
|
+
removeFiles(names) {
|
|
301
|
+
const paths = names ? names.map((n) => (n.startsWith('/') ? n : `/work/${n}`)) : [...this.userFiles];
|
|
302
|
+
for (const p of paths) {
|
|
303
|
+
if (!this.userFiles.delete(p))
|
|
304
|
+
continue;
|
|
305
|
+
try {
|
|
306
|
+
this.M.FS.unlink(p);
|
|
307
|
+
}
|
|
308
|
+
catch { /* ignore */ }
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
clearCache() { this.cache.clear(); }
|
|
312
|
+
async run(source, ro = {}) {
|
|
313
|
+
const o = this.env.options;
|
|
314
|
+
const t0 = now();
|
|
315
|
+
this.stats = { totalMs: 0, metapostMs: 0, texMs: 0, texRuns: 0, metapostRuns: 0, snippetCacheHits: 0, snippetCacheMisses: 0 };
|
|
316
|
+
this.texDiagnostics = [];
|
|
317
|
+
this.recentOutput = [];
|
|
318
|
+
const FS = this.M.FS;
|
|
319
|
+
const jobName = ro.jobName ?? 'job';
|
|
320
|
+
if (ro.files)
|
|
321
|
+
this.addFiles(ro.files);
|
|
322
|
+
const before = new Set(listFiles(FS, '/work'));
|
|
323
|
+
writeFileDeep(FS, `/work/${jobName}.mp`, source);
|
|
324
|
+
// Emscripten's MEMFS mtime granularity is fine for the classic path's staleness check
|
|
325
|
+
const formats = new Set(Array.isArray(ro.format) ? ro.format : [ro.format ?? 'svg']);
|
|
326
|
+
// 1. scan (docs/05 §3)
|
|
327
|
+
this.progress({ phase: 'scanning' });
|
|
328
|
+
const blocks = this.scanAll(source, `${jobName}.mp`);
|
|
329
|
+
this.engine = resolveEngine(ro.tex ?? o.tex ?? 'auto', blocks, source);
|
|
330
|
+
const useBridge = !!this.bridge && this.engine !== 'none' && (o.extensions ?? true);
|
|
331
|
+
const nBtex = blocks.filter((b) => b.kind === 'btex').length;
|
|
332
|
+
this.logger.info('host', `run ${jobName}.mp: ${source.length} bytes, ${[...formats].join('+')}${nBtex ? `, ${plural(nBtex, 'btex block')} via ${this.engine}` : ''}`);
|
|
333
|
+
// 2. typeset the misses found by the scan
|
|
334
|
+
if (useBridge && blocks.some((b) => b.kind === 'btex')) {
|
|
335
|
+
const { misses } = this.bridge.misses(this.engine, blocks);
|
|
336
|
+
if (misses.length)
|
|
337
|
+
await this.typeset(misses);
|
|
338
|
+
}
|
|
339
|
+
// 3.–5. run MetaPost, re-typesetting on cache misses until a fixpoint
|
|
340
|
+
let job = null;
|
|
341
|
+
const maxRuns = 1 + (o.maxTexRuns ?? 5);
|
|
342
|
+
for (let iter = 0; iter < maxRuns; iter++) {
|
|
343
|
+
job?.free();
|
|
344
|
+
this.chain = [];
|
|
345
|
+
this.misses = [];
|
|
346
|
+
this.progress({ phase: 'running', current: iter + 1 });
|
|
347
|
+
job = this.runMetaPost(source, jobName, ro);
|
|
348
|
+
this.logger.debug('host', `MetaPost pass ${iter + 1}: history ${job.history}${this.misses.length ? `, ${plural(this.misses.length, 'label')} to typeset` : ''}`);
|
|
349
|
+
if (this.misses.length === 0 || !useBridge)
|
|
350
|
+
break;
|
|
351
|
+
if (iter === maxRuns - 1) {
|
|
352
|
+
this.texDiagnostics.push({ severity: 'error', source: 'tex', message: `btex blocks still unresolved after ${maxRuns - 1} TeX runs: ${this.misses.map((m) => JSON.stringify(m.body.slice(0, 40))).join(', ')}` });
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
await this.typeset(this.misses);
|
|
356
|
+
}
|
|
357
|
+
const j = job;
|
|
358
|
+
// 6. render
|
|
359
|
+
this.progress({ phase: 'rendering' });
|
|
360
|
+
const figures = [];
|
|
361
|
+
const n = j.figureCount;
|
|
362
|
+
// prologues: an explicit option wins; otherwise the document's own value
|
|
363
|
+
// at the time each figure was shipped out (like mpost; patch 0009), except
|
|
364
|
+
// that SVG defaults to 3 when that value is 0, because prologues<3 SVG has
|
|
365
|
+
// no usable text (docs/07 §3).
|
|
366
|
+
for (let i = 0; i < n; i++) {
|
|
367
|
+
const dims = j.dims(i);
|
|
368
|
+
const fig = { charcode: j.charcode(i), bbox: dims.bbox, width: dims.width, height: dims.height, depth: dims.depth, italicCorrection: dims.italicCorrection };
|
|
369
|
+
if (formats.has('svg')) {
|
|
370
|
+
const svg = j.svg(i, ro.prologues ?? (dims.prologues > 0 ? dims.prologues : 3));
|
|
371
|
+
if (svg != null)
|
|
372
|
+
fig.svg = postProcessSvg(svg, ro.svg ?? {}, i);
|
|
373
|
+
}
|
|
374
|
+
if (formats.has('eps')) {
|
|
375
|
+
const ps = j.ps(i, ro.prologues ?? -1, ro.procset ?? -1);
|
|
376
|
+
if (ps != null)
|
|
377
|
+
fig.eps = ps;
|
|
378
|
+
}
|
|
379
|
+
if (formats.has('json')) {
|
|
380
|
+
const js = j.json(i);
|
|
381
|
+
if (js != null)
|
|
382
|
+
fig.json = JSON.parse(js);
|
|
383
|
+
}
|
|
384
|
+
figures.push(fig);
|
|
385
|
+
}
|
|
386
|
+
// 7. finish
|
|
387
|
+
const termOut = j.termOut;
|
|
388
|
+
const logOut = j.logOut;
|
|
389
|
+
const history = Math.max(0, Math.min(4, j.history));
|
|
390
|
+
const diagnostics = [...parseMetaPostLog(termOut), ...this.texDiagnostics];
|
|
391
|
+
if (this.misses.length && !useBridge) {
|
|
392
|
+
diagnostics.push({ severity: 'error', source: 'host', message: this.bridge ? 'btex used but TeX is disabled (tex: "none")' : 'btex used but tex.wasm is not available in this build', snippet: this.misses[0].body });
|
|
393
|
+
}
|
|
394
|
+
const artifacts = {};
|
|
395
|
+
for (const p of listFiles(FS, '/work')) {
|
|
396
|
+
if (before.has(p) || p === `/work/${jobName}.mp`)
|
|
397
|
+
continue;
|
|
398
|
+
if (/\.(mpx|dvi|tex)$/.test(p) && /\/mpx[0-9a-z]+\./.test(p)) {
|
|
399
|
+
try {
|
|
400
|
+
FS.unlink(p);
|
|
401
|
+
}
|
|
402
|
+
catch { /* ignore */ }
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
artifacts[p.slice('/work/'.length)] = FS.readFile(p, { encoding: 'binary' });
|
|
407
|
+
}
|
|
408
|
+
catch { /* ignore */ }
|
|
409
|
+
}
|
|
410
|
+
if (logOut)
|
|
411
|
+
artifacts[`${jobName}.log`] = new TextEncoder().encode(logOut);
|
|
412
|
+
if (this.logger.enabled('trace'))
|
|
413
|
+
for (const f of j.openedFiles)
|
|
414
|
+
this.logger.trace('metapost', `opened ${f.name} (${ftypeName(f.type)}) = ${f.path}`);
|
|
415
|
+
j.free();
|
|
416
|
+
this.stats.totalMs = now() - t0;
|
|
417
|
+
const status = history === 0 ? 'ok' : history === 1 ? 'warning' : history === 2 ? 'error' : 'fatal';
|
|
418
|
+
this.report(diagnostics);
|
|
419
|
+
if (history >= 3 && !diagnostics.some((d) => d.severity === 'error'))
|
|
420
|
+
this.logger.error('metapost', `fatal error (history ${history}): ${termOut.trimEnd().split('\n').pop() ?? ''}`);
|
|
421
|
+
this.logger.info('host', `run ${jobName}.mp: ${status}, ${plural(figures.length, 'figure')} in ${ms(this.stats.totalMs)} (MetaPost ${ms(this.stats.metapostMs)}${this.stats.metapostRuns > 1 ? ` in ${this.stats.metapostRuns} passes` : ''}${this.stats.texRuns ? `, TeX ${ms(this.stats.texMs)} in ${plural(this.stats.texRuns, 'run')}` : ''}${this.stats.snippetCacheHits ? `, ${plural(this.stats.snippetCacheHits, 'cached label')}` : ''})`);
|
|
422
|
+
return { status, history, figures, log: termOut, texLog: this.lastTexLog || undefined, diagnostics, stats: this.stats, artifacts };
|
|
423
|
+
}
|
|
424
|
+
lastTexLog = '';
|
|
425
|
+
recentOutput = [];
|
|
426
|
+
/**
|
|
427
|
+
* Typeset a complete LaTeX (or plain TeX) document with tex.wasm and convert
|
|
428
|
+
* every page to SVG with dvisvgm.wasm — the TikZ/PGF pipeline. Text becomes
|
|
429
|
+
* glyph outlines unless fonts: 'woff2' is requested.
|
|
430
|
+
*/
|
|
431
|
+
async latex(source, lo = {}) {
|
|
432
|
+
const t0 = now();
|
|
433
|
+
if (!this.env.texFactory)
|
|
434
|
+
throw new Error('mp-tikz-wasm: tex.wasm is not available in this build');
|
|
435
|
+
const pdfOut = lo.output === 'pdf';
|
|
436
|
+
if (!pdfOut && !this.env.dvisvgmFactory)
|
|
437
|
+
throw new Error('mp-tikz-wasm: dvisvgm.wasm is not available in this build');
|
|
438
|
+
const job = lo.jobName ?? 'doc';
|
|
439
|
+
let engine = lo.engine ?? 'latex';
|
|
440
|
+
if (engine === 'auto')
|
|
441
|
+
engine = needsLuaTeX(source) ? 'lualatex' : /\\bye\b/.test(source) ? 'plain' : 'latex';
|
|
442
|
+
const lua = engine === 'lualatex' || engine === 'luatex';
|
|
443
|
+
if (lua && !this.env.luatexFactory)
|
|
444
|
+
throw new Error('mp-tikz-wasm: luatex.wasm is not available in this build');
|
|
445
|
+
// 'plain' is plain TeX with e-TeX (TeX Live's etex), which PGF requires;
|
|
446
|
+
// 'tex' is Knuth-compatible plain.fmt without it; the LuaTeX engines use
|
|
447
|
+
// TeX Live's DVI-mode formats dvilualatex / dviluatex.
|
|
448
|
+
let fmt = engine === 'plain' ? 'etex' : engine === 'lualatex' ? 'dvilualatex' : engine === 'luatex' ? 'dviluatex' : engine;
|
|
449
|
+
const progname = engine === 'plain' ? 'etex' : lua ? fmt : engine;
|
|
450
|
+
// the pre-warmed snapshot: latex.fmt with pgf/pgfplots/tikz-cd preloaded
|
|
451
|
+
const snapshot = lo.snapshot ?? this.env.options.snapshot ?? 'auto';
|
|
452
|
+
// (not for PDF output: the snapshot has PGF's dvisvgm driver built in)
|
|
453
|
+
if (engine === 'latex' && snapshot !== 'none' && !pdfOut && this.hasSnapshot()) {
|
|
454
|
+
const usesTikz = /\\usepackage\s*(\[[^\]]*\])?\s*\{[^}]*\b(tikz|pgfplots|tikz-cd)\b|\\documentclass\s*\[[^\]]*\btikz\b/.test(source);
|
|
455
|
+
if (snapshot === 'tikz' || usesTikz)
|
|
456
|
+
fmt = 'tikz';
|
|
457
|
+
}
|
|
458
|
+
if (lo.files)
|
|
459
|
+
this.addFiles(lo.files);
|
|
460
|
+
// PGF's default DVI driver (dvips) draws with PostScript specials, which
|
|
461
|
+
// dvisvgm can only interpret through Ghostscript. PGF ships a dvisvgm
|
|
462
|
+
// driver that emits SVG directly, so select it unless told otherwise --
|
|
463
|
+
// or, for PDF output, the driver of the engine writing the PDF.
|
|
464
|
+
let doc = source;
|
|
465
|
+
if ((lo.pgfDriver ?? 'dvisvgm') === 'dvisvgm') {
|
|
466
|
+
const driver = !pdfOut ? 'pgfsys-dvisvgm.def' : lua ? 'pgfsys-luatex.def' : 'pgfsys-pdftex.def';
|
|
467
|
+
const line = `\\def\\pgfsysdriver{${driver}}`;
|
|
468
|
+
doc = source.startsWith('%&') ? source.replace(/^([^\n]*\n)/, `$1${line}`) : line + source; // same first line: no line-number shift
|
|
469
|
+
}
|
|
470
|
+
this.logger.info('host', `latex ${job}.tex: ${source.length} bytes, engine ${engine}, format ${fmt}${pdfOut ? ', PDF output' : ''}`);
|
|
471
|
+
this.progress({ phase: 'typesetting', detail: engine });
|
|
472
|
+
// 1. TeX → DVI
|
|
473
|
+
let dvi = null;
|
|
474
|
+
let pdf = null;
|
|
475
|
+
const outName = `${job}.${pdfOut ? 'pdf' : 'dvi'}`;
|
|
476
|
+
let texLog = '';
|
|
477
|
+
const artifacts = {};
|
|
478
|
+
// the files TeX opens for writing: a host's own .aux, passed back in for the next pass,
|
|
479
|
+
// is still TeX's to return when TeX writes it again
|
|
480
|
+
let texWrote = new Set();
|
|
481
|
+
const date = lo.date instanceof Date ? lo.date.getTime() / 1000 : typeof lo.date === 'number' ? lo.date / 1000 : lo.date;
|
|
482
|
+
if (typeof date === 'number' && !Number.isFinite(date))
|
|
483
|
+
throw new Error('mp-tikz-wasm: latex(): date is not a valid time');
|
|
484
|
+
const tex = await runTex(lua ? this.env.luatexFactory : this.env.texFactory, {
|
|
485
|
+
program: lua ? '/bin/luatex' : '/bin/pdftex',
|
|
486
|
+
date,
|
|
487
|
+
// LuaTeX has no -parse-first-line option (%& lines are honoured through texmf.cnf instead)
|
|
488
|
+
// -output-format=pdf switches pdfTeX (\pdfoutput) or LuaTeX (\outputmode) to PDF after the
|
|
489
|
+
// format is loaded, so the same formats serve both outputs and the source is left untouched
|
|
490
|
+
// LuaTeX shows a fixed date in the local time zone unless told --utc (pdfTeX, under
|
|
491
|
+
// FORCE_SOURCE_DATE, always uses UTC): without it a 'fixed' document's \time and PDF dates,
|
|
492
|
+
// and \today west of Greenwich, depended on the host's time zone. 'now' keeps local time.
|
|
493
|
+
args: [`-fmt=${fmt}`, `-progname=${progname}`, '-interaction=nonstopmode', ...(lua ? (date === 'now' ? [] : ['--utc']) : ['-parse-first-line']), ...(pdfOut ? ['-output-format=pdf'] : []), `-jobname=${job}`, `${job}.tex`],
|
|
494
|
+
setup: (M) => { mkdirp(M.FS, '/work'); this.installTexmf(M.FS, M.NODEFS); if (lua)
|
|
495
|
+
this.installTexmfVar(M.FS); this.copyUserFiles(M); writeFileDeep(M.FS, `/work/${job}.tex`, doc); M.FS.chdir('/work'); texWrote = trackWrites(M.FS); },
|
|
496
|
+
collect: (M) => {
|
|
497
|
+
if (lua)
|
|
498
|
+
this.collectTexmfVar(M.FS);
|
|
499
|
+
let out;
|
|
500
|
+
try {
|
|
501
|
+
out = M.FS.readFile(`/work/${outName}`, { encoding: 'binary' });
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
out = null;
|
|
505
|
+
}
|
|
506
|
+
if (pdfOut)
|
|
507
|
+
pdf = out;
|
|
508
|
+
else
|
|
509
|
+
dvi = out;
|
|
510
|
+
try {
|
|
511
|
+
texLog = M.FS.readFile(`/work/${job}.log`, { encoding: 'utf8' });
|
|
512
|
+
}
|
|
513
|
+
catch {
|
|
514
|
+
texLog = '';
|
|
515
|
+
}
|
|
516
|
+
for (const p of listFiles(M.FS, '/work')) {
|
|
517
|
+
const name = p.slice('/work/'.length);
|
|
518
|
+
// a file the host passed in comes back only if TeX wrote it (doc.aux on the third pass)
|
|
519
|
+
if (name === `${job}.tex` || name === outName || name === `${job}.log` || (this.userFiles.has(p) && !texWrote.has(p)))
|
|
520
|
+
continue;
|
|
521
|
+
try {
|
|
522
|
+
artifacts[name] = M.FS.readFile(p, { encoding: 'binary' });
|
|
523
|
+
}
|
|
524
|
+
catch { /* ignore */ }
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
onLine: (l) => { this.env.onLog?.(l); this.logger.debug('tex', l); },
|
|
528
|
+
});
|
|
529
|
+
const written = (pdfOut ? pdf : dvi);
|
|
530
|
+
this.logger.info('host', `TeX: ${written ? `${written.length}-byte ${pdfOut ? 'PDF' : 'DVI'}` : `no ${pdfOut ? 'PDF' : 'DVI'}`} in ${ms(tex.ms)}${tex.exitCode ? `, exit ${tex.exitCode}` : ''}`);
|
|
531
|
+
const diagnostics = parseTexLogDocument(texLog || tex.log, `${job}.tex`).filter((d) => {
|
|
532
|
+
// environmental noise, not the document's doing: there is never a shell here
|
|
533
|
+
if (/^shellesc: Shell escape disabled/.test(d.message))
|
|
534
|
+
return false;
|
|
535
|
+
if (/^epstopdf: Shell escape feature is not enabled/.test(d.message))
|
|
536
|
+
return false; // PDF output: graphics' epstopdf
|
|
537
|
+
// Without the `opentype` bundle LuaLaTeX probes for the OpenType font loader, does
|
|
538
|
+
// not find it and reverts to OT1 and the Type 1 fonts. That is environmental, not the
|
|
539
|
+
// document's doing. Anything else luaotfload says -- a missing face, a bad feature --
|
|
540
|
+
// is about this document and has to get through.
|
|
541
|
+
if (/luaotfload/.test(d.message) && /not found|reverting to OT1/.test(d.message))
|
|
542
|
+
return false;
|
|
543
|
+
// the snapshot preloads pgfplots; its compat notice only concerns documents that use it
|
|
544
|
+
if (fmt === 'tikz' && /^pgfplots: running in backwards compatibility mode/.test(d.message) && !/pgfplots/.test(source))
|
|
545
|
+
return false;
|
|
546
|
+
return true;
|
|
547
|
+
});
|
|
548
|
+
// LaTeX runs are full of benign package warnings, so unlike MetaPost's
|
|
549
|
+
// history the status only turns on errors: ok | error | fatal (no DVI).
|
|
550
|
+
let status = (tex.exitCode === 0 && !diagnostics.some((d) => d.severity === 'error')) ? 'ok' : 'error';
|
|
551
|
+
const pages = [];
|
|
552
|
+
const danglingPages = [];
|
|
553
|
+
let dvisvgmLog = '';
|
|
554
|
+
let dvisvgmMs = 0;
|
|
555
|
+
// 2. DVI → SVG (PDF output is finished: TeX wrote it)
|
|
556
|
+
if (pdfOut) {
|
|
557
|
+
if (!pdf) {
|
|
558
|
+
status = diagnostics.some((d) => d.severity === 'error') ? 'error' : 'fatal';
|
|
559
|
+
if (!diagnostics.length)
|
|
560
|
+
diagnostics.push({ severity: 'error', source: 'tex', message: `TeX produced no PDF (exit ${tex.exitCode})` });
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
else if (dvi) {
|
|
564
|
+
const dviBytes = dvi;
|
|
565
|
+
this.progress({ phase: 'rendering', detail: 'dvisvgm' });
|
|
566
|
+
// dvisvgm's verbosity is a bit set: 3 = errors and warnings, 7 = also its progress messages, wanted when they are being logged
|
|
567
|
+
const args = ['--no-mktexmf', '--exact-bbox', this.logger.enabled('debug') ? '-v7' : '-v3', `--page=${lo.pages === undefined || lo.pages === 'all' ? '1-' : String(lo.pages)}`, '-o', `${job}-%p.svg`];
|
|
568
|
+
if (lo.fonts === 'woff2')
|
|
569
|
+
args.push('--font-format=woff2');
|
|
570
|
+
else
|
|
571
|
+
args.push('--no-fonts');
|
|
572
|
+
if (lo.bbox)
|
|
573
|
+
args.push(`--bbox=${lo.bbox}`);
|
|
574
|
+
args.push(...(lo.dvisvgmArgs ?? []), `${job}.dvi`);
|
|
575
|
+
// PostScript that dvisvgm can only draw through Ghostscript (postscript.ts).
|
|
576
|
+
// Nearly every LaTeX DVI has some PostScript that draws nothing (the l3
|
|
577
|
+
// kernel's header, hyperref's pdfmarks); Ghostscript is loaded only for the
|
|
578
|
+
// rest, so every other document renders exactly as without it.
|
|
579
|
+
const lost = describeLostPostScript(dviSpecials(dviBytes));
|
|
580
|
+
const gs = lost && this.env.ghostscript ? await this.env.ghostscript() : null;
|
|
581
|
+
if (gs)
|
|
582
|
+
this.logger.info('host', `PostScript (${lost}): dvisvgm runs it through Ghostscript`);
|
|
583
|
+
let r;
|
|
584
|
+
try {
|
|
585
|
+
r = await runDvisvgm(this.env.dvisvgmFactory, {
|
|
586
|
+
args,
|
|
587
|
+
...(gs ? { module: { gsBridge: gs.bridge() } } : {}),
|
|
588
|
+
// Ghostscript opens files itself (an EPS is `(fig.eps) run`): it sees dvisvgm's /work and /texmf
|
|
589
|
+
setup: (M) => {
|
|
590
|
+
mkdirp(M.FS, '/work');
|
|
591
|
+
this.installTexmf(M.FS, M.NODEFS);
|
|
592
|
+
this.copyUserFiles(M);
|
|
593
|
+
// what TeX wrote beside the job (an EPS from filecontents, say), as a native
|
|
594
|
+
// dvisvgm run in the same directory would see it
|
|
595
|
+
for (const [name, data] of Object.entries(artifacts))
|
|
596
|
+
writeFileDeep(M.FS, `/work/${name}`, data);
|
|
597
|
+
writeFileDeep(M.FS, `/work/${job}.dvi`, dviBytes);
|
|
598
|
+
M.FS.chdir('/work');
|
|
599
|
+
// Ghostscript runs a document's PostScript without SAFER (dvisvgm asks for
|
|
600
|
+
// -dDELAYSAFER and never sets it), so it gets only in-memory trees: /work, and
|
|
601
|
+
// /texmf unless that is a real directory (texmfDir, mounted with NODEFS)
|
|
602
|
+
gs?.mount(M.FS, this.env.texmfDir ? ['/work'] : ['/work', TEXMF_ROOT]);
|
|
603
|
+
},
|
|
604
|
+
collect: (M) => {
|
|
605
|
+
const names = listFiles(M.FS, '/work').filter((p) => new RegExp(`^/work/${job.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}-(\\d+)\\.svg$`).test(p))
|
|
606
|
+
.sort((a, b) => Number(/-(\d+)\.svg$/.exec(a)[1]) - Number(/-(\d+)\.svg$/.exec(b)[1]));
|
|
607
|
+
for (let i = 0; i < names.length; i++) {
|
|
608
|
+
const svg = M.FS.readFile(names[i], { encoding: 'utf8' });
|
|
609
|
+
const dangling = danglingRefs(svg);
|
|
610
|
+
if (dangling.length)
|
|
611
|
+
danglingPages.push(`page ${i + 1}: ${plural(dangling.length, 'reference')} (${dangling.slice(0, 3).join(', ')}${dangling.length > 3 ? ', …' : ''})`);
|
|
612
|
+
pages.push(lo.svg ? postProcessSvg(svg, lo.svg, i) : svg);
|
|
613
|
+
}
|
|
614
|
+
},
|
|
615
|
+
onLine: (l) => { this.env.onLog?.(l); this.logger.debug('dvisvgm', l); },
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
finally {
|
|
619
|
+
gs?.unmount();
|
|
620
|
+
}
|
|
621
|
+
dvisvgmLog = r.log;
|
|
622
|
+
dvisvgmMs = r.ms;
|
|
623
|
+
// dvisvgm carries on when a font has no outline file (no map entry, no .pfb):
|
|
624
|
+
// it writes <use> references to glyphs it never defines and exits 0, so the
|
|
625
|
+
// text silently vanishes -- and the tags would cache that. Seen with a stale,
|
|
626
|
+
// HTTP-cached font map that predated a font. Make both signs of it errors.
|
|
627
|
+
const noFont = [...new Set([...r.log.matchAll(/no font file found for '([^']+)'/g)].map((m) => m[1]))];
|
|
628
|
+
if (noFont.length) {
|
|
629
|
+
status = 'error';
|
|
630
|
+
diagnostics.push({ severity: 'error', source: 'host', message: `dvisvgm found no font file for ${noFont.join(', ')}: their glyphs are missing from the SVG (no entry in the font map, or the font is not bundled)` });
|
|
631
|
+
}
|
|
632
|
+
else if (danglingPages.length) {
|
|
633
|
+
status = 'error';
|
|
634
|
+
diagnostics.push({ severity: 'error', source: 'host', message: `the SVG refers to glyphs it does not define -- ${danglingPages.join('; ')}` });
|
|
635
|
+
}
|
|
636
|
+
// PostScript that carried part of the picture and was skipped: no Ghostscript
|
|
637
|
+
// (not enabled, or not loadable), or Ghostscript gave up on it
|
|
638
|
+
if (lost && (!gs || /PostScript specials? ignored/.test(r.log))) {
|
|
639
|
+
const why = !this.env.ghostscript ? "dvisvgm runs PostScript through Ghostscript: add the 'ghostscript' bundle"
|
|
640
|
+
: !gs ? 'the Ghostscript module could not be loaded' : 'Ghostscript did not run it';
|
|
641
|
+
diagnostics.push({ severity: 'warning', source: 'host', message: `PostScript was ignored, so the SVG is missing ${lost}: ${why}`, help: ['TikZ, pgfplots, tikz-cd and MetaPost draw without PostScript and are unaffected'] });
|
|
642
|
+
}
|
|
643
|
+
if (gs) {
|
|
644
|
+
for (const m of new Set([...r.log.matchAll(/PostScript error: (.*)/g)].map((x) => x[1].trim()))) {
|
|
645
|
+
diagnostics.push({ severity: 'warning', source: 'host', message: `Ghostscript: PostScript error ${m}` });
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
this.logger.info('host', `dvisvgm: ${plural(pages.length, 'page')} in ${ms(r.ms)}${r.exitCode ? `, exit ${r.exitCode}` : ''}`);
|
|
649
|
+
if (r.exitCode !== 0 && pages.length === 0) {
|
|
650
|
+
status = 'error';
|
|
651
|
+
diagnostics.push({ severity: 'error', source: 'host', message: `dvisvgm failed (exit ${r.exitCode}): ${r.log.split('\n').filter((l) => /error/i.test(l)).join('; ') || r.log.slice(-300)}` });
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
status = diagnostics.some((d) => d.severity === 'error') ? 'error' : 'fatal';
|
|
656
|
+
if (!diagnostics.length)
|
|
657
|
+
diagnostics.push({ severity: 'error', source: 'tex', message: `TeX produced no DVI (exit ${tex.exitCode})` });
|
|
658
|
+
}
|
|
659
|
+
const totalMs = now() - t0;
|
|
660
|
+
this.report(diagnostics);
|
|
661
|
+
this.logger.info('host', pdfOut
|
|
662
|
+
? `latex ${job}.tex: ${status}, ${pdf ? `${pdf.length}-byte PDF` : 'no PDF'} in ${ms(totalMs)}`
|
|
663
|
+
: `latex ${job}.tex: ${status}, ${plural(pages.length, 'page')} in ${ms(totalMs)} (TeX ${ms(tex.ms)}, dvisvgm ${ms(dvisvgmMs)})`);
|
|
664
|
+
return { status, pages, ...(pdf ? { pdf: pdf } : {}), log: tex.log, texLog, dvisvgmLog, diagnostics, stats: { totalMs, texMs: tex.ms, dvisvgmMs, texSetupMs: tex.setupMs, texMainMs: tex.mainMs, instantiateMs: tex.ms - tex.setupMs - tex.mainMs }, format: fmt, artifacts };
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* BibTeX (bibtex.wasm) on the .aux a latex() run wrote: doc.bbl and doc.blg come back in
|
|
668
|
+
* artifacts, for the next latex() run. Like latex(), a fresh module each time.
|
|
669
|
+
*/
|
|
670
|
+
async bibtex(bo = {}) {
|
|
671
|
+
const job = bo.jobName ?? 'doc';
|
|
672
|
+
// BibTeX exits 0 after warnings (only a history above 1 reaches the exit status), 1 when it cannot start, 2 after errors, 3 after a fatal one
|
|
673
|
+
return this.runTool('bibtex', [...(bo.args ?? []), job], bo.files, job, (exit, ds) => (exit === 0 ? (ds.length ? 'warning' : 'ok') : exit < 3 ? 'error' : 'fatal'), (log) => {
|
|
674
|
+
const ds = parseBibtexLog(log);
|
|
675
|
+
for (const d of ds)
|
|
676
|
+
if (/couldn't open style file/.test(d.message))
|
|
677
|
+
d.help = ["TeX Live's styles (plainnat, apalike, plain, unsrt, ...) are in the 'latex-doc' bundle; a document's own .bst goes in `files`"];
|
|
678
|
+
return ds;
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
/** makeindex (makeindex.wasm) on the .idx a latex() run wrote: doc.ind and doc.ilg come back in artifacts. */
|
|
682
|
+
async makeindex(mo = {}) {
|
|
683
|
+
const input = mo.input ?? 'doc.idx';
|
|
684
|
+
const args = [...(mo.style ? ['-s', mo.style] : []), ...(mo.args ?? []), input];
|
|
685
|
+
return this.runTool('makeindex', args, mo.files, input, (exit, ds) => (exit !== 0 ? 'error' : ds.length ? 'warning' : 'ok'), parseMakeindexLog);
|
|
686
|
+
}
|
|
687
|
+
/** One run of a TeX Live utility: what it wrote in /work comes back, its transcript as diagnostics. */
|
|
688
|
+
async runTool(name, args, files, what, statusOf, parse) {
|
|
689
|
+
const t0 = now();
|
|
690
|
+
const factory = await this.env.tools?.(name);
|
|
691
|
+
if (!factory)
|
|
692
|
+
throw new Error(`mp-tikz-wasm: ${name}.wasm is not available in this build`);
|
|
693
|
+
if (files)
|
|
694
|
+
this.addFiles(files);
|
|
695
|
+
this.progress({ phase: 'running', detail: name });
|
|
696
|
+
const artifacts = {};
|
|
697
|
+
let wrote = new Set();
|
|
698
|
+
const r = await runTex(factory, {
|
|
699
|
+
program: `/bin/${name}`,
|
|
700
|
+
args,
|
|
701
|
+
setup: (M) => { mkdirp(M.FS, '/work'); this.installTexmf(M.FS, M.NODEFS); this.copyUserFiles(M); M.FS.chdir('/work'); wrote = trackWrites(M.FS); },
|
|
702
|
+
collect: (M) => {
|
|
703
|
+
for (const p of wrote) {
|
|
704
|
+
if (!p.startsWith('/work/'))
|
|
705
|
+
continue;
|
|
706
|
+
try {
|
|
707
|
+
artifacts[p.slice('/work/'.length)] = M.FS.readFile(p, { encoding: 'binary' });
|
|
708
|
+
}
|
|
709
|
+
catch { /* ignore */ }
|
|
710
|
+
}
|
|
711
|
+
},
|
|
712
|
+
onLine: (l) => { this.env.onLog?.(l); this.logger.debug(name, l); },
|
|
713
|
+
});
|
|
714
|
+
// the transcript (.blg, .ilg) says more than the terminal; a run that stopped before writing one has only the terminal
|
|
715
|
+
const transcript = Object.entries(artifacts).find(([n]) => n.endsWith(name === 'bibtex' ? '.blg' : '.ilg'))?.[1];
|
|
716
|
+
const diagnostics = parse(transcript ? new TextDecoder().decode(transcript) : r.log);
|
|
717
|
+
if (r.exitCode !== 0 && !diagnostics.some((d) => d.severity === 'error')) {
|
|
718
|
+
const said = r.log.split('\n').filter((l) => l.trim() && !/^(Usage:|This is )/.test(l)).slice(-3).join(' ');
|
|
719
|
+
diagnostics.push({ severity: 'error', source: name, message: said || `${name} failed (exit ${r.exitCode})` });
|
|
720
|
+
}
|
|
721
|
+
const status = statusOf(r.exitCode, diagnostics);
|
|
722
|
+
const totalMs = now() - t0;
|
|
723
|
+
this.report(diagnostics);
|
|
724
|
+
this.logger.info('host', `${name} ${what}: ${status}${r.exitCode ? ` (exit ${r.exitCode})` : ''}, ${plural(Object.keys(artifacts).length, 'file')} in ${ms(totalMs)}`);
|
|
725
|
+
return { status, exitCode: r.exitCode, log: r.log, artifacts, diagnostics, stats: { totalMs } };
|
|
726
|
+
}
|
|
727
|
+
hasSnapshot() {
|
|
728
|
+
if (this.env.bundles)
|
|
729
|
+
return this.env.bundles.files.has('web2c/tikz.fmt');
|
|
730
|
+
if (this.env.texmfDir) {
|
|
731
|
+
try {
|
|
732
|
+
return this.M.FS.analyzePath(`${TEXMF_ROOT}/web2c/tikz.fmt`).exists;
|
|
733
|
+
}
|
|
734
|
+
catch {
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return false;
|
|
739
|
+
}
|
|
740
|
+
async typeset(misses) {
|
|
741
|
+
this.progress({ phase: 'typesetting', total: misses.length });
|
|
742
|
+
const r = await this.bridge.typeset(this.engine, misses);
|
|
743
|
+
this.logger.info('host', `TeX (${this.engine}): ${plural(misses.length, 'label')} in ${ms(r.ms)}${r.resolved < misses.length ? `, ${misses.length - r.resolved} unresolved` : ''}${r.exitCode ? `, exit ${r.exitCode}` : ''}`);
|
|
744
|
+
this.stats.texRuns++;
|
|
745
|
+
this.stats.texMs += r.ms;
|
|
746
|
+
this.lastTexLog = r.texLog;
|
|
747
|
+
this.texDiagnostics.push(...r.diagnostics);
|
|
748
|
+
}
|
|
749
|
+
runMetaPost(source, jobName, ro) {
|
|
750
|
+
const o = this.env.options;
|
|
751
|
+
const t0 = now();
|
|
752
|
+
const job = new MpJob(this.M, {
|
|
753
|
+
mathMode: o.numberSystem === 'double' ? 1 : o.numberSystem === 'decimal' ? 3 : 0,
|
|
754
|
+
extensions: o.extensions ?? true,
|
|
755
|
+
interaction: o.interaction === 'batch' ? 1 : o.interaction === 'scroll' ? 3 : 2,
|
|
756
|
+
haltOnError: o.haltOnError,
|
|
757
|
+
randomSeed: o.randomSeed ?? 42,
|
|
758
|
+
memName: 'plain',
|
|
759
|
+
jobName,
|
|
760
|
+
recorder: true,
|
|
761
|
+
searchPaths: SEARCH_PATHS,
|
|
762
|
+
});
|
|
763
|
+
let prefix = '';
|
|
764
|
+
if (o.deterministic ?? true)
|
|
765
|
+
prefix += 'year:=2025; month:=1; day:=1; time:=0; ';
|
|
766
|
+
if (o.numberSystem === 'binary' || o.numberSystem === 'interval') {
|
|
767
|
+
this.texDiagnostics.push({ severity: 'warning', source: 'host', message: `numbersystem ${o.numberSystem} is not built into mplib.wasm; using scaled` });
|
|
768
|
+
}
|
|
769
|
+
for (const [k, v] of Object.entries(ro.internals ?? {})) {
|
|
770
|
+
prefix += typeof v === 'string' ? `${k}:="${v.replace(/"/g, '')}"; ` : `${k}:=${v}; `;
|
|
771
|
+
}
|
|
772
|
+
const cmd = `${prefix}input ${jobName}${(o.autoEnd ?? true) ? '; end.' : ''}`;
|
|
773
|
+
try {
|
|
774
|
+
job.run(cmd);
|
|
775
|
+
}
|
|
776
|
+
catch (e) {
|
|
777
|
+
// mplib calls exit() on a few hard limits (e.g. "input stack overflow",
|
|
778
|
+
// which native mpost hits too); Emscripten surfaces that as ExitStatus.
|
|
779
|
+
if (e && e.name === 'ExitStatus') {
|
|
780
|
+
job.history = 3;
|
|
781
|
+
this.texDiagnostics.push({ severity: 'error', source: 'metapost', message: `MetaPost aborted: ${this.recentOutput.slice(-3).join(' ').trim() || 'exit ' + e.status}` });
|
|
782
|
+
}
|
|
783
|
+
else
|
|
784
|
+
throw e;
|
|
785
|
+
}
|
|
786
|
+
this.stats.metapostRuns++;
|
|
787
|
+
this.stats.metapostMs += now() - t0;
|
|
788
|
+
return job;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Scan the job and every `input`-ed file reachable from it (lexically),
|
|
792
|
+
* splicing each input file's blocks in at the line of the `input` statement
|
|
793
|
+
* so the verbatimtex chain seen by the pre-scan matches the run's order.
|
|
794
|
+
*/
|
|
795
|
+
scanAll(source, fileName) {
|
|
796
|
+
const blocks = [];
|
|
797
|
+
const seen = new Set([fileName]);
|
|
798
|
+
const visit = (src, name) => {
|
|
799
|
+
const mine = scanTexBlocks(src, name);
|
|
800
|
+
const inputs = inputStatements(src);
|
|
801
|
+
let bi = 0;
|
|
802
|
+
const flush = (uptoLine) => {
|
|
803
|
+
while (bi < mine.length && mine[bi].line <= uptoLine) {
|
|
804
|
+
const b = mine[bi++];
|
|
805
|
+
b.index = blocks.length;
|
|
806
|
+
blocks.push(b);
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
for (const inp of inputs) {
|
|
810
|
+
flush(inp.line);
|
|
811
|
+
const resolved = this.resolveInput(inp.name);
|
|
812
|
+
if (!resolved || seen.has(resolved))
|
|
813
|
+
continue;
|
|
814
|
+
seen.add(resolved);
|
|
815
|
+
let text;
|
|
816
|
+
try {
|
|
817
|
+
text = this.M.FS.readFile(resolved, { encoding: 'utf8' });
|
|
818
|
+
}
|
|
819
|
+
catch {
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
visit(text, resolved.startsWith('/work/') ? resolved.slice(6) : resolved);
|
|
823
|
+
}
|
|
824
|
+
flush(Infinity);
|
|
825
|
+
};
|
|
826
|
+
visit(source, fileName);
|
|
827
|
+
return blocks;
|
|
828
|
+
}
|
|
829
|
+
resolveInput(name) {
|
|
830
|
+
const FS = this.M.FS;
|
|
831
|
+
const names = name.endsWith('.mp') ? [name] : [name + '.mp', name];
|
|
832
|
+
for (const dir of SEARCH_PATHS[2 /* MpFtype.program */]) {
|
|
833
|
+
for (const n of names) {
|
|
834
|
+
const p = `${dir}/${n}`;
|
|
835
|
+
if (FS.analyzePath(p).exists)
|
|
836
|
+
return p;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
for (const n of names) {
|
|
840
|
+
const f = this.env.bundles?.lookup(n, ['metapost/']);
|
|
841
|
+
if (f)
|
|
842
|
+
return `${TEXMF_ROOT}/${f.path}`;
|
|
843
|
+
}
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
function now() { return typeof performance !== 'undefined' ? performance.now() : Date.now(); }
|
|
848
|
+
function ftypeName(t) { return t === MPX_FTYPE_TFM ? 'tfm' : t === MPX_FTYPE_VF ? 'vf' : MP_FTYPE_NAMES[t] ?? String(t); }
|
|
849
|
+
/** Errors (`! ...` + `l.N`) and package warnings from a TeX transcript, attributed to the document. */
|
|
850
|
+
export function parseTexLogDocument(log, file) {
|
|
851
|
+
const out = [];
|
|
852
|
+
const lines = log.split('\n');
|
|
853
|
+
for (let i = 0; i < lines.length; i++) {
|
|
854
|
+
const m = /^! (.*)$/.exec(lines[i]);
|
|
855
|
+
if (m) {
|
|
856
|
+
let line;
|
|
857
|
+
const help = [];
|
|
858
|
+
let j = i + 1;
|
|
859
|
+
for (; j < lines.length && j < i + 30; j++) {
|
|
860
|
+
const lm = /^l\.(\d+)/.exec(lines[j]);
|
|
861
|
+
if (lm) {
|
|
862
|
+
line = Number(lm[1]);
|
|
863
|
+
break;
|
|
864
|
+
}
|
|
865
|
+
if (/^! /.test(lines[j]))
|
|
866
|
+
break;
|
|
867
|
+
}
|
|
868
|
+
if (line !== undefined)
|
|
869
|
+
for (let k = j + 2; k < lines.length && k < i + 30; k++) {
|
|
870
|
+
if (lines[k].trim() === '' || /^! |^l\.\d+/.test(lines[k]))
|
|
871
|
+
break;
|
|
872
|
+
help.push(lines[k]);
|
|
873
|
+
}
|
|
874
|
+
out.push({ severity: 'error', source: 'tex', message: m[1], help: help.length ? help : undefined, file, line });
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
// the PDF back ends' own errors: a font with no outline stops the PDF (no bitmap fonts here)
|
|
878
|
+
const e = /^!(pdfTeX|LuaTeX) error: (.*)$/.exec(lines[i]);
|
|
879
|
+
if (e) {
|
|
880
|
+
const font = /\(file ([^)]+)\): Font (\S+) at (\d+) not found/.exec(e[2]);
|
|
881
|
+
if (font) {
|
|
882
|
+
const ec = /^(ec|tc)[a-z]{2,4}\d{4}$/.test(font[2]);
|
|
883
|
+
out.push({ severity: 'error', source: 'tex', file, message: `font ${font[2]} has no outline here: ${e[1]} found no ${font[2]}.pfb (or other outline) to embed, and bitmap fonts cannot be made`,
|
|
884
|
+
help: ec ? ["T1-encoded Computer Modern (the EC fonts) needs cm-super's outlines, which are not bundled; \\usepackage{lmodern} sets the same design in Latin Modern, which is in the default bundles"]
|
|
885
|
+
: ["no bundle loaded here has an outline for it"] });
|
|
886
|
+
}
|
|
887
|
+
else
|
|
888
|
+
out.push({ severity: 'error', source: 'tex', file, message: `${e[1]}: ${e[2]}` });
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
const w = /^(?:LaTeX|Package (\S+)|Class (\S+)) Warning: (.*)$/.exec(lines[i]);
|
|
892
|
+
if (w) {
|
|
893
|
+
let msg = w[3];
|
|
894
|
+
let j = i + 1;
|
|
895
|
+
while (j < lines.length && lines[j].startsWith('(') && /^\([^)]*\) /.test(lines[j])) {
|
|
896
|
+
msg += ' ' + lines[j].replace(/^\([^)]*\)\s*/, '');
|
|
897
|
+
j++;
|
|
898
|
+
}
|
|
899
|
+
const lm = /on input line (\d+)/.exec(msg);
|
|
900
|
+
out.push({ severity: 'warning', source: 'tex', message: (w[1] ? w[1] + ': ' : w[2] ? w[2] + ': ' : '') + msg, file, line: lm ? Number(lm[1]) : undefined });
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
return out;
|
|
904
|
+
}
|
|
905
|
+
/** `input name` statements with their 1-based line numbers (lexical; comments and strings skipped). */
|
|
906
|
+
function inputStatements(src) {
|
|
907
|
+
const out = [];
|
|
908
|
+
const lines = src.split('\n');
|
|
909
|
+
const wanted = new Set(scanInputs(src));
|
|
910
|
+
for (let i = 0; i < lines.length; i++) {
|
|
911
|
+
let l = lines[i];
|
|
912
|
+
const pc = l.indexOf('%');
|
|
913
|
+
if (pc >= 0)
|
|
914
|
+
l = l.slice(0, pc);
|
|
915
|
+
const re = /(^|[^A-Za-z_])input\s+("?)([^\s;"]+)\2/g;
|
|
916
|
+
let m;
|
|
917
|
+
while ((m = re.exec(l)))
|
|
918
|
+
if (wanted.has(m[3]))
|
|
919
|
+
out.push({ name: m[3], line: i + 1 });
|
|
920
|
+
}
|
|
921
|
+
return out;
|
|
922
|
+
}
|
|
923
|
+
/** Does a LaTeX source need LuaTeX? (TikZ graphdrawing, \directlua, luacode.) Used by engine 'auto'. */
|
|
924
|
+
export function needsLuaTeX(source) {
|
|
925
|
+
return /\\usegdlibrary|graphdrawing|\\directlua|\\latelua|luacode|contourlua|contour lua|\\usepackage(\[[^\]]*\])?\{luatexbase\}/.test(source);
|
|
926
|
+
}
|
|
927
|
+
//# sourceMappingURL=core.js.map
|