@tlockney/cdp-kernel 0.6.0 → 0.7.1
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/README.md +5 -3
- package/dist/browser/channels.d.ts +27 -0
- package/dist/browser/channels.js +90 -0
- package/dist/browser/kernel.js +29800 -0
- package/dist/browser/kernel.js.map +7 -0
- package/dist/browser/mod.d.ts +25 -0
- package/dist/browser/mod.js +41 -0
- package/dist/browser/target.d.ts +94 -0
- package/dist/browser/target.js +264 -0
- package/dist/browser/worker.js +1071 -0
- package/dist/browser/worker.js.map +7 -0
- package/dist/bun-target.d.ts +2 -0
- package/dist/bun-target.js +15 -2
- package/dist/cdp.d.ts +11 -0
- package/dist/cdp.js +34 -7
- package/dist/celld-target.js +3 -2
- package/dist/debugger.d.ts +2 -0
- package/dist/debugger.js +63 -11
- package/dist/kernel-core.d.ts +78 -0
- package/dist/kernel-core.js +824 -0
- package/dist/kernel.d.ts +6 -1
- package/dist/kernel.js +104 -723
- package/dist/main.js +11 -4
- package/dist/message.d.ts +34 -0
- package/dist/message.js +14 -0
- package/dist/package.d.ts +2 -2
- package/dist/package.js +2 -2
- package/dist/target-api.d.ts +92 -0
- package/dist/target-api.js +48 -0
- package/dist/target.d.ts +5 -76
- package/dist/target.js +62 -33
- package/dist/traceback.d.ts +75 -0
- package/dist/traceback.js +200 -0
- package/dist/transform.d.ts +1 -0
- package/dist/transform.js +8 -1
- package/dist/wire.d.ts +4 -24
- package/dist/wire.js +3 -12
- package/dist/workerd-target.d.ts +6 -1
- package/dist/workerd-target.js +56 -12
- package/dist/workers-project.d.ts +1 -1
- package/dist/workers-project.js +4 -3
- package/dist/zmq-channels.d.ts +14 -0
- package/dist/zmq-channels.js +59 -0
- package/package.json +9 -2
- package/runtime/bootstrap.mjs +2 -0
- package/runtime/browser-worker.mjs +202 -0
- package/runtime/cdn.d.mts +2 -0
- package/runtime/cdn.mjs +17 -0
- package/runtime/display.mjs +202 -1
- package/runtime/evaluator.mjs +2 -0
- package/runtime/format.d.mts +2 -0
- package/runtime/format.mjs +125 -0
- package/runtime/workerd-worker.mjs +2 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// The driver for the browser target: a module Web Worker that runs cells.
|
|
2
|
+
//
|
|
3
|
+
// The page's kernel (src/browser/target.ts) sends each request as a
|
|
4
|
+
// message and gets a reply message back, as over the workerd driver's
|
|
5
|
+
// WebSocket (runtime/workerd-worker.mjs), whose shape this follows. Cells
|
|
6
|
+
// come from transformCellForEval and run as global code with indirect
|
|
7
|
+
// eval, so their declarations persist from cell to cell.
|
|
8
|
+
//
|
|
9
|
+
// A Worker has no module resolution beyond URLs, so `npm:` and `jsr:`
|
|
10
|
+
// specifiers, and bare package names, load from a CDN (esm.sh by default).
|
|
11
|
+
//
|
|
12
|
+
// The page can also offer cells its own features (#384): `host` requests go
|
|
13
|
+
// to the page's handler, and `preload` modules run before the first cell.
|
|
14
|
+
|
|
15
|
+
import { createJupyter } from "./display.mjs";
|
|
16
|
+
import { createHandler } from "./evaluator.mjs";
|
|
17
|
+
import { format, inspect } from "./format.mjs";
|
|
18
|
+
import { describeValue, inspectPath, resolvePath } from "./introspect.mjs";
|
|
19
|
+
import { DEFAULT_CDN, resolveSpecifier } from "./cdn.mjs";
|
|
20
|
+
|
|
21
|
+
function base64(bytes) {
|
|
22
|
+
let binary = "";
|
|
23
|
+
for (let i = 0; i < bytes.length; i += 0x8000) {
|
|
24
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
|
25
|
+
}
|
|
26
|
+
return btoa(binary);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const post = (message) => globalThis.postMessage(message);
|
|
30
|
+
|
|
31
|
+
// --- Output -----------------------------------------------------------------
|
|
32
|
+
const stream = (name) => (...args) => post({ kind: "stream", name, text: format(...args) + "\n" });
|
|
33
|
+
Object.assign(console, {
|
|
34
|
+
log: stream("stdout"),
|
|
35
|
+
info: stream("stdout"),
|
|
36
|
+
debug: stream("stdout"),
|
|
37
|
+
dir: (value) => post({ kind: "stream", name: "stdout", text: inspect(value, { depth: 4 }) + "\n" }),
|
|
38
|
+
warn: stream("stderr"),
|
|
39
|
+
error: stream("stderr"),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// An error thrown later by a cell's timer or promise goes to stderr, as on
|
|
43
|
+
// the other targets.
|
|
44
|
+
function reportUncaught(kind, err) {
|
|
45
|
+
post({ kind: "stream", name: "stderr", text: `${kind}: ${err instanceof Error ? (err.stack ?? err.message) : inspect(err)}\n` });
|
|
46
|
+
}
|
|
47
|
+
globalThis.addEventListener("unhandledrejection", (event) => {
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
reportUncaught("Uncaught (in promise)", event.reason);
|
|
50
|
+
});
|
|
51
|
+
globalThis.addEventListener("error", (event) => {
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
reportUncaught("Uncaught", event.error ?? event.message);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const jupyter = createJupyter({
|
|
57
|
+
inspect: (value) => inspect(value, { depth: 4 }),
|
|
58
|
+
base64,
|
|
59
|
+
readFile() {
|
|
60
|
+
throw new Error("the browser target has no file system to read from; pass the image bytes instead");
|
|
61
|
+
},
|
|
62
|
+
send(msgType, content, metadata, buffers) {
|
|
63
|
+
post({ kind: "iopub", msg_type: msgType, content, metadata, buffers: buffers.map(base64), barrier: 0 });
|
|
64
|
+
},
|
|
65
|
+
// A synchronous prompt would need SharedArrayBuffer and Atomics.wait, so
|
|
66
|
+
// a cross-origin isolated page; until then, as with no stdin.
|
|
67
|
+
input: () => null,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// --- Modules ----------------------------------------------------------------
|
|
71
|
+
let cdn = DEFAULT_CDN;
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
// --- The host page (#384) -----------------------------------------------------
|
|
75
|
+
// Requests to the page's `host` handler (src/browser/target.ts), and events
|
|
76
|
+
// it sends. Preload modules get this object; cells only through them.
|
|
77
|
+
const hostCalls = new Map();
|
|
78
|
+
const hostListeners = new Set();
|
|
79
|
+
let nextHostId = 0;
|
|
80
|
+
|
|
81
|
+
function hostError(error) {
|
|
82
|
+
const err = new Error(String(error?.message ?? "the host request failed"));
|
|
83
|
+
if (typeof error?.name === "string") err.name = error.name;
|
|
84
|
+
if (error?.code !== undefined) err.code = error.code;
|
|
85
|
+
return err;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const host = {
|
|
89
|
+
/** Resolves with the page's answer; aborting `signal` cancels the call on the page too. */
|
|
90
|
+
request(method, params, { signal, transfer } = {}) {
|
|
91
|
+
if (signal?.aborted) return Promise.reject(signal.reason);
|
|
92
|
+
const id = ++nextHostId;
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const onAbort = () => {
|
|
95
|
+
hostCalls.delete(id);
|
|
96
|
+
post({ kind: "host", id, cancel: true });
|
|
97
|
+
reject(signal.reason);
|
|
98
|
+
};
|
|
99
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
100
|
+
const done = () => signal?.removeEventListener("abort", onAbort);
|
|
101
|
+
try {
|
|
102
|
+
globalThis.postMessage({ kind: "host", id, method, params }, transfer ?? []);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
done(); // params structured clone can't copy
|
|
105
|
+
throw err;
|
|
106
|
+
}
|
|
107
|
+
hostCalls.set(id, { resolve, reject, done });
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
/** Calls `listener(event, value)` for each event the page sends; returns a function that stops it. */
|
|
111
|
+
onEvent(listener) {
|
|
112
|
+
hostListeners.add(listener);
|
|
113
|
+
return () => hostListeners.delete(listener);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
function receiveHost(msg) {
|
|
118
|
+
if (msg.id === undefined) {
|
|
119
|
+
for (const listener of hostListeners) {
|
|
120
|
+
try {
|
|
121
|
+
listener(msg.event, msg.value);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
reportUncaught("Uncaught", err);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const call = hostCalls.get(msg.id);
|
|
129
|
+
if (!call) return; // cancelled
|
|
130
|
+
hostCalls.delete(msg.id);
|
|
131
|
+
call.done();
|
|
132
|
+
if (msg.error !== undefined) call.reject(hostError(msg.error));
|
|
133
|
+
else call.resolve(msg.result);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Each module is imported in order, and a default export that is a function
|
|
137
|
+
// is called with the host. A module that fails is reported, and the kernel
|
|
138
|
+
// runs without it.
|
|
139
|
+
async function preload(urls, base) {
|
|
140
|
+
for (const specifier of Array.isArray(urls) ? urls : []) {
|
|
141
|
+
try {
|
|
142
|
+
const mod = await import(new URL(specifier, base).href);
|
|
143
|
+
if (typeof mod.default === "function") await mod.default({ host, jupyter: jupyter.namespace });
|
|
144
|
+
} catch (err) {
|
|
145
|
+
reportUncaught(`Preloading ${specifier} failed`, err);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// --- The shim cells and the evaluator use -----------------------------------
|
|
151
|
+
let handle = null;
|
|
152
|
+
|
|
153
|
+
function install({ base, cdn: cdnBase }) {
|
|
154
|
+
if (typeof cdnBase === "string") cdn = cdnBase.endsWith("/") ? cdnBase : cdnBase + "/";
|
|
155
|
+
const url = new URL("$cdpk$repl.mjs", base).href;
|
|
156
|
+
globalThis.__cdpk = {
|
|
157
|
+
import: (specifier, options) => import(resolveSpecifier(specifier, url, cdn), options),
|
|
158
|
+
meta: { url, resolve: (specifier) => resolveSpecifier(specifier, url, cdn) },
|
|
159
|
+
// inspect_request (#10): by descriptor, so nothing runs.
|
|
160
|
+
describeForInspect: (value, detail) => describeValue(value, { inspect: (v) => inspect(v, { depth: 2 }), detail }),
|
|
161
|
+
inspectPath: (path, detail) => inspectPath(path, { inspect: (v) => inspect(v, { depth: 2 }), detail }),
|
|
162
|
+
resolvePath,
|
|
163
|
+
barrier: () => 0,
|
|
164
|
+
broadcastResult: jupyter.broadcastResult,
|
|
165
|
+
// execute_request's user_expressions (#340).
|
|
166
|
+
formatJson: async (value) => JSON.stringify(await jupyter.namespace.format(value)),
|
|
167
|
+
async commDispatch(msgType, content, metadata, buffers64) {
|
|
168
|
+
const buffers = buffers64.map((b) => Uint8Array.from(atob(b), (c) => c.charCodeAt(0)));
|
|
169
|
+
return await jupyter.commDispatch(msgType, content, metadata, buffers);
|
|
170
|
+
},
|
|
171
|
+
describeError(value) {
|
|
172
|
+
if (value instanceof Error) {
|
|
173
|
+
return JSON.stringify({ name: value.name, message: value.message, stack: value.stack ?? "" });
|
|
174
|
+
}
|
|
175
|
+
return JSON.stringify({ name: "Error", message: inspect(value), stack: "" });
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
Object.defineProperty(globalThis, "__cdpk", { enumerable: false });
|
|
179
|
+
globalThis.jupyter = jupyter.namespace;
|
|
180
|
+
globalThis.prompt = jupyter.prompt;
|
|
181
|
+
globalThis.confirm = jupyter.confirm;
|
|
182
|
+
handle = createHandler(globalThis.__cdpk, (code) => (0, eval)(code));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function answer(req) {
|
|
186
|
+
if (req.op === "info") {
|
|
187
|
+
install(req);
|
|
188
|
+
await preload(req.preload, req.base);
|
|
189
|
+
return { userAgent: globalThis.navigator?.userAgent ?? "" };
|
|
190
|
+
}
|
|
191
|
+
if (handle === null) throw new Error("the driver wasn't sent info first");
|
|
192
|
+
return await handle(req);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
globalThis.addEventListener("message", (event) => {
|
|
196
|
+
const req = event.data;
|
|
197
|
+
if (req?.op === "host") return receiveHost(req);
|
|
198
|
+
answer(req).then(
|
|
199
|
+
(reply) => post({ kind: "reply", id: req.id, ...reply }),
|
|
200
|
+
(err) => post({ kind: "reply", id: req.id, failure: String(err?.stack ?? err) }),
|
|
201
|
+
);
|
|
202
|
+
});
|
package/runtime/cdn.mjs
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Where a browser cell's imports load from. A Worker resolves only URLs, so
|
|
2
|
+
// packages come from a CDN that serves npm and jsr as ES modules: esm.sh by
|
|
3
|
+
// default, with its jsr/ prefix for jsr packages.
|
|
4
|
+
//
|
|
5
|
+
// No imports: this file must load in every target.
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_CDN = "https://esm.sh/";
|
|
8
|
+
|
|
9
|
+
/** The URL `specifier`, imported from `base`, loads from. */
|
|
10
|
+
export function resolveSpecifier(specifier, base, cdn = DEFAULT_CDN) {
|
|
11
|
+
if (/^(?:https?|data|blob):/.test(specifier)) return specifier;
|
|
12
|
+
if (/^\.{0,2}\//.test(specifier)) return new URL(specifier, base).href;
|
|
13
|
+
if (specifier.startsWith("npm:")) return cdn + specifier.slice(4);
|
|
14
|
+
if (specifier.startsWith("jsr:")) return `${cdn}jsr/${specifier.slice(4).replace(/^\//, "")}`;
|
|
15
|
+
// esm.sh serves polyfills for Node's builtins under node:.
|
|
16
|
+
return cdn + specifier;
|
|
17
|
+
}
|
package/runtime/display.mjs
CHANGED
|
@@ -45,7 +45,201 @@ function extractVega(obj) {
|
|
|
45
45
|
} else if (spec.$schema === "https://vega.github.io/schema/vega-lite/v5.json") {
|
|
46
46
|
mediaType = "application/vnd.vegalite.v5+json";
|
|
47
47
|
}
|
|
48
|
-
|
|
48
|
+
const summary = summarizeChart(spec);
|
|
49
|
+
return summary === undefined ? { [mediaType]: spec } : { [mediaType]: spec, "text/plain": summary };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// A few lines of text/plain next to a chart, for frontends that can't draw
|
|
53
|
+
// it (jupyter console, nbconvert to text, agents reading the notebook):
|
|
54
|
+
// mark, encodings with value ranges, title and row count (#321). The
|
|
55
|
+
// approach follows nteract's repr-llm summarize_viz (BSD-3).
|
|
56
|
+
const VL_TYPES = { Q: "quantitative", N: "nominal", O: "ordinal", T: "temporal", G: "geojson" };
|
|
57
|
+
const MAX_CHILDREN = 6;
|
|
58
|
+
|
|
59
|
+
function clip(str, max = 60) {
|
|
60
|
+
return str.length > max ? `${str.slice(0, max - 1)}…` : str;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function formatNumber(n) {
|
|
64
|
+
return Number.isInteger(n) ? String(n) : String(Number(n.toPrecision(4)));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function titleText(title) {
|
|
68
|
+
const text = typeof title === "object" && title !== null && !Array.isArray(title) ? title.text : title;
|
|
69
|
+
const str = Array.isArray(text) ? text.join(" ") : typeof text === "string" ? text : "";
|
|
70
|
+
return str ? ` "${clip(str, 80)}"` : "";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getField(row, field) {
|
|
74
|
+
if (row === null || typeof row !== "object") return undefined;
|
|
75
|
+
if (field in row) return row[field];
|
|
76
|
+
// Vega-Lite reads "a.b" as a nested path and "a\.b" as a key with a dot.
|
|
77
|
+
if (field.includes("\\")) return row[field.replace(/\\(.)/g, "$1")];
|
|
78
|
+
return field.split(".").reduce((obj, key) => (obj === null || typeof obj !== "object" ? undefined : obj[key]), row);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function fieldStats(rows, field, type) {
|
|
82
|
+
const values = [];
|
|
83
|
+
for (const row of rows) {
|
|
84
|
+
const value = getField(row, field);
|
|
85
|
+
if (value !== undefined && value !== null && value !== "") values.push(value);
|
|
86
|
+
}
|
|
87
|
+
if (values.length === 0) return "";
|
|
88
|
+
if (type === "quantitative") {
|
|
89
|
+
let min = Infinity;
|
|
90
|
+
let max = -Infinity;
|
|
91
|
+
for (const value of values) {
|
|
92
|
+
const n = Number(value);
|
|
93
|
+
if (!Number.isFinite(n)) continue;
|
|
94
|
+
if (n < min) min = n;
|
|
95
|
+
if (n > max) max = n;
|
|
96
|
+
}
|
|
97
|
+
if (min > max) return "";
|
|
98
|
+
return min === max ? formatNumber(min) : `${formatNumber(min)} to ${formatNumber(max)}`;
|
|
99
|
+
}
|
|
100
|
+
if (type === "temporal") {
|
|
101
|
+
let min;
|
|
102
|
+
let max;
|
|
103
|
+
for (const value of values) {
|
|
104
|
+
const t = value instanceof Date ? value.getTime() : typeof value === "number" ? value : Date.parse(String(value));
|
|
105
|
+
if (!Number.isFinite(t)) continue;
|
|
106
|
+
if (min === undefined || t < min.t) min = { t, value };
|
|
107
|
+
if (max === undefined || t > max.t) max = { t, value };
|
|
108
|
+
}
|
|
109
|
+
if (min === undefined) return "";
|
|
110
|
+
const show = ({ value }) =>
|
|
111
|
+
typeof value === "string" ? clip(value, 30) : new Date(value instanceof Date ? value.getTime() : value).toISOString();
|
|
112
|
+
return min.t === max.t ? show(min) : `${show(min)} to ${show(max)}`;
|
|
113
|
+
}
|
|
114
|
+
const distinct = new Set(values.map((value) => typeof value === "object" ? JSON.stringify(value) : String(value)));
|
|
115
|
+
return `${distinct.size} ${distinct.size === 1 ? "value" : "values"}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function encodingSummary(encoding, rows) {
|
|
119
|
+
const parts = [];
|
|
120
|
+
for (const [channel, def] of Object.entries(encoding)) {
|
|
121
|
+
if (channel === "tooltip" || def === null || typeof def !== "object" || Array.isArray(def)) continue;
|
|
122
|
+
if (def.field === undefined && def.aggregate === undefined) continue;
|
|
123
|
+
let name = typeof def.field === "string" ? def.field : def.field?.repeat ? `repeat ${def.field.repeat}` : "";
|
|
124
|
+
if (def.aggregate !== undefined) name = `${def.aggregate}(${name})`;
|
|
125
|
+
else if (def.timeUnit !== undefined) name = `${typeof def.timeUnit === "string" ? def.timeUnit : "time"}(${name})`;
|
|
126
|
+
const type = VL_TYPES[def.type] ?? def.type;
|
|
127
|
+
const details = [];
|
|
128
|
+
if (typeof type === "string") details.push(type);
|
|
129
|
+
if (rows && typeof def.field === "string" && def.aggregate === undefined) {
|
|
130
|
+
const stats = fieldStats(rows, def.field, type);
|
|
131
|
+
if (stats) details.push(stats);
|
|
132
|
+
}
|
|
133
|
+
parts.push(`${channel}=${clip(name, 40)}${details.length ? ` (${details.join(", ")})` : ""}`);
|
|
134
|
+
}
|
|
135
|
+
return parts;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function vlRows(data, datasets) {
|
|
139
|
+
if (data === null || typeof data !== "object") return undefined;
|
|
140
|
+
if (Array.isArray(data.values)) return data.values;
|
|
141
|
+
if (typeof data.name === "string" && Array.isArray(datasets?.[data.name])) return datasets[data.name];
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function vlDataNote(data, rows) {
|
|
146
|
+
if (rows) return `${rows.length} ${rows.length === 1 ? "row" : "rows"}`;
|
|
147
|
+
if (typeof data?.url === "string") return `data from ${clip(data.url)}`;
|
|
148
|
+
return "";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function fieldList(def) {
|
|
152
|
+
if (Array.isArray(def)) return def.join(", ");
|
|
153
|
+
if (def === null || typeof def !== "object") return "";
|
|
154
|
+
if (typeof def.field === "string") return def.field;
|
|
155
|
+
return Object.values(def).map(fieldList).filter(Boolean).join(", ");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function summarizeVegaLiteView(spec, label, inherited, datasets, depth, lines) {
|
|
159
|
+
const indent = " ".repeat(depth);
|
|
160
|
+
const data = spec.data ?? inherited.data;
|
|
161
|
+
const rows = vlRows(data, datasets);
|
|
162
|
+
const head = `${indent}${label}`;
|
|
163
|
+
const title = titleText(spec.title);
|
|
164
|
+
const children = spec.layer ?? spec.concat ?? spec.hconcat ?? spec.vconcat;
|
|
165
|
+
if (Array.isArray(children)) {
|
|
166
|
+
const noun = spec.layer ? "layer" : "view";
|
|
167
|
+
const note = [`${children.length} ${children.length === 1 ? noun : `${noun}s`}`, vlDataNote(data, rows)].filter(Boolean);
|
|
168
|
+
lines.push(`${head}${spec.layer ? "layered" : "concatenated"} chart${title}: ${note.join(", ")}`);
|
|
169
|
+
const encoding = spec.layer ? { ...inherited.encoding, ...spec.encoding } : {};
|
|
170
|
+
children.slice(0, MAX_CHILDREN).forEach((child, i) => {
|
|
171
|
+
if (child !== null && typeof child === "object") {
|
|
172
|
+
summarizeVegaLiteView(child, `${noun} ${i + 1}: `, { data, encoding }, datasets, depth + 1, lines);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
if (children.length > MAX_CHILDREN) lines.push(`${indent} … ${children.length - MAX_CHILDREN} more`);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if ((spec.facet !== undefined || spec.repeat !== undefined) && spec.spec !== null && typeof spec.spec === "object") {
|
|
179
|
+
const what = spec.facet !== undefined ? `faceted by ${fieldList(spec.facet)}` : `repeated over ${fieldList(spec.repeat)}`;
|
|
180
|
+
const note = vlDataNote(data, rows);
|
|
181
|
+
lines.push(`${head}chart${title} ${what}${note ? `, ${note}` : ""}`);
|
|
182
|
+
summarizeVegaLiteView(spec.spec, "each panel: ", { data, encoding: {} }, datasets, depth + 1, lines);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const mark = typeof spec.mark === "string" ? spec.mark : spec.mark?.type;
|
|
186
|
+
const encoding = { ...inherited.encoding, ...spec.encoding };
|
|
187
|
+
const parts = encodingSummary(encoding, rows);
|
|
188
|
+
// A layer's rows were already counted on the layered chart's line.
|
|
189
|
+
if (spec.data !== undefined || depth === 0) {
|
|
190
|
+
const note = vlDataNote(data, rows);
|
|
191
|
+
if (note) parts.push(note);
|
|
192
|
+
}
|
|
193
|
+
lines.push(`${head}${typeof mark === "string" ? `${mark} ` : ""}chart${title}${parts.length ? `: ${parts.join(", ")}` : ""}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function vegaMarkTypes(marks, types) {
|
|
197
|
+
if (!Array.isArray(marks)) return types;
|
|
198
|
+
for (const mark of marks) {
|
|
199
|
+
if (typeof mark?.type === "string") types.add(mark.type);
|
|
200
|
+
vegaMarkTypes(mark?.marks, types);
|
|
201
|
+
}
|
|
202
|
+
return types;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function summarizeVega(spec) {
|
|
206
|
+
const sections = [];
|
|
207
|
+
const marks = [...vegaMarkTypes(spec.marks, new Set())];
|
|
208
|
+
if (marks.length) sections.push(`marks ${marks.join(", ")}`);
|
|
209
|
+
const list = (items, describe) => {
|
|
210
|
+
const shown = items.slice(0, MAX_CHILDREN).map(describe);
|
|
211
|
+
if (items.length > MAX_CHILDREN) shown.push(`… ${items.length - MAX_CHILDREN} more`);
|
|
212
|
+
return shown.join(", ");
|
|
213
|
+
};
|
|
214
|
+
const data = Array.isArray(spec.data) ? spec.data.filter((d) => d !== null && typeof d === "object") : [];
|
|
215
|
+
if (data.length) {
|
|
216
|
+
sections.push(`data ${list(data, (d) => {
|
|
217
|
+
const name = clip(String(d.name ?? "?"), 30);
|
|
218
|
+
if (Array.isArray(d.values)) return `${name} (${d.values.length} ${d.values.length === 1 ? "row" : "rows"})`;
|
|
219
|
+
if (typeof d.url === "string") return `${name} (from ${clip(d.url)})`;
|
|
220
|
+
if (d.source !== undefined) return `${name} (from ${clip(String(d.source), 30)})`;
|
|
221
|
+
return name;
|
|
222
|
+
})}`);
|
|
223
|
+
}
|
|
224
|
+
const scales = Array.isArray(spec.scales) ? spec.scales.filter((s) => s !== null && typeof s === "object") : [];
|
|
225
|
+
if (scales.length) {
|
|
226
|
+
sections.push(`scales ${list(scales, (s) => `${clip(String(s.name ?? "?"), 30)} (${s.type ?? "linear"})`)}`);
|
|
227
|
+
}
|
|
228
|
+
return `Vega chart${titleText(spec.title)}${sections.length ? `: ${sections.join("; ")}` : ""}`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function summarizeChart(spec) {
|
|
232
|
+
try {
|
|
233
|
+
if (typeof spec.$schema === "string" && spec.$schema.includes("/vega-lite/")) {
|
|
234
|
+
const lines = [];
|
|
235
|
+
summarizeVegaLiteView(spec, "Vega-Lite ", { encoding: {} }, spec.datasets, 0, lines);
|
|
236
|
+
return lines.join("\n");
|
|
237
|
+
}
|
|
238
|
+
return summarizeVega(spec);
|
|
239
|
+
} catch {
|
|
240
|
+
// A summary is a fallback; a spec it doesn't understand still renders.
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
49
243
|
}
|
|
50
244
|
|
|
51
245
|
function isDataFrameLike(obj) {
|
|
@@ -95,9 +289,16 @@ function extractDataFrame(df) {
|
|
|
95
289
|
html += "</tr>";
|
|
96
290
|
}
|
|
97
291
|
html += "</tbody></table>";
|
|
292
|
+
// Polars has `height`; the row count is left out when there isn't one.
|
|
293
|
+
const size = [];
|
|
294
|
+
if (Number.isInteger(df.height)) size.push(`${df.height} ${df.height === 1 ? "row" : "rows"}`);
|
|
295
|
+
size.push(`${fields.length} ${fields.length === 1 ? "column" : "columns"}`);
|
|
296
|
+
const columns = fields.slice(0, 20).map((field) => `${clip(String(field.name), 40)} (${field.type})`);
|
|
297
|
+
if (fields.length > 20) columns.push(`… ${fields.length - 20} more`);
|
|
98
298
|
return {
|
|
99
299
|
"application/vnd.dataresource+json": { data, schema: { fields } },
|
|
100
300
|
"text/html": html,
|
|
301
|
+
"text/plain": `DataFrame (${size.join(", ")})${columns.length ? `: ${columns.join(", ")}` : ""}`,
|
|
101
302
|
};
|
|
102
303
|
}
|
|
103
304
|
|
package/runtime/evaluator.mjs
CHANGED
|
@@ -56,6 +56,8 @@ export function createHandler(shim, evaluate = (code) => (0, eval)(code)) {
|
|
|
56
56
|
case "broadcast":
|
|
57
57
|
await shim.broadcastResult(request.count, take(request.handle));
|
|
58
58
|
return {};
|
|
59
|
+
case "format":
|
|
60
|
+
return { value: await shim.formatJson(request.handle === undefined ? undefined : take(request.handle)) };
|
|
59
61
|
case "describe":
|
|
60
62
|
return { value: shim.describeError(take(request.handle)) };
|
|
61
63
|
case "comm":
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// A small stand-in for util.inspect and util.format, for targets that have
|
|
2
|
+
// neither: the browser Worker (runtime/browser-worker.mjs). Output follows
|
|
3
|
+
// Node's for the common cases (strings quoted when nested, objects and
|
|
4
|
+
// arrays on one line, Map, Set, Date, Error, class names, circular
|
|
5
|
+
// references), without colours.
|
|
6
|
+
//
|
|
7
|
+
// No imports: this file must load in every target.
|
|
8
|
+
|
|
9
|
+
const MAX_ITEMS = 100;
|
|
10
|
+
|
|
11
|
+
// Single quotes, unless the string has some and double quotes or
|
|
12
|
+
// backticks would need no escapes, as util.inspect picks.
|
|
13
|
+
function quote(s) {
|
|
14
|
+
const q = !s.includes("'") ? "'" : !s.includes('"') ? '"' : !s.includes("`") && !s.includes("${") ? "`" : "'";
|
|
15
|
+
const escaped = s.replaceAll("\\", "\\\\").replaceAll("\n", "\\n").replaceAll(q, `\\${q}`);
|
|
16
|
+
return `${q}${escaped}${q}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function key(k) {
|
|
20
|
+
return typeof k === "symbol" ? `[${k.toString()}]` : /^[A-Za-z_$][\w$]*$/.test(k) ? k : quote(k);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function className(value) {
|
|
24
|
+
const proto = Object.getPrototypeOf(value);
|
|
25
|
+
if (proto === null) return "[Object: null prototype]";
|
|
26
|
+
const name = proto.constructor?.name;
|
|
27
|
+
return name && name !== "Object" ? name : "";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function list(open, items, close, more) {
|
|
31
|
+
if (more > 0) items.push(`... ${more} more item${more === 1 ? "" : "s"}`);
|
|
32
|
+
return items.length === 0 ? `${open}${close}` : `${open} ${items.join(", ")} ${close}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** `value` as Node's util.inspect would show it, to `depth` levels. */
|
|
36
|
+
export function inspect(value, { depth = 2 } = {}) {
|
|
37
|
+
const seen = [];
|
|
38
|
+
const show = (v, level) => {
|
|
39
|
+
switch (typeof v) {
|
|
40
|
+
case "string":
|
|
41
|
+
return quote(v);
|
|
42
|
+
case "bigint":
|
|
43
|
+
return `${v}n`;
|
|
44
|
+
case "symbol":
|
|
45
|
+
return v.toString();
|
|
46
|
+
case "undefined":
|
|
47
|
+
return "undefined";
|
|
48
|
+
case "function": {
|
|
49
|
+
const isClass = /^class\b/.test(Function.prototype.toString.call(v));
|
|
50
|
+
return isClass ? `[class ${v.name || "(anonymous)"}]` : `[Function${v.name ? `: ${v.name}` : " (anonymous)"}]`;
|
|
51
|
+
}
|
|
52
|
+
case "number":
|
|
53
|
+
return Object.is(v, -0) ? "-0" : String(v);
|
|
54
|
+
case "boolean":
|
|
55
|
+
return String(v);
|
|
56
|
+
}
|
|
57
|
+
if (v === null) return "null";
|
|
58
|
+
if (seen.includes(v)) return "[Circular *1]";
|
|
59
|
+
if (v instanceof Date) return Number.isNaN(v.getTime()) ? "Invalid Date" : v.toISOString();
|
|
60
|
+
if (v instanceof RegExp) return String(v);
|
|
61
|
+
if (v instanceof Error) return v.stack ?? `${v.name}: ${v.message}`;
|
|
62
|
+
if (v instanceof Promise) return "Promise { <unknown> }";
|
|
63
|
+
if (level > depth) return Array.isArray(v) ? "[Array]" : `[${className(v) || "Object"}]`;
|
|
64
|
+
seen.push(v);
|
|
65
|
+
try {
|
|
66
|
+
if (Array.isArray(v) || ArrayBuffer.isView(v)) {
|
|
67
|
+
const items = Array.from(v).slice(0, MAX_ITEMS).map((x) => show(x, level + 1));
|
|
68
|
+
const prefix = Array.isArray(v) ? "" : `${v.constructor.name}(${v.length}) `;
|
|
69
|
+
return prefix + list("[", items, "]", v.length - items.length);
|
|
70
|
+
}
|
|
71
|
+
if (v instanceof Map) {
|
|
72
|
+
const items = [...v].slice(0, MAX_ITEMS).map(([k, x]) => `${show(k, level + 1)} => ${show(x, level + 1)}`);
|
|
73
|
+
return `Map(${v.size}) ` + list("{", items, "}", v.size - items.length);
|
|
74
|
+
}
|
|
75
|
+
if (v instanceof Set) {
|
|
76
|
+
const items = [...v].slice(0, MAX_ITEMS).map((x) => show(x, level + 1));
|
|
77
|
+
return `Set(${v.size}) ` + list("{", items, "}", v.size - items.length);
|
|
78
|
+
}
|
|
79
|
+
const keys = Reflect.ownKeys(v).filter((k) => Object.getOwnPropertyDescriptor(v, k)?.enumerable);
|
|
80
|
+
const items = keys.slice(0, MAX_ITEMS).map((k) => {
|
|
81
|
+
const d = Object.getOwnPropertyDescriptor(v, k);
|
|
82
|
+
const shown = d.get || d.set ? `[${d.get ? "Getter" : ""}${d.get && d.set ? "/" : ""}${d.set ? "Setter" : ""}]` : show(d.value, level + 1);
|
|
83
|
+
return `${key(k)}: ${shown}`;
|
|
84
|
+
});
|
|
85
|
+
const name = className(v);
|
|
86
|
+
return (name ? `${name} ` : "") + list("{", items, "}", keys.length - items.length);
|
|
87
|
+
} finally {
|
|
88
|
+
seen.pop();
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
return show(value, 0);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** console.log's formatting: strings as they are, everything else inspected, %s/%d/%i/%f/%o/%O/%j substituted. */
|
|
95
|
+
export function format(...args) {
|
|
96
|
+
let out = [];
|
|
97
|
+
let rest = args;
|
|
98
|
+
if (typeof args[0] === "string" && args.length > 1 && args[0].includes("%")) {
|
|
99
|
+
let i = 1;
|
|
100
|
+
const text = args[0].replace(/%[sdifoOjc%]/g, (spec) => {
|
|
101
|
+
if (spec === "%%") return "%";
|
|
102
|
+
if (i >= args.length) return spec;
|
|
103
|
+
const a = args[i++];
|
|
104
|
+
switch (spec) {
|
|
105
|
+
case "%s":
|
|
106
|
+
return typeof a === "string" ? a : inspect(a, { depth: 1 });
|
|
107
|
+
case "%d":
|
|
108
|
+
case "%i":
|
|
109
|
+
return typeof a === "bigint" ? `${a}n` : String(spec === "%i" ? Math.trunc(Number(a)) : Number(a));
|
|
110
|
+
case "%f":
|
|
111
|
+
return String(Number(a));
|
|
112
|
+
case "%j":
|
|
113
|
+
return JSON.stringify(a);
|
|
114
|
+
case "%c":
|
|
115
|
+
return "";
|
|
116
|
+
default:
|
|
117
|
+
return inspect(a, { depth: 4 });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
out = [text];
|
|
121
|
+
rest = args.slice(i);
|
|
122
|
+
}
|
|
123
|
+
for (const a of rest) out.push(typeof a === "string" ? a : inspect(a));
|
|
124
|
+
return out.join(" ");
|
|
125
|
+
}
|
|
@@ -141,6 +141,8 @@ function install(env) {
|
|
|
141
141
|
resolvePath,
|
|
142
142
|
barrier: () => 0,
|
|
143
143
|
broadcastResult: jupyter.broadcastResult,
|
|
144
|
+
// execute_request's user_expressions (#340).
|
|
145
|
+
formatJson: async (value) => JSON.stringify(await jupyter.namespace.format(value)),
|
|
144
146
|
async commDispatch(msgType, content, metadata, buffers64) {
|
|
145
147
|
const buffers = buffers64.map((b) => Uint8Array.from(atob(b), (c) => c.charCodeAt(0)));
|
|
146
148
|
return await jupyter.commDispatch(msgType, content, metadata, buffers);
|