@lofcz/tikz 0.2.2 → 0.2.4

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 (128) hide show
  1. package/NOTICE.md +3 -1
  2. package/README.md +21 -2
  3. package/package.json +1 -1
  4. package/runtime/.build-key +1 -0
  5. package/runtime/dist/bundles/cm-tfm/chunks/0.br +0 -0
  6. package/runtime/dist/bundles/cm-tfm/chunks/1.br +0 -0
  7. package/runtime/dist/bundles/cm-tfm/chunks/2.br +0 -0
  8. package/runtime/dist/bundles/cm-tfm/chunks/3.br +0 -0
  9. package/runtime/dist/bundles/cm-tfm/chunks/4.br +0 -0
  10. package/runtime/dist/bundles/cm-tfm/chunks/5.br +0 -0
  11. package/runtime/dist/bundles/cm-tfm/chunks/6.br +0 -0
  12. package/runtime/dist/bundles/cm-tfm/chunks/7.br +0 -0
  13. package/runtime/dist/bundles/cm-tfm/files.index.json +1 -1
  14. package/runtime/dist/bundles/cm-type1/chunks/0.br +0 -0
  15. package/runtime/dist/bundles/cm-type1/chunks/1.br +0 -0
  16. package/runtime/dist/bundles/cm-type1/chunks/10.br +0 -0
  17. package/runtime/dist/bundles/cm-type1/chunks/11.br +0 -0
  18. package/runtime/dist/bundles/cm-type1/chunks/2.br +0 -0
  19. package/runtime/dist/bundles/cm-type1/chunks/3.br +0 -0
  20. package/runtime/dist/bundles/cm-type1/chunks/4.br +0 -0
  21. package/runtime/dist/bundles/cm-type1/chunks/5.br +0 -0
  22. package/runtime/dist/bundles/cm-type1/chunks/6.br +0 -0
  23. package/runtime/dist/bundles/cm-type1/chunks/7.br +0 -0
  24. package/runtime/dist/bundles/cm-type1/chunks/8.br +0 -0
  25. package/runtime/dist/bundles/cm-type1/chunks/9.br +0 -0
  26. package/runtime/dist/bundles/cm-type1/files.index.json +1 -1
  27. package/runtime/dist/bundles/core/chunks/0.br +0 -0
  28. package/runtime/dist/bundles/core/chunks/1.br +0 -0
  29. package/runtime/dist/bundles/core/chunks/2.br +0 -0
  30. package/runtime/dist/bundles/core/chunks/3.br +0 -0
  31. package/runtime/dist/bundles/core/files.index.json +1 -1
  32. package/runtime/dist/bundles/filesystem.json +1 -1
  33. package/runtime/dist/bundles/latex-core/chunks/0.br +0 -0
  34. package/runtime/dist/bundles/latex-core/chunks/1.br +0 -0
  35. package/runtime/dist/bundles/latex-core/chunks/10.br +0 -0
  36. package/runtime/dist/bundles/latex-core/chunks/2.br +0 -0
  37. package/runtime/dist/bundles/latex-core/chunks/3.br +0 -0
  38. package/runtime/dist/bundles/latex-core/chunks/4.br +0 -0
  39. package/runtime/dist/bundles/latex-core/chunks/5.br +0 -0
  40. package/runtime/dist/bundles/latex-core/chunks/6.br +0 -0
  41. package/runtime/dist/bundles/latex-core/chunks/7.br +0 -0
  42. package/runtime/dist/bundles/latex-core/chunks/8.br +0 -0
  43. package/runtime/dist/bundles/latex-core/chunks/9.br +0 -0
  44. package/runtime/dist/bundles/latex-core/files.index.json +1 -1
  45. package/runtime/dist/bundles/latex-extra/chunks/0.br +0 -0
  46. package/runtime/dist/bundles/latex-extra/chunks/1.br +0 -0
  47. package/runtime/dist/bundles/latex-extra/chunks/10.br +0 -0
  48. package/runtime/dist/bundles/latex-extra/chunks/11.br +0 -0
  49. package/runtime/dist/bundles/latex-extra/chunks/12.br +0 -0
  50. package/runtime/dist/bundles/latex-extra/chunks/13.br +0 -0
  51. package/runtime/dist/bundles/latex-extra/chunks/14.br +0 -0
  52. package/runtime/dist/bundles/latex-extra/chunks/15.br +0 -0
  53. package/runtime/dist/bundles/latex-extra/chunks/16.br +0 -0
  54. package/runtime/dist/bundles/latex-extra/chunks/17.br +0 -0
  55. package/runtime/dist/bundles/latex-extra/chunks/18.br +0 -0
  56. package/runtime/dist/bundles/latex-extra/chunks/19.br +0 -0
  57. package/runtime/dist/bundles/latex-extra/chunks/2.br +0 -0
  58. package/runtime/dist/bundles/latex-extra/chunks/20.br +0 -0
  59. package/runtime/dist/bundles/latex-extra/chunks/21.br +0 -0
  60. package/runtime/dist/bundles/latex-extra/chunks/22.br +0 -0
  61. package/runtime/dist/bundles/latex-extra/chunks/23.br +0 -0
  62. package/runtime/dist/bundles/latex-extra/chunks/24.br +0 -0
  63. package/runtime/dist/bundles/latex-extra/chunks/25.br +0 -0
  64. package/runtime/dist/bundles/latex-extra/chunks/26.br +0 -0
  65. package/runtime/dist/bundles/latex-extra/chunks/27.br +0 -0
  66. package/runtime/dist/bundles/latex-extra/chunks/3.br +0 -0
  67. package/runtime/dist/bundles/latex-extra/chunks/4.br +0 -0
  68. package/runtime/dist/bundles/latex-extra/chunks/5.br +0 -0
  69. package/runtime/dist/bundles/latex-extra/chunks/6.br +0 -0
  70. package/runtime/dist/bundles/latex-extra/chunks/7.br +0 -0
  71. package/runtime/dist/bundles/latex-extra/chunks/8.br +0 -0
  72. package/runtime/dist/bundles/latex-extra/chunks/9.br +0 -0
  73. package/runtime/dist/bundles/latex-extra/files.index.json +1 -1
  74. package/runtime/dist/bundles/lm-fonts/chunks/0.br +0 -0
  75. package/runtime/dist/bundles/lm-fonts/chunks/1.br +0 -0
  76. package/runtime/dist/bundles/lm-fonts/chunks/10.br +0 -0
  77. package/runtime/dist/bundles/lm-fonts/chunks/11.br +0 -0
  78. package/runtime/dist/bundles/lm-fonts/chunks/12.br +0 -0
  79. package/runtime/dist/bundles/lm-fonts/chunks/13.br +0 -0
  80. package/runtime/dist/bundles/lm-fonts/chunks/14.br +0 -0
  81. package/runtime/dist/bundles/lm-fonts/chunks/15.br +0 -0
  82. package/runtime/dist/bundles/lm-fonts/chunks/16.br +0 -0
  83. package/runtime/dist/bundles/lm-fonts/chunks/17.br +0 -0
  84. package/runtime/dist/bundles/lm-fonts/chunks/18.br +0 -0
  85. package/runtime/dist/bundles/lm-fonts/chunks/19.br +0 -0
  86. package/runtime/dist/bundles/lm-fonts/chunks/2.br +0 -0
  87. package/runtime/dist/bundles/lm-fonts/chunks/20.br +0 -0
  88. package/runtime/dist/bundles/lm-fonts/chunks/21.br +0 -0
  89. package/runtime/dist/bundles/lm-fonts/chunks/22.br +0 -0
  90. package/runtime/dist/bundles/lm-fonts/chunks/23.br +0 -0
  91. package/runtime/dist/bundles/lm-fonts/chunks/24.br +0 -0
  92. package/runtime/dist/bundles/lm-fonts/chunks/25.br +0 -0
  93. package/runtime/dist/bundles/lm-fonts/chunks/26.br +0 -0
  94. package/runtime/dist/bundles/lm-fonts/chunks/27.br +0 -0
  95. package/runtime/dist/bundles/lm-fonts/chunks/28.br +0 -0
  96. package/runtime/dist/bundles/lm-fonts/chunks/29.br +0 -0
  97. package/runtime/dist/bundles/lm-fonts/chunks/3.br +0 -0
  98. package/runtime/dist/bundles/lm-fonts/chunks/4.br +0 -0
  99. package/runtime/dist/bundles/lm-fonts/chunks/5.br +0 -0
  100. package/runtime/dist/bundles/lm-fonts/chunks/6.br +0 -0
  101. package/runtime/dist/bundles/lm-fonts/chunks/7.br +0 -0
  102. package/runtime/dist/bundles/lm-fonts/chunks/8.br +0 -0
  103. package/runtime/dist/bundles/lm-fonts/chunks/9.br +0 -0
  104. package/runtime/dist/bundles/lm-fonts/files.index.json +1 -1
  105. package/runtime/dist/bundles/luatex/chunks/0.br +0 -0
  106. package/runtime/dist/bundles/luatex/chunks/1.br +0 -0
  107. package/runtime/dist/bundles/luatex/files.index.json +1 -1
  108. package/runtime/dist/bundles/tex-plain/chunks/0.br +0 -0
  109. package/runtime/dist/bundles/tex-plain/chunks/1.br +0 -0
  110. package/runtime/dist/bundles/tex-plain/chunks/3.br +0 -0
  111. package/runtime/dist/bundles/tex-plain/chunks/4.br +0 -0
  112. package/runtime/dist/bundles/tex-plain/chunks/5.br +0 -0
  113. package/runtime/dist/bundles/tex-plain/chunks/6.br +0 -0
  114. package/runtime/dist/bundles/tex-plain/chunks/7.br +0 -0
  115. package/runtime/dist/bundles/tex-plain/files.index.json +1 -1
  116. package/runtime/dist/bundles/tikz-snapshot/chunks/1.br +0 -0
  117. package/runtime/dist/bundles/tikz-snapshot/files.index.json +1 -1
  118. package/runtime/dist/bundles/tikz-snapshot/manifest.json +1 -1
  119. package/runtime/dist/core.js +20 -13
  120. package/runtime/dist/texengine.d.ts +1 -0
  121. package/runtime/dist/types.d.ts +3 -3
  122. package/runtime/dist/vfs/lazyfs.d.ts +6 -1
  123. package/runtime/dist/vfs/lazyfs.js +17 -2
  124. package/src/browser-worker.js +3 -7
  125. package/src/deploy.js +1 -1
  126. package/src/index.js +3 -6
  127. package/src/source.js +97 -0
  128. package/src/worker.js +2 -1
