@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,250 @@
1
+ import { sha256 } from './tex/cache-key.js';
2
+ /** Is this TikZ text a complete document (LaTeX with \documentclass, or plain TeX ending in \bye) rather than a body to wrap? */
3
+ export function isCompleteDocument(source) {
4
+ return /\\documentclass|\\bye\b/.test(source);
5
+ }
6
+ /** Wrap a TikZ body in a standalone document unless it already is one. */
7
+ export function wrapTikz(source, attrs = {}) {
8
+ if (isCompleteDocument(source))
9
+ return source;
10
+ const libs = (attrs.libraries ?? '').split(/[,\s]+/).filter(Boolean);
11
+ const gd = (attrs.gdlibraries ?? '').split(/[,\s]+/).filter(Boolean);
12
+ // graph drawing is used through \graph, which the graphs library provides
13
+ for (const l of ['graphs', 'graphdrawing'])
14
+ if (gd.length && !libs.includes(l))
15
+ libs.push(l);
16
+ const pkgs = (attrs.packages ?? '').split(/[,\s]+/).filter(Boolean);
17
+ // A body that is already a picture is not wrapped in another. Environments that
18
+ // draw their own tikzpicture -- tikzcd, circuitikz, chemfig's \chemfig and
19
+ // \schemestart -- break when nested in a second one: the page keeps its size but
20
+ // the drawing collapses to its edge, or vanishes. Standalone's `tikz` option,
21
+ // though, crops only tikzpicture environments, and left bare under it a circuitikz
22
+ // comes out as a whole letter page; so such a body gets the plain class, which
23
+ // crops whatever the body is, with TikZ loaded by hand. Only a body that *starts*
24
+ // with one is treated so (`\node {\chemfig{...}};` is TikZ code), and every
25
+ // other body keeps the document -- hence the figure hash -- it always had.
26
+ const ownPicture = /^\s*(\\begin\{(tikzcd|circuitikz)\}|\\chemfig\b|\\schemestart\b)/.test(source);
27
+ const isPicture = ownPicture || /\\begin\{tikzpicture\}|\\tikz\b|\\begin\{axis\}/.test(source);
28
+ const body = isPicture ? source : `\\begin{tikzpicture}\n${source}\n\\end{tikzpicture}`;
29
+ const border = attrs.border ?? '2pt';
30
+ return [
31
+ ...(ownPicture ? [`\\documentclass[border=${border}]{standalone}`, '\\usepackage{tikz}'] : [`\\documentclass[tikz,border=${border}]{standalone}`]),
32
+ ...(pkgs.length ? [`\\usepackage{${pkgs.join(',')}}`] : []),
33
+ ...(libs.length ? [`\\usetikzlibrary{${libs.join(',')}}`] : []),
34
+ ...(gd.length ? [`\\usegdlibrary{${gd.join(',')}}`] : []),
35
+ ...(attrs.preamble ? [attrs.preamble] : []),
36
+ '\\begin{document}',
37
+ body,
38
+ '\\end{document}',
39
+ ].join('\n');
40
+ }
41
+ /**
42
+ * Wrap a MetaPost body in one figure unless it already has beginfig/endfig.
43
+ * `input` statements are hoisted out of the figure: `input boxes` inside a
44
+ * figure group makes MetaPost (native mpost too) recurse until its input stack
45
+ * overflows, and macro packages are meant to be loaded at top level anyway.
46
+ */
47
+ export function wrapMetaPost(source, attrs = {}) {
48
+ const prologues = attrs.prologues ?? '3';
49
+ if (/\bbeginfig\s*\(/.test(source))
50
+ return `prologues:=${prologues};\n${source}`;
51
+ const inputs = [];
52
+ const body = source.split('\n').filter((line) => {
53
+ const m = /^\s*(input\s+[^;%]+;?)\s*(%.*)?$/.exec(line);
54
+ if (m) {
55
+ inputs.push(m[1].endsWith(';') ? m[1] : m[1] + ';');
56
+ return false;
57
+ }
58
+ return true;
59
+ }).join('\n');
60
+ return `prologues:=${prologues};\n${inputs.join('\n')}${inputs.length ? '\n' : ''}beginfig(1);\n${body}\nendfig;\nend.`;
61
+ }
62
+ /** The document the engines are given for an element. */
63
+ export function figureDocument(req) {
64
+ return req.kind === 'tikz' ? wrapTikz(req.source, req.attrs) : wrapMetaPost(req.source, req.attrs);
65
+ }
66
+ const HASH_SPACE = 2176782336n; // 36^6
67
+ /** Six lowercase base-36 characters identifying a figure (see the header). */
68
+ export function figureHash(req) {
69
+ const a = req.attrs;
70
+ const d = sha256(new TextEncoder().encode(`${req.kind}\0${a.fonts ?? ''}\0${a.tex ?? ''}\0${a.engine ?? ''}\0${figureDocument(req)}`));
71
+ let n = 0n;
72
+ for (let i = 0; i < 8; i++)
73
+ n = (n << 8n) | BigInt(d[i]);
74
+ return (n % HASH_SPACE).toString(36).padStart(6, '0');
75
+ }
76
+ /** `figure-HASH.svg`. */
77
+ export function figureName(hash) { return `figure-${hash}.svg`; }
78
+ /** Matches a saved figure's file name; group 1 is the hash. */
79
+ export const FIGURE_FILE = /^figure-([0-9a-z]{6})\.svg$/;
80
+ /** Does this text start like an SVG document (a server that answers every path with its index page does not)? */
81
+ export function isSvg(text) {
82
+ return /^\s*(?:<\?xml[^>]*\?>\s*)?(?:<!--[\s\S]*?-->\s*)*(?:<!DOCTYPE[^>]*>\s*)?<svg[\s>]/i.test(text);
83
+ }
84
+ /** Typeset one figure with the engine; the ids are prefixed with its hash so the SVG stands alone. */
85
+ export async function renderFigure(mp, req, hash = figureHash(req)) {
86
+ const doc = figureDocument(req);
87
+ const idPrefix = `mpw${hash}-`;
88
+ if (req.kind === 'tikz') {
89
+ const engine = req.attrs.engine ?? 'auto';
90
+ // A wrapped body is a standalone page with a border, and the SVG must be
91
+ // that page: dvisvgm's default box is the tight one PGF reports, which is
92
+ // the page minus the border — and not always all of the ink. PGF's
93
+ // classic arrow tips (`>=latex`, `stealth`, ...) declare no hull, so TikZ
94
+ // leaves them out of its bounding box (TeX Live 2025); natively the
95
+ // border is what keeps them on the page, and the tight crop cut them off
96
+ // (a `->` on a horizontal line rendered as a line with no head).
97
+ // `papersize` is the page standalone lays out, so the SVG is the PDF
98
+ // page. A complete document keeps the default: an article is not a page.
99
+ const bbox = isCompleteDocument(req.source) ? undefined : 'papersize';
100
+ const r = await mp.latex(doc, { fonts: req.attrs.fonts === 'woff2' ? 'woff2' : 'paths', engine, bbox, svg: { idPrefix, precision: false } });
101
+ return { svg: r.pages.join('\n'), log: r.log, diagnostics: r.diagnostics, ok: r.status === 'ok' && r.pages.length > 0, ms: r.stats.totalMs };
102
+ }
103
+ const tex = (req.attrs.tex ?? 'auto');
104
+ const r = await mp.run(doc, { format: 'svg', tex, svg: { idPrefix } });
105
+ // several beginfig blocks give several <svg> roots joined by newlines: exactly what the tags inject
106
+ return { svg: r.figures.map((f) => f.svg ?? '').join('\n'), log: r.log, diagnostics: r.diagnostics, ok: r.history < 2 && r.figures.length > 0, ms: r.stats.totalMs };
107
+ }
108
+ const SCRIPT_RE = /<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi;
109
+ const ELEMENT_RE = /<(tikz-diagram|metapost-diagram)\b([^>]*)>([\s\S]*?)<\/\1\s*>/gi;
110
+ const ATTR_RE = /([^\s"'<>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'<>`]+)))?/g;
111
+ const ENTITY_RE = /&(?:#(\d+)|#x([0-9a-f]+)|(lt|gt|amp|quot|apos|nbsp));/gi;
112
+ const ENTITIES = { lt: '<', gt: '>', amp: '&', quot: '"', apos: "'", nbsp: ' ' };
113
+ /** Decode the entities an HTML parser would in text and attribute values. */
114
+ export function decodeEntities(s) {
115
+ if (!s.includes('&'))
116
+ return s;
117
+ return s.replace(ENTITY_RE, (_m, dec, hex, name) => dec ? String.fromCodePoint(Number(dec)) : hex ? String.fromCodePoint(parseInt(hex, 16)) : ENTITIES[name.toLowerCase()]);
118
+ }
119
+ /** Attributes of an opening tag, `data-` stripped, as the tags see them (`attrsOf` in auto.ts). */
120
+ export function parseAttributes(s) {
121
+ const out = {};
122
+ for (const m of s.matchAll(ATTR_RE))
123
+ out[m[1].toLowerCase().replace(/^data-/, '')] = decodeEntities(m[2] ?? m[3] ?? m[4] ?? '');
124
+ return out;
125
+ }
126
+ /** The text of an element as `textContent` gives it, then trimmed as the tags trim it. */
127
+ function normalize(text) { return text.replace(/^\s*\n/, '').replace(/\s+$/, ''); }
128
+ /** Every diagram element in a page, in document order. */
129
+ export function extractFigures(html) {
130
+ const found = [];
131
+ for (const m of html.matchAll(SCRIPT_RE)) {
132
+ const attrs = parseAttributes(m[1]);
133
+ const type = (attrs.type ?? '').trim().toLowerCase();
134
+ if (type !== 'text/tikz' && type !== 'text/metapost')
135
+ continue;
136
+ found.push({ kind: type === 'text/metapost' ? 'metapost' : 'tikz', source: normalize(m[2]), attrs, tag: 'script', offset: m.index });
137
+ }
138
+ for (const m of html.matchAll(ELEMENT_RE)) {
139
+ const tag = m[1].toLowerCase();
140
+ found.push({ kind: tag === 'metapost-diagram' ? 'metapost' : 'tikz', source: normalize(decodeEntities(m[3])), attrs: parseAttributes(m[2]), tag, offset: m.index });
141
+ }
142
+ return found.sort((a, b) => a.offset - b.offset);
143
+ }
144
+ /** The `data-*` attributes of the page's auto.js loader tag (`data-figures` → `figures`), or {} if there is none. */
145
+ /**
146
+ * A loader's data-bundles as a bundle list: names prefixed with + are added to the
147
+ * defaults (`+opentype`), a bare list replaces them; mixing the two is a replace
148
+ * with the +names appended. undefined when the attribute is absent or empty. The
149
+ * one parser for auto.js and the pre-renderer, so they can never load different sets.
150
+ */
151
+ export function bundleList(spec, defaults) {
152
+ const names = (spec ?? '').split(/[,\s]+/).filter(Boolean);
153
+ if (!names.length)
154
+ return undefined;
155
+ const added = names.filter((n) => n.startsWith('+')).map((n) => n.slice(1));
156
+ const listed = names.filter((n) => !n.startsWith('+'));
157
+ return [...(listed.length ? listed : defaults), ...added];
158
+ }
159
+ export function loaderAttributes(html) {
160
+ const m = /<script\b([^>]*\bsrc\s*=\s*["'][^"']*auto\.js["'][^>]*)>/i.exec(html);
161
+ if (!m)
162
+ return {};
163
+ const out = {};
164
+ for (const a of m[1].matchAll(ATTR_RE))
165
+ if (a[1].toLowerCase().startsWith('data-'))
166
+ out[a[1].slice(5).toLowerCase()] = decodeEntities(a[2] ?? a[3] ?? a[4] ?? '');
167
+ return out;
168
+ }
169
+ // ----------------------------------------------------------------- a zip file
170
+ // Store-only (SVG compresses well, but the browsers that need the zip are the
171
+ // ones without a directory picker, and a dependency is not worth a few
172
+ // hundred kilobytes). Timestamps are fixed at 1980-01-01 so the archive is
173
+ // reproducible.
174
+ let crcTable = null;
175
+ export function crc32(data) {
176
+ if (!crcTable) {
177
+ crcTable = new Uint32Array(256);
178
+ for (let n = 0; n < 256; n++) {
179
+ let c = n;
180
+ for (let k = 0; k < 8; k++)
181
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
182
+ crcTable[n] = c >>> 0;
183
+ }
184
+ }
185
+ let c = 0xffffffff;
186
+ for (let i = 0; i < data.length; i++)
187
+ c = crcTable[(c ^ data[i]) & 0xff] ^ (c >>> 8);
188
+ return (c ^ 0xffffffff) >>> 0;
189
+ }
190
+ export function makeZip(files) {
191
+ const enc = new TextEncoder();
192
+ const parts = [], central = [];
193
+ let offset = 0;
194
+ for (const f of files) {
195
+ const name = enc.encode(f.name), data = typeof f.data === 'string' ? enc.encode(f.data) : f.data, crc = crc32(data);
196
+ const local = new Uint8Array(30 + name.length), l = new DataView(local.buffer);
197
+ l.setUint32(0, 0x04034b50, true);
198
+ l.setUint16(4, 20, true);
199
+ l.setUint16(6, 0x0800, true);
200
+ l.setUint16(8, 0, true);
201
+ l.setUint16(10, 0, true);
202
+ l.setUint16(12, 0x0021, true);
203
+ l.setUint32(14, crc, true);
204
+ l.setUint32(18, data.length, true);
205
+ l.setUint32(22, data.length, true);
206
+ l.setUint16(26, name.length, true);
207
+ l.setUint16(28, 0, true);
208
+ local.set(name, 30);
209
+ const cd = new Uint8Array(46 + name.length), c = new DataView(cd.buffer);
210
+ c.setUint32(0, 0x02014b50, true);
211
+ c.setUint16(4, 20, true);
212
+ c.setUint16(6, 20, true);
213
+ c.setUint16(8, 0x0800, true);
214
+ c.setUint16(10, 0, true);
215
+ c.setUint16(12, 0, true);
216
+ c.setUint16(14, 0x0021, true);
217
+ c.setUint32(16, crc, true);
218
+ c.setUint32(20, data.length, true);
219
+ c.setUint32(24, data.length, true);
220
+ c.setUint16(28, name.length, true);
221
+ c.setUint16(30, 0, true);
222
+ c.setUint16(32, 0, true);
223
+ c.setUint16(34, 0, true);
224
+ c.setUint16(36, 0, true);
225
+ c.setUint32(38, 0, true);
226
+ c.setUint32(42, offset, true);
227
+ cd.set(name, 46);
228
+ parts.push(local, data);
229
+ central.push(cd);
230
+ offset += local.length + data.length;
231
+ }
232
+ const cdSize = central.reduce((n, c) => n + c.length, 0);
233
+ const end = new Uint8Array(22), e = new DataView(end.buffer);
234
+ e.setUint32(0, 0x06054b50, true);
235
+ e.setUint16(4, 0, true);
236
+ e.setUint16(6, 0, true);
237
+ e.setUint16(8, files.length, true);
238
+ e.setUint16(10, files.length, true);
239
+ e.setUint32(12, cdSize, true);
240
+ e.setUint32(16, offset, true);
241
+ e.setUint16(20, 0, true);
242
+ const out = new Uint8Array(offset + cdSize + 22);
243
+ let p = 0;
244
+ for (const part of [...parts, ...central, end]) {
245
+ out.set(part, p);
246
+ p += part.length;
247
+ }
248
+ return out;
249
+ }
250
+ //# sourceMappingURL=figures.js.map
@@ -0,0 +1,92 @@
1
+ /**
2
+ * ghostscript.ts — Ghostscript for dvisvgm, as a module of its own.
3
+ *
4
+ * dvisvgm interprets PostScript specials (PSTricks, EPS images, `ps:` code,
5
+ * graphicx's rotation and scaling under its dvips driver) through Ghostscript's
6
+ * C API. dvisvgm.wasm asks for "libgs" with dlopen, and src/c/gs-bridge.c
7
+ * answers with gsapi_* proxies that call Module.gsBridge, the object built
8
+ * here. It forwards each call to a separate Ghostscript wasm module
9
+ * (vendor/GHOSTSCRIPT.lock, served from dist/ghostscript/), copying buffers
10
+ * between the two heaps. Ghostscript is never linked into dvisvgm.wasm, and it
11
+ * is loaded only by a document whose DVI carries PostScript worth running
12
+ * (postscript.ts), only when the `ghostscript` bundle is enabled.
13
+ *
14
+ * Ghostscript opens files itself (an EPS goes in as `(fig.eps) run`), so for
15
+ * each dvisvgm run its /work and /texmf are mounted into the Ghostscript
16
+ * module with Emscripten's PROXYFS. One module serves every document in turn:
17
+ * gsapi_new_instance ... gsapi_delete_instance per dvisvgm run, never two at
18
+ * once (a non-threaded Ghostscript refuses a second live instance).
19
+ */
20
+ /** The Emscripten module of the Ghostscript port (its gs.js), as far as it is used here. */
21
+ export interface GhostscriptModule {
22
+ HEAPU8: Uint8Array;
23
+ FS: any;
24
+ PROXYFS: any;
25
+ _malloc(n: number): number;
26
+ _free(p: number): void;
27
+ addFunction(fn: (...args: number[]) => number, sig: string): number;
28
+ stringToUTF8(s: string, ptr: number, max: number): void;
29
+ lengthBytesUTF8(s: string): number;
30
+ _gsapi_revision(p: number, len: number): number;
31
+ _gsapi_new_instance(pinstance: number, caller: number): number;
32
+ _gsapi_delete_instance(instance: number): void;
33
+ _gsapi_set_stdio(instance: number, stdin: number, stdout: number, stderr: number): number;
34
+ _gsapi_init_with_args(instance: number, argc: number, argv: number): number;
35
+ _gsapi_run_string_begin(instance: number, userErrors: number, pexit: number): number;
36
+ _gsapi_run_string_continue(instance: number, str: number, length: number, userErrors: number, pexit: number): number;
37
+ _gsapi_run_string_end(instance: number, userErrors: number, pexit: number): number;
38
+ _gsapi_exit(instance: number): number;
39
+ }
40
+ export type GhostscriptFactory = (moduleArg: Record<string, unknown>) => Promise<GhostscriptModule>;
41
+ /** What gs-bridge.js calls, on Module.gsBridge of a dvisvgm instance. */
42
+ export interface GsBridge {
43
+ available(): boolean;
44
+ revision(): [number, number];
45
+ newInstance(): number;
46
+ init(args: string[]): number;
47
+ run(op: 0 | 1 | 2, bytes: Uint8Array | null, userErrors: number): [number, number];
48
+ exit(): number;
49
+ deleteInstance(): void;
50
+ take(stream: 1 | 2, max: number): Uint8Array | null;
51
+ }
52
+ export declare class Ghostscript {
53
+ private readonly M;
54
+ private instance;
55
+ private readonly out;
56
+ private readonly cell;
57
+ private buf;
58
+ private bufSize;
59
+ private readonly fns;
60
+ private mounted;
61
+ /** set when the module trapped: it is not used again, and the engine loads a fresh one */
62
+ broken: boolean;
63
+ private constructor();
64
+ /** Instantiate the module from a compiled gs.wasm. */
65
+ static create(factory: GhostscriptFactory, wasm: WebAssembly.Module): Promise<Ghostscript>;
66
+ private view;
67
+ /** Give Ghostscript dvisvgm's files: its /work (the cwd) and its /texmf. */
68
+ mount(dvisvgmFS: any, dirs: string[]): void;
69
+ unmount(): void;
70
+ /** Run fn against the module; a trap marks it broken and becomes a Ghostscript fatal error. */
71
+ private guard;
72
+ bridge(): GsBridge;
73
+ }
74
+ export declare function compileGhostscript(key: string, bytes: () => Promise<Uint8Array>): Promise<WebAssembly.Module>;
75
+ /**
76
+ * A lazy loader for one engine: nothing is fetched until a document needs
77
+ * Ghostscript; then the glue is imported, gs.wasm compiled (once per process)
78
+ * and a module instantiated, which serves later documents too. A module that
79
+ * trapped is replaced. If loading fails, it says so once and returns null, so
80
+ * documents still render, with a warning, as without Ghostscript.
81
+ */
82
+ export declare function ghostscriptLoader(o: {
83
+ glueUrl: string;
84
+ wasmUrl: string;
85
+ fetch: (url: string) => Promise<Uint8Array>;
86
+ factory?: GhostscriptFactory;
87
+ log?: (message: string) => void;
88
+ }): () => Promise<Ghostscript | null>;
89
+ /** Whether these options turn Ghostscript on: the option itself, or the `ghostscript` bundle in the list. */
90
+ export declare function ghostscriptWanted(ghostscript: boolean | undefined, bundles: (string | {
91
+ name: string;
92
+ })[]): boolean;
@@ -0,0 +1,218 @@
1
+ /**
2
+ * ghostscript.ts — Ghostscript for dvisvgm, as a module of its own.
3
+ *
4
+ * dvisvgm interprets PostScript specials (PSTricks, EPS images, `ps:` code,
5
+ * graphicx's rotation and scaling under its dvips driver) through Ghostscript's
6
+ * C API. dvisvgm.wasm asks for "libgs" with dlopen, and src/c/gs-bridge.c
7
+ * answers with gsapi_* proxies that call Module.gsBridge, the object built
8
+ * here. It forwards each call to a separate Ghostscript wasm module
9
+ * (vendor/GHOSTSCRIPT.lock, served from dist/ghostscript/), copying buffers
10
+ * between the two heaps. Ghostscript is never linked into dvisvgm.wasm, and it
11
+ * is loaded only by a document whose DVI carries PostScript worth running
12
+ * (postscript.ts), only when the `ghostscript` bundle is enabled.
13
+ *
14
+ * Ghostscript opens files itself (an EPS goes in as `(fig.eps) run`), so for
15
+ * each dvisvgm run its /work and /texmf are mounted into the Ghostscript
16
+ * module with Emscripten's PROXYFS. One module serves every document in turn:
17
+ * gsapi_new_instance ... gsapi_delete_instance per dvisvgm run, never two at
18
+ * once (a non-threaded Ghostscript refuses a second live instance).
19
+ */
20
+ /** gs_error_Fatal: what the proxies report when the module itself failed. */
21
+ const FATAL = -100;
22
+ export class Ghostscript {
23
+ M;
24
+ instance = 0;
25
+ out = [[], [], []]; // index 1 stdout, 2 stderr
26
+ cell; // 16 scratch bytes: instance pointer, exit code, revision
27
+ buf = 0;
28
+ bufSize = 0;
29
+ fns;
30
+ mounted = [];
31
+ /** set when the module trapped: it is not used again, and the engine loads a fresh one */
32
+ broken = false;
33
+ constructor(M) {
34
+ this.M = M;
35
+ this.cell = M._malloc(16);
36
+ const collect = (stream) => (_caller, ptr, len) => {
37
+ if (len > 0)
38
+ this.out[stream].push(this.M.HEAPU8.slice(ptr, ptr + len));
39
+ return len;
40
+ };
41
+ this.fns = [M.addFunction(() => 0, 'iiii'), M.addFunction(collect(1), 'iiii'), M.addFunction(collect(2), 'iiii')];
42
+ }
43
+ /** Instantiate the module from a compiled gs.wasm. */
44
+ static async create(factory, wasm) {
45
+ const M = await factory({
46
+ print: () => { }, printErr: () => { },
47
+ instantiateWasm(imports, receive) {
48
+ WebAssembly.instantiate(wasm, imports).then((inst) => receive(inst, wasm));
49
+ return {};
50
+ },
51
+ });
52
+ return new Ghostscript(M);
53
+ }
54
+ view() { return new DataView(this.M.HEAPU8.buffer); }
55
+ /** Give Ghostscript dvisvgm's files: its /work (the cwd) and its /texmf. */
56
+ mount(dvisvgmFS, dirs) {
57
+ const FS = this.M.FS;
58
+ for (const dir of dirs) {
59
+ if (!dvisvgmFS.analyzePath(dir).exists)
60
+ continue;
61
+ try {
62
+ FS.mkdirTree(dir);
63
+ }
64
+ catch { /* exists */ }
65
+ FS.mount(this.M.PROXYFS, { root: dir, fs: dvisvgmFS }, dir);
66
+ this.mounted.push(dir);
67
+ }
68
+ if (this.mounted.includes('/work'))
69
+ FS.chdir('/work');
70
+ }
71
+ unmount() {
72
+ const FS = this.M.FS;
73
+ try {
74
+ FS.chdir('/');
75
+ }
76
+ catch { /* ignore */ }
77
+ for (const dir of this.mounted.splice(0)) {
78
+ try {
79
+ FS.unmount(dir);
80
+ }
81
+ catch { /* ignore */ }
82
+ }
83
+ }
84
+ /** Run fn against the module; a trap marks it broken and becomes a Ghostscript fatal error. */
85
+ guard(fallback, fn) {
86
+ if (this.broken)
87
+ return fallback;
88
+ try {
89
+ return fn();
90
+ }
91
+ catch {
92
+ this.broken = true;
93
+ return fallback;
94
+ }
95
+ }
96
+ bridge() {
97
+ const M = this.M;
98
+ return {
99
+ available: () => !this.broken,
100
+ revision: () => this.guard([0, 0], () => {
101
+ M._gsapi_revision(this.cell, 16); // wasm32: product, copyright (pointers), revision, revisiondate (longs)
102
+ const v = this.view();
103
+ return [v.getInt32(this.cell + 8, true), v.getInt32(this.cell + 12, true)];
104
+ }),
105
+ newInstance: () => this.guard(FATAL, () => {
106
+ this.out[1] = [];
107
+ this.out[2] = [];
108
+ // *pinstance must be null: a non-null value asks to share that (existing) instance
109
+ this.view().setUint32(this.cell, 0, true);
110
+ const code = M._gsapi_new_instance(this.cell, 0);
111
+ if (code < 0)
112
+ return code;
113
+ this.instance = this.view().getUint32(this.cell, true);
114
+ return M._gsapi_set_stdio(this.instance, ...this.fns);
115
+ }),
116
+ init: (args) => this.guard(FATAL, () => {
117
+ const enc = new TextEncoder();
118
+ const strs = args.map((a) => enc.encode(a));
119
+ const block = M._malloc(4 * strs.length + strs.reduce((n, s) => n + s.length + 1, 0));
120
+ let p = block + 4 * strs.length;
121
+ strs.forEach((s, i) => {
122
+ this.view().setUint32(block + 4 * i, p, true);
123
+ M.HEAPU8.set(s, p);
124
+ M.HEAPU8[p + s.length] = 0;
125
+ p += s.length + 1;
126
+ });
127
+ try {
128
+ return M._gsapi_init_with_args(this.instance, strs.length, block);
129
+ }
130
+ finally {
131
+ M._free(block);
132
+ }
133
+ }),
134
+ run: (op, bytes, userErrors) => this.guard([FATAL, FATAL], () => {
135
+ const exit = this.cell + 4;
136
+ let code;
137
+ if (op === 0)
138
+ code = M._gsapi_run_string_begin(this.instance, userErrors, exit);
139
+ else if (op === 2)
140
+ code = M._gsapi_run_string_end(this.instance, userErrors, exit);
141
+ else {
142
+ const n = bytes.length;
143
+ if (n > this.bufSize) {
144
+ if (this.buf)
145
+ M._free(this.buf);
146
+ this.buf = M._malloc(n);
147
+ this.bufSize = n;
148
+ }
149
+ M.HEAPU8.set(bytes, this.buf);
150
+ code = M._gsapi_run_string_continue(this.instance, this.buf, n, userErrors, exit);
151
+ }
152
+ return [code, this.view().getInt32(exit, true)];
153
+ }),
154
+ exit: () => this.guard(FATAL, () => M._gsapi_exit(this.instance)),
155
+ deleteInstance: () => this.guard(undefined, () => {
156
+ if (this.instance)
157
+ M._gsapi_delete_instance(this.instance);
158
+ this.instance = 0;
159
+ }),
160
+ take: (stream, max) => {
161
+ const q = this.out[stream];
162
+ if (!q.length)
163
+ return null;
164
+ const head = q[0];
165
+ if (head.length <= max) {
166
+ q.shift();
167
+ return head;
168
+ }
169
+ q[0] = head.subarray(max);
170
+ return head.subarray(0, max);
171
+ },
172
+ };
173
+ }
174
+ }
175
+ /** One compile of gs.wasm per process, whoever asks. */
176
+ const compiled = new Map();
177
+ export function compileGhostscript(key, bytes) {
178
+ let p = compiled.get(key);
179
+ if (!p) {
180
+ p = bytes().then((b) => WebAssembly.compile(b));
181
+ p.catch(() => compiled.delete(key));
182
+ compiled.set(key, p);
183
+ }
184
+ return p;
185
+ }
186
+ /**
187
+ * A lazy loader for one engine: nothing is fetched until a document needs
188
+ * Ghostscript; then the glue is imported, gs.wasm compiled (once per process)
189
+ * and a module instantiated, which serves later documents too. A module that
190
+ * trapped is replaced. If loading fails, it says so once and returns null, so
191
+ * documents still render, with a warning, as without Ghostscript.
192
+ */
193
+ export function ghostscriptLoader(o) {
194
+ let current = null;
195
+ let failed = false;
196
+ return async () => {
197
+ if (failed)
198
+ return null;
199
+ if (current && !current.broken)
200
+ return current;
201
+ try {
202
+ const factory = o.factory ?? (await import(/* @vite-ignore */ o.glueUrl)).default;
203
+ const wasm = await compileGhostscript(o.wasmUrl, () => o.fetch(o.wasmUrl));
204
+ current = await Ghostscript.create(factory, wasm);
205
+ return current;
206
+ }
207
+ catch (e) {
208
+ failed = true;
209
+ o.log?.(`Ghostscript could not be loaded from ${o.wasmUrl}: ${e?.message ?? e}`);
210
+ return null;
211
+ }
212
+ };
213
+ }
214
+ /** Whether these options turn Ghostscript on: the option itself, or the `ghostscript` bundle in the list. */
215
+ export function ghostscriptWanted(ghostscript, bundles) {
216
+ return ghostscript ?? bundles.some((b) => (typeof b === 'string' ? b : b.name) === 'ghostscript');
217
+ }
218
+ //# sourceMappingURL=ghostscript.js.map
@@ -0,0 +1,87 @@
1
+ /**
2
+ * mp-tikz-wasm — public API (docs/08). `MetaPost.create()` starts a Web
3
+ * Worker in browsers and runs in-process in Node (or when `worker: false`).
4
+ */
5
+ import type { MetaPostOptions, RunOptions, RunResult, ProgressEvent, BundleName, LatexRunOptions, LatexResult, PrefetchKind, LogLevel, LogRecord, BibtexOptions, MakeindexOptions, ToolResult } from './types.js';
6
+ export * from './types.js';
7
+ export { sanitizeSvg, postProcessSvg } from './render/svg.js';
8
+ export { scanTexBlocks, scanInputs } from './tex/scanner.js';
9
+ export { parseMetaPostLog } from './diagnostics.js';
10
+ export { splitMpx } from './tex/mpx.js';
11
+ export { LOG_LEVELS, formatRecord, consoleSink } from './logger.js';
12
+ export { DEFAULT_BUNDLES } from './bundles-config.js';
13
+ export { figureHash, figureName, figureDocument, extractFigures, renderFigure, wrapTikz, wrapMetaPost, makeZip, FIGURE_FILE } from './figures.js';
14
+ export type { FigureKind, FigureRequest, FigureResult, SavedFigure, FoundFigure } from './figures.js';
15
+ export declare class MetaPost {
16
+ private backend;
17
+ private options;
18
+ private listeners;
19
+ private queue;
20
+ private readonly t0;
21
+ readonly version: {
22
+ metapost: string;
23
+ tex: string;
24
+ build: string;
25
+ };
26
+ private constructor();
27
+ static create(options?: MetaPostOptions): Promise<MetaPost>;
28
+ /**
29
+ * The stall watchdog (worker only): the run is killed when `timeoutMs` pass without a progress
30
+ * event or a line of engine output. Every engine phase and every on-demand file fetch is progress,
31
+ * so a first run on a slow host that spends a minute fetching files one after another is left
32
+ * alone, and so is a long TeX run that is still printing; a silent, hung engine is not.
33
+ */
34
+ private watchdog;
35
+ /** Events from either backend: log records reach the console (or the `logger` option), raw lines the `log` option, then any listeners. */
36
+ private handleEvent;
37
+ /** A record from this side of the worker boundary (a failed call, the watchdog), subject to the same level. */
38
+ private hostRecord;
39
+ private failing;
40
+ /** How much reaches the console (see `MetaPostOptions.logLevel`); can be changed at any time, also mid-run. */
41
+ get logLevel(): LogLevel;
42
+ set logLevel(level: LogLevel);
43
+ /** Compile MetaPost source. Calls are serialised on this instance. */
44
+ run(source: string, options?: RunOptions): Promise<RunResult>;
45
+ /** Typeset a whole LaTeX/TikZ (or plain TeX) document to one SVG per page. */
46
+ latex(source: string, options?: LatexRunOptions): Promise<LatexResult>;
47
+ /**
48
+ * BibTeX on the .aux a latex() run wrote (`files: result.artifacts`, plus the .bib databases):
49
+ * doc.bbl and doc.blg come back in `artifacts`, for the next latex() run. TeX Live's styles
50
+ * are in the `latex-doc` bundle. Serialised with run() and latex() on this instance.
51
+ */
52
+ bibtex(options?: BibtexOptions): Promise<ToolResult>;
53
+ /** makeindex on the .idx a latex() run wrote: doc.ind and doc.ilg come back in `artifacts`. */
54
+ makeindex(options?: MakeindexOptions): Promise<ToolResult>;
55
+ private serialise;
56
+ addFiles(files: Record<string, string | Uint8Array>): Promise<void>;
57
+ /**
58
+ * Take off this instance files that addFiles or a run's `files` put there, named as they were
59
+ * given (all of them with no names). Passed files otherwise stay for the instance's lifetime:
60
+ * a host typesetting several documents should remove one document's .aux, .toc, .bbl before
61
+ * the next, or the next one reads them.
62
+ */
63
+ removeFiles(names?: string[]): Promise<void>;
64
+ preload(bundles: BundleName[]): Promise<void>;
65
+ /** Fetch in parallel the files a first run of these kinds needs (see `MetaPostOptions.prefetch`). Resolves to the number fetched. */
66
+ prefetch(kinds: PrefetchKind[]): Promise<number>;
67
+ clearCache(): Promise<void>;
68
+ on(event: 'progress', fn: (e: ProgressEvent) => void): () => void;
69
+ on(event: 'log', fn: (line: string) => void): () => void;
70
+ on(event: 'record', fn: (r: LogRecord) => void): () => void;
71
+ private emit;
72
+ dispose(): void;
73
+ }
74
+ /** A pool of independent instances for batch work (docs/10 §2.3). */
75
+ export declare class MetaPostPool {
76
+ private all;
77
+ private idle;
78
+ private waiters;
79
+ private constructor();
80
+ static create(options?: MetaPostOptions & {
81
+ size?: number;
82
+ }): Promise<MetaPostPool>;
83
+ private acquire;
84
+ private release;
85
+ run(source: string, options?: RunOptions): Promise<RunResult>;
86
+ dispose(): void;
87
+ }