@danielsimonjr/mathts-plot 0.4.11 → 0.4.13

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 (3) hide show
  1. package/dist/index.js +1235 -1126
  2. package/dist/render-file.js +140 -154
  3. package/package.json +8 -8
@@ -1,166 +1,152 @@
1
- // src/render-file.ts
2
- import { spawn } from "child_process";
3
- import { writeFile, rm, rename, mkdtemp, readFile } from "fs/promises";
4
- import { extname, join as joinPath } from "path";
5
- import { tmpdir } from "os";
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
- constructor(message, missingTool) {
8
- super(message);
9
- this.missingTool = missingTool;
10
- this.name = "PlotRenderError";
11
- }
12
- missingTool;
7
+ missingTool;
8
+ constructor(message, missingTool) {
9
+ super(message);
10
+ this.missingTool = missingTool;
11
+ this.name = "PlotRenderError";
12
+ }
13
13
  };
14
- var ALLOWED_TOOLS = ["rsvg-convert", "resvg", "pdflatex", "xelatex", "lualatex", "tectonic"];
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
- return new Promise((resolve, reject) => {
17
- if (!ALLOWED_TOOLS.includes(cmd)) {
18
- reject(new PlotRenderError(`Security Error: Execution of tool '${cmd}' is not allowed.`));
19
- return;
20
- }
21
- const child = spawn(cmd, args, { timeout: opts.timeoutMs ?? 3e4 });
22
- const out = [];
23
- let err = "";
24
- child.stdout.on("data", (d) => out.push(d));
25
- child.stderr.on("data", (d) => err += d.toString());
26
- child.on(
27
- "error",
28
- (e) => reject(new PlotRenderError(`${cmd} could not be run: ${e.message}`, cmd))
29
- );
30
- child.on("close", (code) => resolve({ code, stdout: Buffer.concat(out), stderr: err }));
31
- if (opts.input !== void 0) {
32
- child.stdin.on("error", () => {
33
- });
34
- child.stdin.end(opts.input);
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
- try {
40
- const r = await runTool(name, ["--version"], { timeoutMs: 5e3 });
41
- return r.code === 0;
42
- } catch {
43
- return false;
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
- if (opts.tool && !ALLOWED_TOOLS.includes(opts.tool)) {
48
- throw new PlotRenderError(`Security Error: Execution of tool '${opts.tool}' is not allowed.`);
49
- }
50
- const ext = extname(outPath).toLowerCase();
51
- if (ext === ".svg") {
52
- await writeFile(outPath, svg, "utf-8");
53
- return;
54
- }
55
- if (ext !== ".png" && ext !== ".pdf") {
56
- throw new PlotRenderError(
57
- `Unsupported output extension '${ext}' (expected .svg, .png, or .pdf)`
58
- );
59
- }
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 = ["-f", format, "-o", tmpOut];
67
- if (format === "png" && opts.density)
68
- args.push("--dpi-x", String(opts.density), "--dpi-y", String(opts.density));
69
- if (format === "png" && opts.background) args.push("--background-color", opts.background);
70
- const r = await runTool("rsvg-convert", args, { timeoutMs: opts.timeoutMs, input: svg });
71
- if (r.code !== 0) {
72
- await rm(tmpOut, { force: true });
73
- throw new PlotRenderError(`rsvg-convert failed: ${r.stderr}`);
74
- }
75
- await rename(tmpOut, outPath);
76
- return;
77
- }
78
- const tmpSvg = outPath + ".tmp.svg";
79
- await writeFile(tmpSvg, svg, "utf-8");
80
- try {
81
- const args = [tmpSvg, tmpOut];
82
- const r = await runTool("resvg", args, { timeoutMs: opts.timeoutMs });
83
- if (r.code !== 0) {
84
- await rm(tmpOut, { force: true });
85
- throw new PlotRenderError(`resvg failed: ${r.stderr}`);
86
- }
87
- } finally {
88
- await rm(tmpSvg, { force: true });
89
- }
90
- if (format === "pdf") {
91
- await rm(tmpOut, { force: true });
92
- throw new PlotRenderError(
93
- "resvg does not emit PDF; install rsvg-convert for SVG\u2192PDF",
94
- "rsvg-convert"
95
- );
96
- }
97
- await rename(tmpOut, outPath);
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
- if (engine === "tectonic") {
107
- return ["--outdir", workDir, texPath];
108
- }
109
- return [
110
- "-no-shell-escape",
111
- "-interaction=nonstopmode",
112
- "-halt-on-error",
113
- "-output-directory",
114
- workDir,
115
- texPath
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
- if (opts.tool && !ALLOWED_TOOLS.includes(opts.tool)) {
120
- throw new PlotRenderError(`Security Error: Execution of tool '${opts.tool}' is not allowed.`);
121
- }
122
- if (extname(outPath).toLowerCase() !== ".pdf") {
123
- throw new PlotRenderError(`latexToPdf output must be a .pdf path (got '${outPath}')`);
124
- }
125
- const candidates = opts.tool ? [opts.tool] : ["pdflatex", "tectonic"];
126
- let engine;
127
- for (const c of candidates) {
128
- if (await hasTool(c)) {
129
- engine = c;
130
- break;
131
- }
132
- }
133
- if (!engine) {
134
- throw new PlotRenderError(
135
- "No LaTeX engine found. Install TeX Live (pdflatex) or tectonic and ensure it is on PATH.",
136
- "pdflatex"
137
- );
138
- }
139
- const work = await mkdtemp(joinPath(tmpdir(), "plot-tex-"));
140
- try {
141
- const texPath = joinPath(work, "doc.tex");
142
- await writeFile(texPath, texSource, "utf-8");
143
- const args = latexArgs(engine, work, texPath);
144
- const r = await runTool(engine, args, { timeoutMs: opts.timeoutMs ?? 6e4 });
145
- const pdfPath = joinPath(work, "doc.pdf");
146
- let pdf;
147
- try {
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
- export {
160
- PlotRenderError,
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.11",
3
+ "version": "0.4.13",
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": "tsup && tsc -p tsconfig.dts.json",
27
- "build:prod": "tsup --minify --treeshake && tsc -p tsconfig.dts.json",
28
- "dev": "tsup --watch --onSuccess \"tsc -p tsconfig.dts.json\"",
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",
@@ -35,14 +35,14 @@
35
35
  "clean": "rm -rf dist"
36
36
  },
37
37
  "dependencies": {
38
- "@danielsimonjr/mathts-core": "^0.18.0",
39
- "@danielsimonjr/mathts-functions": "^0.68.2",
40
- "@danielsimonjr/mathts-expression": "^0.10.2"
38
+ "@danielsimonjr/mathts-core": "^0.19.0",
39
+ "@danielsimonjr/mathts-functions": "^0.69.1",
40
+ "@danielsimonjr/mathts-expression": "^0.10.3"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "^26.4.0",
44
44
  "@xmldom/xmldom": "^0.9.12",
45
- "tsup": "^8.0.0",
45
+ "tsdown": "^0.23.0",
46
46
  "typescript": "^7.0.2",
47
47
  "vitest": "^5.0.0"
48
48
  },