@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,106 @@
1
+ /**
2
+ * prerender.ts — `mpost-wasm --prerender`: typeset the diagram elements of
3
+ * HTML pages in Node and save each result as figure-HASH.svg, the file the
4
+ * drop-in tags look for when the loader carries data-figures (auto.ts,
5
+ * figures.ts). A page whose figures are all saved loads them as small static
6
+ * files and never starts the engines in the visitor's browser.
7
+ *
8
+ * For each page the figures go to the directory its loader names in
9
+ * data-figures (default `figures/`), resolved against the page, unless a
10
+ * directory is given for all of them (or a base directory to resolve each
11
+ * page's data-figures against, for a rendered copy saved elsewhere). Each page's
12
+ * figures are typeset with the bundles its loader names in data-bundles, read
13
+ * exactly as auto.js reads them, so an opt-in bundle (`+classico`) is there as
14
+ * in the browser; pages wanting different bundles get separate engines. A file
15
+ * that already exists is kept —
16
+ * the name is the content, so it is current — unless `force` is set. The
17
+ * engine is created with the browser's defaults (deterministic, seed 42) so
18
+ * the bytes are the ones the tags would have produced.
19
+ */
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
22
+ import { MetaPost } from './index.js';
23
+ import { extractFigures, figureHash, figureName, loaderAttributes, renderFigure, bundleList } from './figures.js';
24
+ import { DEFAULT_BUNDLES } from './bundles-config.js';
25
+ /** The figures a page needs, with the directory they belong in. */
26
+ export function planPage(page, figuresDir, baseDir) {
27
+ const html = fs.readFileSync(page, 'utf8');
28
+ const loader = loaderAttributes(html);
29
+ const named = loader.figures;
30
+ const dir = figuresDir ?? path.resolve(baseDir ?? path.dirname(page), named && named !== 'off' ? named : 'figures');
31
+ const figures = extractFigures(html).map((f) => { const hash = figureHash(f); return { ...f, hash, name: figureName(hash) }; });
32
+ return { dir, bundles: bundleList(loader.bundles, DEFAULT_BUNDLES), figures };
33
+ }
34
+ export async function prerender(pages, opts = {}) {
35
+ const report = opts.report ?? (() => { });
36
+ const entries = [];
37
+ const seen = new Set();
38
+ const todo = [];
39
+ for (const page of pages) {
40
+ const { dir, figures, bundles: pageBundles } = planPage(page, opts.figuresDir, opts.baseDir);
41
+ const bundles = [...new Set([...(opts.createOptions?.bundles ?? pageBundles ?? DEFAULT_BUNDLES), ...(opts.addBundles ?? [])])];
42
+ for (const f of figures) {
43
+ const file = path.join(dir, f.name);
44
+ if (seen.has(file))
45
+ continue; // the same figure on two pages (or twice on one) is one file
46
+ seen.add(file);
47
+ const entry = { page, file, name: f.name, hash: f.hash, kind: f.kind, status: 'pending' };
48
+ entries.push(entry);
49
+ if (!opts.force && fs.existsSync(file)) {
50
+ entry.status = 'exists';
51
+ report(line(entry));
52
+ continue;
53
+ }
54
+ todo.push({ entry, req: { kind: f.kind, source: f.source, attrs: f.attrs }, bundles });
55
+ }
56
+ }
57
+ if (opts.dryRun) {
58
+ for (const { entry } of todo)
59
+ report(line(entry));
60
+ }
61
+ else if (todo.length) {
62
+ // one engine per distinct bundle list, in first-use order (or the caller's engine for all)
63
+ const groups = new Map();
64
+ for (const t of todo) {
65
+ const k = opts.mp ? '' : t.bundles.join(',');
66
+ groups.set(k, [...(groups.get(k) ?? []), t]);
67
+ }
68
+ for (const group of groups.values()) {
69
+ const own = !opts.mp;
70
+ const mp = opts.mp ?? await MetaPost.create({ logLevel: 'warn', ...opts.createOptions, bundles: group[0].bundles });
71
+ try {
72
+ for (const { entry, req } of group) {
73
+ const r = await renderFigure(mp, req, entry.hash);
74
+ entry.ms = r.ms;
75
+ if (r.ok) {
76
+ fs.mkdirSync(path.dirname(entry.file), { recursive: true });
77
+ fs.writeFileSync(entry.file, r.svg);
78
+ entry.status = 'rendered';
79
+ }
80
+ else {
81
+ entry.status = 'failed';
82
+ entry.diagnostics = r.diagnostics;
83
+ }
84
+ report(line(entry));
85
+ }
86
+ }
87
+ finally {
88
+ if (own)
89
+ mp.dispose();
90
+ }
91
+ }
92
+ }
93
+ const count = (s) => entries.filter((e) => e.status === s).length;
94
+ return { entries, rendered: count('rendered'), existing: count('exists'), failed: count('failed') };
95
+ }
96
+ /** `figures/figure-abc123.svg tikz rendered 312 ms (tags.html)`, diagnostics indented under a failure. */
97
+ export function line(e) {
98
+ const rel = path.relative(process.cwd(), e.file) || e.file;
99
+ const status = e.status === 'pending' ? 'to render' : e.status;
100
+ const ms = e.ms !== undefined ? ` ${Math.round(e.ms)} ms` : '';
101
+ let s = `${rel} ${e.kind.padEnd(8)} ${status.padEnd(9)}${ms} (${path.basename(e.page)})`;
102
+ for (const d of e.diagnostics ?? [])
103
+ s += `\n ${d.severity}: ${d.message}${d.line ? ` (line ${d.line})` : ''}`;
104
+ return s;
105
+ }
106
+ //# sourceMappingURL=prerender.js.map
@@ -0,0 +1,20 @@
1
+ /**
2
+ * svg.ts — post-processing of MetaPost's SVG output (docs/07 §3.1). The C
3
+ * output is left byte-identical to upstream; everything here is optional
4
+ * polish applied on the string.
5
+ */
6
+ import type { SvgPostOptions } from '../types.js';
7
+ export declare function postProcessSvg(svg: string, opts?: SvgPostOptions, figureIndex?: number): string;
8
+ /**
9
+ * A regular-expression allow-list over the markup: keeps the SVG elements
10
+ * MetaPost emits and drops everything else (scripts, event handlers,
11
+ * foreignObject, external references).
12
+ *
13
+ * @deprecated Not a security boundary, and too narrow for what the engines
14
+ * produce: it drops gradients (every PGF shading), patterns, `<style>` (the
15
+ * faces of `fonts: 'woff2'`) and the geometry of circles, ellipses and lines.
16
+ * Sanitise untrusted output with DOMPurify instead — the README's
17
+ * "Untrusted sources" has the configuration. Kept, unchanged, so existing
18
+ * callers do not break; to be removed at 1.0.
19
+ */
20
+ export declare function sanitizeSvg(svg: string): string;
@@ -0,0 +1,96 @@
1
+ const NUM = /-?\d+\.\d{4,}/g;
2
+ export function postProcessSvg(svg, opts = {}, figureIndex = 0) {
3
+ let s = svg;
4
+ if (opts.precision !== false) {
5
+ const p = opts.precision ?? 3;
6
+ s = s.replace(NUM, (m) => {
7
+ const v = Number(m);
8
+ let r = v.toFixed(p);
9
+ if (r.includes('.'))
10
+ r = r.replace(/\.?0+$/, '');
11
+ if (r === '-0')
12
+ r = '0';
13
+ return r;
14
+ });
15
+ }
16
+ if (opts.idPrefix !== false) {
17
+ // MetaPost's ids (GLYPHcmr10_77, CLIP1, ...) are global per figure and
18
+ // collide when several figures are inlined on one page: url(#CLIP1) then
19
+ // resolves to the first CLIP1 in the document. Namespace every id and
20
+ // every reference to one.
21
+ // MetaPost's backend writes double-quoted attributes, dvisvgm single-quoted ones
22
+ const prefix = opts.idPrefix ?? `mp${figureIndex}-`;
23
+ s = s.replace(/\bid=(["'])([^"']+)\1/g, (_m, q, id) => `id=${q}${prefix}${id}${q}`)
24
+ .replace(/href=(["'])#([^"']+)\1/g, (_m, q, id) => `href=${q}#${prefix}${id}${q}`)
25
+ .replace(/url\(#([^)]+)\)/g, (_m, id) => `url(#${prefix}${id})`);
26
+ // fonts: 'woff2' has the same problem one level up. dvisvgm names each embedded
27
+ // face nf0, nf1, ... and styles the runs with `text.f0 {font-family:nf0}`. Once the
28
+ // SVG is inlined both the @font-face family names and the class selectors are
29
+ // document-global, so a second figure's nf0 wins and the first renders from the
30
+ // wrong subset — with a fallback glyph wherever that subset has no such character,
31
+ // which is how it shows up: a word half in one weight and half in another.
32
+ s = s.replace(/font-family:nf(\d+)/g, (_m, n) => `font-family:${prefix}nf${n}`)
33
+ .replace(/\btext\.f(\d+)\b/g, (_m, n) => `text.${prefix}f${n}`)
34
+ .replace(/\bclass=(["'])f(\d+)\1/g, (_m, q, n) => `class=${q}${prefix}f${n}${q}`);
35
+ }
36
+ if (opts.modernHref)
37
+ s = s.replace(/xlink:href=/g, 'href=');
38
+ if (opts.units === 'px') {
39
+ s = s.replace(/<svg([^>]*)width="([\d.]+)"([^>]*)height="([\d.]+)"/, (_m, a, w, b, h) => `<svg${a}width="${trim(Number(w) * 96 / 72)}"${b}height="${trim(Number(h) * 96 / 72)}"`);
40
+ }
41
+ else if (opts.units === 'none') {
42
+ s = s.replace(/<svg([^>]*)\swidth="[\d.]+"/, '<svg$1').replace(/<svg([^>]*)\sheight="[\d.]+"/, '<svg$1');
43
+ }
44
+ if (opts.title) {
45
+ const t = escapeXml(opts.title);
46
+ s = s.replace(/<svg([^>]*)>/, `<svg$1 role="img" aria-label="${t}"><title>${t}</title>`);
47
+ }
48
+ return s;
49
+ }
50
+ function trim(v) { let r = v.toFixed(3); r = r.replace(/\.?0+$/, ''); return r; }
51
+ function escapeXml(s) { return s.replace(/[<>&"]/g, (c) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;' }[c])); }
52
+ /**
53
+ * A regular-expression allow-list over the markup: keeps the SVG elements
54
+ * MetaPost emits and drops everything else (scripts, event handlers,
55
+ * foreignObject, external references).
56
+ *
57
+ * @deprecated Not a security boundary, and too narrow for what the engines
58
+ * produce: it drops gradients (every PGF shading), patterns, `<style>` (the
59
+ * faces of `fonts: 'woff2'`) and the geometry of circles, ellipses and lines.
60
+ * Sanitise untrusted output with DOMPurify instead — the README's
61
+ * "Untrusted sources" has the configuration. Kept, unchanged, so existing
62
+ * callers do not break; to be removed at 1.0.
63
+ */
64
+ export function sanitizeSvg(svg) {
65
+ const allowedTags = new Set(['svg', 'g', 'path', 'defs', 'use', 'text', 'title', 'desc', 'clipPath', 'rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'tspan']);
66
+ const allowedAttr = /^(id|class|d|x|y|width|height|viewBox|version|xmlns|xmlns:xlink|xlink:href|href|transform|style|fill|fill-rule|stroke|stroke-width|stroke-linecap|stroke-linejoin|stroke-miterlimit|stroke-dasharray|stroke-dashoffset|clip-path|font-size|font-family|role|aria-label|opacity|fill-opacity|stroke-opacity)$/;
67
+ // comments are harmless but drop them to be safe; scripts and foreign
68
+ // content go with their contents, not just their tags
69
+ let out = svg.replace(/<!--[\s\S]*?-->/g, '');
70
+ out = out.replace(/<\?xml[^>]*\?>/, '');
71
+ out = out.replace(/<(script|style|foreignObject|iframe|object|embed)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, '');
72
+ out = out.replace(/<\/?([a-zA-Z:]+)([^>]*)>/g, (m, tag, attrs) => {
73
+ if (!allowedTags.has(tag))
74
+ return '';
75
+ if (m.startsWith('</'))
76
+ return `</${tag}>`;
77
+ const kept = (attrs.match(/([a-zA-Z:-]+)\s*=\s*("[^"]*"|'[^']*')/g) || []).filter((a) => {
78
+ const name = a.split('=')[0].trim();
79
+ if (!allowedAttr.test(name))
80
+ return false;
81
+ const val = a.slice(a.indexOf('=') + 1).trim().slice(1, -1);
82
+ if (/href/.test(name) && !val.startsWith('#'))
83
+ return false;
84
+ // only same-document url(#id) references (clip-path, fill patterns); nothing external
85
+ if (/url\(/i.test(val) && !/^url\(#[^)]+\)$/.test(val.trim()))
86
+ return false;
87
+ if (/javascript:|expression\(|@import|data:/i.test(val))
88
+ return false;
89
+ return true;
90
+ });
91
+ const selfClose = attrs.trim().endsWith('/') ? '/' : '';
92
+ return `<${tag}${kept.length ? ' ' + kept.join(' ') : ''}${selfClose}>`;
93
+ });
94
+ return out.trim();
95
+ }
96
+ //# sourceMappingURL=svg.js.map
@@ -0,0 +1,93 @@
1
+ /**
2
+ * bridge.ts — the TeX Bridge (docs/05): lift TeX out of the MetaPost run.
3
+ *
4
+ * scan → keys → cache → batch TeX (tex.wasm) → dvitomp (mplib.wasm) → split
5
+ *
6
+ * The bridge never runs inside make_text; it fills the snippet cache before
7
+ * a MetaPost run and again between fixpoint iterations.
8
+ */
9
+ import type { TexBlock } from './scanner.js';
10
+ import { type TexFactory, type TexModule } from '../texengine.js';
11
+ import { type MplibModule } from '../mplib.js';
12
+ import type { Diagnostic, TexEngine } from '../types.js';
13
+ export interface Snippet {
14
+ key: string;
15
+ chain: string[];
16
+ body: string;
17
+ line?: number;
18
+ file?: string;
19
+ }
20
+ export interface SnippetCache {
21
+ get(key: string): string | undefined;
22
+ set(key: string, chunk: string): void;
23
+ clear(): void;
24
+ readonly size: number;
25
+ }
26
+ export declare class MemorySnippetCache implements SnippetCache {
27
+ private m;
28
+ get(k: string): string | undefined;
29
+ set(k: string, v: string): void;
30
+ clear(): void;
31
+ get size(): number;
32
+ }
33
+ /** Canonical form of a snippet body for cache keys: whitespace runs collapsed,
34
+ * trimmed. mplib hands make_text a trimmed, newline-to-space converted body
35
+ * (texscriptmode=1) while the pre-scan sees the raw text; both map here. */
36
+ export declare function canonicalBody(s: string): string;
37
+ export type ResolvedEngine = 'plain' | 'etex' | 'latex';
38
+ export interface EngineInfo {
39
+ engine: ResolvedEngine;
40
+ format: string;
41
+ progname: string;
42
+ formatId: string;
43
+ engineId: string;
44
+ }
45
+ export declare function resolveEngine(engine: TexEngine, blocks: TexBlock[], source: string): ResolvedEngine | 'none';
46
+ export interface BridgeOptions {
47
+ texFactory: TexFactory;
48
+ mplib: MplibModule;
49
+ /** prepare a fresh tex.wasm FS: mount/install /texmf, create /work */
50
+ setupTexFS: (M: TexModule) => void;
51
+ cache: SnippetCache;
52
+ texPreamble?: string;
53
+ workDir?: string;
54
+ onLine?: (line: string) => void;
55
+ engineId: string;
56
+ formatIds: Record<ResolvedEngine, string>;
57
+ }
58
+ export interface TypesetResult {
59
+ texLog: string;
60
+ exitCode: number;
61
+ ms: number;
62
+ pages: number;
63
+ diagnostics: Diagnostic[];
64
+ resolved: number;
65
+ }
66
+ export declare class TexBridge {
67
+ private o;
68
+ constructor(o: BridgeOptions);
69
+ key(engine: ResolvedEngine, chain: string[], body: string): string;
70
+ /**
71
+ * Given the ordered blocks of a document, return the snippets whose chunk is
72
+ * not cached (with their verbatimtex chain), in source order.
73
+ */
74
+ misses(engine: ResolvedEngine, blocks: TexBlock[]): {
75
+ hits: number;
76
+ misses: Snippet[];
77
+ chainFor: Map<number, string[]>;
78
+ };
79
+ /**
80
+ * Typeset a list of snippets in ONE TeX run and store the resulting chunks.
81
+ * Snippets must be in source order; chains are emitted incrementally.
82
+ */
83
+ typeset(engine: ResolvedEngine, snippets: Snippet[]): Promise<TypesetResult>;
84
+ }
85
+ /**
86
+ * Map TeX errors back to btex blocks: the generated .tex has one
87
+ * `\mpxshipout% line N file` wrapper line per block; a TeX `l.<n>` line number
88
+ * inside [wrapper, \stopmpxshipout] belongs to that block.
89
+ */
90
+ export declare function parseTexLog(log: string, tex: string, blocks: {
91
+ block: TexBlock;
92
+ snippet: Snippet;
93
+ }[]): Diagnostic[];
@@ -0,0 +1,223 @@
1
+ import { buildTexJob } from './mpto.js';
2
+ import { splitMpx } from './mpx.js';
3
+ import { snippetKey } from './cache-key.js';
4
+ import { runTex } from '../texengine.js';
5
+ import { dvitomp, mpxLastError } from '../mplib.js';
6
+ import { writeFileDeep } from '../vfs/lazyfs.js';
7
+ export class MemorySnippetCache {
8
+ m = new Map();
9
+ get(k) { return this.m.get(k); }
10
+ set(k, v) { this.m.set(k, v); }
11
+ clear() { this.m.clear(); }
12
+ get size() { return this.m.size; }
13
+ }
14
+ /** Canonical form of a snippet body for cache keys: whitespace runs collapsed,
15
+ * trimmed. mplib hands make_text a trimmed, newline-to-space converted body
16
+ * (texscriptmode=1) while the pre-scan sees the raw text; both map here. */
17
+ export function canonicalBody(s) { return s.replace(/\s+/g, ' ').trim(); }
18
+ export function resolveEngine(engine, blocks, source) {
19
+ if (engine === 'none' || engine === 'plain' || engine === 'etex' || engine === 'latex')
20
+ return engine;
21
+ // auto (docs/05 §8)
22
+ const first = source.split('\n', 1)[0] ?? '';
23
+ if (/^%&\s*latex\b/i.test(first))
24
+ return 'latex';
25
+ const firstVerb = blocks.find((b) => b.kind === 'verbatimtex');
26
+ if (firstVerb && /\\documentclass|\\documentstyle/.test(firstVerb.body))
27
+ return 'latex';
28
+ if (/%&\s*etex\b/i.test(first))
29
+ return 'etex';
30
+ return 'plain';
31
+ }
32
+ const PROG = {
33
+ plain: { fmt: 'plain', progname: 'tex' },
34
+ etex: { fmt: 'etex', progname: 'etex' },
35
+ latex: { fmt: 'latex', progname: 'latex' },
36
+ };
37
+ let jobCounter = 0;
38
+ export class TexBridge {
39
+ o;
40
+ constructor(o) {
41
+ this.o = o;
42
+ }
43
+ key(engine, chain, body) {
44
+ return snippetKey({ engineId: this.o.engineId, formatId: this.o.formatIds[engine], chain: chain.map(canonicalBody), body: canonicalBody(body) });
45
+ }
46
+ /**
47
+ * Given the ordered blocks of a document, return the snippets whose chunk is
48
+ * not cached (with their verbatimtex chain), in source order.
49
+ */
50
+ misses(engine, blocks) {
51
+ const chain = [];
52
+ const misses = [];
53
+ let hits = 0;
54
+ const chainFor = new Map();
55
+ for (const b of blocks) {
56
+ if (b.kind === 'verbatimtex') {
57
+ chain.push(b.body);
58
+ continue;
59
+ }
60
+ const key = this.key(engine, chain, b.body);
61
+ chainFor.set(b.index, [...chain]);
62
+ if (this.o.cache.get(key) !== undefined) {
63
+ hits++;
64
+ continue;
65
+ }
66
+ misses.push({ key, chain: [...chain], body: b.body, line: b.line, file: b.file });
67
+ }
68
+ return { hits, misses, chainFor };
69
+ }
70
+ /**
71
+ * Typeset a list of snippets in ONE TeX run and store the resulting chunks.
72
+ * Snippets must be in source order; chains are emitted incrementally.
73
+ */
74
+ async typeset(engine, snippets) {
75
+ const blocks = [];
76
+ let emitted = [];
77
+ let idx = 0;
78
+ const lineOf = [];
79
+ for (const s of snippets) {
80
+ // emit any verbatimtex entries not yet emitted (chains normally extend each other)
81
+ let common = 0;
82
+ while (common < emitted.length && common < s.chain.length && emitted[common] === s.chain[common])
83
+ common++;
84
+ for (let i = common; i < s.chain.length; i++) {
85
+ blocks.push({ kind: 'verbatimtex', body: s.chain[i], raw: s.chain[i], line: s.line ?? 0, file: s.file ?? 'job.mp', index: idx++ });
86
+ }
87
+ emitted = [...s.chain];
88
+ const b = { kind: 'btex', body: s.body, raw: s.body, line: s.line ?? 0, file: s.file ?? 'job.mp', index: idx++ };
89
+ blocks.push(b);
90
+ lineOf.push({ block: b, snippet: s });
91
+ }
92
+ const tex = buildTexJob(blocks, { mode: 'tex', mptexpre: this.o.texPreamble });
93
+ const name = `mpx${(++jobCounter).toString(36)}`;
94
+ const { fmt, progname } = PROG[engine];
95
+ let dvi = null;
96
+ let logFile = '';
97
+ const r = await runTex(this.o.texFactory, {
98
+ args: [`-fmt=${fmt}`, `-progname=${progname}`, '-interaction=nonstopmode', '-parse-first-line', `${name}.tex`],
99
+ setup: (M) => {
100
+ this.o.setupTexFS(M);
101
+ writeFileDeep(M.FS, `/work/${name}.tex`, tex);
102
+ M.FS.chdir('/work');
103
+ },
104
+ collect: (M) => {
105
+ try {
106
+ dvi = M.FS.readFile(`/work/${name}.dvi`, { encoding: 'binary' });
107
+ }
108
+ catch {
109
+ dvi = null;
110
+ }
111
+ try {
112
+ logFile = M.FS.readFile(`/work/${name}.log`, { encoding: 'utf8' });
113
+ }
114
+ catch {
115
+ logFile = r?.log ?? '';
116
+ }
117
+ },
118
+ onLine: this.o.onLine,
119
+ });
120
+ const texLog = logFile || r.log;
121
+ const diagnostics = parseTexLog(texLog, tex, lineOf);
122
+ let pages = 0, resolved = 0;
123
+ if (dvi) {
124
+ const M = this.o.mplib;
125
+ const work = this.o.workDir ?? '/work';
126
+ writeFileDeep(M.FS, `${work}/${name}.dvi`, dvi);
127
+ const rc = dvitomp(M, `${work}/${name}.dvi`, `${work}/${name}.mpx`, '% Written by mp-tikz-wasm');
128
+ if (rc !== 0) {
129
+ let err = mpxLastError(M);
130
+ try {
131
+ err += '\n' + M.FS.readFile(`${work}/mpxerr.log`, { encoding: 'utf8' });
132
+ }
133
+ catch { /* none */ }
134
+ diagnostics.push({ severity: 'error', source: 'tex', message: `dvitomp failed: ${err.trim()}` });
135
+ }
136
+ else {
137
+ const mpx = M.FS.readFile(`${work}/${name}.mpx`, { encoding: 'utf8' });
138
+ const chunks = splitMpx(mpx);
139
+ pages = chunks.length;
140
+ for (let i = 0; i < lineOf.length; i++) {
141
+ const chunk = chunks[i];
142
+ if (chunk !== undefined) {
143
+ this.o.cache.set(lineOf[i].snippet.key, chunk);
144
+ resolved++;
145
+ }
146
+ else {
147
+ this.o.cache.set(lineOf[i].snippet.key, 'nullpicture');
148
+ diagnostics.push({ severity: 'warning', source: 'tex', message: 'btex block produced no output; replaced by nullpicture', file: lineOf[i].snippet.file, line: lineOf[i].snippet.line, snippet: lineOf[i].snippet.body });
149
+ }
150
+ }
151
+ try {
152
+ M.FS.unlink(`${work}/${name}.mpx`);
153
+ }
154
+ catch { /* ignore */ }
155
+ }
156
+ try {
157
+ M.FS.unlink(`${work}/${name}.dvi`);
158
+ }
159
+ catch { /* ignore */ }
160
+ }
161
+ else {
162
+ // no DVI at all: every snippet becomes nullpicture so the run can proceed
163
+ for (const s of snippets)
164
+ this.o.cache.set(s.key, 'nullpicture');
165
+ if (!diagnostics.some((d) => d.severity === 'error')) {
166
+ diagnostics.push({ severity: 'error', source: 'tex', message: `TeX produced no DVI (exit ${r.exitCode})` });
167
+ }
168
+ }
169
+ return { texLog, exitCode: r.exitCode, ms: r.ms, pages, diagnostics, resolved };
170
+ }
171
+ }
172
+ /**
173
+ * Map TeX errors back to btex blocks: the generated .tex has one
174
+ * `\mpxshipout% line N file` wrapper line per block; a TeX `l.<n>` line number
175
+ * inside [wrapper, \stopmpxshipout] belongs to that block.
176
+ */
177
+ export function parseTexLog(log, tex, blocks) {
178
+ const out = [];
179
+ const texLines = tex.split('\n');
180
+ // wrapper line index (0-based) for each btex block, in order of appearance
181
+ const starts = [];
182
+ texLines.forEach((l, i) => { if (l.startsWith('\\mpxshipout%'))
183
+ starts.push(i); });
184
+ const lines = log.split('\n');
185
+ for (let i = 0; i < lines.length; i++) {
186
+ const m = /^! (.*)$/.exec(lines[i]);
187
+ if (!m)
188
+ continue;
189
+ let texLine;
190
+ const help = [];
191
+ let j = i + 1;
192
+ for (; j < lines.length && j < i + 40; j++) {
193
+ const lm = /^l\.(\d+)/.exec(lines[j]);
194
+ if (lm) {
195
+ texLine = Number(lm[1]);
196
+ break;
197
+ }
198
+ if (/^! /.test(lines[j]))
199
+ break;
200
+ }
201
+ // help paragraph follows the context lines
202
+ for (let k = (texLine !== undefined ? j + 2 : i + 1); k < lines.length && k < i + 40; k++) {
203
+ if (lines[k].trim() === '' || /^! /.test(lines[k]) || /^l\.\d+/.test(lines[k]))
204
+ break;
205
+ help.push(lines[k]);
206
+ }
207
+ let blockIdx = -1;
208
+ if (texLine !== undefined) {
209
+ for (let b = 0; b < starts.length; b++)
210
+ if (starts[b] < texLine)
211
+ blockIdx = b;
212
+ }
213
+ const d = { severity: 'error', source: 'tex', message: m[1], help: help.length ? help : undefined };
214
+ if (blockIdx >= 0 && blocks[blockIdx]) {
215
+ d.file = blocks[blockIdx].snippet.file;
216
+ d.line = blocks[blockIdx].snippet.line;
217
+ d.snippet = blocks[blockIdx].snippet.body;
218
+ }
219
+ out.push(d);
220
+ }
221
+ return out;
222
+ }
223
+ //# sourceMappingURL=bridge.js.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * cache-key.ts — content-addressed keys for the two TeX-bridge caches
3
+ * (docs/05 §6): L2 snippet keys and L1 document keys.
4
+ *
5
+ * Both are SHA-256 over a canonical, length-prefixed encoding, so no two
6
+ * distinct inputs can collide by concatenation ("ab"+"c" vs "a"+"bc"), and a
7
+ * snippet key can never equal a document key (each kind carries its own tag).
8
+ *
9
+ * The digest is computed by a small pure-TypeScript SHA-256 rather than
10
+ * `crypto.subtle.digest`, because the `make_text` hook that consults the
11
+ * snippet cache must answer synchronously and Web Crypto is Promise-only.
12
+ * Dependency-free and browser-safe.
13
+ */
14
+ export interface SnippetKeyParts {
15
+ /** Build id of tex.wasm. */
16
+ engineId: string;
17
+ /** Hash of the format file (plain.fmt / latex.fmt). */
18
+ formatId: string;
19
+ /** The verbatimtex blocks that precede the snippet, in source order. */
20
+ chain: readonly string[];
21
+ /** The btex text as handed to make_text. */
22
+ body: string;
23
+ /** MetaPost's `texscriptmode` internal; defaults to 1 like MetaPost. */
24
+ texscriptmode?: number;
25
+ }
26
+ /** L2 key: H(engineId, formatId, verbatimtex chain, body, texscriptmode). */
27
+ export declare function snippetKey(parts: SnippetKeyParts): string;
28
+ /** L1 key: H(full generated .tex, engineId, formatId). */
29
+ export declare function documentKey(texSource: string, engineId: string, formatId: string): string;
30
+ /** SHA-256 of `data` (a string is UTF-8 encoded first) as lowercase hex. */
31
+ export declare function sha256Hex(data: Uint8Array | string): string;
32
+ /** SHA-256 of `msg`, as 32 bytes. */
33
+ export declare function sha256(msg: Uint8Array): Uint8Array;