diff --git a/.changeset/four-bulldogs-judge.md b/.changeset/four-bulldogs-judge.md new file mode 100644 index 00000000..1db81eaf --- /dev/null +++ b/.changeset/four-bulldogs-judge.md @@ -0,0 +1,6 @@ +--- +'thebe-react': patch +'thebe-core': patch +--- + +Changed DOM attachement and passive rendering behaviour for integration with the [myst-theme](https://github.com/executablebooks/myst-theme/pull/21) diff --git a/packages/core/src/cell.ts b/packages/core/src/cell.ts index 9aaed1fd..11772be8 100644 --- a/packages/core/src/cell.ts +++ b/packages/core/src/cell.ts @@ -21,8 +21,8 @@ class ThebeCell extends PassiveCellRenderer implements IThebeCell { notebookId: string, source: string, config: Config, - metadata: JsonObject = {}, - rendermime?: IRenderMimeRegistry, + metadata: JsonObject, + rendermime: IRenderMimeRegistry, ) { super(id, rendermime); this.events = new EventEmitter(id, config, EventSubject.cell, this); @@ -37,7 +37,7 @@ class ThebeCell extends PassiveCellRenderer implements IThebeCell { icc: ICodeCell, notebookId: string, config: Config, - rendermime?: IRenderMimeRegistry, + rendermime: IRenderMimeRegistry, ) { const cell = new ThebeCell( icc.id ?? shortId(), diff --git a/packages/core/src/cell_noexec.ts b/packages/core/src/cell_noexec.ts index 19d312b9..de6e2886 100644 --- a/packages/core/src/cell_noexec.ts +++ b/packages/core/src/cell_noexec.ts @@ -1,45 +1,44 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import type { ICell, IOutput } from '@jupyterlab/nbformat'; -import type { IRenderMimeRegistry } from '@jupyterlab/rendermime'; -import { getRenderMimeRegistry } from './rendermime'; +import type { IRenderMimeRegistry, RenderMimeRegistry } from '@jupyterlab/rendermime'; +import PassiveCellRenderer from './passive'; import type ThebeSession from './session'; -import type { IThebeCell, IThebeCellExecuteReturn, JsonObject, MathjaxOptions } from './types'; +import type { IThebeCell, IThebeCellExecuteReturn, JsonObject } from './types'; import { ensureString, shortId } from './utils'; -export default class NonExecutableCell implements IThebeCell { +export default class NonExecutableCell extends PassiveCellRenderer implements IThebeCell { id: string; notebookId: string; source: string; busy: boolean; metadata: JsonObject; - constructor(id: string, notebookId: string, source: string) { + constructor( + id: string, + notebookId: string, + source: string, + metadata: JsonObject, + rendermime: IRenderMimeRegistry, + ) { + super(id, rendermime); this.id = id; this.notebookId = notebookId; this.source = source; this.busy = false; - this.metadata = {}; + this.metadata = metadata; } - static fromICell( - ic: ICell, - notebookId: string, - rendermime?: IRenderMimeRegistry, - mathjaxOptions?: MathjaxOptions, - ) { + static fromICell(ic: ICell, notebookId: string, rendermime: IRenderMimeRegistry) { const cell = new NonExecutableCell( typeof ic.id === 'string' ? ic.id : shortId(), notebookId, ensureString(ic.source), + ic.metadata, + rendermime, ); - Object.assign(cell.metadata, ic.metadata); return cell; } - get rendermime() { - return getRenderMimeRegistry(); - } - get isAttachedToDOM() { return false; } diff --git a/packages/core/src/index.css b/packages/core/src/index.css index 80a5bc04..16a6072e 100644 --- a/packages/core/src/index.css +++ b/packages/core/src/index.css @@ -57,3 +57,16 @@ transform: rotate(360deg); } } + +.thebe-ipywidgets-placeholder { + display: flex; + flex-direction: column; + align-items: center; + font-size: 95%; +} + +.thebe-ipywidgets-placeholder > pre { + font-size: 80%; + font-family: monospace; + margin: 8px 16px; +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b33fc344..df34470a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -2,6 +2,7 @@ export { default as ThebeServer } from './server'; export { default as ThebeSession } from './session'; export { default as ThebeNotebook, CodeBlock } from './notebook'; export { default as ThebeCell } from './cell'; +export { default as ThebeNonExecutableCell } from './cell_noexec'; export { default as PassiveCellRenderer } from './passive'; export * from './options'; diff --git a/packages/core/src/passive.ts b/packages/core/src/passive.ts index 84d085f4..586a9602 100644 --- a/packages/core/src/passive.ts +++ b/packages/core/src/passive.ts @@ -1,18 +1,59 @@ import type * as nbformat from '@jupyterlab/nbformat'; import { getRenderMimeRegistry } from './rendermime'; import { OutputArea, OutputAreaModel } from '@jupyterlab/outputarea'; -import { Widget } from '@lumino/widgets'; import type { IRenderMimeRegistry } from '@jupyterlab/rendermime'; import type { IPassiveCell, MathjaxOptions } from './types'; import { makeMathjaxOptions } from './options'; +import { Widget } from '@lumino/widgets'; +import { MessageLoop } from '@lumino/messaging'; +import { WIDGET_MIMETYPE } from './manager'; +import { ensureString } from './utils'; + +function isMimeBundle({ output_type }: nbformat.IOutput) { + return output_type === 'display_data' || output_type === 'execute_result'; +} + +const placeholder = (plainText?: string) => ` +
+
+
ipywidgets - a Jupyter kernel connection is required to fully display this output.
+ ${plainText && `
${plainText}
`} +
+`; + +function stripWidgets(outputs: nbformat.IOutput[], hideWidgets?: boolean) { + return outputs.map((output: nbformat.IOutput) => { + if (!isMimeBundle(output)) return output; + const { [WIDGET_MIMETYPE]: widgets, ...others } = output.data as nbformat.IMimeBundle; + if (!widgets) return output; + const data = { ...others }; + if (!hideWidgets && !('text/html' in data)) + // if there is not already an html bundle, add a placeholder to hide the plain/text field + data['text/html'] = placeholder(ensureString(data['text/plain'] as string | string[])); + else if (hideWidgets) { + delete data['text/plain']; + } + const stripped = { + ...output, + data, + }; + return stripped; + }); +} class PassiveCellRenderer implements IPassiveCell { readonly id: string; readonly rendermime: IRenderMimeRegistry; + readonly hideWidgets: boolean; protected model: OutputAreaModel; protected area: OutputArea; - constructor(id: string, rendermime?: IRenderMimeRegistry, mathjax?: MathjaxOptions) { + constructor( + id: string, + rendermime?: IRenderMimeRegistry, + mathjax?: MathjaxOptions, + hideWidgets = false, + ) { this.id = id; this.rendermime = rendermime ?? getRenderMimeRegistry(mathjax ?? makeMathjaxOptions()); this.model = new OutputAreaModel({ trusted: true }); @@ -20,6 +61,7 @@ class PassiveCellRenderer implements IPassiveCell { model: this.model, rendermime: this.rendermime, }); + this.hideWidgets = hideWidgets; } /** @@ -33,7 +75,7 @@ class PassiveCellRenderer implements IPassiveCell { return this.area.isAttached; } - attachToDOM(el?: HTMLElement) { + attachToDOM(el?: HTMLElement, strict = false) { if (!this.area || !el) { console.error( `thebe:renderer:attachToDOM - could not attach to DOM - area: ${this.area}, el: ${el}`, @@ -41,8 +83,8 @@ class PassiveCellRenderer implements IPassiveCell { return; } if (this.area.isAttached) { - console.warn(`thebe:renderer:attachToDOM - already attached, returning`); - return; + console.warn(`thebe:renderer:attachToDOM - already attached`); + if (strict) return; } console.debug(`thebe:renderer:attachToDOM ${this.id}`); @@ -62,7 +104,9 @@ class PassiveCellRenderer implements IPassiveCell { div.className = 'thebe-output'; el.append(div); - Widget.attach(this.area, div); + MessageLoop.sendMessage(this.area, Widget.Msg.BeforeAttach); + div.appendChild(this.area.node); + MessageLoop.sendMessage(this.area, Widget.Msg.AfterAttach); } setOutputText(text: string) { @@ -108,7 +152,7 @@ class PassiveCellRenderer implements IPassiveCell { * @returns */ render(outputs: nbformat.IOutput[]) { - this.model.fromJSON(outputs); + this.model.fromJSON(stripWidgets(outputs, this.hideWidgets)); } } diff --git a/packages/react/src/hooks/notebook.ts b/packages/react/src/hooks/notebook.ts index 1d7cf06f..6409ade4 100644 --- a/packages/react/src/hooks/notebook.ts +++ b/packages/react/src/hooks/notebook.ts @@ -27,9 +27,10 @@ export function findErrors(execReturns: (IThebeCellExecuteReturn | null)[]) { ); } -function useNotebookBase() { +export function useNotebookBase() { const { session, ready: sessionReady } = useThebeSession(); const [notebook, setNotebook] = useState(); + // TODO move the refs to caller hooks as it does so little to maintain them in here. const [refs, setRefs] = useState<((node: HTMLDivElement) => void)[]>([]); const [sessionAttached, setSessionAttached] = useState(false); const [executing, setExecuting] = useState(false); @@ -105,7 +106,7 @@ function useNotebookBase() { } /** - * @paran name - provided to the fetcher function + * @param name - provided to the fetcher function * @param fetchNotebook - an async function, that given a name, can return a JSON representation of an ipynb file (INotebookContent) * @param opts - options.refsForWidgetsOnly=false allows refs to be generated for all notebook cells, rather than onlythose with widget tags * @returns