@danielsimonjr/mathts-workbook 0.4.9 → 0.4.11
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/cli.js +1608 -1587
- package/dist/formatter-8ja3r-YP.js +609 -0
- package/dist/fs-atomic.d.ts +10 -2
- package/dist/fs-atomic.d.ts.map +1 -1
- package/dist/index.js +18 -72
- package/dist/run-worker.js +40 -37
- package/dist/timeout-runner-BNZheTmR.js +567 -0
- package/package.json +9 -9
- package/dist/chunk-KP3ZC37Y.js +0 -587
- package/dist/chunk-M2K7SQG2.js +0 -740
package/dist/cli.js
CHANGED
|
@@ -1,607 +1,527 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
Session,
|
|
5
|
-
VERSION,
|
|
6
|
-
addCell,
|
|
7
|
-
capabilitiesInfo,
|
|
8
|
-
describeData,
|
|
9
|
-
editCell,
|
|
10
|
-
handleRequest,
|
|
11
|
-
listFunctions,
|
|
12
|
-
moveCell,
|
|
13
|
-
removeCell,
|
|
14
|
-
renameCell,
|
|
15
|
-
runWorkbookWithTimeout,
|
|
16
|
-
setMetadata,
|
|
17
|
-
writeFileAtomic
|
|
18
|
-
} from "./chunk-KP3ZC37Y.js";
|
|
19
|
-
import {
|
|
20
|
-
buildDependencyGraph,
|
|
21
|
-
createExecutor,
|
|
22
|
-
detectCycles,
|
|
23
|
-
formatResult,
|
|
24
|
-
importWorkbook,
|
|
25
|
-
parseWorkbook,
|
|
26
|
-
parseYamlSafe,
|
|
27
|
-
serializeWorkbook,
|
|
28
|
-
stripOutputs,
|
|
29
|
-
toDOT,
|
|
30
|
-
toMermaid
|
|
31
|
-
} from "./chunk-M2K7SQG2.js";
|
|
32
|
-
|
|
33
|
-
// src/cli.ts
|
|
34
|
-
import { createHash } from "crypto";
|
|
35
|
-
import { readFileSync, writeFileSync, lstatSync, realpathSync } from "fs";
|
|
36
|
-
import { pathToFileURL } from "url";
|
|
37
|
-
import { basename } from "path";
|
|
38
|
-
import { createInterface } from "readline";
|
|
2
|
+
import { c as parseWorkbook, d as buildDependencyGraph, f as detectCycles, g as toMermaid, h as toDOT, l as serializeWorkbook, o as importWorkbook, r as createExecutor, t as formatResult, u as stripOutputs, v as parseYamlSafe } from "./formatter-8ja3r-YP.js";
|
|
3
|
+
import { a as capabilitiesInfo, c as writeFileAtomic, d as addCell, f as editCell, g as setMetadata, h as renameCell, i as describeData, l as SCHEMA_VERSION, m as removeCell, n as runWorkbookWithTimeout, o as listFunctions, p as moveCell, r as handleRequest, s as Session, u as VERSION } from "./timeout-runner-BNZheTmR.js";
|
|
39
4
|
import * as mathFunctions from "@danielsimonjr/mathts-functions";
|
|
40
|
-
|
|
41
|
-
|
|
5
|
+
import { lstatSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
import { basename } from "node:path";
|
|
9
|
+
import { createInterface } from "node:readline";
|
|
42
10
|
import { mathMLDocument, mathMLError } from "@danielsimonjr/mathts-expression";
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
11
|
+
import { bar, line, scatter } from "@danielsimonjr/mathts-plot";
|
|
12
|
+
import { latexToPdf } from "@danielsimonjr/mathts-plot/render";
|
|
13
|
+
//#region src/markdown.ts
|
|
14
|
+
function esc$1(s) {
|
|
15
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
47
16
|
}
|
|
48
17
|
function sanitizeHrefRaw(href) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (probe.startsWith("/") || probe.startsWith("./") || probe.startsWith("../") || probe.startsWith("#")) {
|
|
64
|
-
return h;
|
|
65
|
-
}
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
18
|
+
const h = href.trim();
|
|
19
|
+
let probe = h.replace(/&/g, "&");
|
|
20
|
+
try {
|
|
21
|
+
probe = decodeURIComponent(probe);
|
|
22
|
+
} catch {}
|
|
23
|
+
if (probe.startsWith("//")) return null;
|
|
24
|
+
try {
|
|
25
|
+
const url = new URL(probe);
|
|
26
|
+
if (url.protocol === "http:" || url.protocol === "https:" || url.protocol === "mailto:") return h;
|
|
27
|
+
return null;
|
|
28
|
+
} catch {
|
|
29
|
+
if (probe.startsWith("/") || probe.startsWith("./") || probe.startsWith("../") || probe.startsWith("#")) return h;
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
68
32
|
}
|
|
69
33
|
function sanitizeHref(href) {
|
|
70
|
-
|
|
71
|
-
|
|
34
|
+
const raw = sanitizeHrefRaw(href);
|
|
35
|
+
return raw === null ? null : raw.replace(/"/g, """);
|
|
72
36
|
}
|
|
73
37
|
function inline(escaped) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
function isBlockStart(
|
|
88
|
-
|
|
38
|
+
let s = escaped.replace(/`([^`]+)`/g, (_m, code) => `<code>${code}</code>`);
|
|
39
|
+
s = s.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
40
|
+
s = s.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
|
41
|
+
s = s.replace(/\[([^\]]*)\]\(([^)\s]+)\)/g, (_m, text, href) => {
|
|
42
|
+
const safe = sanitizeHref(href);
|
|
43
|
+
return safe ? `<a href="${safe}">${text}</a>` : text;
|
|
44
|
+
});
|
|
45
|
+
return s;
|
|
46
|
+
}
|
|
47
|
+
const HR = /^ {0,3}(-{3,}|\*{3,}|_{3,})\s*$/;
|
|
48
|
+
const HEADING = /^(#{1,6})\s+(.*)$/;
|
|
49
|
+
const UL = /^\s*[-*+]\s+/;
|
|
50
|
+
const OL = /^\s*\d+\.\s+/;
|
|
51
|
+
function isBlockStart(line) {
|
|
52
|
+
return line.trim() === "" || /^```/.test(line.trim()) || HEADING.test(line) || UL.test(line) || OL.test(line) || HR.test(line);
|
|
89
53
|
}
|
|
90
54
|
function markdownToHtml(src) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
149
|
-
|
|
55
|
+
if (!src) return "";
|
|
56
|
+
const lines = src.replace(/\r\n?/g, "\n").split("\n");
|
|
57
|
+
const out = [];
|
|
58
|
+
let i = 0;
|
|
59
|
+
while (i < lines.length) {
|
|
60
|
+
const line = lines[i];
|
|
61
|
+
if (/^```/.test(line.trim())) {
|
|
62
|
+
const buf = [];
|
|
63
|
+
i++;
|
|
64
|
+
while (i < lines.length && !/^```/.test(lines[i].trim())) {
|
|
65
|
+
buf.push(lines[i]);
|
|
66
|
+
i++;
|
|
67
|
+
}
|
|
68
|
+
i++;
|
|
69
|
+
out.push(`<pre><code>${esc$1(buf.join("\n"))}</code></pre>`);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (line.trim() === "") {
|
|
73
|
+
i++;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (HR.test(line)) {
|
|
77
|
+
out.push("<hr>");
|
|
78
|
+
i++;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const h = HEADING.exec(line);
|
|
82
|
+
if (h) {
|
|
83
|
+
const lvl = h[1].length;
|
|
84
|
+
out.push(`<h${lvl}>${inline(esc$1(h[2].trim()))}</h${lvl}>`);
|
|
85
|
+
i++;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (UL.test(line)) {
|
|
89
|
+
const items = [];
|
|
90
|
+
while (i < lines.length && UL.test(lines[i])) {
|
|
91
|
+
items.push(`<li>${inline(esc$1(lines[i].replace(UL, "")))}</li>`);
|
|
92
|
+
i++;
|
|
93
|
+
}
|
|
94
|
+
out.push(`<ul>${items.join("")}</ul>`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (OL.test(line)) {
|
|
98
|
+
const items = [];
|
|
99
|
+
while (i < lines.length && OL.test(lines[i])) {
|
|
100
|
+
items.push(`<li>${inline(esc$1(lines[i].replace(OL, "")))}</li>`);
|
|
101
|
+
i++;
|
|
102
|
+
}
|
|
103
|
+
out.push(`<ol>${items.join("")}</ol>`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const para = [];
|
|
107
|
+
while (i < lines.length && !isBlockStart(lines[i])) {
|
|
108
|
+
para.push(lines[i]);
|
|
109
|
+
i++;
|
|
110
|
+
}
|
|
111
|
+
out.push(`<p>${inline(esc$1(para.join(" ")))}</p>`);
|
|
112
|
+
}
|
|
113
|
+
return out.join("\n");
|
|
150
114
|
}
|
|
151
115
|
function texEscape(s) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
default:
|
|
161
|
-
return `\\${c}`;
|
|
162
|
-
}
|
|
163
|
-
});
|
|
116
|
+
return s.replace(/[\\&%$#_{}~^]/g, (c) => {
|
|
117
|
+
switch (c) {
|
|
118
|
+
case "\\": return "\\textbackslash{}";
|
|
119
|
+
case "~": return "\\textasciitilde{}";
|
|
120
|
+
case "^": return "\\textasciicircum{}";
|
|
121
|
+
default: return `\\${c}`;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
164
124
|
}
|
|
165
125
|
function verbatimBody(body, env) {
|
|
166
|
-
|
|
126
|
+
return body.split(`\\end{${env}}`).join(`\\end {${env}}`);
|
|
167
127
|
}
|
|
168
128
|
function escapeInlineNonLink(text) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
129
|
+
let s = texEscape(text);
|
|
130
|
+
s = s.replace(/`([^`]+)`/g, (_m, code) => `\\texttt{${code}}`);
|
|
131
|
+
s = s.replace(/\*\*([^*]+)\*\*/g, "\\textbf{$1}");
|
|
132
|
+
s = s.replace(/\*([^*]+)\*/g, "\\emph{$1}");
|
|
133
|
+
return s;
|
|
174
134
|
}
|
|
175
135
|
function inlineTex(text) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
136
|
+
const linkRe = /\[([^\]]*)\]\(([^)\s]+)\)/g;
|
|
137
|
+
let out = "";
|
|
138
|
+
let last = 0;
|
|
139
|
+
let m;
|
|
140
|
+
while ((m = linkRe.exec(text)) !== null) {
|
|
141
|
+
out += escapeInlineNonLink(text.slice(last, m.index));
|
|
142
|
+
const raw = sanitizeHrefRaw(m[2]);
|
|
143
|
+
out += raw ? `\\href{${texEscape(raw)}}{${escapeInlineNonLink(m[1])}}` : escapeInlineNonLink(m[1]);
|
|
144
|
+
last = linkRe.lastIndex;
|
|
145
|
+
}
|
|
146
|
+
out += escapeInlineNonLink(text.slice(last));
|
|
147
|
+
return out;
|
|
188
148
|
}
|
|
189
149
|
function markdownToTex(src) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
${items.join("\n")}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// src/svg.ts
|
|
261
|
-
import { line, scatter, bar } from "@danielsimonjr/mathts-plot";
|
|
150
|
+
if (!src) return "";
|
|
151
|
+
const lines = src.replace(/\r\n?/g, "\n").split("\n");
|
|
152
|
+
const out = [];
|
|
153
|
+
let i = 0;
|
|
154
|
+
const SEC = [
|
|
155
|
+
"\\section*",
|
|
156
|
+
"\\subsection*",
|
|
157
|
+
"\\subsubsection*",
|
|
158
|
+
"\\paragraph"
|
|
159
|
+
];
|
|
160
|
+
while (i < lines.length) {
|
|
161
|
+
const line = lines[i];
|
|
162
|
+
if (/^```/.test(line.trim())) {
|
|
163
|
+
const buf = [];
|
|
164
|
+
i++;
|
|
165
|
+
while (i < lines.length && !/^```/.test(lines[i].trim())) {
|
|
166
|
+
buf.push(lines[i]);
|
|
167
|
+
i++;
|
|
168
|
+
}
|
|
169
|
+
i++;
|
|
170
|
+
out.push(`\\begin{lstlisting}\n${verbatimBody(buf.join("\n"), "lstlisting")}\n\\end{lstlisting}`);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (line.trim() === "") {
|
|
174
|
+
i++;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (HR.test(line)) {
|
|
178
|
+
out.push("\\par\\noindent\\rule{\\linewidth}{0.4pt}\\par");
|
|
179
|
+
i++;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const h = HEADING.exec(line);
|
|
183
|
+
if (h) {
|
|
184
|
+
const lvl = Math.min(h[1].length, 4);
|
|
185
|
+
out.push(`${SEC[lvl - 1]}{${inlineTex(h[2].trim())}}`);
|
|
186
|
+
i++;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (UL.test(line)) {
|
|
190
|
+
const items = [];
|
|
191
|
+
while (i < lines.length && UL.test(lines[i])) {
|
|
192
|
+
items.push(` \\item ${inlineTex(lines[i].replace(UL, ""))}`);
|
|
193
|
+
i++;
|
|
194
|
+
}
|
|
195
|
+
out.push(`\\begin{itemize}\n${items.join("\n")}\n\\end{itemize}`);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (OL.test(line)) {
|
|
199
|
+
const items = [];
|
|
200
|
+
while (i < lines.length && OL.test(lines[i])) {
|
|
201
|
+
items.push(` \\item ${inlineTex(lines[i].replace(OL, ""))}`);
|
|
202
|
+
i++;
|
|
203
|
+
}
|
|
204
|
+
out.push(`\\begin{enumerate}\n${items.join("\n")}\n\\end{enumerate}`);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const para = [];
|
|
208
|
+
while (i < lines.length && !isBlockStart(lines[i])) {
|
|
209
|
+
para.push(lines[i]);
|
|
210
|
+
i++;
|
|
211
|
+
}
|
|
212
|
+
out.push(inlineTex(para.join(" ")));
|
|
213
|
+
}
|
|
214
|
+
return out.join("\n\n");
|
|
215
|
+
}
|
|
216
|
+
//#endregion
|
|
217
|
+
//#region src/svg.ts
|
|
262
218
|
function coerce(v) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
return Number(v);
|
|
219
|
+
if (typeof v === "number") return v;
|
|
220
|
+
if (typeof v === "bigint") return Number(v);
|
|
221
|
+
const u = v;
|
|
222
|
+
if (u && typeof u.toNumeric === "function") try {
|
|
223
|
+
return Number(u.toNumeric());
|
|
224
|
+
} catch {
|
|
225
|
+
return NaN;
|
|
226
|
+
}
|
|
227
|
+
return Number(v);
|
|
274
228
|
}
|
|
275
229
|
function toNums(raw) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
return Array.isArray(arr) ? arr.flat(Infinity).map(coerce) : [];
|
|
230
|
+
let arr = raw;
|
|
231
|
+
const m = raw;
|
|
232
|
+
if (m && typeof m.toArray === "function") try {
|
|
233
|
+
arr = m.toArray();
|
|
234
|
+
} catch {
|
|
235
|
+
arr = raw;
|
|
236
|
+
}
|
|
237
|
+
return Array.isArray(arr) ? arr.flat(Infinity).map(coerce) : [];
|
|
286
238
|
}
|
|
287
239
|
function renderChart(spec, xRaw, yRaw, format = "svg") {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const note = cell.note ? `<p class="note">\u26A0 ${esc2(cell.note)}</p>` : "";
|
|
360
|
-
return `<figure class="cell cell-chart">${cap}${cell.chart ? renderChart(cell.chart.spec, cell.chart.x, cell.chart.y, "svg") : '<p class="note">no chart</p>'}${note}</figure>`;
|
|
361
|
-
}
|
|
362
|
-
default:
|
|
363
|
-
return `<figure class="cell"><pre><code>${esc2(cell.content)}</code></pre></figure>`;
|
|
364
|
-
}
|
|
240
|
+
const xs = toNums(xRaw);
|
|
241
|
+
const ys = toNums(yRaw);
|
|
242
|
+
const opts = format === "tikz" ? {
|
|
243
|
+
title: spec.title,
|
|
244
|
+
xLabel: spec.xLabel,
|
|
245
|
+
yLabel: spec.yLabel,
|
|
246
|
+
format: "tikz",
|
|
247
|
+
tikz: { standalone: false }
|
|
248
|
+
} : {
|
|
249
|
+
title: spec.title,
|
|
250
|
+
xLabel: spec.xLabel,
|
|
251
|
+
yLabel: spec.yLabel
|
|
252
|
+
};
|
|
253
|
+
if (spec.type === "scatter") return scatter(xs, ys, opts);
|
|
254
|
+
if (spec.type === "bar") return bar(xs, ys, opts);
|
|
255
|
+
return line(xs, ys, opts);
|
|
256
|
+
}
|
|
257
|
+
//#endregion
|
|
258
|
+
//#region src/html.ts
|
|
259
|
+
function esc(s) {
|
|
260
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
261
|
+
}
|
|
262
|
+
function renderEquation(content, parse) {
|
|
263
|
+
let math;
|
|
264
|
+
if (!parse) math = mathMLError(content);
|
|
265
|
+
else try {
|
|
266
|
+
math = mathMLDocument(parse(content));
|
|
267
|
+
} catch {
|
|
268
|
+
math = mathMLError(content);
|
|
269
|
+
}
|
|
270
|
+
return `<figure class="cell cell-eq">${math}</figure>`;
|
|
271
|
+
}
|
|
272
|
+
function renderCell(cell, parse) {
|
|
273
|
+
const cap = cell.id ? `<figcaption>${esc(cell.id)}</figcaption>` : "";
|
|
274
|
+
switch (cell.type) {
|
|
275
|
+
case "markdown": return `<section class="cell cell-md">${markdownToHtml(cell.content)}</section>`;
|
|
276
|
+
case "equation": return renderEquation(cell.content, parse);
|
|
277
|
+
case "code": {
|
|
278
|
+
const src = `<pre class="src"><code>${esc(cell.content)}</code></pre>`;
|
|
279
|
+
let res = "";
|
|
280
|
+
if (cell.error !== void 0) res = `<div class="cell-error">${esc(cell.error)}</div>`;
|
|
281
|
+
else if (cell.output !== void 0) res = `<div class="cell-output">${esc(cell.output)}</div>`;
|
|
282
|
+
return `<figure class="cell cell-code">${cap}${src}${res}</figure>`;
|
|
283
|
+
}
|
|
284
|
+
case "test": {
|
|
285
|
+
const err = cell.error;
|
|
286
|
+
let badge;
|
|
287
|
+
let cls;
|
|
288
|
+
if (err !== void 0) {
|
|
289
|
+
badge = "⚠";
|
|
290
|
+
cls = "err";
|
|
291
|
+
} else if (cell.passed === true) {
|
|
292
|
+
badge = "✓";
|
|
293
|
+
cls = "pass";
|
|
294
|
+
} else if (cell.passed === false) {
|
|
295
|
+
badge = "✗";
|
|
296
|
+
cls = "fail";
|
|
297
|
+
} else {
|
|
298
|
+
badge = "○";
|
|
299
|
+
cls = "nrun";
|
|
300
|
+
}
|
|
301
|
+
const note = err !== void 0 ? ` <span class="note">${esc(err)}</span>` : "";
|
|
302
|
+
return `<figure class="cell cell-test ${cls}"><span class="badge">${badge}</span> <code>${esc(cell.content)}</code>${note}</figure>`;
|
|
303
|
+
}
|
|
304
|
+
case "data": return `<figure class="cell cell-data">${cap}<pre><code>${cell.error !== void 0 ? esc(cell.error) : esc(cell.output ?? "")}</code></pre></figure>`;
|
|
305
|
+
case "chart": {
|
|
306
|
+
const note = cell.note ? `<p class="note">⚠ ${esc(cell.note)}</p>` : "";
|
|
307
|
+
return `<figure class="cell cell-chart">${cap}${cell.chart ? renderChart(cell.chart.spec, cell.chart.x, cell.chart.y, "svg") : "<p class=\"note\">no chart</p>"}${note}</figure>`;
|
|
308
|
+
}
|
|
309
|
+
default: return `<figure class="cell"><pre><code>${esc(cell.content)}</code></pre></figure>`;
|
|
310
|
+
}
|
|
365
311
|
}
|
|
366
312
|
function toCSS() {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
313
|
+
return [
|
|
314
|
+
":root{--fg:#1a1a2e;--muted:#5a5a72;--bg:#fff;--code-bg:#f4f4f8;--border:#e0e0ea;--accent:#2a4d8f;--pass:#1a7f37;--fail:#cf222e;}",
|
|
315
|
+
"*{box-sizing:border-box}",
|
|
316
|
+
"body{margin:0;color:var(--fg);background:var(--bg);font:16px/1.6 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}",
|
|
317
|
+
"main{max-width:48rem;margin:0 auto;padding:2rem 1.25rem}",
|
|
318
|
+
"header{border-bottom:2px solid var(--border);margin-bottom:1.5rem;padding-bottom:1rem}",
|
|
319
|
+
".doc-title{margin:0 0 .25rem;font-size:1.9rem}",
|
|
320
|
+
".doc-meta,.doc-desc{color:var(--muted);margin:.15rem 0}",
|
|
321
|
+
".tag{display:inline-block;background:var(--code-bg);border:1px solid var(--border);border-radius:1rem;padding:.05rem .55rem;font-size:.8rem;margin-right:.3rem}",
|
|
322
|
+
".cell{margin:1.1rem 0}",
|
|
323
|
+
"h1,h2,h3,h4{line-height:1.25}",
|
|
324
|
+
"code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}",
|
|
325
|
+
"pre{background:var(--code-bg);border:1px solid var(--border);border-radius:6px;padding:.7rem .9rem;overflow:auto}",
|
|
326
|
+
".cell-code{border-left:3px solid var(--accent);padding-left:.9rem}",
|
|
327
|
+
".cell-code figcaption{color:var(--muted);font-size:.8rem;font-family:ui-monospace,monospace;margin-bottom:.2rem}",
|
|
328
|
+
".cell-output{background:#fafaff;border:1px solid var(--border);border-radius:6px;padding:.5rem .9rem;margin-top:.4rem;white-space:pre-wrap}",
|
|
329
|
+
".cell-error{background:#fff0f0;border:1px solid #f3c0c0;color:var(--fail);border-radius:6px;padding:.5rem .9rem;margin-top:.4rem}",
|
|
330
|
+
".cell-eq{text-align:center;margin:1.3rem 0}",
|
|
331
|
+
"math{font-size:1.15rem}",
|
|
332
|
+
".cell-test{display:flex;align-items:center;gap:.5rem;font-size:.95rem}",
|
|
333
|
+
".cell-test .badge{font-weight:700;width:1.4rem;height:1.4rem;display:inline-flex;align-items:center;justify-content:center;border-radius:50%;color:#fff}",
|
|
334
|
+
".cell-test.pass .badge{background:var(--pass)}",
|
|
335
|
+
".cell-test.fail .badge{background:var(--fail)}",
|
|
336
|
+
".cell-test.err .badge{background:#9a6700}",
|
|
337
|
+
".cell-test.nrun .badge{background:#8a8a9a}",
|
|
338
|
+
".cell-test .note{color:var(--fail);font-size:.85rem}",
|
|
339
|
+
".cell-chart{text-align:center}",
|
|
340
|
+
".note{color:var(--muted)}"
|
|
341
|
+
].join("");
|
|
396
342
|
}
|
|
397
343
|
function toHTML(doc, options = {}) {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
"\\usepackage{listings}",
|
|
433
|
-
"\\usepackage{xcolor}",
|
|
434
|
-
"\\usepackage[margin=1in]{geometry}",
|
|
435
|
-
"\\usepackage{hyperref}"
|
|
344
|
+
const title = doc.title ?? "Workbook";
|
|
345
|
+
const headerBits = [`<h1 class="doc-title">${esc(title)}</h1>`];
|
|
346
|
+
if (doc.author) headerBits.push(`<p class="doc-meta">by ${esc(doc.author)}</p>`);
|
|
347
|
+
if (doc.description) headerBits.push(`<p class="doc-desc">${esc(doc.description)}</p>`);
|
|
348
|
+
if (doc.tags && doc.tags.length) headerBits.push(`<p class="doc-tags">${doc.tags.map((t) => `<span class="tag">${esc(t)}</span>`).join("")}</p>`);
|
|
349
|
+
const body = doc.cells.map((c) => renderCell(c, options.parse)).join("\n");
|
|
350
|
+
return [
|
|
351
|
+
"<!doctype html>",
|
|
352
|
+
"<html lang=\"en\">",
|
|
353
|
+
"<head>",
|
|
354
|
+
"<meta charset=\"utf-8\">",
|
|
355
|
+
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">",
|
|
356
|
+
`<title>${esc(title)}</title>`,
|
|
357
|
+
`<style>${toCSS()}</style>`,
|
|
358
|
+
"</head>",
|
|
359
|
+
"<body>",
|
|
360
|
+
"<main>",
|
|
361
|
+
`<header>${headerBits.join("")}</header>`,
|
|
362
|
+
body,
|
|
363
|
+
"</main>",
|
|
364
|
+
"</body>",
|
|
365
|
+
"</html>"
|
|
366
|
+
].join("\n");
|
|
367
|
+
}
|
|
368
|
+
//#endregion
|
|
369
|
+
//#region src/tex.ts
|
|
370
|
+
const PREAMBLE = [
|
|
371
|
+
"\\documentclass{article}",
|
|
372
|
+
"\\usepackage{amsmath}",
|
|
373
|
+
"\\usepackage{tikz}",
|
|
374
|
+
"\\usepackage{listings}",
|
|
375
|
+
"\\usepackage{xcolor}",
|
|
376
|
+
"\\usepackage[margin=1in]{geometry}",
|
|
377
|
+
"\\usepackage{hyperref}"
|
|
436
378
|
].join("\n");
|
|
437
|
-
function renderEquationTex(content,
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
${
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
\\end{verbatim}`;
|
|
470
|
-
return `${cap}${src}${res}`;
|
|
471
|
-
}
|
|
472
|
-
case "test": {
|
|
473
|
-
const label = `\\texttt{${texEscape(cell.content)}}`;
|
|
474
|
-
if (cell.error !== void 0)
|
|
475
|
-
return `\\textcolor{orange}{[ERROR]} ${label} --- ${texEscape(cell.error)}`;
|
|
476
|
-
if (cell.passed === true) return `\\textcolor{green!60!black}{[PASS]} ${label}`;
|
|
477
|
-
if (cell.passed === false) return `\\textcolor{red}{[FAIL]} ${label}`;
|
|
478
|
-
return `\\textcolor{gray}{[NOT RUN]} ${label}`;
|
|
479
|
-
}
|
|
480
|
-
case "data": {
|
|
481
|
-
const val = cell.error !== void 0 ? cell.error : cell.output ?? "";
|
|
482
|
-
const cap = cell.id ? `\\textit{${texEscape(cell.id)}}\\\\
|
|
483
|
-
` : "";
|
|
484
|
-
return `${cap}\\begin{verbatim}
|
|
485
|
-
${verbatimBody(val, "verbatim")}
|
|
486
|
-
\\end{verbatim}`;
|
|
487
|
-
}
|
|
488
|
-
case "chart": {
|
|
489
|
-
const chart = cell.chart ? renderChart(cell.chart.spec, cell.chart.x, cell.chart.y, "tikz") : "% no chart";
|
|
490
|
-
const note = cell.note ? `
|
|
491
|
-
|
|
492
|
-
\\textit{${texEscape(cell.note)}}` : "";
|
|
493
|
-
return `\\begin{center}
|
|
494
|
-
${chart}
|
|
495
|
-
\\end{center}${note}`;
|
|
496
|
-
}
|
|
497
|
-
default:
|
|
498
|
-
return `\\begin{verbatim}
|
|
499
|
-
${verbatimBody(cell.content, "verbatim")}
|
|
500
|
-
\\end{verbatim}`;
|
|
501
|
-
}
|
|
379
|
+
function renderEquationTex(content, parse) {
|
|
380
|
+
if (parse) try {
|
|
381
|
+
return `\\[ ${parse(content).toTex()} \\]`;
|
|
382
|
+
} catch {}
|
|
383
|
+
return `\\texttt{${texEscape(content)}}`;
|
|
384
|
+
}
|
|
385
|
+
function renderCellTex(cell, parse) {
|
|
386
|
+
switch (cell.type) {
|
|
387
|
+
case "markdown": return markdownToTex(cell.content);
|
|
388
|
+
case "equation": return renderEquationTex(cell.content, parse);
|
|
389
|
+
case "code": {
|
|
390
|
+
const cap = cell.id ? `\\textit{${texEscape(cell.id)}}\\\\\n` : "";
|
|
391
|
+
const src = `\\begin{lstlisting}\n${verbatimBody(cell.content, "lstlisting")}\n\\end{lstlisting}`;
|
|
392
|
+
let res = "";
|
|
393
|
+
if (cell.error !== void 0) res = `\n\n\\textcolor{red}{\\texttt{${texEscape(cell.error)}}}`;
|
|
394
|
+
else if (cell.output !== void 0) res = `\n\n\\begin{verbatim}\n${verbatimBody(cell.output, "verbatim")}\n\\end{verbatim}`;
|
|
395
|
+
return `${cap}${src}${res}`;
|
|
396
|
+
}
|
|
397
|
+
case "test": {
|
|
398
|
+
const label = `\\texttt{${texEscape(cell.content)}}`;
|
|
399
|
+
if (cell.error !== void 0) return `\\textcolor{orange}{[ERROR]} ${label} --- ${texEscape(cell.error)}`;
|
|
400
|
+
if (cell.passed === true) return `\\textcolor{green!60!black}{[PASS]} ${label}`;
|
|
401
|
+
if (cell.passed === false) return `\\textcolor{red}{[FAIL]} ${label}`;
|
|
402
|
+
return `\\textcolor{gray}{[NOT RUN]} ${label}`;
|
|
403
|
+
}
|
|
404
|
+
case "data": {
|
|
405
|
+
const val = cell.error !== void 0 ? cell.error : cell.output ?? "";
|
|
406
|
+
return `${cell.id ? `\\textit{${texEscape(cell.id)}}\\\\\n` : ""}\\begin{verbatim}\n${verbatimBody(val, "verbatim")}\n\\end{verbatim}`;
|
|
407
|
+
}
|
|
408
|
+
case "chart": return `\\begin{center}\n${cell.chart ? renderChart(cell.chart.spec, cell.chart.x, cell.chart.y, "tikz") : "% no chart"}\n\\end{center}${cell.note ? `\n\n\\textit{${texEscape(cell.note)}}` : ""}`;
|
|
409
|
+
default: return `\\begin{verbatim}\n${verbatimBody(cell.content, "verbatim")}\n\\end{verbatim}`;
|
|
410
|
+
}
|
|
502
411
|
}
|
|
503
412
|
function toTeX(doc, options = {}) {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
413
|
+
const body = doc.cells.map((c) => renderCellTex(c, options.parse)).join("\n\n");
|
|
414
|
+
if (options.fragment) return body;
|
|
415
|
+
const meta = [];
|
|
416
|
+
if (doc.title) meta.push(`\\title{${texEscape(doc.title)}}`);
|
|
417
|
+
if (doc.author) meta.push(`\\author{${texEscape(doc.author)}}`);
|
|
418
|
+
const maketitle = doc.title ? "\\maketitle\n" : "";
|
|
419
|
+
return [
|
|
420
|
+
PREAMBLE,
|
|
421
|
+
...meta,
|
|
422
|
+
"\\begin{document}",
|
|
423
|
+
`${maketitle}${body}`,
|
|
424
|
+
"\\end{document}"
|
|
425
|
+
].join("\n");
|
|
426
|
+
}
|
|
427
|
+
//#endregion
|
|
428
|
+
//#region src/pdf.ts
|
|
517
429
|
function toPDF(doc, outPath, options = {}) {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
430
|
+
const { parse, ...renderOpts } = options;
|
|
431
|
+
return latexToPdf(toTeX(doc, {
|
|
432
|
+
parse,
|
|
433
|
+
fragment: false
|
|
434
|
+
}), outPath, renderOpts);
|
|
435
|
+
}
|
|
436
|
+
//#endregion
|
|
437
|
+
//#region src/ipynb.ts
|
|
523
438
|
function toSourceLines(content) {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
` : line2);
|
|
439
|
+
if (content === "") return [];
|
|
440
|
+
const lines = content.split("\n");
|
|
441
|
+
return lines.map((line, i) => i < lines.length - 1 ? `${line}\n` : line);
|
|
528
442
|
}
|
|
529
443
|
function textData(text) {
|
|
530
|
-
|
|
444
|
+
return { "text/plain": toSourceLines(text) };
|
|
531
445
|
}
|
|
532
446
|
function renderCodeCell(cell, counter) {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
447
|
+
const outputs = [];
|
|
448
|
+
let executionCount = null;
|
|
449
|
+
if (cell.error !== void 0) outputs.push({
|
|
450
|
+
output_type: "error",
|
|
451
|
+
ename: "Error",
|
|
452
|
+
evalue: cell.error,
|
|
453
|
+
traceback: [cell.error]
|
|
454
|
+
});
|
|
455
|
+
else if (cell.type === "chart") {
|
|
456
|
+
if (cell.chart !== void 0) {
|
|
457
|
+
const svg = renderChart(cell.chart.spec, cell.chart.x, cell.chart.y, "svg");
|
|
458
|
+
outputs.push({
|
|
459
|
+
output_type: "display_data",
|
|
460
|
+
data: {
|
|
461
|
+
"image/svg+xml": toSourceLines(svg),
|
|
462
|
+
"text/plain": ["<chart>"]
|
|
463
|
+
},
|
|
464
|
+
metadata: {}
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
} else if (cell.type === "test" && cell.passed !== void 0) {
|
|
468
|
+
executionCount = counter.n++;
|
|
469
|
+
outputs.push({
|
|
470
|
+
output_type: "execute_result",
|
|
471
|
+
data: textData(cell.passed ? "true" : "false"),
|
|
472
|
+
metadata: {},
|
|
473
|
+
execution_count: executionCount
|
|
474
|
+
});
|
|
475
|
+
} else if (cell.output !== void 0) {
|
|
476
|
+
executionCount = counter.n++;
|
|
477
|
+
outputs.push({
|
|
478
|
+
output_type: "execute_result",
|
|
479
|
+
data: textData(cell.output),
|
|
480
|
+
metadata: {},
|
|
481
|
+
execution_count: executionCount
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
cell_type: "code",
|
|
486
|
+
source: toSourceLines(cell.content),
|
|
487
|
+
metadata: {},
|
|
488
|
+
execution_count: executionCount,
|
|
489
|
+
outputs
|
|
490
|
+
};
|
|
575
491
|
}
|
|
576
492
|
function renderIpynbCell(cell, counter) {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
493
|
+
if (cell.type === "markdown") return {
|
|
494
|
+
cell_type: "markdown",
|
|
495
|
+
source: toSourceLines(cell.content),
|
|
496
|
+
metadata: {}
|
|
497
|
+
};
|
|
498
|
+
return renderCodeCell(cell, counter);
|
|
581
499
|
}
|
|
582
500
|
function toIpynb(doc) {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
501
|
+
const counter = { n: 1 };
|
|
502
|
+
const cells = doc.cells.map((cell) => renderIpynbCell(cell, counter));
|
|
503
|
+
const mathts = {};
|
|
504
|
+
if (doc.title !== void 0) mathts.title = doc.title;
|
|
505
|
+
if (doc.author !== void 0) mathts.author = doc.author;
|
|
506
|
+
if (doc.description !== void 0) mathts.description = doc.description;
|
|
507
|
+
if (doc.tags !== void 0) mathts.tags = doc.tags;
|
|
508
|
+
return JSON.stringify({
|
|
509
|
+
nbformat: 4,
|
|
510
|
+
nbformat_minor: 5,
|
|
511
|
+
metadata: {
|
|
512
|
+
mathts,
|
|
513
|
+
language_info: {
|
|
514
|
+
name: "mathts",
|
|
515
|
+
file_extension: ".mtsw"
|
|
516
|
+
}
|
|
517
|
+
},
|
|
518
|
+
cells
|
|
519
|
+
}, null, 2);
|
|
520
|
+
}
|
|
521
|
+
//#endregion
|
|
522
|
+
//#region src/cli.ts
|
|
523
|
+
const parse = mathFunctions.parse;
|
|
524
|
+
const HELP = `
|
|
605
525
|
mtsw - MathTS Workbook CLI
|
|
606
526
|
|
|
607
527
|
Usage:
|
|
@@ -613,7 +533,7 @@ Usage:
|
|
|
613
533
|
outputs back to the file. --timeout runs
|
|
614
534
|
the whole workbook in a worker thread and
|
|
615
535
|
kills it (WorkbookTimeoutError) if it
|
|
616
|
-
exceeds the budget
|
|
536
|
+
exceeds the budget — a runaway cell can't
|
|
617
537
|
hang the process. Incompatible with -c/-v.
|
|
618
538
|
--expect-hash refuses to run unless the
|
|
619
539
|
file's SHA-256 matches (optimistic lock).
|
|
@@ -665,68 +585,67 @@ Notes:
|
|
|
665
585
|
valid identifiers ([A-Za-z_][A-Za-z0-9_]*).
|
|
666
586
|
`.trimStart();
|
|
667
587
|
function jsonEnvelope(command, ok, data, problems = []) {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
588
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
589
|
+
const replacer = (_key, value) => {
|
|
590
|
+
if (typeof value === "bigint") return `${value}n`;
|
|
591
|
+
if (value !== null && typeof value === "object") {
|
|
592
|
+
if (seen.has(value)) return "[Circular]";
|
|
593
|
+
seen.add(value);
|
|
594
|
+
}
|
|
595
|
+
return value;
|
|
596
|
+
};
|
|
597
|
+
return JSON.stringify({
|
|
598
|
+
schemaVersion: SCHEMA_VERSION,
|
|
599
|
+
command,
|
|
600
|
+
ok,
|
|
601
|
+
data,
|
|
602
|
+
problems: problems ?? []
|
|
603
|
+
}, replacer, 2);
|
|
604
|
+
}
|
|
605
|
+
const VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
606
|
+
"-f",
|
|
607
|
+
"--format",
|
|
608
|
+
"-t",
|
|
609
|
+
"--template",
|
|
610
|
+
"-c",
|
|
611
|
+
"--cell",
|
|
612
|
+
"--timeout",
|
|
613
|
+
"--id",
|
|
614
|
+
"--content",
|
|
615
|
+
"--content-file",
|
|
616
|
+
"--depends-on",
|
|
617
|
+
"--before",
|
|
618
|
+
"--after",
|
|
619
|
+
"--at",
|
|
620
|
+
"--title",
|
|
621
|
+
"--author",
|
|
622
|
+
"--description",
|
|
623
|
+
"--tags",
|
|
624
|
+
"-o",
|
|
625
|
+
"--output"
|
|
704
626
|
]);
|
|
705
627
|
function firstPositional(args) {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
return void 0;
|
|
628
|
+
for (let i = 0; i < args.length; i++) {
|
|
629
|
+
const arg = args[i];
|
|
630
|
+
if (VALUE_FLAGS.has(arg)) {
|
|
631
|
+
i++;
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
if (!arg.startsWith("-")) return arg;
|
|
635
|
+
}
|
|
715
636
|
}
|
|
716
637
|
function flagValue(args, flag) {
|
|
717
|
-
|
|
718
|
-
|
|
638
|
+
const i = args.indexOf(flag);
|
|
639
|
+
return i >= 0 ? args[i + 1] : void 0;
|
|
719
640
|
}
|
|
720
641
|
function readFile(file) {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
}
|
|
729
|
-
var BASIC_TEMPLATE = `version: "1.0"
|
|
642
|
+
try {
|
|
643
|
+
return { content: readFileSync(file, "utf-8") };
|
|
644
|
+
} catch (error) {
|
|
645
|
+
return { error: `Cannot read file '${file}': ${error instanceof Error ? error.message : String(error)}` };
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
const BASIC_TEMPLATE = `version: "1.0"
|
|
730
649
|
metadata:
|
|
731
650
|
title: "<NAME>"
|
|
732
651
|
runtime:
|
|
@@ -748,644 +667,760 @@ cells:
|
|
|
748
667
|
depends_on: [example]
|
|
749
668
|
`;
|
|
750
669
|
function bullets(items) {
|
|
751
|
-
|
|
670
|
+
return items.map((item) => ` - ${item}`).join("\n");
|
|
752
671
|
}
|
|
753
672
|
function statusMark(status) {
|
|
754
|
-
|
|
673
|
+
return status === "success" || status === "pass" ? "✓" : "✗";
|
|
755
674
|
}
|
|
756
675
|
function humanCellLine(cell) {
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
676
|
+
const mark = statusMark(cell.status);
|
|
677
|
+
const body = cell.status === "error" || cell.status === "fail" ? cell.error ?? "(failed)" : formatResult(cell.output);
|
|
678
|
+
return `${mark} ${cell.id} (${cell.type}): ${body}`;
|
|
760
679
|
}
|
|
761
680
|
function failureSummary(cells) {
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
${bullets(failures.map((c) => `${c.id}: ${c.error ?? "(failed)"}`))}`;
|
|
681
|
+
const failures = cells.filter((c) => c.status === "error" || c.status === "fail");
|
|
682
|
+
if (failures.length === 0) return "";
|
|
683
|
+
return `${failures.length} cell(s) failed:\n${bullets(failures.map((c) => `${c.id}: ${c.error ?? "(failed)"}`))}`;
|
|
766
684
|
}
|
|
767
685
|
function errMessage(error) {
|
|
768
|
-
|
|
686
|
+
return error instanceof Error ? error.message : String(error);
|
|
769
687
|
}
|
|
770
688
|
function applyResults(workbook, results) {
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
689
|
+
for (const result of results) {
|
|
690
|
+
const cell = workbook.cells.find((c) => c.id === result.id);
|
|
691
|
+
if (!cell) continue;
|
|
692
|
+
cell.output = result.output;
|
|
693
|
+
cell.error = result.error;
|
|
694
|
+
}
|
|
777
695
|
}
|
|
778
696
|
function jsonCells(cells) {
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
697
|
+
return cells.map((c) => ({
|
|
698
|
+
id: c.id,
|
|
699
|
+
type: c.type,
|
|
700
|
+
status: c.status,
|
|
701
|
+
output: c.output ?? null,
|
|
702
|
+
error: c.error ?? null
|
|
703
|
+
}));
|
|
786
704
|
}
|
|
787
705
|
function failureList(cells) {
|
|
788
|
-
|
|
706
|
+
return cells.filter((c) => c.status === "error" || c.status === "fail").map((c) => `${c.id}: ${c.error ?? "(failed)"}`);
|
|
789
707
|
}
|
|
790
708
|
async function runCommand(args) {
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
const stdout2 = jsonEnvelope("run", report.ok, { cells: jsonCells(report.cells) }, problems);
|
|
882
|
-
return { stdout: stdout2, stderr: "", exitCode: report.ok ? 0 : 1 };
|
|
883
|
-
}
|
|
884
|
-
const body = report.cells.map(humanCellLine).join("\n");
|
|
885
|
-
const stdout = verbose && events.length > 0 ? `${events.join("\n")}
|
|
886
|
-
|
|
887
|
-
${body}` : body;
|
|
888
|
-
return { stdout, stderr: humanFailures, exitCode: report.ok ? 0 : 1 };
|
|
709
|
+
const json = args.includes("--json");
|
|
710
|
+
const fail = (problems, data = null) => json ? {
|
|
711
|
+
stdout: jsonEnvelope("run", false, data, problems),
|
|
712
|
+
stderr: "",
|
|
713
|
+
exitCode: 1
|
|
714
|
+
} : {
|
|
715
|
+
stdout: "",
|
|
716
|
+
stderr: problems.join("\n"),
|
|
717
|
+
exitCode: 1
|
|
718
|
+
};
|
|
719
|
+
const file = firstPositional(args);
|
|
720
|
+
if (!file) return json ? fail(["Usage: mtsw run <file> [-c <id>] [-v] [--json] [--write]"]) : {
|
|
721
|
+
stdout: "",
|
|
722
|
+
stderr: "Usage: mtsw run <file> [-c <id>] [-v] [--json] [--write]",
|
|
723
|
+
exitCode: 1
|
|
724
|
+
};
|
|
725
|
+
const read = readFile(file);
|
|
726
|
+
if (read.error) return fail([read.error]);
|
|
727
|
+
const expectHash = flagValue(args, "--expect-hash");
|
|
728
|
+
if (expectHash !== void 0) {
|
|
729
|
+
const actual = createHash("sha256").update(read.content, "utf8").digest("hex");
|
|
730
|
+
if (actual.toLowerCase() !== expectHash.toLowerCase()) return fail([`--expect-hash mismatch: file SHA-256 is ${actual}, expected ${expectHash}`]);
|
|
731
|
+
}
|
|
732
|
+
const parsed = parseWorkbook(read.content);
|
|
733
|
+
if (!parsed.success) {
|
|
734
|
+
const problems = parsed.errors ?? [];
|
|
735
|
+
return json ? fail(problems, { cells: [] }) : {
|
|
736
|
+
stdout: "",
|
|
737
|
+
stderr: `Parse errors:\n${bullets(problems)}`,
|
|
738
|
+
exitCode: 1
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
const cellId = flagValue(args, "-c") ?? flagValue(args, "--cell");
|
|
742
|
+
if (cellId !== void 0 && !parsed.workbook.cells.some((c) => c.id === cellId)) return json ? fail([`No such cell: '${cellId}'`], { cells: [] }) : {
|
|
743
|
+
stdout: "",
|
|
744
|
+
stderr: `No such cell: '${cellId}'`,
|
|
745
|
+
exitCode: 1
|
|
746
|
+
};
|
|
747
|
+
const timeoutStr = flagValue(args, "--timeout");
|
|
748
|
+
const verbose = args.includes("-v") || args.includes("--verbose");
|
|
749
|
+
let report;
|
|
750
|
+
const events = [];
|
|
751
|
+
if (timeoutStr !== void 0) {
|
|
752
|
+
const timeoutMs = Number(timeoutStr);
|
|
753
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return fail([`--timeout must be a positive number of milliseconds (got '${timeoutStr}')`]);
|
|
754
|
+
if (cellId !== void 0 || verbose) return fail(["--timeout does not support -c/--cell or -v/--verbose (it runs the whole workbook, in a worker, to completion or termination)"]);
|
|
755
|
+
try {
|
|
756
|
+
report = await runWorkbookWithTimeout(read.content, { timeoutMs });
|
|
757
|
+
} catch (error) {
|
|
758
|
+
return fail([error instanceof Error ? error.message : String(error)], { cells: [] });
|
|
759
|
+
}
|
|
760
|
+
} else {
|
|
761
|
+
const executor = createExecutor(parsed.workbook);
|
|
762
|
+
if (verbose) executor.on((event) => events.push(`[${event.type}] ${event.cellId ?? ""}`.trimEnd()));
|
|
763
|
+
report = await executor.runReport(cellId !== void 0 ? { only: cellId } : {});
|
|
764
|
+
}
|
|
765
|
+
const problems = report.ok ? [] : failureList(report.cells);
|
|
766
|
+
const humanFailures = report.ok ? "" : failureSummary(report.cells);
|
|
767
|
+
if (args.includes("--write")) {
|
|
768
|
+
applyResults(parsed.workbook, report.cells);
|
|
769
|
+
try {
|
|
770
|
+
writeFileAtomic(file, serializeWorkbook(parsed.workbook));
|
|
771
|
+
} catch (error) {
|
|
772
|
+
return fail([`Failed to write '${file}': ${errMessage(error)}`], { cells: jsonCells(report.cells) });
|
|
773
|
+
}
|
|
774
|
+
if (json) return {
|
|
775
|
+
stdout: jsonEnvelope("run", report.ok, {
|
|
776
|
+
cells: jsonCells(report.cells),
|
|
777
|
+
written: file
|
|
778
|
+
}, problems),
|
|
779
|
+
stderr: "",
|
|
780
|
+
exitCode: report.ok ? 0 : 1
|
|
781
|
+
};
|
|
782
|
+
return {
|
|
783
|
+
stdout: "",
|
|
784
|
+
stderr: [`Updated ${file}`, humanFailures].filter(Boolean).join("\n"),
|
|
785
|
+
exitCode: report.ok ? 0 : 1
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
if (json) return {
|
|
789
|
+
stdout: jsonEnvelope("run", report.ok, { cells: jsonCells(report.cells) }, problems),
|
|
790
|
+
stderr: "",
|
|
791
|
+
exitCode: report.ok ? 0 : 1
|
|
792
|
+
};
|
|
793
|
+
const body = report.cells.map(humanCellLine).join("\n");
|
|
794
|
+
return {
|
|
795
|
+
stdout: verbose && events.length > 0 ? `${events.join("\n")}\n\n${body}` : body,
|
|
796
|
+
stderr: humanFailures,
|
|
797
|
+
exitCode: report.ok ? 0 : 1
|
|
798
|
+
};
|
|
889
799
|
}
|
|
890
800
|
function validateChartSpecs(workbook) {
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
return problems;
|
|
801
|
+
const problems = [];
|
|
802
|
+
const ids = new Set(workbook.cells.map((c) => c.id));
|
|
803
|
+
for (const cell of workbook.cells) {
|
|
804
|
+
if (cell.type !== "visualization") continue;
|
|
805
|
+
let spec;
|
|
806
|
+
try {
|
|
807
|
+
spec = parseYamlSafe(cell.content);
|
|
808
|
+
} catch (error) {
|
|
809
|
+
problems.push(`Cell "${cell.id}": invalid chart spec (${errMessage(error)})`);
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
if (!spec || typeof spec !== "object" || Array.isArray(spec)) {
|
|
813
|
+
problems.push(`Cell "${cell.id}": chart spec must be a mapping with type/x/y`);
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
const s = spec;
|
|
817
|
+
if (s.type !== void 0 && ![
|
|
818
|
+
"line",
|
|
819
|
+
"scatter",
|
|
820
|
+
"bar"
|
|
821
|
+
].includes(String(s.type))) problems.push(`Cell "${cell.id}": chart type must be line|scatter|bar`);
|
|
822
|
+
for (const axis of ["x", "y"]) {
|
|
823
|
+
const a = s[axis];
|
|
824
|
+
if (!a || typeof a !== "object") {
|
|
825
|
+
problems.push(`Cell "${cell.id}": chart "${axis}" must be a mapping with a "data" field`);
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
const data = a.data;
|
|
829
|
+
if (data === void 0) problems.push(`Cell "${cell.id}": chart ${axis}.data is required`);
|
|
830
|
+
else if (typeof data === "string" && !ids.has(data)) problems.push(`Cell "${cell.id}": chart ${axis}.data references unknown cell "${data}"`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
return problems;
|
|
925
834
|
}
|
|
926
835
|
function computeProblems(parsed) {
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
}
|
|
935
|
-
return problems;
|
|
836
|
+
const problems = [...parsed.errors ?? []];
|
|
837
|
+
if (parsed.success && parsed.workbook) {
|
|
838
|
+
const graph = buildDependencyGraph(parsed.workbook.cells);
|
|
839
|
+
for (const cycle of detectCycles(graph)) problems.push(`Dependency cycle: ${cycle.join(" -> ")}`);
|
|
840
|
+
problems.push(...validateChartSpecs(parsed.workbook));
|
|
841
|
+
}
|
|
842
|
+
return problems;
|
|
936
843
|
}
|
|
937
844
|
function validateCommand(args) {
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
845
|
+
const json = args.includes("--json");
|
|
846
|
+
const file = firstPositional(args);
|
|
847
|
+
if (!file) return json ? {
|
|
848
|
+
stdout: jsonEnvelope("validate", false, null, ["Usage: mtsw validate <file>"]),
|
|
849
|
+
stderr: "",
|
|
850
|
+
exitCode: 1
|
|
851
|
+
} : {
|
|
852
|
+
stdout: "",
|
|
853
|
+
stderr: "Usage: mtsw validate <file>",
|
|
854
|
+
exitCode: 1
|
|
855
|
+
};
|
|
856
|
+
const read = readFile(file);
|
|
857
|
+
if (read.error) return json ? {
|
|
858
|
+
stdout: jsonEnvelope("validate", false, null, [read.error]),
|
|
859
|
+
stderr: "",
|
|
860
|
+
exitCode: 1
|
|
861
|
+
} : {
|
|
862
|
+
stdout: "",
|
|
863
|
+
stderr: read.error,
|
|
864
|
+
exitCode: 1
|
|
865
|
+
};
|
|
866
|
+
const parsed = parseWorkbook(read.content);
|
|
867
|
+
const problems = computeProblems(parsed);
|
|
868
|
+
const ok = problems.length === 0;
|
|
869
|
+
const cellCount = parsed.workbook ? parsed.workbook.cells.length : 0;
|
|
870
|
+
if (json) return {
|
|
871
|
+
stdout: jsonEnvelope("validate", ok, { cellCount }, problems),
|
|
872
|
+
stderr: "",
|
|
873
|
+
exitCode: ok ? 0 : 1
|
|
874
|
+
};
|
|
875
|
+
if (ok) return {
|
|
876
|
+
stdout: `OK: '${file}' is valid (${cellCount} cell(s))`,
|
|
877
|
+
stderr: "",
|
|
878
|
+
exitCode: 0
|
|
879
|
+
};
|
|
880
|
+
return {
|
|
881
|
+
stdout: "",
|
|
882
|
+
stderr: `Invalid workbook:\n${bullets(problems)}`,
|
|
883
|
+
exitCode: 1
|
|
884
|
+
};
|
|
967
885
|
}
|
|
968
886
|
function describeCommand(args) {
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
887
|
+
const json = args.includes("--json");
|
|
888
|
+
const file = firstPositional(args);
|
|
889
|
+
if (!file) return json ? {
|
|
890
|
+
stdout: jsonEnvelope("describe", false, null, ["Usage: mtsw describe <file>"]),
|
|
891
|
+
stderr: "",
|
|
892
|
+
exitCode: 1
|
|
893
|
+
} : {
|
|
894
|
+
stdout: "",
|
|
895
|
+
stderr: "Usage: mtsw describe <file>",
|
|
896
|
+
exitCode: 1
|
|
897
|
+
};
|
|
898
|
+
const read = readFile(file);
|
|
899
|
+
if (read.error) return json ? {
|
|
900
|
+
stdout: jsonEnvelope("describe", false, null, [read.error]),
|
|
901
|
+
stderr: "",
|
|
902
|
+
exitCode: 1
|
|
903
|
+
} : {
|
|
904
|
+
stdout: "",
|
|
905
|
+
stderr: read.error,
|
|
906
|
+
exitCode: 1
|
|
907
|
+
};
|
|
908
|
+
const parsed = parseWorkbook(read.content);
|
|
909
|
+
const problems = computeProblems(parsed);
|
|
910
|
+
const ok = parsed.success && problems.length === 0;
|
|
911
|
+
const data = describeData(parsed.workbook);
|
|
912
|
+
const cells = data.cells;
|
|
913
|
+
if (json) return {
|
|
914
|
+
stdout: jsonEnvelope("describe", ok, data, problems),
|
|
915
|
+
stderr: "",
|
|
916
|
+
exitCode: ok ? 0 : 1
|
|
917
|
+
};
|
|
918
|
+
if (!parsed.success) return {
|
|
919
|
+
stdout: "",
|
|
920
|
+
stderr: `Parse errors:\n${bullets(problems)}`,
|
|
921
|
+
exitCode: 1
|
|
922
|
+
};
|
|
923
|
+
const byType = cells.reduce((acc, c) => {
|
|
924
|
+
acc[c.type] = (acc[c.type] ?? 0) + 1;
|
|
925
|
+
return acc;
|
|
926
|
+
}, {});
|
|
927
|
+
return {
|
|
928
|
+
stdout: [
|
|
929
|
+
`${data.metadata.title ?? "(untitled)"} — ${cells.length} cell(s)`,
|
|
930
|
+
...Object.entries(byType).map(([t, n]) => ` ${t}: ${n}`),
|
|
931
|
+
problems.length ? `${problems.length} problem(s)` : "no problems"
|
|
932
|
+
].join("\n"),
|
|
933
|
+
stderr: "",
|
|
934
|
+
exitCode: ok ? 0 : 1
|
|
935
|
+
};
|
|
1008
936
|
}
|
|
1009
937
|
function capabilitiesCommand(args) {
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
938
|
+
const data = capabilitiesInfo();
|
|
939
|
+
if (args.includes("--json")) return {
|
|
940
|
+
stdout: jsonEnvelope("capabilities", true, data, []),
|
|
941
|
+
stderr: "",
|
|
942
|
+
exitCode: 0
|
|
943
|
+
};
|
|
944
|
+
return {
|
|
945
|
+
stdout: [
|
|
946
|
+
`mtsw ${data.version} (schema ${SCHEMA_VERSION.major}.${SCHEMA_VERSION.minor})`,
|
|
947
|
+
`supported cells: ${data.cellTypes.supported.join(", ")}`,
|
|
948
|
+
`deferred cells: ${data.cellTypes.deferred.join(", ")}`,
|
|
949
|
+
`commands: ${data.commands.join(", ")}`
|
|
950
|
+
].join("\n"),
|
|
951
|
+
stderr: "",
|
|
952
|
+
exitCode: 0
|
|
953
|
+
};
|
|
1021
954
|
}
|
|
1022
955
|
function templatesCommand(args) {
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
956
|
+
const templates = Object.entries(TEMPLATES).map(([name, t]) => ({
|
|
957
|
+
name,
|
|
958
|
+
description: t.description
|
|
959
|
+
}));
|
|
960
|
+
if (args.includes("--json")) return {
|
|
961
|
+
stdout: jsonEnvelope("templates", true, { templates }, []),
|
|
962
|
+
stderr: "",
|
|
963
|
+
exitCode: 0
|
|
964
|
+
};
|
|
965
|
+
return {
|
|
966
|
+
stdout: templates.map((t) => `${t.name} - ${t.description}`).join("\n"),
|
|
967
|
+
stderr: "",
|
|
968
|
+
exitCode: 0
|
|
969
|
+
};
|
|
1035
970
|
}
|
|
1036
971
|
function functionsCommand(args) {
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
972
|
+
const data = listFunctions();
|
|
973
|
+
if (args.includes("--json")) return {
|
|
974
|
+
stdout: jsonEnvelope("functions", true, data, []),
|
|
975
|
+
stderr: "",
|
|
976
|
+
exitCode: 0
|
|
977
|
+
};
|
|
978
|
+
return {
|
|
979
|
+
stdout: [`functions (${data.functions.length}): ${data.functions.join(", ")}`, `constants (${data.constants.length}): ${data.constants.join(", ")}`].join("\n"),
|
|
980
|
+
stderr: "",
|
|
981
|
+
exitCode: 0
|
|
982
|
+
};
|
|
1046
983
|
}
|
|
1047
984
|
function metaCommand(args) {
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
985
|
+
const json = args.includes("--json");
|
|
986
|
+
const fail = (problems) => json ? {
|
|
987
|
+
stdout: jsonEnvelope("meta", false, null, problems),
|
|
988
|
+
stderr: "",
|
|
989
|
+
exitCode: 1
|
|
990
|
+
} : {
|
|
991
|
+
stdout: "",
|
|
992
|
+
stderr: problems.join("\n"),
|
|
993
|
+
exitCode: 1
|
|
994
|
+
};
|
|
995
|
+
const verb = args[0];
|
|
996
|
+
if (verb !== "get" && verb !== "set") return fail(["Usage: mtsw meta <get|set> <file> ..."]);
|
|
997
|
+
const vargs = args.slice(1);
|
|
998
|
+
const file = positionals(vargs)[0];
|
|
999
|
+
if (!file) return fail([`Usage: mtsw meta ${verb} <file> ...`]);
|
|
1000
|
+
const read = readFile(file);
|
|
1001
|
+
if (read.error) return fail([read.error]);
|
|
1002
|
+
const parsed = parseWorkbook(read.content);
|
|
1003
|
+
if (!parsed.success) return fail(parsed.errors ?? ["Parse error"]);
|
|
1004
|
+
const wb = parsed.workbook;
|
|
1005
|
+
if (verb === "get") {
|
|
1006
|
+
if (json) return {
|
|
1007
|
+
stdout: jsonEnvelope("meta", true, wb.metadata, []),
|
|
1008
|
+
stderr: "",
|
|
1009
|
+
exitCode: 0
|
|
1010
|
+
};
|
|
1011
|
+
return {
|
|
1012
|
+
stdout: Object.entries(wb.metadata).map(([k, v]) => `${k}: ${Array.isArray(v) ? v.join(", ") : String(v)}`).join("\n") || "(no metadata)",
|
|
1013
|
+
stderr: "",
|
|
1014
|
+
exitCode: 0
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
const changes = {};
|
|
1018
|
+
const title = flagValue(vargs, "--title");
|
|
1019
|
+
if (title !== void 0) changes.title = title;
|
|
1020
|
+
const author = flagValue(vargs, "--author");
|
|
1021
|
+
if (author !== void 0) changes.author = author;
|
|
1022
|
+
const description = flagValue(vargs, "--description");
|
|
1023
|
+
if (description !== void 0) changes.description = description;
|
|
1024
|
+
const tags = flagValue(vargs, "--tags");
|
|
1025
|
+
if (tags !== void 0) changes.tags = tags.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1026
|
+
if (Object.keys(changes).length === 0) return fail(["meta set requires at least one of --title/--author/--description/--tags"]);
|
|
1027
|
+
let next;
|
|
1028
|
+
try {
|
|
1029
|
+
next = setMetadata(wb, changes);
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
return fail([errMessage(error)]);
|
|
1032
|
+
}
|
|
1033
|
+
try {
|
|
1034
|
+
writeFileAtomic(file, serializeWorkbook(next));
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
return fail([`Failed to write '${file}': ${errMessage(error)}`]);
|
|
1037
|
+
}
|
|
1038
|
+
if (json) return {
|
|
1039
|
+
stdout: jsonEnvelope("meta", true, next.metadata, []),
|
|
1040
|
+
stderr: "",
|
|
1041
|
+
exitCode: 0
|
|
1042
|
+
};
|
|
1043
|
+
return {
|
|
1044
|
+
stdout: "",
|
|
1045
|
+
stderr: `Updated metadata in ${file}`,
|
|
1046
|
+
exitCode: 0
|
|
1047
|
+
};
|
|
1095
1048
|
}
|
|
1096
1049
|
function buildRenderDoc(workbook, byId) {
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1050
|
+
const lookup = (ref) => {
|
|
1051
|
+
if (typeof ref !== "string") return ref;
|
|
1052
|
+
return byId?.get(ref)?.output ?? workbook.cells.find((c) => c.id === ref)?.output;
|
|
1053
|
+
};
|
|
1054
|
+
const cells = workbook.cells.map((c) => {
|
|
1055
|
+
const rc = {
|
|
1056
|
+
type: c.type,
|
|
1057
|
+
content: c.content,
|
|
1058
|
+
id: c.id
|
|
1059
|
+
};
|
|
1060
|
+
if (c.type === "markdown" || c.type === "equation") return rc;
|
|
1061
|
+
if (c.type === "visualization") {
|
|
1062
|
+
rc.type = "chart";
|
|
1063
|
+
try {
|
|
1064
|
+
const spec = parseYamlSafe(c.content);
|
|
1065
|
+
rc.chart = {
|
|
1066
|
+
spec: {
|
|
1067
|
+
type: spec?.type,
|
|
1068
|
+
title: spec?.title,
|
|
1069
|
+
xLabel: spec?.x?.label,
|
|
1070
|
+
yLabel: spec?.y?.label
|
|
1071
|
+
},
|
|
1072
|
+
x: lookup(spec?.x?.data),
|
|
1073
|
+
y: lookup(spec?.y?.data)
|
|
1074
|
+
};
|
|
1075
|
+
const unresolved = [];
|
|
1076
|
+
for (const ref of [spec?.x?.data, spec?.y?.data]) {
|
|
1077
|
+
if (typeof ref !== "string") continue;
|
|
1078
|
+
if (byId) {
|
|
1079
|
+
const r = byId.get(ref);
|
|
1080
|
+
if (!r || r.status === "error" || r.status === "fail") unresolved.push(ref);
|
|
1081
|
+
} else if (workbook.cells.find((x) => x.id === ref)?.output === void 0) unresolved.push(ref);
|
|
1082
|
+
}
|
|
1083
|
+
if (unresolved.length > 0) rc.note = `chart data did not resolve: ${unresolved.join(", ")}${byId ? "" : " (try without --no-run)"}`;
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
rc.chart = {
|
|
1086
|
+
spec: {},
|
|
1087
|
+
x: [],
|
|
1088
|
+
y: []
|
|
1089
|
+
};
|
|
1090
|
+
rc.note = `invalid chart spec: ${errMessage(error)}`;
|
|
1091
|
+
}
|
|
1092
|
+
return rc;
|
|
1093
|
+
}
|
|
1094
|
+
const r = byId?.get(c.id);
|
|
1095
|
+
const status = r?.status;
|
|
1096
|
+
const output = r ? r.output : c.output;
|
|
1097
|
+
const error = r ? r.error : c.error;
|
|
1098
|
+
if (c.type === "test") {
|
|
1099
|
+
if (status === "error") rc.error = error ?? "error";
|
|
1100
|
+
else if (status !== void 0) rc.passed = status === "pass";
|
|
1101
|
+
else if (typeof output === "boolean") rc.passed = output;
|
|
1102
|
+
return rc;
|
|
1103
|
+
}
|
|
1104
|
+
if (status === "error" || status === "fail" || status === void 0 && error !== void 0) rc.error = error ?? "error";
|
|
1105
|
+
else if (output !== void 0) rc.output = formatResult(output);
|
|
1106
|
+
return rc;
|
|
1107
|
+
});
|
|
1108
|
+
return {
|
|
1109
|
+
title: workbook.metadata.title,
|
|
1110
|
+
author: workbook.metadata.author,
|
|
1111
|
+
description: workbook.metadata.description,
|
|
1112
|
+
tags: workbook.metadata.tags,
|
|
1113
|
+
cells
|
|
1114
|
+
};
|
|
1161
1115
|
}
|
|
1162
1116
|
async function exportCommand(args) {
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1117
|
+
const json = args.includes("--json");
|
|
1118
|
+
const fail = (problems) => json ? {
|
|
1119
|
+
stdout: jsonEnvelope("export", false, null, problems),
|
|
1120
|
+
stderr: "",
|
|
1121
|
+
exitCode: 1
|
|
1122
|
+
} : {
|
|
1123
|
+
stdout: "",
|
|
1124
|
+
stderr: problems.join("\n"),
|
|
1125
|
+
exitCode: 1
|
|
1126
|
+
};
|
|
1127
|
+
const format = flagValue(args, "--format") ?? "html";
|
|
1128
|
+
if (format !== "html" && format !== "tex" && format !== "json" && format !== "pdf" && format !== "ipynb") return fail([`Unknown format '${format}' (supported: html, tex, json, pdf, ipynb)`]);
|
|
1129
|
+
const file = firstPositional(args);
|
|
1130
|
+
if (!file) return fail(["Usage: mtsw export <file> [--format html|tex|json|pdf|ipynb] [--fragment] [-o out] [--no-run]"]);
|
|
1131
|
+
const read = readFile(file);
|
|
1132
|
+
if (read.error) return fail([read.error]);
|
|
1133
|
+
const parsed = parseWorkbook(read.content);
|
|
1134
|
+
if (!parsed.success) return fail(parsed.errors ?? ["Parse error"]);
|
|
1135
|
+
const workbook = parsed.workbook;
|
|
1136
|
+
let byId = null;
|
|
1137
|
+
let report = null;
|
|
1138
|
+
if (!args.includes("--no-run")) {
|
|
1139
|
+
report = await createExecutor(workbook).runReport();
|
|
1140
|
+
const fatal = report.cells.find((r) => r.id === "(workbook)");
|
|
1141
|
+
if (fatal) return fail([fatal.error ?? "workbook run failed"]);
|
|
1142
|
+
byId = new Map(report.cells.map((r) => [r.id, r]));
|
|
1143
|
+
}
|
|
1144
|
+
if (format === "json") {
|
|
1145
|
+
if (!report) return fail(["--format json requires running the notebook (remove --no-run)"]);
|
|
1146
|
+
const payload = {
|
|
1147
|
+
ok: report.ok,
|
|
1148
|
+
cells: report.cells.map((c) => ({
|
|
1149
|
+
id: c.id,
|
|
1150
|
+
type: c.type,
|
|
1151
|
+
status: c.status,
|
|
1152
|
+
output: c.output === void 0 ? void 0 : formatResult(c.output),
|
|
1153
|
+
error: c.error
|
|
1154
|
+
}))
|
|
1155
|
+
};
|
|
1156
|
+
const jsonStr = JSON.stringify(payload, null, 2);
|
|
1157
|
+
const outPath = flagValue(args, "-o") ?? flagValue(args, "--output");
|
|
1158
|
+
if (outPath) {
|
|
1159
|
+
try {
|
|
1160
|
+
writeFileAtomic(outPath, jsonStr);
|
|
1161
|
+
} catch (error) {
|
|
1162
|
+
return fail([`Failed to write '${outPath}': ${errMessage(error)}`]);
|
|
1163
|
+
}
|
|
1164
|
+
if (json) return {
|
|
1165
|
+
stdout: jsonEnvelope("export", true, {
|
|
1166
|
+
path: outPath,
|
|
1167
|
+
bytes: Buffer.byteLength(jsonStr)
|
|
1168
|
+
}, []),
|
|
1169
|
+
stderr: "",
|
|
1170
|
+
exitCode: 0
|
|
1171
|
+
};
|
|
1172
|
+
return {
|
|
1173
|
+
stdout: "",
|
|
1174
|
+
stderr: `Exported ${file} -> ${outPath} (${Buffer.byteLength(jsonStr)} bytes)`,
|
|
1175
|
+
exitCode: 0
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
return {
|
|
1179
|
+
stdout: jsonStr,
|
|
1180
|
+
stderr: "",
|
|
1181
|
+
exitCode: 0
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
if (format === "pdf") {
|
|
1185
|
+
const outPath = flagValue(args, "-o") ?? flagValue(args, "--output");
|
|
1186
|
+
if (!outPath) return fail(["--format pdf requires an output path: -o <file.pdf>"]);
|
|
1187
|
+
try {
|
|
1188
|
+
await toPDF(buildRenderDoc(workbook, byId), outPath, { parse });
|
|
1189
|
+
} catch (error) {
|
|
1190
|
+
return fail([`PDF export failed: ${errMessage(error)}`]);
|
|
1191
|
+
}
|
|
1192
|
+
return json ? {
|
|
1193
|
+
stdout: jsonEnvelope("export", true, { path: outPath }, []),
|
|
1194
|
+
stderr: "",
|
|
1195
|
+
exitCode: 0
|
|
1196
|
+
} : {
|
|
1197
|
+
stdout: "",
|
|
1198
|
+
stderr: `Exported ${file} -> ${outPath}`,
|
|
1199
|
+
exitCode: 0
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
const fragment = args.includes("--fragment");
|
|
1203
|
+
const rendered = format === "tex" ? toTeX(buildRenderDoc(workbook, byId), {
|
|
1204
|
+
parse,
|
|
1205
|
+
fragment
|
|
1206
|
+
}) : format === "ipynb" ? toIpynb(buildRenderDoc(workbook, byId)) : toHTML(buildRenderDoc(workbook, byId), { parse });
|
|
1207
|
+
const bytes = Buffer.byteLength(rendered, "utf-8");
|
|
1208
|
+
const outPath = flagValue(args, "-o") ?? flagValue(args, "--output");
|
|
1209
|
+
if (outPath) {
|
|
1210
|
+
try {
|
|
1211
|
+
writeFileAtomic(outPath, rendered);
|
|
1212
|
+
} catch (error) {
|
|
1213
|
+
return fail([`Failed to write '${outPath}': ${errMessage(error)}`]);
|
|
1214
|
+
}
|
|
1215
|
+
if (json) return {
|
|
1216
|
+
stdout: jsonEnvelope("export", true, {
|
|
1217
|
+
path: outPath,
|
|
1218
|
+
bytes
|
|
1219
|
+
}, []),
|
|
1220
|
+
stderr: "",
|
|
1221
|
+
exitCode: 0
|
|
1222
|
+
};
|
|
1223
|
+
return {
|
|
1224
|
+
stdout: "",
|
|
1225
|
+
stderr: `Exported ${file} -> ${outPath} (${bytes} bytes)`,
|
|
1226
|
+
exitCode: 0
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
if (json) return {
|
|
1230
|
+
stdout: jsonEnvelope("export", true, { bytes }, []),
|
|
1231
|
+
stderr: "",
|
|
1232
|
+
exitCode: 0
|
|
1233
|
+
};
|
|
1234
|
+
return {
|
|
1235
|
+
stdout: rendered,
|
|
1236
|
+
stderr: "",
|
|
1237
|
+
exitCode: 0
|
|
1238
|
+
};
|
|
1258
1239
|
}
|
|
1259
1240
|
function runServer(input = process.stdin, output = process.stdout) {
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1241
|
+
const session = new Session();
|
|
1242
|
+
const rl = createInterface({
|
|
1243
|
+
input,
|
|
1244
|
+
crlfDelay: Infinity
|
|
1245
|
+
});
|
|
1246
|
+
const write = (obj) => {
|
|
1247
|
+
output.write(`${JSON.stringify(obj)}\n`);
|
|
1248
|
+
};
|
|
1249
|
+
return new Promise((resolve) => {
|
|
1250
|
+
let done = false;
|
|
1251
|
+
const finish = () => {
|
|
1252
|
+
if (done) return;
|
|
1253
|
+
done = true;
|
|
1254
|
+
process.removeListener("SIGINT", finish);
|
|
1255
|
+
process.removeListener("SIGTERM", finish);
|
|
1256
|
+
rl.close();
|
|
1257
|
+
resolve();
|
|
1258
|
+
};
|
|
1259
|
+
const processLine = async (line) => {
|
|
1260
|
+
if (done) return;
|
|
1261
|
+
const text = line.trim();
|
|
1262
|
+
if (!text) return;
|
|
1263
|
+
let request;
|
|
1264
|
+
try {
|
|
1265
|
+
request = JSON.parse(text);
|
|
1266
|
+
} catch {
|
|
1267
|
+
write({
|
|
1268
|
+
jsonrpc: "2.0",
|
|
1269
|
+
id: null,
|
|
1270
|
+
error: {
|
|
1271
|
+
code: -32700,
|
|
1272
|
+
message: "Parse error"
|
|
1273
|
+
}
|
|
1274
|
+
});
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
if (Array.isArray(request)) {
|
|
1278
|
+
write({
|
|
1279
|
+
jsonrpc: "2.0",
|
|
1280
|
+
id: null,
|
|
1281
|
+
error: {
|
|
1282
|
+
code: -32600,
|
|
1283
|
+
message: "Batch requests are not supported"
|
|
1284
|
+
}
|
|
1285
|
+
});
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
try {
|
|
1289
|
+
const { response, events, shutdown } = await handleRequest(session, request);
|
|
1290
|
+
for (const event of events) write(event);
|
|
1291
|
+
write(response);
|
|
1292
|
+
if (shutdown) finish();
|
|
1293
|
+
} catch (error) {
|
|
1294
|
+
write({
|
|
1295
|
+
jsonrpc: "2.0",
|
|
1296
|
+
id: null,
|
|
1297
|
+
error: {
|
|
1298
|
+
code: -32603,
|
|
1299
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1300
|
+
}
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
};
|
|
1304
|
+
let queue = Promise.resolve();
|
|
1305
|
+
rl.on("line", (line) => {
|
|
1306
|
+
queue = queue.then(() => processLine(line));
|
|
1307
|
+
});
|
|
1308
|
+
rl.on("close", () => {
|
|
1309
|
+
queue.then(finish);
|
|
1310
|
+
});
|
|
1311
|
+
process.once("SIGINT", finish);
|
|
1312
|
+
process.once("SIGTERM", finish);
|
|
1313
|
+
});
|
|
1321
1314
|
}
|
|
1322
1315
|
async function serveCommand() {
|
|
1323
|
-
|
|
1324
|
-
|
|
1316
|
+
await runServer();
|
|
1317
|
+
return {
|
|
1318
|
+
stdout: "",
|
|
1319
|
+
stderr: "",
|
|
1320
|
+
exitCode: 0
|
|
1321
|
+
};
|
|
1325
1322
|
}
|
|
1326
1323
|
function graphCommand(args) {
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1324
|
+
if (args.includes("--json")) return {
|
|
1325
|
+
stdout: jsonEnvelope("graph", false, null, ["graph has no --json output; use `describe --json` for structured graph data"]),
|
|
1326
|
+
stderr: "",
|
|
1327
|
+
exitCode: 1
|
|
1328
|
+
};
|
|
1329
|
+
const file = firstPositional(args);
|
|
1330
|
+
if (!file) return {
|
|
1331
|
+
stdout: "",
|
|
1332
|
+
stderr: "Usage: mtsw graph <file> [-f mermaid|dot]",
|
|
1333
|
+
exitCode: 1
|
|
1334
|
+
};
|
|
1335
|
+
const read = readFile(file);
|
|
1336
|
+
if (read.error) return {
|
|
1337
|
+
stdout: "",
|
|
1338
|
+
stderr: read.error,
|
|
1339
|
+
exitCode: 1
|
|
1340
|
+
};
|
|
1341
|
+
const parsed = parseWorkbook(read.content);
|
|
1342
|
+
if (!parsed.success) return {
|
|
1343
|
+
stdout: "",
|
|
1344
|
+
stderr: `Parse errors:\n${bullets(parsed.errors ?? [])}`,
|
|
1345
|
+
exitCode: 1
|
|
1346
|
+
};
|
|
1347
|
+
const graph = buildDependencyGraph(parsed.workbook.cells);
|
|
1348
|
+
if (flagValue(args, "-f") === "mermaid" || flagValue(args, "--format") === "mermaid") return {
|
|
1349
|
+
stdout: toMermaid(graph),
|
|
1350
|
+
stderr: "",
|
|
1351
|
+
exitCode: 0
|
|
1352
|
+
};
|
|
1353
|
+
if (flagValue(args, "-f") === "dot" || flagValue(args, "--format") === "dot") return {
|
|
1354
|
+
stdout: toDOT(graph),
|
|
1355
|
+
stderr: "",
|
|
1356
|
+
exitCode: 0
|
|
1357
|
+
};
|
|
1358
|
+
return {
|
|
1359
|
+
stdout: [...graph.nodes].map(([id, node]) => `${id}: [${node.dependencies.join(", ")}]`).join("\n"),
|
|
1360
|
+
stderr: "",
|
|
1361
|
+
exitCode: 0
|
|
1362
|
+
};
|
|
1356
1363
|
}
|
|
1357
1364
|
function stripCommand(args) {
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1365
|
+
const file = firstPositional(args);
|
|
1366
|
+
if (!file) return {
|
|
1367
|
+
stdout: "",
|
|
1368
|
+
stderr: "Usage: mtsw strip <file> [-w|--write]",
|
|
1369
|
+
exitCode: 1
|
|
1370
|
+
};
|
|
1371
|
+
const read = readFile(file);
|
|
1372
|
+
if (read.error) return {
|
|
1373
|
+
stdout: "",
|
|
1374
|
+
stderr: read.error,
|
|
1375
|
+
exitCode: 1
|
|
1376
|
+
};
|
|
1377
|
+
const parsed = parseWorkbook(read.content);
|
|
1378
|
+
if (!parsed.success) return {
|
|
1379
|
+
stdout: "",
|
|
1380
|
+
stderr: `Parse errors:\n${bullets(parsed.errors ?? [])}`,
|
|
1381
|
+
exitCode: 1
|
|
1382
|
+
};
|
|
1383
|
+
const yaml = serializeWorkbook(stripOutputs(parsed.workbook));
|
|
1384
|
+
if (args.includes("-w") || args.includes("--write")) {
|
|
1385
|
+
try {
|
|
1386
|
+
writeFileAtomic(file, yaml);
|
|
1387
|
+
} catch (error) {
|
|
1388
|
+
return {
|
|
1389
|
+
stdout: "",
|
|
1390
|
+
stderr: `Failed to write '${file}': ${errMessage(error)}`,
|
|
1391
|
+
exitCode: 1
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
return {
|
|
1395
|
+
stdout: "",
|
|
1396
|
+
stderr: `Stripped outputs -> ${file}`,
|
|
1397
|
+
exitCode: 0
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
return {
|
|
1401
|
+
stdout: yaml,
|
|
1402
|
+
stderr: "",
|
|
1403
|
+
exitCode: 0
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
const TEMPLATES = {
|
|
1407
|
+
basic: {
|
|
1408
|
+
content: BASIC_TEMPLATE,
|
|
1409
|
+
description: "Markdown intro + a code cell + a passing test cell"
|
|
1410
|
+
},
|
|
1411
|
+
empty: {
|
|
1412
|
+
content: `version: "1.0"
|
|
1381
1413
|
metadata:
|
|
1382
1414
|
title: "<NAME>"
|
|
1383
1415
|
runtime:
|
|
1384
1416
|
engine: mathts
|
|
1385
1417
|
execution: reactive
|
|
1386
1418
|
cells: []
|
|
1387
|
-
|
|
1388
|
-
|
|
1419
|
+
`,
|
|
1420
|
+
description: "A blank workbook (no cells)"
|
|
1421
|
+
},
|
|
1422
|
+
chart: {
|
|
1423
|
+
content: `version: "1.0"
|
|
1389
1424
|
metadata:
|
|
1390
1425
|
title: "<NAME>"
|
|
1391
1426
|
runtime:
|
|
@@ -1405,404 +1440,390 @@ cells:
|
|
|
1405
1440
|
y: { label: "y", data: ys }
|
|
1406
1441
|
id: chart
|
|
1407
1442
|
depends_on: [xs, ys]
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
content: BASIC_TEMPLATE,
|
|
1412
|
-
description: "Markdown intro + a code cell + a passing test cell"
|
|
1413
|
-
},
|
|
1414
|
-
empty: { content: EMPTY_TEMPLATE, description: "A blank workbook (no cells)" },
|
|
1415
|
-
chart: {
|
|
1416
|
-
content: CHART_TEMPLATE,
|
|
1417
|
-
description: "A line chart over two data cells (visualization example)"
|
|
1418
|
-
}
|
|
1443
|
+
`,
|
|
1444
|
+
description: "A line chart over two data cells (visualization example)"
|
|
1445
|
+
}
|
|
1419
1446
|
};
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1447
|
+
const RESERVED_DEVICE_NAMES = /* @__PURE__ */ new Set([
|
|
1448
|
+
"CON",
|
|
1449
|
+
"PRN",
|
|
1450
|
+
"AUX",
|
|
1451
|
+
"NUL",
|
|
1452
|
+
...Array.from({ length: 9 }, (_unused, i) => `COM${i + 1}`),
|
|
1453
|
+
...Array.from({ length: 9 }, (_unused, i) => `LPT${i + 1}`)
|
|
1427
1454
|
]);
|
|
1428
1455
|
function newCommand(args) {
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
return { stdout: `Created ${target}`, stderr: "", exitCode: 0 };
|
|
1456
|
+
const outPath = flagValue(args, "-o") ?? flagValue(args, "--output");
|
|
1457
|
+
const name = firstPositional(args);
|
|
1458
|
+
const templateName = args.includes("--empty") ? "empty" : flagValue(args, "-t") ?? flagValue(args, "--template") ?? "basic";
|
|
1459
|
+
const template = TEMPLATES[templateName];
|
|
1460
|
+
if (!template) return {
|
|
1461
|
+
stdout: "",
|
|
1462
|
+
stderr: `Unknown template '${templateName}'. Available: ${Object.keys(TEMPLATES).join(", ")}`,
|
|
1463
|
+
exitCode: 1
|
|
1464
|
+
};
|
|
1465
|
+
let target;
|
|
1466
|
+
let bare;
|
|
1467
|
+
if (outPath) {
|
|
1468
|
+
target = outPath.endsWith(".mtsw") ? outPath : `${outPath}.mtsw`;
|
|
1469
|
+
bare = basename(target).slice(0, -5);
|
|
1470
|
+
} else {
|
|
1471
|
+
if (!name) return {
|
|
1472
|
+
stdout: "",
|
|
1473
|
+
stderr: "Usage: mtsw new <name> [-t basic|empty|chart] [--empty] [-o <path>] [--force]",
|
|
1474
|
+
exitCode: 1
|
|
1475
|
+
};
|
|
1476
|
+
if (!/^[A-Za-z0-9._-]+$/.test(name)) return {
|
|
1477
|
+
stdout: "",
|
|
1478
|
+
stderr: `Invalid name '${name}': must be a bare filename — or use -o <path> for an explicit location`,
|
|
1479
|
+
exitCode: 1
|
|
1480
|
+
};
|
|
1481
|
+
bare = name.endsWith(".mtsw") ? name.slice(0, -5) : name;
|
|
1482
|
+
if (bare === "" || bare === "." || bare === ".." || RESERVED_DEVICE_NAMES.has(bare.toUpperCase())) return {
|
|
1483
|
+
stdout: "",
|
|
1484
|
+
stderr: `Invalid name '${name}'`,
|
|
1485
|
+
exitCode: 1
|
|
1486
|
+
};
|
|
1487
|
+
target = `${bare}.mtsw`;
|
|
1488
|
+
}
|
|
1489
|
+
const content = template.content.replace(/<NAME>/g, bare);
|
|
1490
|
+
const force = args.includes("--force");
|
|
1491
|
+
try {
|
|
1492
|
+
if (force) {
|
|
1493
|
+
try {
|
|
1494
|
+
if (lstatSync(target).isSymbolicLink()) return {
|
|
1495
|
+
stdout: "",
|
|
1496
|
+
stderr: `Refusing to overwrite symlink '${target}'`,
|
|
1497
|
+
exitCode: 1
|
|
1498
|
+
};
|
|
1499
|
+
} catch {}
|
|
1500
|
+
writeFileAtomic(target, content);
|
|
1501
|
+
} else writeFileSync(target, content, {
|
|
1502
|
+
encoding: "utf-8",
|
|
1503
|
+
flag: "wx"
|
|
1504
|
+
});
|
|
1505
|
+
} catch (error) {
|
|
1506
|
+
if (error.code === "EEXIST") return {
|
|
1507
|
+
stdout: "",
|
|
1508
|
+
stderr: `File '${target}' already exists (use --force to overwrite)`,
|
|
1509
|
+
exitCode: 1
|
|
1510
|
+
};
|
|
1511
|
+
return {
|
|
1512
|
+
stdout: "",
|
|
1513
|
+
stderr: `Failed to create '${target}': ${errMessage(error)}`,
|
|
1514
|
+
exitCode: 1
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
return {
|
|
1518
|
+
stdout: `Created ${target}`,
|
|
1519
|
+
stderr: "",
|
|
1520
|
+
exitCode: 0
|
|
1521
|
+
};
|
|
1496
1522
|
}
|
|
1497
1523
|
function importCommand(args) {
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1524
|
+
const json = args.includes("--json");
|
|
1525
|
+
const fail = (problems) => json ? {
|
|
1526
|
+
stdout: jsonEnvelope("import", false, null, problems),
|
|
1527
|
+
stderr: "",
|
|
1528
|
+
exitCode: 1
|
|
1529
|
+
} : {
|
|
1530
|
+
stdout: "",
|
|
1531
|
+
stderr: problems.join("\n"),
|
|
1532
|
+
exitCode: 1
|
|
1533
|
+
};
|
|
1534
|
+
const file = firstPositional(args);
|
|
1535
|
+
let input;
|
|
1536
|
+
if (!file || file === "-") {
|
|
1537
|
+
if (process.stdin.isTTY) return fail(["Provide an input file, or pipe JSON/YAML to stdin"]);
|
|
1538
|
+
try {
|
|
1539
|
+
input = readFileSync(0, "utf-8");
|
|
1540
|
+
} catch (error) {
|
|
1541
|
+
return fail([`Failed to read stdin: ${errMessage(error)}`]);
|
|
1542
|
+
}
|
|
1543
|
+
} else {
|
|
1544
|
+
const read = readFile(file);
|
|
1545
|
+
if (read.error) return fail([read.error]);
|
|
1546
|
+
input = read.content;
|
|
1547
|
+
}
|
|
1548
|
+
const result = importWorkbook(input);
|
|
1549
|
+
if (!result.success || !result.workbook) return fail(result.errors ?? ["Import failed"]);
|
|
1550
|
+
const content = serializeWorkbook(result.workbook);
|
|
1551
|
+
const count = result.workbook.cells.length;
|
|
1552
|
+
const outPath = flagValue(args, "-o") ?? flagValue(args, "--output");
|
|
1553
|
+
if (outPath) {
|
|
1554
|
+
try {
|
|
1555
|
+
writeFileAtomic(outPath, content);
|
|
1556
|
+
} catch (error) {
|
|
1557
|
+
return fail([`Failed to write '${outPath}': ${errMessage(error)}`]);
|
|
1558
|
+
}
|
|
1559
|
+
if (json) return {
|
|
1560
|
+
stdout: jsonEnvelope("import", true, {
|
|
1561
|
+
path: outPath,
|
|
1562
|
+
cells: count
|
|
1563
|
+
}, []),
|
|
1564
|
+
stderr: "",
|
|
1565
|
+
exitCode: 0
|
|
1566
|
+
};
|
|
1567
|
+
return {
|
|
1568
|
+
stdout: "",
|
|
1569
|
+
stderr: `Imported ${count} cell(s) -> ${outPath}`,
|
|
1570
|
+
exitCode: 0
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
if (json) return {
|
|
1574
|
+
stdout: jsonEnvelope("import", true, {
|
|
1575
|
+
content,
|
|
1576
|
+
cells: count
|
|
1577
|
+
}, []),
|
|
1578
|
+
stderr: "",
|
|
1579
|
+
exitCode: 0
|
|
1580
|
+
};
|
|
1581
|
+
return {
|
|
1582
|
+
stdout: content,
|
|
1583
|
+
stderr: "",
|
|
1584
|
+
exitCode: 0
|
|
1585
|
+
};
|
|
1540
1586
|
}
|
|
1541
1587
|
function positionals(args) {
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1588
|
+
const out = [];
|
|
1589
|
+
for (let i = 0; i < args.length; i++) {
|
|
1590
|
+
const a = args[i];
|
|
1591
|
+
if (VALUE_FLAGS.has(a)) {
|
|
1592
|
+
i++;
|
|
1593
|
+
continue;
|
|
1594
|
+
}
|
|
1595
|
+
if (!a.startsWith("-")) out.push(a);
|
|
1596
|
+
}
|
|
1597
|
+
return out;
|
|
1552
1598
|
}
|
|
1553
1599
|
function parseDependsOn(args) {
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
...new Set(
|
|
1558
|
-
value.split(",").map((s) => s.trim()).filter(Boolean)
|
|
1559
|
-
)
|
|
1560
|
-
];
|
|
1600
|
+
const value = flagValue(args, "--depends-on");
|
|
1601
|
+
if (value === void 0) return void 0;
|
|
1602
|
+
return [...new Set(value.split(",").map((s) => s.trim()).filter(Boolean))];
|
|
1561
1603
|
}
|
|
1562
1604
|
function parsePosition(args) {
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
return void 0;
|
|
1605
|
+
const before = flagValue(args, "--before");
|
|
1606
|
+
if (before !== void 0) return { before };
|
|
1607
|
+
const after = flagValue(args, "--after");
|
|
1608
|
+
if (after !== void 0) return { after };
|
|
1609
|
+
const at = flagValue(args, "--at");
|
|
1610
|
+
if (at !== void 0) {
|
|
1611
|
+
const n = Number(at);
|
|
1612
|
+
if (!Number.isInteger(n)) throw new Error(`--at must be an integer (got '${at}')`);
|
|
1613
|
+
return { at: n };
|
|
1614
|
+
}
|
|
1574
1615
|
}
|
|
1575
1616
|
function resolveContent(args) {
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1617
|
+
const inline = flagValue(args, "--content");
|
|
1618
|
+
const fromFile = flagValue(args, "--content-file");
|
|
1619
|
+
if (inline !== void 0 && fromFile !== void 0) return { error: "Use only one of --content / --content-file" };
|
|
1620
|
+
if (inline !== void 0) return { content: inline };
|
|
1621
|
+
if (fromFile !== void 0) {
|
|
1622
|
+
if (fromFile === "-") {
|
|
1623
|
+
if (process.stdin.isTTY) return { error: "No piped input for --content-file - (pipe content via stdin, or use --content)" };
|
|
1624
|
+
try {
|
|
1625
|
+
return { content: readFileSync(0, "utf-8") };
|
|
1626
|
+
} catch (error) {
|
|
1627
|
+
return { error: `Cannot read stdin: ${errMessage(error)}` };
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
const r = readFile(fromFile);
|
|
1631
|
+
return r.error ? { error: r.error } : { content: r.content };
|
|
1632
|
+
}
|
|
1633
|
+
return {};
|
|
1634
|
+
}
|
|
1635
|
+
const CELL_VERBS = [
|
|
1636
|
+
"add",
|
|
1637
|
+
"edit",
|
|
1638
|
+
"rm",
|
|
1639
|
+
"move",
|
|
1640
|
+
"rename"
|
|
1641
|
+
];
|
|
1601
1642
|
function cellCommand(args) {
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1643
|
+
const json = args.includes("--json");
|
|
1644
|
+
const fail = (problems) => json ? {
|
|
1645
|
+
stdout: jsonEnvelope("cell", false, null, problems),
|
|
1646
|
+
stderr: "",
|
|
1647
|
+
exitCode: 1
|
|
1648
|
+
} : {
|
|
1649
|
+
stdout: "",
|
|
1650
|
+
stderr: problems.join("\n"),
|
|
1651
|
+
exitCode: 1
|
|
1652
|
+
};
|
|
1653
|
+
const verb = args[0];
|
|
1654
|
+
if (!verb || !CELL_VERBS.includes(verb)) return fail([`Usage: mtsw cell <${CELL_VERBS.join("|")}> <file> ...`]);
|
|
1655
|
+
const vargs = args.slice(1);
|
|
1656
|
+
const pos = positionals(vargs);
|
|
1657
|
+
const file = pos[0];
|
|
1658
|
+
if (!file) return fail([`Usage: mtsw cell ${verb} <file> ...`]);
|
|
1659
|
+
const read = readFile(file);
|
|
1660
|
+
if (read.error) return fail([read.error]);
|
|
1661
|
+
const parsed = parseWorkbook(read.content);
|
|
1662
|
+
if (!parsed.success) return fail(parsed.errors ?? ["Parse error"]);
|
|
1663
|
+
const wb = parsed.workbook;
|
|
1664
|
+
let result;
|
|
1665
|
+
let changedCells = [];
|
|
1666
|
+
try {
|
|
1667
|
+
switch (verb) {
|
|
1668
|
+
case "add": {
|
|
1669
|
+
const id = flagValue(vargs, "--id");
|
|
1670
|
+
const type = flagValue(vargs, "--type") ?? flagValue(vargs, "-t");
|
|
1671
|
+
if (!id) return fail(["cell add requires --id"]);
|
|
1672
|
+
if (!type) return fail(["cell add requires --type"]);
|
|
1673
|
+
const content = resolveContent(vargs);
|
|
1674
|
+
if (content.error) return fail([content.error]);
|
|
1675
|
+
result = addCell(wb, {
|
|
1676
|
+
id,
|
|
1677
|
+
type,
|
|
1678
|
+
content: content.content,
|
|
1679
|
+
dependsOn: parseDependsOn(vargs)
|
|
1680
|
+
}, parsePosition(vargs));
|
|
1681
|
+
break;
|
|
1682
|
+
}
|
|
1683
|
+
case "edit": {
|
|
1684
|
+
const id = pos[1];
|
|
1685
|
+
if (!id) return fail(["cell edit requires <id>"]);
|
|
1686
|
+
const content = resolveContent(vargs);
|
|
1687
|
+
if (content.error) return fail([content.error]);
|
|
1688
|
+
const type = flagValue(vargs, "--type") ?? flagValue(vargs, "-t");
|
|
1689
|
+
const deps = parseDependsOn(vargs);
|
|
1690
|
+
const changes = {};
|
|
1691
|
+
if (content.content !== void 0) changes.content = content.content;
|
|
1692
|
+
if (type !== void 0) changes.type = type;
|
|
1693
|
+
if (deps !== void 0) changes.dependsOn = deps;
|
|
1694
|
+
result = editCell(wb, id, changes);
|
|
1695
|
+
break;
|
|
1696
|
+
}
|
|
1697
|
+
case "rm": {
|
|
1698
|
+
const id = pos[1];
|
|
1699
|
+
if (!id) return fail(["cell rm requires <id>"]);
|
|
1700
|
+
const removed = removeCell(wb, id, { force: vargs.includes("--force") });
|
|
1701
|
+
result = removed.workbook;
|
|
1702
|
+
changedCells = removed.changedCells;
|
|
1703
|
+
break;
|
|
1704
|
+
}
|
|
1705
|
+
case "move": {
|
|
1706
|
+
const id = pos[1];
|
|
1707
|
+
if (!id) return fail(["cell move requires <id>"]);
|
|
1708
|
+
const position = parsePosition(vargs);
|
|
1709
|
+
if (!position) return fail(["cell move requires --before/--after/--at"]);
|
|
1710
|
+
result = moveCell(wb, id, position);
|
|
1711
|
+
break;
|
|
1712
|
+
}
|
|
1713
|
+
case "rename": {
|
|
1714
|
+
const oldId = pos[1];
|
|
1715
|
+
const newId = pos[2];
|
|
1716
|
+
if (!oldId || !newId) return fail(["cell rename requires <oldId> <newId>"]);
|
|
1717
|
+
result = renameCell(wb, oldId, newId);
|
|
1718
|
+
break;
|
|
1719
|
+
}
|
|
1720
|
+
default: return fail([`Unknown cell verb: ${verb}`]);
|
|
1721
|
+
}
|
|
1722
|
+
} catch (error) {
|
|
1723
|
+
return fail([errMessage(error)]);
|
|
1724
|
+
}
|
|
1725
|
+
const after = serializeWorkbook(result);
|
|
1726
|
+
const changed = after !== serializeWorkbook(wb);
|
|
1727
|
+
if (vargs.includes("--dry-run")) {
|
|
1728
|
+
if (json) return {
|
|
1729
|
+
stdout: jsonEnvelope("cell", true, {
|
|
1730
|
+
dryRun: true,
|
|
1731
|
+
changed,
|
|
1732
|
+
content: after,
|
|
1733
|
+
...changedCells.length ? { changedCells } : {}
|
|
1734
|
+
}, []),
|
|
1735
|
+
stderr: "",
|
|
1736
|
+
exitCode: 0
|
|
1737
|
+
};
|
|
1738
|
+
return {
|
|
1739
|
+
stdout: after,
|
|
1740
|
+
stderr: "",
|
|
1741
|
+
exitCode: 0
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
if (changed) try {
|
|
1745
|
+
writeFileAtomic(file, after);
|
|
1746
|
+
} catch (error) {
|
|
1747
|
+
return fail([`Failed to write '${file}': ${errMessage(error)}`]);
|
|
1748
|
+
}
|
|
1749
|
+
if (json) return {
|
|
1750
|
+
stdout: jsonEnvelope("cell", true, {
|
|
1751
|
+
changed,
|
|
1752
|
+
...changedCells.length ? { changedCells } : {},
|
|
1753
|
+
...describeData(result)
|
|
1754
|
+
}, []),
|
|
1755
|
+
stderr: "",
|
|
1756
|
+
exitCode: 0
|
|
1757
|
+
};
|
|
1758
|
+
return {
|
|
1759
|
+
stdout: "",
|
|
1760
|
+
stderr: (changed ? `Updated ${file}` : `No change (${file})`) + (changedCells.length ? `\nDetached from: ${changedCells.join(", ")}` : ""),
|
|
1761
|
+
exitCode: 0
|
|
1762
|
+
};
|
|
1716
1763
|
}
|
|
1717
1764
|
async function dispatch(argv) {
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
stderr: "",
|
|
1765
|
-
exitCode: 1
|
|
1766
|
-
};
|
|
1767
|
-
}
|
|
1768
|
-
return { stdout: "", stderr: `Internal error: ${message}`, exitCode: 1 };
|
|
1769
|
-
}
|
|
1765
|
+
if (argv.length === 0 || argv.includes("-h") || argv.includes("--help")) return {
|
|
1766
|
+
stdout: HELP,
|
|
1767
|
+
stderr: "",
|
|
1768
|
+
exitCode: 0
|
|
1769
|
+
};
|
|
1770
|
+
if (argv.includes("-V") || argv.includes("--version")) return {
|
|
1771
|
+
stdout: `mtsw version ${VERSION}`,
|
|
1772
|
+
stderr: "",
|
|
1773
|
+
exitCode: 0
|
|
1774
|
+
};
|
|
1775
|
+
const [command, ...rest] = argv;
|
|
1776
|
+
try {
|
|
1777
|
+
switch (command) {
|
|
1778
|
+
case "run": return await runCommand(rest);
|
|
1779
|
+
case "validate": return validateCommand(rest);
|
|
1780
|
+
case "graph": return graphCommand(rest);
|
|
1781
|
+
case "describe": return describeCommand(rest);
|
|
1782
|
+
case "strip": return stripCommand(rest);
|
|
1783
|
+
case "new": return newCommand(rest);
|
|
1784
|
+
case "capabilities": return capabilitiesCommand(rest);
|
|
1785
|
+
case "templates": return templatesCommand(rest);
|
|
1786
|
+
case "cell": return cellCommand(rest);
|
|
1787
|
+
case "functions": return functionsCommand(rest);
|
|
1788
|
+
case "meta": return metaCommand(rest);
|
|
1789
|
+
case "import": return importCommand(rest);
|
|
1790
|
+
case "export": return exportCommand(rest);
|
|
1791
|
+
case "serve": return serveCommand();
|
|
1792
|
+
default: return {
|
|
1793
|
+
stdout: "",
|
|
1794
|
+
stderr: `Unknown command: ${command}\n${HELP}`,
|
|
1795
|
+
exitCode: 1
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
} catch (error) {
|
|
1799
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1800
|
+
if (rest.includes("--json")) return {
|
|
1801
|
+
stdout: jsonEnvelope(command, false, null, [`Internal error: ${message}`]),
|
|
1802
|
+
stderr: "",
|
|
1803
|
+
exitCode: 1
|
|
1804
|
+
};
|
|
1805
|
+
return {
|
|
1806
|
+
stdout: "",
|
|
1807
|
+
stderr: `Internal error: ${message}`,
|
|
1808
|
+
exitCode: 1
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1770
1811
|
}
|
|
1771
1812
|
async function main() {
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
`);
|
|
1775
|
-
|
|
1776
|
-
`);
|
|
1777
|
-
process.exit(result.exitCode);
|
|
1813
|
+
const result = await dispatch(process.argv.slice(2));
|
|
1814
|
+
if (result.stdout) process.stdout.write(`${result.stdout}\n`);
|
|
1815
|
+
if (result.stderr) process.stderr.write(`${result.stderr}\n`);
|
|
1816
|
+
process.exit(result.exitCode);
|
|
1778
1817
|
}
|
|
1779
1818
|
function isEntryPoint() {
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
}
|
|
1788
|
-
if (isEntryPoint())
|
|
1789
|
-
|
|
1790
|
-
}
|
|
1791
|
-
export {
|
|
1792
|
-
capabilitiesCommand,
|
|
1793
|
-
cellCommand,
|
|
1794
|
-
describeCommand,
|
|
1795
|
-
dispatch,
|
|
1796
|
-
exportCommand,
|
|
1797
|
-
functionsCommand,
|
|
1798
|
-
graphCommand,
|
|
1799
|
-
importCommand,
|
|
1800
|
-
metaCommand,
|
|
1801
|
-
newCommand,
|
|
1802
|
-
runCommand,
|
|
1803
|
-
runServer,
|
|
1804
|
-
serveCommand,
|
|
1805
|
-
stripCommand,
|
|
1806
|
-
templatesCommand,
|
|
1807
|
-
validateCommand
|
|
1808
|
-
};
|
|
1819
|
+
const entry = process.argv[1];
|
|
1820
|
+
if (!entry) return false;
|
|
1821
|
+
try {
|
|
1822
|
+
return import.meta.url === pathToFileURL(realpathSync(entry)).href;
|
|
1823
|
+
} catch {
|
|
1824
|
+
return import.meta.url === pathToFileURL(entry).href;
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
if (isEntryPoint()) main();
|
|
1828
|
+
//#endregion
|
|
1829
|
+
export { capabilitiesCommand, cellCommand, describeCommand, dispatch, exportCommand, functionsCommand, graphCommand, importCommand, metaCommand, newCommand, runCommand, runServer, serveCommand, stripCommand, templatesCommand, validateCommand };
|