mirror of
https://github.com/denoland/deno.git
synced 2024-12-25 00:29:09 -05:00
feat(unstable): add Deno.jupyter.display API (#20819)
This brings in [`display`](https://github.com/rgbkrk/display.js) as part of the `Deno.jupyter` namespace. Additionally these APIs were added: - "Deno.jupyter.md" - "Deno.jupyter.html" - "Deno.jupyter.svg" - "Deno.jupyter.format" These APIs greatly extend capabilities of rendering output in Jupyter notebooks. --------- Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
This commit is contained in:
parent
c464cd7073
commit
48e695a2c8
5 changed files with 4218 additions and 148 deletions
|
@ -1,17 +1,429 @@
|
||||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @module mod
|
||||||
|
* @description
|
||||||
|
* This module provides a `display()` function for the Jupyter Deno Kernel, similar to IPython's display.
|
||||||
|
* It can be used to asynchronously display objects in Jupyter frontends. There are also tagged template functions
|
||||||
|
* for quickly creating HTML, Markdown, and SVG views.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* Displaying objects asynchronously in Jupyter frontends.
|
||||||
|
* ```typescript
|
||||||
|
* import { display, html, md } from "https://deno.land/x/deno_jupyter/mod.ts";
|
||||||
|
*
|
||||||
|
* await display(html`<h1>Hello, world!</h1>`);
|
||||||
|
* await display(md`# Notebooks in TypeScript via Deno ![Deno logo](https://github.com/denoland.png?size=32)
|
||||||
|
*
|
||||||
|
* * TypeScript ${Deno.version.typescript}
|
||||||
|
* * V8 ${Deno.version.v8}
|
||||||
|
* * Deno ${Deno.version.deno}
|
||||||
|
*
|
||||||
|
* Interactive compute with Jupyter _built into Deno_!
|
||||||
|
* `);
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* Emitting raw MIME bundles.
|
||||||
|
* ```typescript
|
||||||
|
* import { display } from "https://deno.land/x/deno_jupyter/mod.ts";
|
||||||
|
*
|
||||||
|
* await display({
|
||||||
|
* "text/plain": "Hello, world!",
|
||||||
|
* "text/html": "<h1>Hello, world!</h1>",
|
||||||
|
* "text/markdown": "# Hello, world!",
|
||||||
|
* }, { raw: true });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
const core = globalThis.Deno.core;
|
const core = globalThis.Deno.core;
|
||||||
|
|
||||||
const internals = globalThis.__bootstrap.internals;
|
const internals = globalThis.__bootstrap.internals;
|
||||||
|
|
||||||
|
const $display = Symbol.for("Jupyter.display");
|
||||||
|
|
||||||
|
/** Escape copied from https://deno.land/std@0.192.0/html/entities.ts */
|
||||||
|
const rawToEntityEntries = [
|
||||||
|
["&", "&"],
|
||||||
|
["<", "<"],
|
||||||
|
[">", ">"],
|
||||||
|
['"', """],
|
||||||
|
["'", "'"],
|
||||||
|
];
|
||||||
|
|
||||||
|
const rawToEntity = new Map(rawToEntityEntries);
|
||||||
|
|
||||||
|
const rawRe = new RegExp(`[${[...rawToEntity.keys()].join("")}]`, "g");
|
||||||
|
|
||||||
|
function escapeHTML(str) {
|
||||||
|
return str.replaceAll(
|
||||||
|
rawRe,
|
||||||
|
(m) => rawToEntity.has(m) ? rawToEntity.get(m) : m,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Duck typing our way to common visualization and tabular libraries */
|
||||||
|
/** Vegalite */
|
||||||
|
function isVegaLike(obj) {
|
||||||
|
return obj !== null && typeof obj === "object" && "toSpec" in obj;
|
||||||
|
}
|
||||||
|
function extractVega(obj) {
|
||||||
|
const spec = obj.toSpec();
|
||||||
|
if (!("$schema" in spec)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (typeof spec !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let mediaType = "application/vnd.vega.v5+json";
|
||||||
|
if (spec.$schema === "https://vega.github.io/schema/vega-lite/v4.json") {
|
||||||
|
mediaType = "application/vnd.vegalite.v4+json";
|
||||||
|
} else if (
|
||||||
|
spec.$schema === "https://vega.github.io/schema/vega-lite/v5.json"
|
||||||
|
) {
|
||||||
|
mediaType = "application/vnd.vegalite.v5+json";
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
[mediaType]: spec,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/** Polars */
|
||||||
|
function isDataFrameLike(obj) {
|
||||||
|
const isObject = obj !== null && typeof obj === "object";
|
||||||
|
if (!isObject) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const df = obj;
|
||||||
|
return df.schema !== void 0 && typeof df.schema === "object" &&
|
||||||
|
df.head !== void 0 && typeof df.head === "function" &&
|
||||||
|
df.toRecords !== void 0 && typeof df.toRecords === "function";
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Map Polars DataType to JSON Schema data types.
|
||||||
|
* @param dataType - The Polars DataType.
|
||||||
|
* @returns The corresponding JSON Schema data type.
|
||||||
|
*/
|
||||||
|
function mapPolarsTypeToJSONSchema(colType) {
|
||||||
|
const typeMapping = {
|
||||||
|
Null: "null",
|
||||||
|
Bool: "boolean",
|
||||||
|
Int8: "integer",
|
||||||
|
Int16: "integer",
|
||||||
|
Int32: "integer",
|
||||||
|
Int64: "integer",
|
||||||
|
UInt8: "integer",
|
||||||
|
UInt16: "integer",
|
||||||
|
UInt32: "integer",
|
||||||
|
UInt64: "integer",
|
||||||
|
Float32: "number",
|
||||||
|
Float64: "number",
|
||||||
|
Date: "string",
|
||||||
|
Datetime: "string",
|
||||||
|
Utf8: "string",
|
||||||
|
Categorical: "string",
|
||||||
|
List: "array",
|
||||||
|
Struct: "object",
|
||||||
|
};
|
||||||
|
// These colTypes are weird. When you console.dir or console.log them
|
||||||
|
// they show a `DataType` field, however you can't access it directly until you
|
||||||
|
// convert it to JSON
|
||||||
|
const dataType = colType.toJSON()["DataType"];
|
||||||
|
return typeMapping[dataType] || "string";
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractDataFrame(df) {
|
||||||
|
const fields = [];
|
||||||
|
const schema = {
|
||||||
|
fields,
|
||||||
|
};
|
||||||
|
let data = [];
|
||||||
|
// Convert DataFrame schema to Tabular DataResource schema
|
||||||
|
for (const [colName, colType] of Object.entries(df.schema)) {
|
||||||
|
const dataType = mapPolarsTypeToJSONSchema(colType);
|
||||||
|
schema.fields.push({
|
||||||
|
name: colName,
|
||||||
|
type: dataType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Convert DataFrame data to row-oriented JSON
|
||||||
|
//
|
||||||
|
// TODO(rgbkrk): Determine how to get the polars format max rows
|
||||||
|
// Since pl.setTblRows just sets env var POLARS_FMT_MAX_ROWS,
|
||||||
|
// we probably just have to pick a number for now.
|
||||||
|
//
|
||||||
|
|
||||||
|
data = df.head(50).toRecords();
|
||||||
|
let htmlTable = "<table>";
|
||||||
|
htmlTable += "<thead><tr>";
|
||||||
|
schema.fields.forEach((field) => {
|
||||||
|
htmlTable += `<th>${escapeHTML(String(field.name))}</th>`;
|
||||||
|
});
|
||||||
|
htmlTable += "</tr></thead>";
|
||||||
|
htmlTable += "<tbody>";
|
||||||
|
df.head(10).toRecords().forEach((row) => {
|
||||||
|
htmlTable += "<tr>";
|
||||||
|
schema.fields.forEach((field) => {
|
||||||
|
htmlTable += `<td>${escapeHTML(String(row[field.name]))}</td>`;
|
||||||
|
});
|
||||||
|
htmlTable += "</tr>";
|
||||||
|
});
|
||||||
|
htmlTable += "</tbody></table>";
|
||||||
|
return {
|
||||||
|
"application/vnd.dataresource+json": { data, schema },
|
||||||
|
"text/html": htmlTable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Canvas */
|
||||||
|
function isCanvasLike(obj) {
|
||||||
|
return obj !== null && typeof obj === "object" && "toDataURL" in obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Possible HTML and SVG Elements */
|
||||||
|
function isSVGElementLike(obj) {
|
||||||
|
return obj !== null && typeof obj === "object" && "outerHTML" in obj &&
|
||||||
|
typeof obj.outerHTML === "string" && obj.outerHTML.startsWith("<svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHTMLElementLike(obj) {
|
||||||
|
return obj !== null && typeof obj === "object" && "outerHTML" in obj &&
|
||||||
|
typeof obj.outerHTML === "string";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check to see if an object already contains a `Symbol.for("Jupyter.display") */
|
||||||
|
function hasDisplaySymbol(obj) {
|
||||||
|
return obj !== null && typeof obj === "object" && $display in obj &&
|
||||||
|
typeof obj[$display] === "function";
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeDisplayable(obj) {
|
||||||
|
return {
|
||||||
|
[$display]: () => obj,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format an object for displaying in Deno
|
||||||
|
*
|
||||||
|
* @param obj - The object to be displayed
|
||||||
|
* @returns MediaBundle
|
||||||
|
*/
|
||||||
|
async function format(obj) {
|
||||||
|
if (hasDisplaySymbol(obj)) {
|
||||||
|
return await obj[$display]();
|
||||||
|
}
|
||||||
|
if (typeof obj !== "object") {
|
||||||
|
return {
|
||||||
|
"text/plain": Deno[Deno.internal].inspectArgs(["%o", obj], {
|
||||||
|
colors: !Deno.noColor,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCanvasLike(obj)) {
|
||||||
|
const dataURL = obj.toDataURL();
|
||||||
|
const parts = dataURL.split(",");
|
||||||
|
const mime = parts[0].split(":")[1].split(";")[0];
|
||||||
|
const data = parts[1];
|
||||||
|
return {
|
||||||
|
[mime]: data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (isVegaLike(obj)) {
|
||||||
|
return extractVega(obj);
|
||||||
|
}
|
||||||
|
if (isDataFrameLike(obj)) {
|
||||||
|
return extractDataFrame(obj);
|
||||||
|
}
|
||||||
|
if (isSVGElementLike(obj)) {
|
||||||
|
return {
|
||||||
|
"image/svg+xml": obj.outerHTML,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (isHTMLElementLike(obj)) {
|
||||||
|
return {
|
||||||
|
"text/html": obj.outerHTML,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"text/plain": Deno[Deno.internal].inspectArgs(["%o", obj], {
|
||||||
|
colors: !Deno.noColor,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function creates a tagged template function for a given media type.
|
||||||
|
* The tagged template function takes a template string and returns a displayable object.
|
||||||
|
*
|
||||||
|
* @param mediatype - The media type for the tagged template function.
|
||||||
|
* @returns A function that takes a template string and returns a displayable object.
|
||||||
|
*/
|
||||||
|
function createTaggedTemplateDisplayable(mediatype) {
|
||||||
|
return (strings, ...values) => {
|
||||||
|
const payload = strings.reduce(
|
||||||
|
(acc, string, i) =>
|
||||||
|
acc + string + (values[i] !== undefined ? values[i] : ""),
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
return makeDisplayable({ [mediatype]: payload });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show Markdown in Jupyter frontends with a tagged template function.
|
||||||
|
*
|
||||||
|
* Takes a template string and returns a displayable object for Jupyter frontends.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* Create a Markdown view.
|
||||||
|
*
|
||||||
|
* ```typescript
|
||||||
|
* md`# Notebooks in TypeScript via Deno ![Deno logo](https://github.com/denoland.png?size=32)
|
||||||
|
*
|
||||||
|
* * TypeScript ${Deno.version.typescript}
|
||||||
|
* * V8 ${Deno.version.v8}
|
||||||
|
* * Deno ${Deno.version.deno}
|
||||||
|
*
|
||||||
|
* Interactive compute with Jupyter _built into Deno_!
|
||||||
|
* `
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
const md = createTaggedTemplateDisplayable("text/markdown");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show HTML in Jupyter frontends with a tagged template function.
|
||||||
|
*
|
||||||
|
* Takes a template string and returns a displayable object for Jupyter frontends.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* Create an HTML view.
|
||||||
|
* ```typescript
|
||||||
|
* html`<h1>Hello, world!</h1>`
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
const html = createTaggedTemplateDisplayable("text/html");
|
||||||
|
/**
|
||||||
|
* SVG Tagged Template Function.
|
||||||
|
*
|
||||||
|
* Takes a template string and returns a displayable object for Jupyter frontends.
|
||||||
|
*
|
||||||
|
* Example usage:
|
||||||
|
*
|
||||||
|
* svg`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||||
|
* <circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" />
|
||||||
|
* </svg>`
|
||||||
|
*/
|
||||||
|
const svg = createTaggedTemplateDisplayable("image/svg+xml");
|
||||||
|
|
||||||
|
function isMediaBundle(obj) {
|
||||||
|
if (obj == null || typeof obj !== "object" || Array.isArray(obj)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const key in obj) {
|
||||||
|
if (typeof key !== "string") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function formatInner(obj, raw) {
|
||||||
|
if (raw && isMediaBundle(obj)) {
|
||||||
|
return obj;
|
||||||
|
} else {
|
||||||
|
return await format(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internals.jupyter = { formatInner };
|
||||||
|
|
||||||
function enableJupyter() {
|
function enableJupyter() {
|
||||||
const {
|
const {
|
||||||
op_jupyter_broadcast,
|
op_jupyter_broadcast,
|
||||||
} = core.ensureFastOps();
|
} = core.ensureFastOps();
|
||||||
|
|
||||||
globalThis.Deno.jupyter = {
|
async function broadcast(
|
||||||
async broadcast(msgType, content, { metadata = {}, buffers = [] } = {}) {
|
msgType,
|
||||||
|
content,
|
||||||
|
{ metadata = {}, buffers = [] } = {},
|
||||||
|
) {
|
||||||
await op_jupyter_broadcast(msgType, content, metadata, buffers);
|
await op_jupyter_broadcast(msgType, content, metadata, buffers);
|
||||||
},
|
}
|
||||||
|
|
||||||
|
async function broadcastResult(executionCount, result) {
|
||||||
|
try {
|
||||||
|
if (result === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await format(result);
|
||||||
|
await broadcast("execute_result", {
|
||||||
|
execution_count: executionCount,
|
||||||
|
data,
|
||||||
|
metadata: {},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
const stack = err.stack || "";
|
||||||
|
await broadcast("error", {
|
||||||
|
ename: err.name,
|
||||||
|
evalue: err.message,
|
||||||
|
traceback: stack.split("\n"),
|
||||||
|
});
|
||||||
|
} else if (typeof err == "string") {
|
||||||
|
await broadcast("error", {
|
||||||
|
ename: "Error",
|
||||||
|
evalue: err,
|
||||||
|
traceback: [],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await broadcast("error", {
|
||||||
|
ename: "Error",
|
||||||
|
evalue:
|
||||||
|
"An error occurred while formatting a result, but it could not be identified",
|
||||||
|
traceback: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internals.jupyter.broadcastResult = broadcastResult;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display function for Jupyter Deno Kernel.
|
||||||
|
* Mimics the behavior of IPython's `display(obj, raw=True)` function to allow
|
||||||
|
* asynchronous displaying of objects in Jupyter.
|
||||||
|
*
|
||||||
|
* @param obj - The object to be displayed
|
||||||
|
* @param options - Display options
|
||||||
|
*/
|
||||||
|
async function display(obj, options = { raw: false, update: false }) {
|
||||||
|
const bundle = await formatInner(obj, options.raw);
|
||||||
|
let messageType = "display_data";
|
||||||
|
if (options.update) {
|
||||||
|
messageType = "update_display_data";
|
||||||
|
}
|
||||||
|
let transient = {};
|
||||||
|
if (options.display_id) {
|
||||||
|
transient = { display_id: options.display_id };
|
||||||
|
}
|
||||||
|
await broadcast(messageType, {
|
||||||
|
data: bundle,
|
||||||
|
metadata: {},
|
||||||
|
transient,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.Deno.jupyter = {
|
||||||
|
broadcast,
|
||||||
|
display,
|
||||||
|
format,
|
||||||
|
md,
|
||||||
|
html,
|
||||||
|
svg,
|
||||||
|
$display,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
3641
cli/tests/testdata/jupyter/integration_test.ipynb
vendored
3641
cli/tests/testdata/jupyter/integration_test.ipynb
vendored
File diff suppressed because one or more lines are too long
|
@ -1,5 +1,9 @@
|
||||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||||
import { assertThrows } from "./test_util.ts";
|
|
||||||
|
import { assertEquals, assertThrows } from "./test_util.ts";
|
||||||
|
|
||||||
|
// @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol
|
||||||
|
const format = Deno[Deno.internal].jupyter.formatInner;
|
||||||
|
|
||||||
Deno.test("Deno.jupyter is not available", () => {
|
Deno.test("Deno.jupyter is not available", () => {
|
||||||
assertThrows(
|
assertThrows(
|
||||||
|
@ -7,3 +11,69 @@ Deno.test("Deno.jupyter is not available", () => {
|
||||||
"Deno.jupyter is only available in `deno jupyter` subcommand.",
|
"Deno.jupyter is only available in `deno jupyter` subcommand.",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export async function assertFormattedAs(obj: unknown, result: object) {
|
||||||
|
const formatted = await format(obj);
|
||||||
|
assertEquals(formatted, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
Deno.test("display(canvas) creates a PNG", async () => {
|
||||||
|
// Let's make a fake Canvas with a fake Data URL
|
||||||
|
class FakeCanvas {
|
||||||
|
toDataURL() {
|
||||||
|
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAAAAVSURBVAiZY/zPwPCfAQ0woQtQQRAAzqkCCB/D3o0AAAAASUVORK5CYII=";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const canvas = new FakeCanvas();
|
||||||
|
|
||||||
|
await assertFormattedAs(canvas, {
|
||||||
|
"image/png":
|
||||||
|
"iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAAAAVSURBVAiZY/zPwPCfAQ0woQtQQRAAzqkCCB/D3o0AAAAASUVORK5CYII=",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
"class with a Symbol.for('Jupyter.display') function gets displayed",
|
||||||
|
async () => {
|
||||||
|
class Example {
|
||||||
|
x: number;
|
||||||
|
|
||||||
|
constructor(x: number) {
|
||||||
|
this.x = x;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.for("Jupyter.display")]() {
|
||||||
|
return { "application/json": { x: this.x } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const example = new Example(5);
|
||||||
|
|
||||||
|
// Now to check on the broadcast call being made
|
||||||
|
await assertFormattedAs(example, { "application/json": { x: 5 } });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
"class with an async Symbol.for('Jupyter.display') function gets displayed",
|
||||||
|
async () => {
|
||||||
|
class Example {
|
||||||
|
x: number;
|
||||||
|
|
||||||
|
constructor(x: number) {
|
||||||
|
this.x = x;
|
||||||
|
}
|
||||||
|
|
||||||
|
async [Symbol.for("Jupyter.display")]() {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
return { "application/json": { x: this.x } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const example = new Example(3);
|
||||||
|
|
||||||
|
// Now to check on the broadcast call being made
|
||||||
|
await assertFormattedAs(example, { "application/json": { x: 3 } });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
|
@ -388,21 +388,9 @@ impl JupyterServer {
|
||||||
} = evaluate_response.value;
|
} = evaluate_response.value;
|
||||||
|
|
||||||
if exception_details.is_none() {
|
if exception_details.is_none() {
|
||||||
let output =
|
publish_result(&mut self.repl_session, &result, self.execution_count)
|
||||||
get_jupyter_display_or_eval_value(&mut self.repl_session, &result)
|
|
||||||
.await?;
|
.await?;
|
||||||
// Don't bother sending `execute_result` reply if the MIME bundle is empty
|
|
||||||
if !output.is_empty() {
|
|
||||||
msg
|
|
||||||
.new_message("execute_result")
|
|
||||||
.with_content(json!({
|
|
||||||
"execution_count": self.execution_count,
|
|
||||||
"data": output,
|
|
||||||
"metadata": {},
|
|
||||||
}))
|
|
||||||
.send(&mut *self.iopub_socket.lock().await)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
msg
|
msg
|
||||||
.new_reply()
|
.new_reply()
|
||||||
.with_content(json!({
|
.with_content(json!({
|
||||||
|
@ -543,32 +531,33 @@ fn kernel_info() -> serde_json::Value {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_jupyter_display(
|
async fn publish_result(
|
||||||
session: &mut repl::ReplSession,
|
session: &mut repl::ReplSession,
|
||||||
evaluate_result: &cdp::RemoteObject,
|
evaluate_result: &cdp::RemoteObject,
|
||||||
|
execution_count: usize,
|
||||||
) -> Result<Option<HashMap<String, serde_json::Value>>, AnyError> {
|
) -> Result<Option<HashMap<String, serde_json::Value>>, AnyError> {
|
||||||
|
let arg0 = cdp::CallArgument {
|
||||||
|
value: Some(serde_json::Value::Number(execution_count.into())),
|
||||||
|
unserializable_value: None,
|
||||||
|
object_id: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let arg1 = cdp::CallArgument::from(evaluate_result);
|
||||||
|
|
||||||
let response = session
|
let response = session
|
||||||
.post_message_with_event_loop(
|
.post_message_with_event_loop(
|
||||||
"Runtime.callFunctionOn",
|
"Runtime.callFunctionOn",
|
||||||
Some(json!({
|
Some(json!({
|
||||||
"functionDeclaration": r#"async function (object) {
|
"functionDeclaration": r#"async function (execution_count, result) {
|
||||||
if (typeof object[Symbol.for("Jupyter.display")] !== "function") {
|
await Deno[Deno.internal].jupyter.broadcastResult(execution_count, result);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const representation = await object[Symbol.for("Jupyter.display")]();
|
|
||||||
return JSON.stringify(representation);
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}"#,
|
}"#,
|
||||||
"arguments": [cdp::CallArgument::from(evaluate_result)],
|
"arguments": [arg0, arg1],
|
||||||
"executionContextId": session.context_id,
|
"executionContextId": session.context_id,
|
||||||
"awaitPromise": true,
|
"awaitPromise": true,
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let response: cdp::CallFunctionOnResponse = serde_json::from_value(response)?;
|
let response: cdp::CallFunctionOnResponse = serde_json::from_value(response)?;
|
||||||
|
|
||||||
if let Some(exception_details) = &response.exception_details {
|
if let Some(exception_details) = &response.exception_details {
|
||||||
|
@ -578,75 +567,9 @@ async fn get_jupyter_display(
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
match response.result.value {
|
|
||||||
Some(serde_json::Value::String(json_str)) => {
|
|
||||||
let Ok(data) =
|
|
||||||
serde_json::from_str::<HashMap<String, serde_json::Value>>(&json_str)
|
|
||||||
else {
|
|
||||||
eprintln!("Unexpected response from Jupyter.display: {json_str}");
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
if !data.is_empty() {
|
|
||||||
return Ok(Some(data));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(serde_json::Value::Null) => {
|
|
||||||
// Object did not have the Jupyter display spec
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
eprintln!(
|
|
||||||
"Unexpected response from Jupyter.display: {:?}",
|
|
||||||
response.result
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_jupyter_display_or_eval_value(
|
|
||||||
session: &mut repl::ReplSession,
|
|
||||||
evaluate_result: &cdp::RemoteObject,
|
|
||||||
) -> Result<HashMap<String, serde_json::Value>, AnyError> {
|
|
||||||
// Printing "undefined" generates a lot of noise, so let's skip
|
|
||||||
// these.
|
|
||||||
if evaluate_result.kind == "undefined" {
|
|
||||||
return Ok(HashMap::default());
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the response is a primitive value we don't need to try and format
|
|
||||||
// Jupyter response.
|
|
||||||
if evaluate_result.object_id.is_some() {
|
|
||||||
if let Some(data) = get_jupyter_display(session, evaluate_result).await? {
|
|
||||||
return Ok(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let response = session
|
|
||||||
.call_function_on_args(
|
|
||||||
format!(
|
|
||||||
r#"function (object) {{
|
|
||||||
try {{
|
|
||||||
return {0}.inspectArgs(["%o", object], {{ colors: !{0}.noColor }});
|
|
||||||
}} catch (err) {{
|
|
||||||
return {0}.inspectArgs(["%o", err]);
|
|
||||||
}}
|
|
||||||
}}"#,
|
|
||||||
*repl::REPL_INTERNALS_NAME
|
|
||||||
),
|
|
||||||
&[evaluate_result.clone()],
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let mut data = HashMap::default();
|
|
||||||
if let Some(value) = response.result.value {
|
|
||||||
data.insert("text/plain".to_string(), value);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(bartlomieju): dedup with repl::editor
|
// TODO(bartlomieju): dedup with repl::editor
|
||||||
fn get_expr_from_line_at_pos(line: &str, cursor_pos: usize) -> &str {
|
fn get_expr_from_line_at_pos(line: &str, cursor_pos: usize) -> &str {
|
||||||
let start = line[..cursor_pos].rfind(is_word_boundary).unwrap_or(0);
|
let start = line[..cursor_pos].rfind(is_word_boundary).unwrap_or(0);
|
||||||
|
|
124
cli/tsc/dts/lib.deno.unstable.d.ts
vendored
124
cli/tsc/dts/lib.deno.unstable.d.ts
vendored
|
@ -2076,6 +2076,130 @@ declare namespace Deno {
|
||||||
*
|
*
|
||||||
* @category Jupyter */
|
* @category Jupyter */
|
||||||
export namespace jupyter {
|
export namespace jupyter {
|
||||||
|
export interface DisplayOptions {
|
||||||
|
raw?: boolean;
|
||||||
|
update?: boolean;
|
||||||
|
display_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type VegaObject = {
|
||||||
|
$schema: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A collection of supported media types and data for Jupyter frontends.
|
||||||
|
*/
|
||||||
|
export type MediaBundle = {
|
||||||
|
"text/plain"?: string;
|
||||||
|
"text/html"?: string;
|
||||||
|
"image/svg+xml"?: string;
|
||||||
|
"text/markdown"?: string;
|
||||||
|
"application/javascript"?: string;
|
||||||
|
|
||||||
|
// Images (per Jupyter spec) must be base64 encoded. We could _allow_
|
||||||
|
// accepting Uint8Array or ArrayBuffer within `display` calls, however we still
|
||||||
|
// must encode them for jupyter.
|
||||||
|
"image/png"?: string; // WISH: Uint8Array | ArrayBuffer
|
||||||
|
"image/jpeg"?: string; // WISH: Uint8Array | ArrayBuffer
|
||||||
|
"image/gif"?: string; // WISH: Uint8Array | ArrayBuffer
|
||||||
|
"application/pdf"?: string; // WISH: Uint8Array | ArrayBuffer
|
||||||
|
|
||||||
|
// NOTE: all JSON types must be objects at the top level (no arrays, strings, or other primitives)
|
||||||
|
"application/json"?: object;
|
||||||
|
"application/geo+json"?: object;
|
||||||
|
"application/vdom.v1+json"?: object;
|
||||||
|
"application/vnd.plotly.v1+json"?: object;
|
||||||
|
"application/vnd.vega.v5+json"?: VegaObject;
|
||||||
|
"application/vnd.vegalite.v4+json"?: VegaObject;
|
||||||
|
"application/vnd.vegalite.v5+json"?: VegaObject;
|
||||||
|
|
||||||
|
// Must support a catch all for custom media types / mimetypes
|
||||||
|
[key: string]: string | object | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const $display: unique symbol;
|
||||||
|
|
||||||
|
export type Displayable = {
|
||||||
|
[$display]: () => MediaBundle | Promise<MediaBundle>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display function for Jupyter Deno Kernel.
|
||||||
|
* Mimics the behavior of IPython's `display(obj, raw=True)` function to allow
|
||||||
|
* asynchronous displaying of objects in Jupyter.
|
||||||
|
*
|
||||||
|
* @param obj - The object to be displayed
|
||||||
|
* @param options - Display options with a default { raw: true }
|
||||||
|
*/
|
||||||
|
export function display(obj: unknown, options?: DisplayOptions): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show Markdown in Jupyter frontends with a tagged template function.
|
||||||
|
*
|
||||||
|
* Takes a template string and returns a displayable object for Jupyter frontends.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* Create a Markdown view.
|
||||||
|
*
|
||||||
|
* ```typescript
|
||||||
|
* const { md } = Deno.jupyter;
|
||||||
|
* md`# Notebooks in TypeScript via Deno ![Deno logo](https://github.com/denoland.png?size=32)
|
||||||
|
*
|
||||||
|
* * TypeScript ${Deno.version.typescript}
|
||||||
|
* * V8 ${Deno.version.v8}
|
||||||
|
* * Deno ${Deno.version.deno}
|
||||||
|
*
|
||||||
|
* Interactive compute with Jupyter _built into Deno_!
|
||||||
|
* `
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function md(
|
||||||
|
strings: TemplateStringsArray,
|
||||||
|
...values: unknown[]
|
||||||
|
): Displayable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show HTML in Jupyter frontends with a tagged template function.
|
||||||
|
*
|
||||||
|
* Takes a template string and returns a displayable object for Jupyter frontends.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* Create an HTML view.
|
||||||
|
* ```typescript
|
||||||
|
* const { html } = Deno.jupyter;
|
||||||
|
* html`<h1>Hello, world!</h1>`
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function html(
|
||||||
|
strings: TemplateStringsArray,
|
||||||
|
...values: unknown[]
|
||||||
|
): Displayable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SVG Tagged Template Function.
|
||||||
|
*
|
||||||
|
* Takes a template string and returns a displayable object for Jupyter frontends.
|
||||||
|
*
|
||||||
|
* Example usage:
|
||||||
|
*
|
||||||
|
* svg`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||||
|
* <circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" />
|
||||||
|
* </svg>`
|
||||||
|
*/
|
||||||
|
export function svg(
|
||||||
|
strings: TemplateStringsArray,
|
||||||
|
...values: unknown[]
|
||||||
|
): Displayable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format an object for displaying in Deno
|
||||||
|
*
|
||||||
|
* @param obj - The object to be displayed
|
||||||
|
* @returns MediaBundle
|
||||||
|
*/
|
||||||
|
export function format(obj: unknown): MediaBundle;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Broadcast a message on IO pub channel.
|
* Broadcast a message on IO pub channel.
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in a new issue