@@ -101,9 +101,9 @@ export interface MetaPostOptions {
101
101
  /** Defer engine glue and MetaPost WASM until used. MetaPost version fields in creation
102
102
  * metadata remain empty in this mode. No cross-request cache is required. */
103
103
  lazy?: boolean;
104
- /** Default for latex()'s snapshot option. Node: 'auto' (tikz.fmt from local files is free). Browser: 'none',
105
- * because the 5.8 MB format does not compress and costs more to download than the files it replaces
106
- * (2.5 MB gzipped); set 'auto' when many TikZ figures amortise it or the format is already cached. */
104
+ /** Default for latex()'s snapshot option: Node 'auto', browser 'none'. Browser hosts
105
+ * may enable 'auto' when shipping precompiled formats (the @lofcz/tikz browser entry does).
106
+ * Auto uses basic TikZ when available and the full format for explicit plotting packages. */
107
107
  snapshot?: 'auto' | 'none';
108
108
  }
109
109
  export type OutputFormat = 'svg' | 'eps' | 'json' | 'none';
@@ -12,6 +12,7 @@ export interface EmscriptenFS {
12
12
  mkdir(path: string, mode?: number): void;
13
13
  writeFile(path: string, data: Uint8Array | string, opts?: {
14
14
  encoding?: string;
15
+ canOwn?: boolean;
15
16
  }): void;
16
17
  readFile(path: string, opts?: {
17
18
  encoding?: 'binary' | 'utf8';
@@ -50,7 +51,7 @@ export declare function isUnloadedLazy(FS: EmscriptenFS, path: string): boolean;
50
51
  /** Force-load a lazy node (no-op for ordinary files). */
51
52
  export declare function ensureLoaded(FS: EmscriptenFS, path: string): void;
52
53
  /** Write a text or binary file, creating directories as needed. */
53
- export declare function writeFileDeep(FS: EmscriptenFS, path: string, data: Uint8Array | string): void;
54
+ export declare function writeFileDeep(FS: EmscriptenFS, path: string, data: Uint8Array | string, canOwn?: boolean): void;
54
55
  /**
55
56
  * Record every file opened for writing from now on (absolute paths, into the
56
57
  * returned set). The engine's open syscall reaches the filesystem through
@@ -60,3 +61,7 @@ export declare function writeFileDeep(FS: EmscriptenFS, path: string, data: Uint
60
61
  export declare function trackWrites(FS: EmscriptenFS): Set<string>;
61
62
  /** Recursively collect regular files under `dir` (used to harvest artifacts). */
62
63
  export declare function listFiles(FS: EmscriptenFS, dir: string): string[];
64
+ /** Borrow a completed MEMFS file; never retain a view into a module's WASM heap. */
65
+ export declare function completedFileBytes(FS: EmscriptenFS, path: string, heap?: Uint8Array): Uint8Array;
66
+ /** Auxiliary inputs remain available, but are copied only if the consumer opens them. */
67
+ export declare function writeDeferredFile(FS: EmscriptenFS, path: string, size: number, load: () => Uint8Array): void;
@@ -82,7 +82,7 @@ export function ensureLoaded(FS, path) {
82
82
  }
83
83
  }
84
84
  /** Write a text or binary file, creating directories as needed. */
85
- export function writeFileDeep(FS, path, data) {
85
+ export function writeFileDeep(FS, path, data, canOwn = false) {
86
86
  const i = path.lastIndexOf('/');
87
87
  if (i > 0)
88
88
  mkdirp(FS, path.slice(0, i));
@@ -92,7 +92,7 @@ export function writeFileDeep(FS, path, data) {
92
92
  }
93
93
  catch { /* ignore */ }
94
94
  }
95
- FS.writeFile(path, data);
95
+ FS.writeFile(path, data, { canOwn });
96
96
  }
97
97
  /**
98
98
  * Record every file opened for writing from now on (absolute paths, into the
@@ -130,4 +130,19 @@ export function listFiles(FS, dir) {
130
130
  walk(dir);
131
131
  return out;
132
132
  }
133
+ /** Borrow a completed MEMFS file; never retain a view into a module's WASM heap. */
134
+ export function completedFileBytes(FS, path, heap) {
135
+ const node = FS.lookupPath(path).node;
136
+ if (heap && node.contents instanceof Uint8Array && node.contents.buffer !== heap.buffer &&
137
+ Number.isSafeInteger(node.usedBytes) && node.usedBytes >= 0 && node.usedBytes <= node.contents.length)
138
+ return node.contents.subarray(0, node.usedBytes);
139
+ return FS.readFile(path, { encoding: 'binary' });
140
+ }
141
+ /** Auxiliary inputs remain available, but are copied only if the consumer opens them. */
142
+ export function writeDeferredFile(FS, path, size, load) {
143
+ mkdirp(FS, path.slice(0, path.lastIndexOf('/')) || '/');
144
+ if (FS.analyzePath(path).exists)
145
+ FS.unlink(path);
146
+ createLazyFile(FS, path, size, () => load().slice());
147
+ }
133
148
  //# sourceMappingURL=lazyfs.js.map
@@ -1,15 +1,11 @@
1
1
  import { MetaPost } from '../runtime/dist/index.js';
2
+ import { prepareTikzSource } from './source.js';
2
3
 
3
4
  try {
4
- const engine = await MetaPost.create({ worker: false, lazy: true, logLevel: 'silent' });
5
+ const engine = await MetaPost.create({ worker: false, lazy: true, snapshot: 'auto', logLevel: 'silent' });
5
6
  self.onmessage = async ({ data: { source: code, maxOutputBytes } }) => {
6
7
  try {
7
- const source = /^\s*\\documentclass\b/m.test(code) ? code : String.raw`\documentclass[tikz,border=6pt]{standalone}
8
- \usetikzlibrary{calc,intersections,angles,quotes,arrows.meta,positioning}
9
- \begin{document}
10
- ${code}
11
- \end{document}`;
12
- const result = await engine.latex(source, { engine: 'auto', fonts: 'paths' });
8
+ const result = await engine.latex(prepareTikzSource(code), { engine: 'auto', fonts: 'paths' });
13
9
  if (result.status === 'error' || result.status === 'fatal' || result.pages?.length !== 1)
14
10
  throw new Error('TikZ requires one valid page. ' + JSON.stringify(result.diagnostics).slice(0, 2000));
15
11
  const svg = result.pages[0], bytes = new TextEncoder().encode(svg);
package/src/deploy.js CHANGED
@@ -28,7 +28,7 @@ export async function copyTikzBrowserAssets(directory) {
28
28
  throw new Error('TikZ browser assets must be outside the installed package');
29
29
  await mkdir(join(destination, 'src'), { recursive: true });
30
30
  await cp(join(source, 'runtime'), join(destination, 'runtime'), { recursive: true });
31
- for (const name of ['browser.js', 'browser-worker.js'])
31
+ for (const name of ['browser.js', 'browser-worker.js', 'source.js'])
32
32
  await cp(join(source, 'src', name), join(destination, 'src', name));
33
33
  for (const name of ['LICENSE', 'NOTICE.md', 'upstream.json'])
34
34
  await cp(join(source, name), join(destination, name));
package/src/index.js CHANGED
@@ -89,13 +89,10 @@ export function createTikzRenderer(options = {}) {
89
89
  async render(code, { signal } = {}) {
90
90
  if (typeof code !== 'string' || !code.trim() || code.length > settings.maxSourceLength)
91
91
  throw new Error(`TikZ: use 1–${settings.maxSourceLength.toLocaleString('en-US')} characters.`);
92
- const source = /^\s*\\documentclass\b/m.test(code) ? code : String.raw`\documentclass[tikz,border=6pt]{standalone}
93
- \usetikzlibrary{calc,intersections,angles,quotes,arrows.meta,positioning}
94
- \begin{document}
95
- ${code}
96
- \end{document}`;
92
+ // The worker normalizes and wraps the picture (src/source.js), so this
93
+ // entry stays relocatable: it only needs the package directory.
97
94
  await acquire(signal);
98
- try { return await compile(source, signal); }
95
+ try { return await compile(code, signal); }
99
96
  finally { release(); }
100
97
  },
101
98
  };
package/src/source.js ADDED
@@ -0,0 +1,97 @@
1
+ // Source normalization shared by the Node worker and the browser worker.
2
+ //
3
+ // A bare picture is wrapped in a standalone document with the common
4
+ // libraries. `\color{…}` inside a `font=` key is rewritten into the node's
5
+ // `text=` key: in this DVI→SVG pipeline the colour special opened by a font
6
+ // switch is never popped inside the picture, and dvisvgm then rejects the page
7
+ // ("missing closing tag(s): </g>"). `text=` paints the same colour the TikZ way.
8
+
9
+ const PREAMBLE = String.raw`\documentclass[tikz,border=6pt]{standalone}
10
+ \usetikzlibrary{calc,intersections,angles,quotes,arrows.meta,positioning}
11
+ \begin{document}
12
+ `;
13
+
14
+ /** Wrap a bare picture into a complete standalone document. */
15
+ export function wrapStandalone(code) {
16
+ return /^\s*\\documentclass\b/m.test(code) ? code : `${PREAMBLE}${code}\n\\end{document}`;
17
+ }
18
+
19
+ /** Index just past the balanced group that opens at `start` (`{`/`[`), or -1. */
20
+ function groupEnd(text, start) {
21
+ const open = text[start], close = open === '{' ? '}' : ']';
22
+ let depth = 0;
23
+ for (let i = start; i < text.length; i++) {
24
+ const char = text[i];
25
+ if (char === '\\') { i++; continue; }
26
+ if (char === open) depth++;
27
+ else if (char === close && --depth === 0) return i + 1;
28
+ }
29
+ return -1;
30
+ }
31
+
32
+ /** End of a pgfkeys value starting at `start`: the next top-level `,`, `]` or `}`. */
33
+ function valueEnd(text, start) {
34
+ for (let i = start; i < text.length; i++) {
35
+ const char = text[i];
36
+ if (char === '\\') { i++; continue; }
37
+ if (char === '{' || char === '[') { const end = groupEnd(text, i); if (end < 0) return -1; i = end - 1; continue; }
38
+ if (char === ',' || char === ']' || char === '}') return i;
39
+ }
40
+ return text.length;
41
+ }
42
+
43
+ /** Position of the `]`/`}` that closes the option list containing `from`, or -1. */
44
+ function listEnd(text, from) {
45
+ let depth = 0;
46
+ for (let i = from; i < text.length; i++) {
47
+ const char = text[i];
48
+ if (char === '\\') { i++; continue; }
49
+ if (char === '{' || char === '[') depth++;
50
+ else if (char === '}' || char === ']') { if (depth === 0) return i; depth--; }
51
+ }
52
+ return -1;
53
+ }
54
+
55
+ const COLOR = /\\color\s*\{([^{}]*)\}/g;
56
+
57
+ /** Move every `\color{name}` out of `font=` values into a trailing `text=name` key. */
58
+ export function hoistFontColors(source) {
59
+ let text = source;
60
+ const keyPattern = /(^|[\[{,])(\s*font\s*=\s*)/g;
61
+ for (let match = keyPattern.exec(text); match; match = keyPattern.exec(text)) {
62
+ const keyStart = match.index + match[1].length;
63
+ const valueStart = match.index + match[0].length;
64
+ const end = valueEnd(text, valueStart);
65
+ if (end < 0) break;
66
+ const value = text.slice(valueStart, end);
67
+ const colors = [...value.matchAll(COLOR)].map(m => m[1].trim()).filter(Boolean);
68
+ if (colors.length === 0) continue;
69
+ const braced = /^\{[\s\S]*\}$/.test(value.trim());
70
+ let cleaned = value.replace(COLOR, '').replace(/[ \t]+$/, '');
71
+ if (braced && !cleaned.replace(/^\{|\}$/g, '').trim()) cleaned = '';
72
+ else if (!braced && !cleaned.trim()) cleaned = '';
73
+ const close = listEnd(text, end);
74
+ if (close < 0) break;
75
+ const closer = text.slice(close);
76
+ let replaced;
77
+ if (cleaned) {
78
+ replaced = text.slice(0, valueStart) + cleaned + text.slice(end, close);
79
+ } else {
80
+ // Drop the whole `font=` key with the comma that separated it.
81
+ let keyRemoveStart = keyStart, keyRemoveEnd = end;
82
+ if (text[end] === ',') keyRemoveEnd = end + 1;
83
+ else if (match[1] === ',') keyRemoveStart = match.index;
84
+ replaced = text.slice(0, keyRemoveStart) + text.slice(keyRemoveEnd, close);
85
+ }
86
+ const replacedHead = replaced.replace(/[\s,]+$/, '');
87
+ const separator = /[[{]$/.test(replacedHead) ? '' : ', ';
88
+ text = `${replacedHead}${separator}text=${colors[colors.length - 1]}${closer}`;
89
+ keyPattern.lastIndex = Math.max(0, replacedHead.length - value.length);
90
+ }
91
+ return text;
92
+ }
93
+
94
+ /** Everything the TeX engine receives: colour-safe keys inside a complete document. */
95
+ export function prepareTikzSource(code) {
96
+ return wrapStandalone(hoistFontColors(code));
97
+ }
package/src/worker.js CHANGED
@@ -2,10 +2,11 @@
2
2
  // filesystem is in-memory; no host texmf tree, shell or caller environment.
3
3
  import { readFileSync } from 'node:fs';
4
4
  import { MetaPost } from '../runtime/dist/index.js';
5
+ import { prepareTikzSource } from './source.js';
5
6
  const { source } = JSON.parse(readFileSync(0, 'utf8'));
6
7
  const engine = await MetaPost.create({ worker: false, lazy: true, logLevel: 'silent' });
7
8
  try {
8
- const result = await engine.latex(source, { engine: 'auto', fonts: 'paths' });
9
+ const result = await engine.latex(prepareTikzSource(source), { engine: 'auto', fonts: 'paths' });
9
10
  if (result.status === 'error' || result.status === 'fatal' || result.pages?.length !== 1)
10
11
  throw new Error('TikZ requires one valid page. ' + JSON.stringify(result.diagnostics).slice(0, 2000));
11
12
  process.stdout.write(result.pages[0]);