@danielsimonjr/mathts-plot 0.4.11 → 0.4.12
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/dist/index.js +1235 -1126
- package/dist/render-file.js +140 -154
- package/package.json +6 -6
package/dist/render-file.js
CHANGED
|
@@ -1,166 +1,152 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { extname, join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
//#region src/render-file.ts
|
|
6
6
|
var PlotRenderError = class extends Error {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
missingTool;
|
|
8
|
+
constructor(message, missingTool) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.missingTool = missingTool;
|
|
11
|
+
this.name = "PlotRenderError";
|
|
12
|
+
}
|
|
13
13
|
};
|
|
14
|
-
|
|
14
|
+
const ALLOWED_TOOLS = [
|
|
15
|
+
"rsvg-convert",
|
|
16
|
+
"resvg",
|
|
17
|
+
"pdflatex",
|
|
18
|
+
"xelatex",
|
|
19
|
+
"lualatex",
|
|
20
|
+
"tectonic"
|
|
21
|
+
];
|
|
15
22
|
function runTool(cmd, args, opts = {}) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
if (!ALLOWED_TOOLS.includes(cmd)) {
|
|
25
|
+
reject(new PlotRenderError(`Security Error: Execution of tool '${cmd}' is not allowed.`));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const child = spawn(cmd, args, { timeout: opts.timeoutMs ?? 3e4 });
|
|
29
|
+
const out = [];
|
|
30
|
+
let err = "";
|
|
31
|
+
child.stdout.on("data", (d) => out.push(d));
|
|
32
|
+
child.stderr.on("data", (d) => err += d.toString());
|
|
33
|
+
child.on("error", (e) => reject(new PlotRenderError(`${cmd} could not be run: ${e.message}`, cmd)));
|
|
34
|
+
child.on("close", (code) => resolve({
|
|
35
|
+
code,
|
|
36
|
+
stdout: Buffer.concat(out),
|
|
37
|
+
stderr: err
|
|
38
|
+
}));
|
|
39
|
+
if (opts.input !== void 0) {
|
|
40
|
+
child.stdin.on("error", () => {});
|
|
41
|
+
child.stdin.end(opts.input);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
37
44
|
}
|
|
38
45
|
async function hasTool(name) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
46
|
+
try {
|
|
47
|
+
return (await runTool(name, ["--version"], { timeoutMs: 5e3 })).code === 0;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
45
51
|
}
|
|
46
52
|
async function renderToFile(svg, outPath, opts = {}) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
throw new PlotRenderError(
|
|
101
|
-
`No SVG converter found for .${format}. Install rsvg-convert (librsvg) or resvg and ensure it is on PATH.`,
|
|
102
|
-
"rsvg-convert"
|
|
103
|
-
);
|
|
53
|
+
if (opts.tool && !ALLOWED_TOOLS.includes(opts.tool)) throw new PlotRenderError(`Security Error: Execution of tool '${opts.tool}' is not allowed.`);
|
|
54
|
+
const ext = extname(outPath).toLowerCase();
|
|
55
|
+
if (ext === ".svg") {
|
|
56
|
+
await writeFile(outPath, svg, "utf-8");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (ext !== ".png" && ext !== ".pdf") throw new PlotRenderError(`Unsupported output extension '${ext}' (expected .svg, .png, or .pdf)`);
|
|
60
|
+
const format = ext.slice(1);
|
|
61
|
+
const candidates = opts.tool ? [opts.tool] : ["rsvg-convert", "resvg"];
|
|
62
|
+
for (const tool of candidates) {
|
|
63
|
+
if (!await hasTool(tool)) continue;
|
|
64
|
+
const tmpOut = outPath + ".tmp";
|
|
65
|
+
if (tool === "rsvg-convert") {
|
|
66
|
+
const args = [
|
|
67
|
+
"-f",
|
|
68
|
+
format,
|
|
69
|
+
"-o",
|
|
70
|
+
tmpOut
|
|
71
|
+
];
|
|
72
|
+
if (format === "png" && opts.density) args.push("--dpi-x", String(opts.density), "--dpi-y", String(opts.density));
|
|
73
|
+
if (format === "png" && opts.background) args.push("--background-color", opts.background);
|
|
74
|
+
const r = await runTool("rsvg-convert", args, {
|
|
75
|
+
timeoutMs: opts.timeoutMs,
|
|
76
|
+
input: svg
|
|
77
|
+
});
|
|
78
|
+
if (r.code !== 0) {
|
|
79
|
+
await rm(tmpOut, { force: true });
|
|
80
|
+
throw new PlotRenderError(`rsvg-convert failed: ${r.stderr}`);
|
|
81
|
+
}
|
|
82
|
+
await rename(tmpOut, outPath);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const tmpSvg = outPath + ".tmp.svg";
|
|
86
|
+
await writeFile(tmpSvg, svg, "utf-8");
|
|
87
|
+
try {
|
|
88
|
+
const r = await runTool("resvg", [tmpSvg, tmpOut], { timeoutMs: opts.timeoutMs });
|
|
89
|
+
if (r.code !== 0) {
|
|
90
|
+
await rm(tmpOut, { force: true });
|
|
91
|
+
throw new PlotRenderError(`resvg failed: ${r.stderr}`);
|
|
92
|
+
}
|
|
93
|
+
} finally {
|
|
94
|
+
await rm(tmpSvg, { force: true });
|
|
95
|
+
}
|
|
96
|
+
if (format === "pdf") {
|
|
97
|
+
await rm(tmpOut, { force: true });
|
|
98
|
+
throw new PlotRenderError("resvg does not emit PDF; install rsvg-convert for SVG→PDF", "rsvg-convert");
|
|
99
|
+
}
|
|
100
|
+
await rename(tmpOut, outPath);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
throw new PlotRenderError(`No SVG converter found for .${format}. Install rsvg-convert (librsvg) or resvg and ensure it is on PATH.`, "rsvg-convert");
|
|
104
104
|
}
|
|
105
105
|
function latexArgs(engine, workDir, texPath) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
106
|
+
if (engine === "tectonic") return [
|
|
107
|
+
"--outdir",
|
|
108
|
+
workDir,
|
|
109
|
+
texPath
|
|
110
|
+
];
|
|
111
|
+
return [
|
|
112
|
+
"-no-shell-escape",
|
|
113
|
+
"-interaction=nonstopmode",
|
|
114
|
+
"-halt-on-error",
|
|
115
|
+
"-output-directory",
|
|
116
|
+
workDir,
|
|
117
|
+
texPath
|
|
118
|
+
];
|
|
117
119
|
}
|
|
118
120
|
async function latexToPdf(texSource, outPath, opts = {}) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
pdf = await readFile(pdfPath);
|
|
149
|
-
} catch {
|
|
150
|
-
throw new PlotRenderError(
|
|
151
|
-
`${engine} produced no PDF (exit ${r.code}): ${r.stderr || r.stdout.toString().slice(-500)}`
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
await writeFile(outPath, pdf);
|
|
155
|
-
} finally {
|
|
156
|
-
await rm(work, { recursive: true, force: true });
|
|
157
|
-
}
|
|
121
|
+
if (opts.tool && !ALLOWED_TOOLS.includes(opts.tool)) throw new PlotRenderError(`Security Error: Execution of tool '${opts.tool}' is not allowed.`);
|
|
122
|
+
if (extname(outPath).toLowerCase() !== ".pdf") throw new PlotRenderError(`latexToPdf output must be a .pdf path (got '${outPath}')`);
|
|
123
|
+
const candidates = opts.tool ? [opts.tool] : ["pdflatex", "tectonic"];
|
|
124
|
+
let engine;
|
|
125
|
+
for (const c of candidates) if (await hasTool(c)) {
|
|
126
|
+
engine = c;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
if (!engine) throw new PlotRenderError("No LaTeX engine found. Install TeX Live (pdflatex) or tectonic and ensure it is on PATH.", "pdflatex");
|
|
130
|
+
const work = await mkdtemp(join(tmpdir(), "plot-tex-"));
|
|
131
|
+
try {
|
|
132
|
+
const texPath = join(work, "doc.tex");
|
|
133
|
+
await writeFile(texPath, texSource, "utf-8");
|
|
134
|
+
const args = latexArgs(engine, work, texPath);
|
|
135
|
+
const r = await runTool(engine, args, { timeoutMs: opts.timeoutMs ?? 6e4 });
|
|
136
|
+
const pdfPath = join(work, "doc.pdf");
|
|
137
|
+
let pdf;
|
|
138
|
+
try {
|
|
139
|
+
pdf = await readFile(pdfPath);
|
|
140
|
+
} catch {
|
|
141
|
+
throw new PlotRenderError(`${engine} produced no PDF (exit ${r.code}): ${r.stderr || r.stdout.toString().slice(-500)}`);
|
|
142
|
+
}
|
|
143
|
+
await writeFile(outPath, pdf);
|
|
144
|
+
} finally {
|
|
145
|
+
await rm(work, {
|
|
146
|
+
recursive: true,
|
|
147
|
+
force: true
|
|
148
|
+
});
|
|
149
|
+
}
|
|
158
150
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
hasTool,
|
|
162
|
-
latexArgs,
|
|
163
|
-
latexToPdf,
|
|
164
|
-
renderToFile,
|
|
165
|
-
runTool
|
|
166
|
-
};
|
|
151
|
+
//#endregion
|
|
152
|
+
export { PlotRenderError, hasTool, latexArgs, latexToPdf, renderToFile, runTool };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danielsimonjr/mathts-plot",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.12",
|
|
4
4
|
"description": "Headless SVG 2D/3D plotting for MathTS — dependency-light, expression-aware, built on core/functions/expression",
|
|
5
5
|
"author": "Daniel Simon Jr.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
"README.md"
|
|
24
24
|
],
|
|
25
25
|
"scripts": {
|
|
26
|
-
"build": "
|
|
27
|
-
"build:prod": "
|
|
28
|
-
"dev": "
|
|
26
|
+
"build": "tsdown && tsc -p tsconfig.dts.json",
|
|
27
|
+
"build:prod": "tsdown --minify && tsc -p tsconfig.dts.json",
|
|
28
|
+
"dev": "tsdown --watch --on-success \"tsc -p tsconfig.dts.json\"",
|
|
29
29
|
"test": "bun test --isolate --timeout 30000",
|
|
30
30
|
"test:watch": "vitest",
|
|
31
31
|
"test:coverage": "bun test --isolate --timeout 30000 --coverage",
|
|
@@ -36,13 +36,13 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@danielsimonjr/mathts-core": "^0.18.0",
|
|
39
|
-
"@danielsimonjr/mathts-functions": "^0.
|
|
39
|
+
"@danielsimonjr/mathts-functions": "^0.69.0",
|
|
40
40
|
"@danielsimonjr/mathts-expression": "^0.10.2"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "^26.4.0",
|
|
44
44
|
"@xmldom/xmldom": "^0.9.12",
|
|
45
|
-
"
|
|
45
|
+
"tsdown": "^0.23.0",
|
|
46
46
|
"typescript": "^7.0.2",
|
|
47
47
|
"vitest": "^5.0.0"
|
|
48
48
|
},
|