In the previous two posts in this series, I reviewed the HTML DOM structure or rendered notebook markdown and code cells in JupyterLab (I assume the same structure exists in RetroLab, but need to check…). In this post, I’ll review one way in which we can set classes on those elements from a JupyterLab extension.
To simplify the extension building process, the following recipe has been tested using the JupyterLab Plugin Playground, a JupyterLab extension that lets you write and test simple extensions purely within a JupyterLab context (i.e. without any need to build and install an extension from the command line).
Note that this is a playground with caveats: you need to be strict in your Javascript or things will fail to run, even though they could be executed in a more forgiving environment. I’m also not sure if this is a supersitious behaviour or a necessary one: each time I modified the script, I closed my test notebook, saved the script, reloaded the browser page, clicked the jigsaw icon in the playground editor to “install” the extension, then opened the test notebook to see if things had worked. Certainly, I often had no success in effecting changes without the above steps. But it’s probably quicker than rebuilding JupyterLab each time.
To begin with, I’ll see if I can create a simple extension to add class tags to various parts of the cell HTML DOM based on the presence of cell metadata tags.
Docs for how to drive changes from notebook events seem to be in JupyterLab API notebook module notebookactions, but they aren’t relevant if you want to take action over a notebook on load, as, for example, when cells have been rendered into the DOM and are available for classing.
The notebook class docs briefly reference various accessor methods that may be connected to. I thought a promising one (from the name of it) might be the fullyrendered
accessor, which seems to be “[a] signal emitted when the notebook is fully rendered” but a Stack Overflow comment suggests otherwise:
@krassowski
notebook.fullyRendered
is fairly new and may not be what you are looking for. Previously there was no concept of “notebook.ready” (it was just cells getting added one by one on load), but with the virtualized rendering – if configured to rely solely on content intersection) users may never see a fully rendered notebook (if they do not scroll down to the very last cell); in any case this is all subject to change based on the feedback we got about problems with this feature (a lot of changes were already made), so personally I would just go one-by-one on every change.
That said, I found the fullyRendered
signal did fire, albeit multiple times, when I loaded a notebook and could be used to class the cells based on metadata, but the modelChanged
signal did not seem to do anyhting when the notebook loaded, when I added metadata to a cell, or when I moved a cell? (I note there is also a stateChanged
signal?)
Just as an aside, I note some signals that may be useful for a cell execution status extension in the notebookActions class, specifically the executed
(“A signal that emits whenever a cell execution is scheduled.”) and executionScheduled
(“A signal that emits whenever a cell execution is scheduled”) signals, although I note that selectionExecuted
also claims to be “A signal that emits whenever a cell execution is scheduled”? An example of connecting to the NotebookActions.executed
signal can be found in the jupyterlab-cell-flash
extension. I’m not sure if there’s a way of detecting whether the execution ran successfully or returned an error? There NotebookActions.executed
signal does seem to return an error?
(yes, the ?
is there…) KernelError
object and a success
boolean, so maybe we can use those. In the classic cell execution status extension, it looks like there was a code cell _handle_execute_reply
action that returned various status messages (ok
, aborted
) that could be reasoned around.
Another possibly useful crib is the agoose77/jupyterlab-imarkdown
extension which resembles the classic notebook Python Markdown extension which renders variables into markdown content (via this discourse forum post by @agoose77).
Anywhere, here’s the code I used, which was largely inspired by @krassowski’s response to my Stack Overflow question on Accessing notebook cell metadata and HTML class attributes in JupyterLab Extensions.
// Most of the code cribbed from @krassowski
// https://stackoverflow.com/questions/71736749/accessing-notebook-cell-metadata-and-html-class-attributes-in-jupyterlab-extensi/71744107?noredirect=1#comment126807644_71744107
// Testing this in:
// https://github.com/jupyterlab/jupyterlab-plugin-playground
import { IDisposable, DisposableDelegate } from '@lumino/disposable';
import {
JupyterFrontEnd,
JupyterFrontEndPlugin,
} from '@jupyterlab/application';
import { DocumentRegistry } from '@jupyterlab/docregistry';
import {
NotebookActions,
NotebookPanel,
INotebookModel,
} from '@jupyterlab/notebook';
/**
* The plugin registration information.
*/
const plugin: JupyterFrontEndPlugin<void> = {
activate,
id: 'ouseful-tag-class-demo',
autoStart: true,
};
/*
The most relevant docs appear to be:
https://jupyterlab.readthedocs.io/en/stable/api/modules/notebook.notebookactions.html
*/
export class ClassDemoExtension
implements DocumentRegistry.IWidgetExtension<NotebookPanel, INotebookModel>
{
/**
* Create a new extension for the notebook panel widget.
*
* @param panel Notebook panel
* @param context Notebook context
* @returns Disposable
*/
createNew(
panel: NotebookPanel,
context: DocumentRegistry.IContext<INotebookModel>
): IDisposable {
const notebook = panel.content;
function check_tags(notebook){
/*
Iterate through all the cells
If we see a cell with a tag that starts with class-
then add a corresponding tag to differnt elements in the
notebook HTML DOM
*/
for (const cell of notebook.widgets) {
let tagList = cell.model.metadata.get('tags') as string[];
if (tagList) {
for (let i = 0; i < tagList.length; i++) {
var tag = tagList[i];
if tag.startsWith("class-") {
/*
The plugin playground extension is rather hostile
to have-a-go end-user devs (it's rather strict in
its Javascript epxectations) so if you don't
declare things with let, const or var then an
error of the following form is likely to be thrown
that stops further execution:
ReferenceError: class_name is not definred
*/
//This will replace just the first instance of class-
const class_name = tag.replace("class-", "tag-")
// Add class tags to various parts of the DOM
// What sort of cell are we?
console.log("Cell type: " + cell.model.type)
// Try to see what's going on...
console.log("Try node...")
cell.node.classList.add(class_name + "-" + cell.model.type + "-node");
console.log("Try inputArea.node...")
cell.inputArea.node.classList.add(class_name + "-" + cell.model.type + "-inputArea_node");
console.log("Try editor.node...")
cell.editor.host.classList.add(class_name + "-" + cell.model.type + "-editor_host");
console.log("Try .inputArea.promptNode...")
cell.inputArea.promptNode.classList.add(class_name + "-" + cell.model.type + "-inputArea_promptNode");
if (cell.model.type=="markdown") {
//console.log("Try RenderedHTMLCommon.node...")
//cell.RenderedHTMLCommon.node.classList.add(class_name + "-" + cell.model.type + "-RenderedHTMLCommon.node")
//TO DO - access lm-Widget p-Widget jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput
// DOESN'T WORK: cell.MarkdownOutput.node, MarkdownOutput.host, RenderedMarkdown.node, RenderedHTMLCommon.node
}
else {
console.log("Try outputArea.node...")
cell.outputArea.node.classList.add(class_name + "-" + cell.model.type + "-outputArea_node")
}
} // end: if class starts with tag...
} // end: tags iterator
} //end: if there are tags
} // end: iterate cells
} // end: check_tags function definition
notebook.modelChanged.connect((notebook) => {
console.log("I think we're changed");
// This doesn't seem to do anything on notebook load
// iterate cells and toggle DOM classes as needed, e.g.
//check_tags(notebook);
});
notebook.fullyRendered.connect((notebook) => {
/*
I don't think this means fully rendered on a cells becuase it seems
like we try to add the tags mutliple times on notebook load
which is really inefficient.
This may be unstable anyway, eg the following comment:
https://stackoverflow.com/questions/71736749/accessing-notebook-cell-metadata-and-html-class-attributes-in-jupyterlab-extensi/71744107?noredirect=1#comment126807644_71744107
I'll with go with in now in the expectation it will be break
and I will hopefully be able to find another even to fire from.
I get the impression the UI is some some of signal hell and
the solution is just to keep redoing things on everything
if anything changes. Who needs efficiency or elegance anyway...!
After all, this is just an end-user hack-from-a-position-of-ignorance
and works sort of enough to do something that I couldn't do before...
*/
console.log("I think we're fullyRendered...(?!)");
// iterate cells and toggle DOM classes as needed, e.g.
check_tags(notebook);
});
return new DisposableDelegate(() => {
});
}
}
/**
* Activate the extension.
*
* @param app Main application object
*/
function activate(app: JupyterFrontEnd): void {
app.docRegistry.addWidgetExtension('Notebook', new ClassDemoExtension());
}
/**
* Export the plugin as default.
*/
export default plugin;
With the extension, when I open a notebook with some class-TEST
tags on various markdown and code cells, the following class attributes are set on a markdown cell:
And the following class attributes are set on a code cell:
Having set classes on various elements, we now need to style them. An example of how to do that will (hopefully!) be provided in the next post in this series…
PS Just as an aside, one interesting possibility that occurs to me is if we can intercept a cell run command and swap in our own execution call, we could define “magic-tags” that, if detected on a cell, allow us to grab the cell content and then treat it in a magic way. That is, rather than use a block magic at the start of a cell, magic-tag the cell and treat its contents via some tag powered magic. Various bits of signalling around cell execution scheduling requests might be relevant here, eg in the context of trying to set up a pipeline around the processing of code in a code cell before (and after) its execution.