Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🧩 Changes to support myst-theme integration #611

Merged
merged 5 commits into from
Apr 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/four-bulldogs-judge.md
Original file line number Diff line number Diff line change
@@ -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)
6 changes: 3 additions & 3 deletions packages/core/src/cell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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(),
Expand Down
33 changes: 16 additions & 17 deletions packages/core/src/cell_noexec.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Expand Down
13 changes: 13 additions & 0 deletions packages/core/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
58 changes: 51 additions & 7 deletions packages/core/src/passive.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,67 @@
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) => `
<div class="thebe-ipywidgets-placeholder">
<div class="thebe-ipywidgets-placeholder-image"></div>
<div class="thebe-ipywidgets-placeholder-message"><code>ipywidgets</code> - a Jupyter kernel connection is required to fully display this output.</div>
${plainText && `<pre>${plainText}</pre>`}
</div>
`;

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 });
this.area = new OutputArea({
model: this.model,
rendermime: this.rendermime,
});
this.hideWidgets = hideWidgets;
}

/**
Expand All @@ -33,16 +75,16 @@ 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}`,
);
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}`);

Expand All @@ -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) {
Expand Down Expand Up @@ -108,7 +152,7 @@ class PassiveCellRenderer implements IPassiveCell {
* @returns
*/
render(outputs: nbformat.IOutput[]) {
this.model.fromJSON(outputs);
this.model.fromJSON(stripWidgets(outputs, this.hideWidgets));
}
}

Expand Down
5 changes: 3 additions & 2 deletions packages/react/src/hooks/notebook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ export function findErrors(execReturns: (IThebeCellExecuteReturn | null)[]) {
);
}

function useNotebookBase() {
export function useNotebookBase() {
const { session, ready: sessionReady } = useThebeSession();
const [notebook, setNotebook] = useState<ThebeNotebook | undefined>();
// 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<boolean>(false);
Expand Down Expand Up @@ -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
Expand Down