@marimo-team/marimo-export 0.0.0 → 0.0.2
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/LICENSE +202 -0
- package/README.md +113 -2
- package/dist/export-6118xT3K.mjs +823 -0
- package/dist/index-Ca3yEJUD.d.mts +15 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.mjs +4 -0
- package/dist/loader/anywidget.d.mts +76 -0
- package/dist/loader/anywidget.mjs +1561 -0
- package/dist/loader/arrow.d.mts +10 -0
- package/dist/loader/arrow.mjs +38 -0
- package/dist/loader/html.d.mts +6 -0
- package/dist/loader/html.mjs +10 -0
- package/dist/loader/json.d.mts +6 -0
- package/dist/loader/json.mjs +15 -0
- package/dist/loader/marimo-cell.d.mts +7 -0
- package/dist/loader/marimo-cell.mjs +16 -0
- package/dist/loader/marimo-output.d.mts +7 -0
- package/dist/loader/marimo-output.mjs +16 -0
- package/dist/loader/numpy.d.mts +19 -0
- package/dist/loader/numpy.mjs +168 -0
- package/dist/loader/parquet.d.mts +14 -0
- package/dist/loader/parquet.mjs +43 -0
- package/dist/loader/text.d.mts +6 -0
- package/dist/loader/text.mjs +10 -0
- package/dist/loader/vegalite.d.mts +18 -0
- package/dist/loader/vegalite.mjs +98 -0
- package/dist/loader-CDfnJ5Yb.mjs +441 -0
- package/dist/marimo-snapshot--nNjqcq_.mjs +280 -0
- package/dist/marimo-snapshot-Df66Xan0.d.mts +74 -0
- package/dist/prepared/index.d.mts +116 -0
- package/dist/prepared/index.mjs +703 -0
- package/dist/schema-SJ_aIgcO.mjs +713 -0
- package/dist/types-D52GHn1Q.d.mts +201 -0
- package/dist/utf8-DI-jI2Nm.mjs +17 -0
- package/package.json +112 -5
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
import { _ as portableJsonValue, c as isNotebookExportError, d as isJsonNumber, f as isJsonString, g as portableJsonObject, m as isRecordValue, o as parseMediaType, s as NotebookExportError, u as isJsonBoolean, v as MAX_JSON_VALUES } from "./loader-CDfnJ5Yb.mjs";
|
|
2
|
+
//#region ../portable-json/src/parse.ts
|
|
3
|
+
const MAX_JSON_NUMBER_LEXEME_BYTES = 1024;
|
|
4
|
+
/** Parse one JSON value while rejecting duplicate object keys at every depth. */
|
|
5
|
+
function parseStrictJson(text, maximumValues) {
|
|
6
|
+
const scanner = new JsonScanner(text, jsonValueLimit(maximumValues));
|
|
7
|
+
scanner.value(0);
|
|
8
|
+
scanner.whitespace();
|
|
9
|
+
if (!scanner.done) scanner.fail("Unexpected content after the JSON value");
|
|
10
|
+
return JSON.parse(text);
|
|
11
|
+
}
|
|
12
|
+
/** Parse strict JSON and return a detached portable value. */
|
|
13
|
+
function parsePortableJson(text) {
|
|
14
|
+
return portableJsonValue(parseStrictJson(text, MAX_JSON_VALUES));
|
|
15
|
+
}
|
|
16
|
+
var JsonScanner = class {
|
|
17
|
+
#maximumValues;
|
|
18
|
+
#text;
|
|
19
|
+
#offset = 0;
|
|
20
|
+
#values = 0;
|
|
21
|
+
constructor(text, maximumValues) {
|
|
22
|
+
this.#text = text;
|
|
23
|
+
this.#maximumValues = maximumValues;
|
|
24
|
+
}
|
|
25
|
+
get done() {
|
|
26
|
+
return this.#offset === this.#text.length;
|
|
27
|
+
}
|
|
28
|
+
whitespace() {
|
|
29
|
+
while (this.#offset < this.#text.length) {
|
|
30
|
+
const code = this.#text.charCodeAt(this.#offset);
|
|
31
|
+
if (code !== 32 && code !== 9 && code !== 10 && code !== 13) return;
|
|
32
|
+
this.#offset += 1;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
value(depth) {
|
|
36
|
+
this.countValue();
|
|
37
|
+
if (depth > 256) this.fail("JSON exceeds the maximum nesting depth");
|
|
38
|
+
this.whitespace();
|
|
39
|
+
const token = this.#text[this.#offset];
|
|
40
|
+
if (token === "{") {
|
|
41
|
+
this.object(depth);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (token === "[") {
|
|
45
|
+
this.array(depth);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (token === "\"") {
|
|
49
|
+
this.string(false);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (token === "t") {
|
|
53
|
+
this.literal("true");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (token === "f") {
|
|
57
|
+
this.literal("false");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (token === "n") {
|
|
61
|
+
this.literal("null");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.number();
|
|
65
|
+
}
|
|
66
|
+
object(depth) {
|
|
67
|
+
this.#offset += 1;
|
|
68
|
+
this.whitespace();
|
|
69
|
+
if (this.consume("}")) return;
|
|
70
|
+
const keys = /* @__PURE__ */ new Set();
|
|
71
|
+
while (true) {
|
|
72
|
+
this.whitespace();
|
|
73
|
+
if (this.#text[this.#offset] !== "\"") this.fail("Object keys must be strings");
|
|
74
|
+
this.countValue();
|
|
75
|
+
const key = this.string(true);
|
|
76
|
+
if (key === void 0) this.fail("Object keys must be strings");
|
|
77
|
+
if (keys.has(key)) this.fail(`Duplicate object key ${quoteJsonDiagnostic(key)}`);
|
|
78
|
+
keys.add(key);
|
|
79
|
+
this.whitespace();
|
|
80
|
+
if (!this.consume(":")) this.fail("Expected ':' after an object key");
|
|
81
|
+
this.value(depth + 1);
|
|
82
|
+
this.whitespace();
|
|
83
|
+
if (this.consume("}")) return;
|
|
84
|
+
if (!this.consume(",")) this.fail("Expected ',' or '}' in an object");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
array(depth) {
|
|
88
|
+
this.#offset += 1;
|
|
89
|
+
this.whitespace();
|
|
90
|
+
if (this.consume("]")) return;
|
|
91
|
+
while (true) {
|
|
92
|
+
this.value(depth + 1);
|
|
93
|
+
this.whitespace();
|
|
94
|
+
if (this.consume("]")) return;
|
|
95
|
+
if (!this.consume(",")) this.fail("Expected ',' or ']' in an array");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
string(decode) {
|
|
99
|
+
const start = this.#offset;
|
|
100
|
+
this.#offset += 1;
|
|
101
|
+
while (this.#offset < this.#text.length) {
|
|
102
|
+
const code = this.#text.charCodeAt(this.#offset);
|
|
103
|
+
if (code === 34) {
|
|
104
|
+
this.#offset += 1;
|
|
105
|
+
if (!decode) return void 0;
|
|
106
|
+
return JSON.parse(this.#text.slice(start, this.#offset));
|
|
107
|
+
}
|
|
108
|
+
if (code < 32) this.fail("Unescaped control character in a string");
|
|
109
|
+
if (code === 92) {
|
|
110
|
+
this.#offset += 1;
|
|
111
|
+
const escape = this.#text[this.#offset];
|
|
112
|
+
if (escape === "u") {
|
|
113
|
+
const digits = this.#text.slice(this.#offset + 1, this.#offset + 5);
|
|
114
|
+
if (!/^[0-9a-fA-F]{4}$/.test(digits)) this.fail("Invalid Unicode escape");
|
|
115
|
+
this.#offset += 5;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (escape === void 0 || !"\"\\/bfnrt".includes(escape)) this.fail("Invalid string escape");
|
|
119
|
+
}
|
|
120
|
+
this.#offset += 1;
|
|
121
|
+
}
|
|
122
|
+
this.fail("Unterminated string");
|
|
123
|
+
}
|
|
124
|
+
number() {
|
|
125
|
+
const start = this.#offset;
|
|
126
|
+
if (this.#text[this.#offset] === "-") this.#advanceNumber(start);
|
|
127
|
+
const first = this.#text[this.#offset];
|
|
128
|
+
if (first === "0") this.#advanceNumber(start);
|
|
129
|
+
else if (first !== void 0 && first >= "1" && first <= "9") do
|
|
130
|
+
this.#advanceNumber(start);
|
|
131
|
+
while (isDigit(this.#text[this.#offset]));
|
|
132
|
+
else this.fail("Expected a JSON value");
|
|
133
|
+
if (this.#text[this.#offset] === ".") {
|
|
134
|
+
this.#advanceNumber(start);
|
|
135
|
+
if (!isDigit(this.#text[this.#offset])) this.fail("Expected a digit after the decimal point");
|
|
136
|
+
do
|
|
137
|
+
this.#advanceNumber(start);
|
|
138
|
+
while (isDigit(this.#text[this.#offset]));
|
|
139
|
+
}
|
|
140
|
+
if (this.#text[this.#offset] === "e" || this.#text[this.#offset] === "E") {
|
|
141
|
+
this.#advanceNumber(start);
|
|
142
|
+
if (this.#text[this.#offset] === "+" || this.#text[this.#offset] === "-") this.#advanceNumber(start);
|
|
143
|
+
if (!isDigit(this.#text[this.#offset])) this.fail("Expected an exponent digit");
|
|
144
|
+
do
|
|
145
|
+
this.#advanceNumber(start);
|
|
146
|
+
while (isDigit(this.#text[this.#offset]));
|
|
147
|
+
}
|
|
148
|
+
const value = this.#text.slice(start, this.#offset);
|
|
149
|
+
if (value.includes(".") || /[eE]/u.test(value)) {
|
|
150
|
+
const converted = Number(value);
|
|
151
|
+
if (!decimalLexemeIsIntegral(value) && Number.isInteger(converted)) this.fail("JSON number loses its fractional component as a JavaScript number");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
#advanceNumber(start) {
|
|
155
|
+
this.#offset += 1;
|
|
156
|
+
if (this.#offset - start > MAX_JSON_NUMBER_LEXEME_BYTES) this.fail("JSON number exceeds the maximum lexeme length");
|
|
157
|
+
}
|
|
158
|
+
literal(value) {
|
|
159
|
+
if (!this.#text.startsWith(value, this.#offset)) this.fail(`Expected ${value}`);
|
|
160
|
+
this.#offset += value.length;
|
|
161
|
+
}
|
|
162
|
+
consume(value) {
|
|
163
|
+
if (this.#text[this.#offset] !== value) return false;
|
|
164
|
+
this.#offset += 1;
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
countValue() {
|
|
168
|
+
this.#values += 1;
|
|
169
|
+
if (this.#values > this.#maximumValues) this.fail("JSON exceeds the maximum value count");
|
|
170
|
+
}
|
|
171
|
+
fail(message) {
|
|
172
|
+
throw new SyntaxError(`${message} at character ${this.#offset}.`);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
function decimalLexemeIsIntegral(value) {
|
|
176
|
+
let unsigned = value[0] === "-" ? value.slice(1) : value;
|
|
177
|
+
let exponent = 0;
|
|
178
|
+
const exponentIndex = unsigned.search(/[eE]/u);
|
|
179
|
+
if (exponentIndex >= 0) {
|
|
180
|
+
exponent = Number(unsigned.slice(exponentIndex + 1));
|
|
181
|
+
unsigned = unsigned.slice(0, exponentIndex);
|
|
182
|
+
}
|
|
183
|
+
const decimalIndex = unsigned.indexOf(".");
|
|
184
|
+
const fractionDigits = decimalIndex < 0 ? 0 : unsigned.length - decimalIndex - 1;
|
|
185
|
+
const digits = decimalIndex < 0 ? unsigned : `${unsigned.slice(0, decimalIndex)}${unsigned.slice(decimalIndex + 1)}`;
|
|
186
|
+
if (/^0+$/u.test(digits)) return true;
|
|
187
|
+
const trailingZeros = countTrailingZeros(digits);
|
|
188
|
+
return exponent - fractionDigits + trailingZeros >= 0;
|
|
189
|
+
}
|
|
190
|
+
function isDigit(value) {
|
|
191
|
+
return value !== void 0 && value >= "0" && value <= "9";
|
|
192
|
+
}
|
|
193
|
+
function countTrailingZeros(value) {
|
|
194
|
+
let count = 0;
|
|
195
|
+
for (let index = value.length - 1; index >= 0 && value[index] === "0"; index -= 1) count += 1;
|
|
196
|
+
return count;
|
|
197
|
+
}
|
|
198
|
+
function quoteJsonDiagnostic(value) {
|
|
199
|
+
const bodyLimit = 251;
|
|
200
|
+
let body = "";
|
|
201
|
+
let truncated = false;
|
|
202
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
203
|
+
const codeUnit = value.charCodeAt(index);
|
|
204
|
+
let token;
|
|
205
|
+
if (codeUnit === 34 || codeUnit === 92) token = `\\${value[index]}`;
|
|
206
|
+
else if (codeUnit <= 31 || codeUnit >= 127 && codeUnit <= 159 || codeUnit >= 55296 && codeUnit <= 57343) {
|
|
207
|
+
if (codeUnit >= 55296 && codeUnit <= 56319 && index + 1 < value.length && value.charCodeAt(index + 1) >= 56320 && value.charCodeAt(index + 1) <= 57343) {
|
|
208
|
+
token = value.slice(index, index + 2);
|
|
209
|
+
index += 1;
|
|
210
|
+
} else token = `\\u${codeUnit.toString(16).padStart(4, "0")}`;
|
|
211
|
+
} else token = value[index];
|
|
212
|
+
if (body.length + token.length > bodyLimit) {
|
|
213
|
+
truncated = true;
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
body += token;
|
|
217
|
+
}
|
|
218
|
+
return `"${body}${truncated ? "..." : ""}"`;
|
|
219
|
+
}
|
|
220
|
+
function jsonValueLimit(input) {
|
|
221
|
+
if (input === void 0) return MAX_JSON_VALUES;
|
|
222
|
+
if (!Number.isSafeInteger(input) || input <= 0) throw new TypeError("JSON value limits must be positive safe integers.");
|
|
223
|
+
return input;
|
|
224
|
+
}
|
|
225
|
+
//#endregion
|
|
226
|
+
//#region src/schema.ts
|
|
227
|
+
const encoder = new TextEncoder();
|
|
228
|
+
const SHA256 = /^[0-9a-f]{64}$/u;
|
|
229
|
+
const BIGINT = /^(?:0|-[1-9][0-9]*|[1-9][0-9]*)$/u;
|
|
230
|
+
const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER;
|
|
231
|
+
const MAX_EXPORT_NAME_BYTES = 255;
|
|
232
|
+
const MAX_CONTROL_ID_BYTES = 1024;
|
|
233
|
+
const MAX_CONTROL_PATH_STEPS = 256;
|
|
234
|
+
const MAX_PROVENANCE_BYTES = 2048;
|
|
235
|
+
const MAX_ASSET_SIZE = 2147483647;
|
|
236
|
+
const MAX_METADATA_BYTES = 262144;
|
|
237
|
+
const WINDOWS_RESERVED = /[<>:"/\\|?*]/u;
|
|
238
|
+
const WINDOWS_DEVICE = /^(?:CON|CONIN\$|CONOUT\$|PRN|AUX|NUL|COM[1-9¹²³]|LPT[1-9¹²³])$/iu;
|
|
239
|
+
const EDGE_WHITESPACE = /* @__PURE__ */ new Set([
|
|
240
|
+
9,
|
|
241
|
+
10,
|
|
242
|
+
11,
|
|
243
|
+
12,
|
|
244
|
+
13,
|
|
245
|
+
28,
|
|
246
|
+
29,
|
|
247
|
+
30,
|
|
248
|
+
31,
|
|
249
|
+
32,
|
|
250
|
+
133,
|
|
251
|
+
160,
|
|
252
|
+
5760,
|
|
253
|
+
8192,
|
|
254
|
+
8193,
|
|
255
|
+
8194,
|
|
256
|
+
8195,
|
|
257
|
+
8196,
|
|
258
|
+
8197,
|
|
259
|
+
8198,
|
|
260
|
+
8199,
|
|
261
|
+
8200,
|
|
262
|
+
8201,
|
|
263
|
+
8202,
|
|
264
|
+
8232,
|
|
265
|
+
8233,
|
|
266
|
+
8239,
|
|
267
|
+
8287,
|
|
268
|
+
12288,
|
|
269
|
+
65279
|
|
270
|
+
]);
|
|
271
|
+
function parseExportIndex(input) {
|
|
272
|
+
try {
|
|
273
|
+
const root = strictRecord(portableJsonObject(input, "export"), "export", [
|
|
274
|
+
"aliases",
|
|
275
|
+
"control_bindings",
|
|
276
|
+
"default_state",
|
|
277
|
+
"inputs",
|
|
278
|
+
"notebook",
|
|
279
|
+
"outputs",
|
|
280
|
+
"producer",
|
|
281
|
+
"schema",
|
|
282
|
+
"spec_sha256",
|
|
283
|
+
"states"
|
|
284
|
+
]);
|
|
285
|
+
literal(root.schema, "marimo-export.export.v1", "export.schema");
|
|
286
|
+
const specSha256 = digest(root.spec_sha256, "export.spec_sha256");
|
|
287
|
+
const defaultState = digest(root.default_state, "export.default_state");
|
|
288
|
+
const notebook = parseNotebook(root.notebook);
|
|
289
|
+
const producer = parseProducer(root.producer);
|
|
290
|
+
const inputs = opaqueNameArray(root.inputs, "export.inputs");
|
|
291
|
+
const outputs = nameArray(root.outputs, "export.outputs", true);
|
|
292
|
+
const inputSet = new Set(inputs);
|
|
293
|
+
const controlBindingRecord = record(root.control_bindings, "export.control_bindings");
|
|
294
|
+
const controlBindings = Object.freeze(Object.fromEntries(Object.entries(controlBindingRecord).map(([objectId, binding]) => {
|
|
295
|
+
const controlId = boundedPrintable(objectId, "export.control_bindings key", MAX_CONTROL_ID_BYTES);
|
|
296
|
+
return [controlId, parseControlBinding(binding, `export.control_bindings[${JSON.stringify(controlId)}]`, inputSet)];
|
|
297
|
+
})));
|
|
298
|
+
const outputSet = new Set(outputs);
|
|
299
|
+
const statesRecord = record(root.states, "export.states");
|
|
300
|
+
if (Object.keys(statesRecord).length === 0) fail("export.states must not be empty");
|
|
301
|
+
const states = Object.freeze(Object.fromEntries(Object.entries(statesRecord).map(([key, value]) => {
|
|
302
|
+
const fingerprint = digest(key, "export.states key");
|
|
303
|
+
const path = `export.states[${JSON.stringify(fingerprint)}]`;
|
|
304
|
+
const state = strictRecord(value, path, ["inputs", "outputs"]);
|
|
305
|
+
const stateInputs = portableJsonObject(state.inputs, `${path}.inputs`);
|
|
306
|
+
requireExactKeys(stateInputs, inputSet, `${path}.inputs`);
|
|
307
|
+
const outputRecord = record(state.outputs, `${path}.outputs`);
|
|
308
|
+
requireExactKeys(outputRecord, outputSet, `${path}.outputs`);
|
|
309
|
+
const parsedOutputs = Object.freeze(Object.fromEntries(Object.entries(outputRecord).map(([outputName, descriptor]) => [exportName(outputName, `${path}.outputs key`), parseDescriptor(descriptor, outputName)])));
|
|
310
|
+
return [fingerprint, Object.freeze({
|
|
311
|
+
inputs: stateInputs,
|
|
312
|
+
outputs: parsedOutputs
|
|
313
|
+
})];
|
|
314
|
+
})));
|
|
315
|
+
const aliasesRecord = record(root.aliases, "export.aliases");
|
|
316
|
+
const aliases = Object.freeze(Object.fromEntries(Object.entries(aliasesRecord).map(([name, value]) => {
|
|
317
|
+
const alias = exportName(name, "export.aliases key");
|
|
318
|
+
const fingerprint = digest(value, `export.aliases[${JSON.stringify(alias)}]`);
|
|
319
|
+
if (!Object.hasOwn(states, fingerprint)) fail(`export.aliases[${JSON.stringify(alias)}] references an unknown state fingerprint`);
|
|
320
|
+
return [alias, fingerprint];
|
|
321
|
+
})));
|
|
322
|
+
if (!Object.hasOwn(states, defaultState)) fail("export.default_state must reference a declared state fingerprint");
|
|
323
|
+
validateRepresentations(states, outputs);
|
|
324
|
+
validateAssets(states);
|
|
325
|
+
return Object.freeze({
|
|
326
|
+
specSha256,
|
|
327
|
+
defaultState,
|
|
328
|
+
notebook,
|
|
329
|
+
producer,
|
|
330
|
+
inputs,
|
|
331
|
+
controlBindings,
|
|
332
|
+
outputs,
|
|
333
|
+
aliases,
|
|
334
|
+
states
|
|
335
|
+
});
|
|
336
|
+
} catch (error) {
|
|
337
|
+
if (isNotebookExportError(error)) throw error;
|
|
338
|
+
throw new NotebookExportError("export_invalid", boundedMessage(error), { cause: error });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function parseNotebook(input) {
|
|
342
|
+
const value = strictRecord(input, "export.notebook", ["document_sha256", "filename"]);
|
|
343
|
+
const filename = value.filename === null ? null : portableBasename(value.filename, "export.notebook.filename");
|
|
344
|
+
return Object.freeze({
|
|
345
|
+
filename,
|
|
346
|
+
documentSha256: digest(value.document_sha256, "export.notebook.document_sha256")
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
function parseProducer(input) {
|
|
350
|
+
const value = strictRecord(input, "export.producer", [
|
|
351
|
+
"implementation_sha256",
|
|
352
|
+
"marimo",
|
|
353
|
+
"marimo_export"
|
|
354
|
+
]);
|
|
355
|
+
return Object.freeze({
|
|
356
|
+
implementationSha256: digest(value.implementation_sha256, "export.producer.implementation_sha256"),
|
|
357
|
+
marimo: boundedPrintable(value.marimo, "export.producer.marimo", MAX_EXPORT_NAME_BYTES),
|
|
358
|
+
marimoExport: boundedPrintable(value.marimo_export, "export.producer.marimo_export", MAX_EXPORT_NAME_BYTES)
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
function parseControlBinding(input, path, inputNames) {
|
|
362
|
+
const value = strictRecord(input, path, ["input", "path"]);
|
|
363
|
+
const inputName = opaqueInputName(value.input, `${path}.input`);
|
|
364
|
+
if (!inputNames.has(inputName)) fail(`${path}.input must name a declared input`);
|
|
365
|
+
if (!Array.isArray(value.path) || value.path.length > MAX_CONTROL_PATH_STEPS) fail(`${path}.path must be an array of at most ${MAX_CONTROL_PATH_STEPS} steps`);
|
|
366
|
+
return Object.freeze({
|
|
367
|
+
input: inputName,
|
|
368
|
+
path: Object.freeze(value.path.map((step, index) => parseControlPathStep(step, `${path}.path[${index}]`)))
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
function parseControlPathStep(input, path) {
|
|
372
|
+
const value = record(input, path);
|
|
373
|
+
if (value.kind === "element") {
|
|
374
|
+
exactFields(value, ["kind"], path);
|
|
375
|
+
return Object.freeze({ kind: "element" });
|
|
376
|
+
}
|
|
377
|
+
if (value.kind === "index") {
|
|
378
|
+
exactFields(value, ["kind", "value"], path);
|
|
379
|
+
if (!isJsonNumber(value.value) || !Number.isSafeInteger(value.value) || value.value < 0) fail(`${path}.value must be a nonnegative safe integer`);
|
|
380
|
+
return Object.freeze({
|
|
381
|
+
kind: "index",
|
|
382
|
+
value: value.value
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
if (value.kind === "key") {
|
|
386
|
+
exactFields(value, ["kind", "value"], path);
|
|
387
|
+
if (!isJsonString(value.value)) fail(`${path}.value must be a string`);
|
|
388
|
+
unicodeScalar(value.value, `${path}.value`);
|
|
389
|
+
if (encoder.encode(value.value).byteLength > MAX_CONTROL_ID_BYTES) fail(`${path}.value exceeds ${MAX_CONTROL_ID_BYTES} UTF-8 bytes`);
|
|
390
|
+
return Object.freeze({
|
|
391
|
+
kind: "key",
|
|
392
|
+
value: value.value
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
fail(`${path}.kind must be index, key, or element`);
|
|
396
|
+
}
|
|
397
|
+
function parseDescriptor(input, outputName) {
|
|
398
|
+
const value = record(input, `output ${JSON.stringify(outputName)}`);
|
|
399
|
+
const codec = value.codec;
|
|
400
|
+
if (codec === "marimo.scalar.v1") return parseScalar(value, outputName);
|
|
401
|
+
if (codec === "marimo.json.v1") return parseJson(value, outputName);
|
|
402
|
+
if (codec === "marimo.output.v1") return parseMarimoOutput(value, outputName);
|
|
403
|
+
if (codec === "marimo.cell.v1") return parseMarimoCell(value, outputName);
|
|
404
|
+
if (codec === "numpy.npy.v1") return parseNumpy(value, outputName);
|
|
405
|
+
if (codec === "apache.arrow.file.v1") return parseArrow(value, outputName);
|
|
406
|
+
if (codec === "marimo.blob-asset.msgpack.v1") return parseBlobAsset(value, outputName);
|
|
407
|
+
fail(`output ${JSON.stringify(outputName)} has an unknown codec`);
|
|
408
|
+
}
|
|
409
|
+
function parseJson(value, outputName) {
|
|
410
|
+
const path = `output ${JSON.stringify(outputName)}`;
|
|
411
|
+
exactFields(value, [
|
|
412
|
+
"codec",
|
|
413
|
+
"media_type",
|
|
414
|
+
"provenance",
|
|
415
|
+
"value"
|
|
416
|
+
], path);
|
|
417
|
+
literal(value.media_type, "application/vnd.marimo.json.v1+json", `${path}.media_type`);
|
|
418
|
+
return Object.freeze({
|
|
419
|
+
codec: "marimo.json.v1",
|
|
420
|
+
mediaType: "application/vnd.marimo.json.v1+json",
|
|
421
|
+
provenance: parseProvenance(value.provenance, path),
|
|
422
|
+
value: portableJsonValue(value.value, `${path}.value`)
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
function parseMarimoOutput(value, outputName) {
|
|
426
|
+
const path = `output ${JSON.stringify(outputName)}`;
|
|
427
|
+
exactFields(value, [
|
|
428
|
+
"asset",
|
|
429
|
+
"codec",
|
|
430
|
+
"media_type",
|
|
431
|
+
"provenance"
|
|
432
|
+
], path);
|
|
433
|
+
literal(value.media_type, "application/vnd.marimo.output.v1+json", `${path}.media_type`);
|
|
434
|
+
return Object.freeze({
|
|
435
|
+
codec: "marimo.output.v1",
|
|
436
|
+
mediaType: "application/vnd.marimo.output.v1+json",
|
|
437
|
+
provenance: parseProvenance(value.provenance, path),
|
|
438
|
+
asset: parseAsset(value.asset, path)
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
function parseMarimoCell(value, outputName) {
|
|
442
|
+
const path = `output ${JSON.stringify(outputName)}`;
|
|
443
|
+
exactFields(value, [
|
|
444
|
+
"asset",
|
|
445
|
+
"codec",
|
|
446
|
+
"media_type",
|
|
447
|
+
"provenance"
|
|
448
|
+
], path);
|
|
449
|
+
literal(value.media_type, "application/vnd.marimo.cell.v1+json", `${path}.media_type`);
|
|
450
|
+
return Object.freeze({
|
|
451
|
+
codec: "marimo.cell.v1",
|
|
452
|
+
mediaType: "application/vnd.marimo.cell.v1+json",
|
|
453
|
+
provenance: parseProvenance(value.provenance, path),
|
|
454
|
+
asset: parseAsset(value.asset, path)
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
function parseScalar(value, outputName) {
|
|
458
|
+
const path = `output ${JSON.stringify(outputName)}`;
|
|
459
|
+
exactFields(value, [
|
|
460
|
+
"codec",
|
|
461
|
+
"media_type",
|
|
462
|
+
"provenance",
|
|
463
|
+
"value"
|
|
464
|
+
], path);
|
|
465
|
+
literal(value.media_type, "application/vnd.marimo.scalar.v1+json", `${path}.media_type`);
|
|
466
|
+
const provenance = parseProvenance(value.provenance, path);
|
|
467
|
+
return Object.freeze({
|
|
468
|
+
codec: "marimo.scalar.v1",
|
|
469
|
+
mediaType: "application/vnd.marimo.scalar.v1+json",
|
|
470
|
+
provenance,
|
|
471
|
+
value: parseScalarValue(value.value)
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
function parseNumpy(value, outputName) {
|
|
475
|
+
const path = `output ${JSON.stringify(outputName)}`;
|
|
476
|
+
exactFields(value, [
|
|
477
|
+
"asset",
|
|
478
|
+
"codec",
|
|
479
|
+
"media_type",
|
|
480
|
+
"provenance"
|
|
481
|
+
], path);
|
|
482
|
+
literal(value.media_type, "application/x-npy", `${path}.media_type`);
|
|
483
|
+
return Object.freeze({
|
|
484
|
+
codec: "numpy.npy.v1",
|
|
485
|
+
mediaType: "application/x-npy",
|
|
486
|
+
provenance: parseProvenance(value.provenance, path),
|
|
487
|
+
asset: parseAsset(value.asset, path)
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
function parseArrow(value, outputName) {
|
|
491
|
+
const path = `output ${JSON.stringify(outputName)}`;
|
|
492
|
+
exactFields(value, [
|
|
493
|
+
"asset",
|
|
494
|
+
"codec",
|
|
495
|
+
"media_type",
|
|
496
|
+
"provenance"
|
|
497
|
+
], path);
|
|
498
|
+
literal(value.media_type, "application/vnd.apache.arrow.file", `${path}.media_type`);
|
|
499
|
+
return Object.freeze({
|
|
500
|
+
codec: "apache.arrow.file.v1",
|
|
501
|
+
mediaType: "application/vnd.apache.arrow.file",
|
|
502
|
+
provenance: parseProvenance(value.provenance, path),
|
|
503
|
+
asset: parseAsset(value.asset, path)
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
function parseBlobAsset(value, outputName) {
|
|
507
|
+
const path = `output ${JSON.stringify(outputName)}`;
|
|
508
|
+
exactFields(value, [
|
|
509
|
+
"asset",
|
|
510
|
+
"codec",
|
|
511
|
+
"filename",
|
|
512
|
+
"media_type",
|
|
513
|
+
"metadata",
|
|
514
|
+
"provenance"
|
|
515
|
+
], path);
|
|
516
|
+
if (!isJsonString(value.media_type)) fail(`${path}.media_type must be a string`);
|
|
517
|
+
parseMediaType(value.media_type);
|
|
518
|
+
const filename = value.filename === null ? null : portableBasename(value.filename, `${path}.filename`);
|
|
519
|
+
const metadata = portableJsonObject(value.metadata, `${path}.metadata`);
|
|
520
|
+
if (encoder.encode(canonicalJson(metadata)).byteLength > MAX_METADATA_BYTES) fail(`${path}.metadata exceeds ${MAX_METADATA_BYTES} canonical JSON bytes`);
|
|
521
|
+
return Object.freeze({
|
|
522
|
+
codec: "marimo.blob-asset.msgpack.v1",
|
|
523
|
+
mediaType: value.media_type,
|
|
524
|
+
filename,
|
|
525
|
+
metadata,
|
|
526
|
+
provenance: parseProvenance(value.provenance, path),
|
|
527
|
+
asset: parseAsset(value.asset, path)
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
function parseProvenance(input, parent) {
|
|
531
|
+
const path = `${parent}.provenance`;
|
|
532
|
+
const value = strictRecord(input, path, ["python_type"]);
|
|
533
|
+
return Object.freeze({ pythonType: boundedPrintable(value.python_type, `${path}.python_type`, MAX_PROVENANCE_BYTES) });
|
|
534
|
+
}
|
|
535
|
+
function parseAsset(input, parent) {
|
|
536
|
+
const path = `${parent}.asset`;
|
|
537
|
+
const value = strictRecord(input, path, ["sha256", "size"]);
|
|
538
|
+
if (!isJsonNumber(value.size) || !Number.isInteger(value.size) || value.size < 1 || value.size > MAX_ASSET_SIZE) fail(`${path}.size must be an integer from 1 through ${MAX_ASSET_SIZE}`);
|
|
539
|
+
return Object.freeze({
|
|
540
|
+
sha256: digest(value.sha256, `${path}.sha256`),
|
|
541
|
+
size: value.size
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
function parseScalarValue(input) {
|
|
545
|
+
if (input === null) return null;
|
|
546
|
+
if (isJsonBoolean(input) || isJsonString(input)) return input;
|
|
547
|
+
if (isJsonNumber(input)) {
|
|
548
|
+
if (Number.isInteger(input) && !Number.isSafeInteger(input)) fail("untagged scalar integer exceeds the safe integer range");
|
|
549
|
+
return input;
|
|
550
|
+
}
|
|
551
|
+
const value = strictRecord(input, "scalar.value", ["type", "value"]);
|
|
552
|
+
if (value.type === "bigint") {
|
|
553
|
+
if (!isJsonString(value.value) || !BIGINT.test(value.value)) fail("tagged bigint has an invalid decimal value");
|
|
554
|
+
const result = BigInt(value.value);
|
|
555
|
+
if (result >= -BigInt(MAX_SAFE_INTEGER) && result <= BigInt(MAX_SAFE_INTEGER)) fail("tagged bigint must lie outside the safe integer range");
|
|
556
|
+
return result;
|
|
557
|
+
}
|
|
558
|
+
if (value.type === "float") {
|
|
559
|
+
if (value.value === "nan") return NaN;
|
|
560
|
+
if (value.value === "infinity") return Number.POSITIVE_INFINITY;
|
|
561
|
+
if (value.value === "-infinity") return Number.NEGATIVE_INFINITY;
|
|
562
|
+
if (value.value === "negative-zero") return -0;
|
|
563
|
+
fail("tagged float has an invalid value");
|
|
564
|
+
}
|
|
565
|
+
fail("scalar tag type must be bigint or float");
|
|
566
|
+
}
|
|
567
|
+
function validateRepresentations(states, outputNames) {
|
|
568
|
+
const representations = /* @__PURE__ */ new Map();
|
|
569
|
+
for (const state of Object.values(states)) for (const name of outputNames) {
|
|
570
|
+
const descriptor = state.outputs[name];
|
|
571
|
+
const representation = `${descriptor.codec}\0${descriptor.mediaType}`;
|
|
572
|
+
const previous = representations.get(name);
|
|
573
|
+
if (previous !== void 0 && previous !== representation) throw new NotebookExportError("output_representation_changed", `Output ${JSON.stringify(name)} changes codec or media type across states.`, { details: { output: name } });
|
|
574
|
+
representations.set(name, representation);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
function validateAssets(states) {
|
|
578
|
+
const assets = /* @__PURE__ */ new Map();
|
|
579
|
+
let total = 0;
|
|
580
|
+
for (const state of Object.values(states)) for (const descriptor of Object.values(state.outputs)) {
|
|
581
|
+
if (descriptor.codec === "marimo.scalar.v1" || descriptor.codec === "marimo.json.v1") continue;
|
|
582
|
+
const identity = `${descriptor.codec}\0${descriptor.asset.sha256}`;
|
|
583
|
+
const facts = descriptor.codec === "marimo.blob-asset.msgpack.v1" ? `${descriptor.asset.size}\0${descriptor.mediaType}\0${descriptor.filename ?? ""}\0${canonicalJson(descriptor.metadata)}` : `${descriptor.asset.size}\0${descriptor.mediaType}`;
|
|
584
|
+
const previous = assets.get(identity);
|
|
585
|
+
if (previous !== void 0 && previous !== facts) fail(`asset ${descriptor.asset.sha256} has conflicting descriptor facts`);
|
|
586
|
+
if (previous === void 0) {
|
|
587
|
+
assets.set(identity, facts);
|
|
588
|
+
total += descriptor.asset.size;
|
|
589
|
+
if (!Number.isSafeInteger(total)) fail("aggregate unique asset size exceeds the safe range");
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
function canonicalJson(value) {
|
|
594
|
+
if (value === null || isJsonBoolean(value)) return JSON.stringify(value);
|
|
595
|
+
if (isJsonString(value)) {
|
|
596
|
+
unicodeScalar(value, "canonical JSON string");
|
|
597
|
+
return JSON.stringify(value);
|
|
598
|
+
}
|
|
599
|
+
if (isJsonNumber(value)) {
|
|
600
|
+
if (!Number.isFinite(value)) fail("canonical JSON number must be finite");
|
|
601
|
+
return JSON.stringify(Object.is(value, -0) ? 0 : value);
|
|
602
|
+
}
|
|
603
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
604
|
+
if (!isJsonObject(value)) fail("canonical JSON object must use string keys");
|
|
605
|
+
const keys = Object.keys(value);
|
|
606
|
+
keys.forEach((key) => unicodeScalar(key, "canonical JSON key"));
|
|
607
|
+
return `{${keys.sort(compareUnicodeScalarStrings).map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
|
|
608
|
+
}
|
|
609
|
+
function compareUnicodeScalarStrings(left, right) {
|
|
610
|
+
const leftPoints = left[Symbol.iterator]();
|
|
611
|
+
const rightPoints = right[Symbol.iterator]();
|
|
612
|
+
while (true) {
|
|
613
|
+
const a = leftPoints.next();
|
|
614
|
+
const b = rightPoints.next();
|
|
615
|
+
if (a.done || b.done) {
|
|
616
|
+
if (a.done && b.done) return 0;
|
|
617
|
+
return a.done ? -1 : 1;
|
|
618
|
+
}
|
|
619
|
+
const difference = a.value.codePointAt(0) - b.value.codePointAt(0);
|
|
620
|
+
if (difference !== 0) return difference;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
function strictRecord(input, path, fields) {
|
|
624
|
+
const value = record(input, path);
|
|
625
|
+
exactFields(value, fields, path);
|
|
626
|
+
return value;
|
|
627
|
+
}
|
|
628
|
+
function record(input, path) {
|
|
629
|
+
if (!isJsonObject(input)) fail(`${path} must be an object`);
|
|
630
|
+
return input;
|
|
631
|
+
}
|
|
632
|
+
function exactFields(value, fields, path) {
|
|
633
|
+
const expected = new Set(fields);
|
|
634
|
+
const actual = Object.keys(value);
|
|
635
|
+
const missing = fields.filter((field) => !Object.hasOwn(value, field));
|
|
636
|
+
const extra = actual.filter((field) => !expected.has(field));
|
|
637
|
+
if (missing.length > 0) fail(`${path} is missing fields: ${missing.slice(0, 8).join(", ")}`);
|
|
638
|
+
if (extra.length > 0) fail(`${path} has unknown fields: ${extra.slice(0, 8).join(", ")}`);
|
|
639
|
+
}
|
|
640
|
+
function requireExactKeys(value, expected, path) {
|
|
641
|
+
const keys = Object.keys(value);
|
|
642
|
+
const missing = [...expected].filter((key) => !Object.hasOwn(value, key));
|
|
643
|
+
const extra = keys.filter((key) => !expected.has(key));
|
|
644
|
+
if (missing.length > 0 || extra.length > 0) fail(`${path} must match the root name set`);
|
|
645
|
+
}
|
|
646
|
+
function nameArray(input, path, nonempty) {
|
|
647
|
+
if (!Array.isArray(input)) fail(`${path} must be an array`);
|
|
648
|
+
const values = input.map((name, index) => exportName(name, `${path}[${index}]`));
|
|
649
|
+
if (nonempty && values.length === 0) fail(`${path} must not be empty`);
|
|
650
|
+
if (new Set(values).size !== values.length) fail(`${path} must contain unique names`);
|
|
651
|
+
return Object.freeze(values);
|
|
652
|
+
}
|
|
653
|
+
function opaqueNameArray(input, path) {
|
|
654
|
+
if (!Array.isArray(input)) fail(`${path} must be an array`);
|
|
655
|
+
const values = input.map((name, index) => opaqueInputName(name, `${path}[${index}]`));
|
|
656
|
+
if (new Set(values).size !== values.length) fail(`${path} must contain unique names`);
|
|
657
|
+
return Object.freeze(values);
|
|
658
|
+
}
|
|
659
|
+
function opaqueInputName(input, path) {
|
|
660
|
+
if (!isJsonString(input)) fail(`${path} must be a string`);
|
|
661
|
+
unicodeScalar(input, path);
|
|
662
|
+
if (input.length === 0 || encoder.encode(input).byteLength > MAX_EXPORT_NAME_BYTES) fail(`${path} must be a bounded nonempty UTF-8 string`);
|
|
663
|
+
return input;
|
|
664
|
+
}
|
|
665
|
+
function exportName(input, path) {
|
|
666
|
+
return boundedPrintable(input, path, MAX_EXPORT_NAME_BYTES);
|
|
667
|
+
}
|
|
668
|
+
function boundedPrintable(input, path, maxBytes) {
|
|
669
|
+
if (!isJsonString(input)) fail(`${path} must be a string`);
|
|
670
|
+
unicodeScalar(input, path);
|
|
671
|
+
if (input.length === 0 || hasEdgeWhitespace(input) || hasControl(input) || encoder.encode(input).byteLength > maxBytes) fail(`${path} must be a bounded printable string`);
|
|
672
|
+
return input;
|
|
673
|
+
}
|
|
674
|
+
function hasEdgeWhitespace(value) {
|
|
675
|
+
return EDGE_WHITESPACE.has(value.charCodeAt(0)) || EDGE_WHITESPACE.has(value.charCodeAt(value.length - 1));
|
|
676
|
+
}
|
|
677
|
+
function portableBasename(input, path) {
|
|
678
|
+
if (!isJsonString(input)) fail(`${path} must be a string`);
|
|
679
|
+
unicodeScalar(input, path);
|
|
680
|
+
const stem = input.split(".", 1)[0].replace(/[ .]+$/u, "");
|
|
681
|
+
if (input.length === 0 || input === "." || input === ".." || hasEdgeWhitespace(input) || input.endsWith(".") || hasControl(input) || WINDOWS_RESERVED.test(input) || WINDOWS_DEVICE.test(stem) || encoder.encode(input).byteLength > MAX_EXPORT_NAME_BYTES) fail(`${path} must be a portable basename`);
|
|
682
|
+
return input;
|
|
683
|
+
}
|
|
684
|
+
function digest(input, path) {
|
|
685
|
+
if (!isJsonString(input) || !SHA256.test(input)) fail(`${path} must be a lowercase SHA-256 digest`);
|
|
686
|
+
return input;
|
|
687
|
+
}
|
|
688
|
+
function literal(input, expected, path) {
|
|
689
|
+
if (input !== expected) fail(`${path} must be ${JSON.stringify(expected)}`);
|
|
690
|
+
return expected;
|
|
691
|
+
}
|
|
692
|
+
function unicodeScalar(value, path) {
|
|
693
|
+
if (/[\ud800-\udfff]/u.test(value)) fail(`${path} must contain Unicode scalar values`);
|
|
694
|
+
}
|
|
695
|
+
function hasControl(value) {
|
|
696
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
697
|
+
const codeUnit = value.charCodeAt(index);
|
|
698
|
+
if (codeUnit < 32 || codeUnit === 127) return true;
|
|
699
|
+
}
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
702
|
+
function boundedMessage(cause) {
|
|
703
|
+
const source = cause instanceof Error ? cause.message : "Export index validation failed.";
|
|
704
|
+
return `Invalid export index: ${source.length > 2048 ? `${source.slice(0, 2045)}...` : source}`;
|
|
705
|
+
}
|
|
706
|
+
function isJsonObject(value) {
|
|
707
|
+
return isRecordValue(value);
|
|
708
|
+
}
|
|
709
|
+
function fail(message) {
|
|
710
|
+
throw new NotebookExportError("export_invalid", message.length > 2048 ? `${message.slice(0, 2045)}...` : message);
|
|
711
|
+
}
|
|
712
|
+
//#endregion
|
|
713
|
+
export { parseStrictJson as a, parsePortableJson as i, compareUnicodeScalarStrings as n, parseExportIndex as r, canonicalJson as t };
|