Notebook#

背景#

2016 年 6 月 16 日的 JupyterLab 架构概览提供了笔记本架构的概述。

JupyterLab 应用程序中最复杂的插件是 Notebook 插件

NotebookWidgetFactory 从模型构造一个新的 NotebookPanel,并用默认组件填充工具栏。

Notebook 插件的结构#

Notebook 插件提供了处理笔记本文件的模型和组件。

模型#

NotebookModel 包含一个可观察的单元格列表。

一个 单元格模型 可以是

  • 代码单元格

  • Markdown 单元格

  • 原始单元格

代码单元格包含一个输出模型列表。单元格列表和输出列表都可以观察到更改。

单元格操作#

NotebookModel 单元格列表支持单步操作,例如移动、添加或删除单元格。NotebookModel 还支持复合单元格列表操作,例如撤销/重做。目前,撤销/重做仅支持单元格,不支持笔记本属性,例如笔记本元数据。目前,CodeMirror 编辑器的撤销功能支持单个单元格输入内容的撤销/重做。(注意:CodeMirror 编辑器的撤销不包括单元格元数据更改。)

元数据#

笔记本模型和单元格模型(即笔记本单元格)支持通过 getMetadatasetMetadatadeleteMetadata 方法获取和设置元数据(参见 NotebookModel单元格模型)。您可以通过 sharedModel.metadataChanged 属性监听元数据更改(参见 单元格共享模型笔记本共享模型)。

笔记本组件#

NotebookModel 创建后,NotebookWidgetFactory 会根据模型构造一个新的 NotebookPanel。NotebookPanel 组件被添加到 DockPanel。NotebookPanel 包含

NotebookPanel 还添加了补全逻辑。

笔记本工具栏维护一个要添加到工具栏的组件列表。笔记本组件包含笔记本的渲染,并处理与笔记本本身的大部分交互逻辑(例如跟踪交互,如选定和活动单元格,以及当前的编辑/命令模式)。

NotebookModel 单元格列表提供了对单元格列表进行细粒度更改的方法。

使用 NotebookActions 的高级操作#

高级操作包含在 NotebookActions 命名空间中,该命名空间具有函数,给定一个笔记本组件,可以运行单元格并选择下一个单元格,在光标处合并或拆分单元格,删除选定的单元格等。

组件层次结构#

笔记本组件包含一个 单元格组件 列表,对应于其单元格列表中的单元格模型。

  • 每个单元格组件包含一个 InputArea

    • 其中包含一个 CodeEditorWrapper

      • 其中包含一个 JavaScript CodeMirror 实例。

CodeCell 还包含一个 OutputArea。OutputArea 负责渲染 OutputAreaModel 列表中的输出。OutputArea 使用笔记本特定的 RenderMimeRegistry 对象来渲染 display_data 输出消息。

笔记本组件在 DOM 中表示为一个具有 CSS 类 jp-Notebookjp-NotebookPanel-notebook<div> 元素。它包含一系列单元格组件。

  • 代码单元格具有以下 DOM 结构

    ../_images/code-cell-dom.svg
  • 渲染的 Markdown 单元格具有以下 DOM 结构

    ../_images/rendered-markdown-cell-dom.svg
  • 活动的 Markdown 单元格具有以下 DOM 结构

    ../_images/active-markdown-cell-dom.svg

注意

HTML 导出器的默认 nbconvert 模板生成与 JupyterLab 笔记本相同的 DOM,从而可以直接使用 JupyterLab CSS。在 JupyterLab 中,输入区域使用 CodeMirror 渲染,并使用自定义主题利用 JupyterLab 的 CSS 变量。在 nbconvert 的情况下,代码单元格使用 Pygments Python 库渲染,该库生成带有语法高亮的静态 HTML。jupyterlab_pygments Pygments 主题模仿 JupyterLab 的默认 CodeMirror 主题。

注意

呈现不同单元格类型的 DOM 结构的 SVG 图形是用 Draw.io 生成的,并包含允许它们直接用 Draw.io 打开和编辑的元数据。

渲染输出消息#

Rendermime 插件提供了一个可插入的系统来渲染输出消息。为 Markdown、HTML、图像、文本等提供了默认渲染器。扩展可以通过在 rendermime 注册表中注册处理程序和 mimetype 来注册在整个应用程序中使用的渲染器。创建笔记本时,它会复制全局 Rendermime 单例,以便可以添加笔记本特定的渲染器。ipywidgets 组件管理器是添加笔记本特定渲染器的扩展示例,因为渲染组件取决于笔记本特定的组件状态。

键盘交互模型#

多个元素可以在笔记本中接收焦点:- 主工具栏,- 单元格,- 单元格组件(编辑器、工具栏、输出)。

当焦点在单元格输入编辑器之外时,笔记本会切换到所谓的“命令”模式。在命令模式下,用户可以访问额外的键盘快捷键,从而可以快速访问单元格和笔记本特定的操作。这些快捷键仅在笔记本处于命令模式且活动元素不可编辑时才激活,由笔记本节点上不存在 .jp-mod-readWrite 类表示。如果活动元素可编辑(通过匹配 :read-write 伪选择器来确定),并且考虑了开放阴影 DOM 中嵌套的任何元素,则会设置此类,但不适用于封闭阴影 DOM 或带有自定义按键事件处理程序的不可编辑元素(例如 <div contenteditable="false" onkeydown="alert()" tabindex="0"></div>)。如果您的输出组件(例如使用 IPython.display.HTML 创建,或由您的 MIME 渲染器在笔记本或控制台中的单元格输出上创建)使用封闭阴影 DOM 或带有自定义按键事件处理程序的不可编辑元素,您可能希望在主机元素上设置 lm-suppress-shortcuts 数据属性以防止命令模式操作产生副作用,例如

<div
  contenteditable="false"
  onkeydown="alert()"
  tabindex="1"
  data-lm-suppress-shortcuts="true"
>
  Click on me and press "A" with and without "lm-suppress-shortcuts"
</div>

如何扩展 Notebook 插件#

我们将通过两个笔记本扩展来讲解

  • 向工具栏添加按钮

  • 向笔记本标题添加组件

  • 添加 ipywidgets 扩展

向工具栏添加按钮#

自 JupyterLab 3.2 起,添加工具栏项可以使用 工具栏注册表 和设置完成。特别是对于笔记本,如果按钮链接到新命令,您可以在扩展设置文件中使用以下 JSON 片段在工具栏中添加按钮

"jupyter.lab.toolbars": {
  "Notebook": [ // Widget factory name for which you want to add a toolbar item.
    // Item with default button widget triggering a command
    { "name": "run", "command": "runmenu:run" }
  ]
}

您可以添加 rank 属性来修改项目位置(默认值为 50)。

向笔记本标题添加组件#

从扩展模板开始。

pip install "copier~=9" jinja2-time
mkdir myextension
cd myextension
copier copy --trust https://github.com/jupyterlab/extension-template .

安装依赖项。请注意,扩展是根据已发布的 npm 包构建的,而不是开发版本。

jlpm add -D @jupyterlab/notebook @jupyterlab/application @jupyterlab/ui-components @jupyterlab/docregistry @lumino/disposable @lumino/widgets

将以下内容复制到 src/index.ts

import { IDisposable, DisposableDelegate } from '@lumino/disposable';

import { Widget } from '@lumino/widgets';

import {
  JupyterFrontEnd,
  JupyterFrontEndPlugin
} from '@jupyterlab/application';

import { DocumentRegistry } from '@jupyterlab/docregistry';

import { NotebookPanel, INotebookModel } from '@jupyterlab/notebook';

/**
* The plugin registration information.
*/
const plugin: JupyterFrontEndPlugin<void> = {
  activate,
  id: 'my-extension-name:widgetPlugin',
  description: 'Add a widget to the notebook header.',
  autoStart: true
};

/**
* A notebook widget extension that adds a widget in the notebook header (widget below the toolbar).
*/
export class WidgetExtension
  implements DocumentRegistry.IWidgetExtension<NotebookPanel, INotebookModel>
{
  /**
  * Create a new extension object.
  */
  createNew(
    panel: NotebookPanel,
    context: DocumentRegistry.IContext<INotebookModel>
  ): IDisposable {
    const widget = new Widget({ node: Private.createNode() });
    widget.addClass('jp-myextension-myheader');

    panel.contentHeader.insertWidget(0, widget);
    return new DisposableDelegate(() => {
      widget.dispose();
    });
  }
}

/**
* Activate the extension.
*/
function activate(app: JupyterFrontEnd): void {
  app.docRegistry.addWidgetExtension('Notebook', new WidgetExtension());
}

/**
* Export the plugin as default.
*/
export default plugin;

/**
* Private helpers
*/
namespace Private {
  /**
  * Generate the widget node
  */
  export function createNode(): HTMLElement {
    const span = document.createElement('span');
    span.textContent = 'My custom header';
    return span;
  }
}

并将以下内容复制到 style/base.css

.jp-myextension-myheader {
    min-height: 20px;
    background-color: lightsalmon;
}

运行以下命令

pip install -e .
jupyter labextension develop . --overwrite
jupyter lab

打开笔记本并观察新的“Header”组件。

ipywidgets 第三方扩展#

此讨论会有点令人困惑,因为我们一直使用术语 widget 来指代 lumino widgets。在下面的讨论中,Jupyter 交互式组件将被称为 ipywidgetslumino widgetsJupyter 交互式组件之间没有内在关系。

ipywidgets 扩展使用 文档注册表 为笔记本 widget 扩展注册一个工厂。调用 createNew() 函数时会提供 NotebookPanel 和 DocumentContext。然后插件会创建一个 ipywidget 管理器(它使用上下文与内核和内核的 comm 管理器交互)。然后插件会向笔记本实例的 rendermime 注册一个 ipywidget 渲染器(该渲染器特定于该特定笔记本)。

当 ipywidget 模型在内核中创建时,会向浏览器发送一个 comm 消息,并由 ipywidget 管理器处理以创建浏览器端的 ipywidget 模型。当模型在内核中显示时,会将 display_data 输出与 ipywidget 模型 ID 一起发送到浏览器。请求在该笔记本的 rendermime 中注册的渲染器来渲染输出。渲染器请求 ipywidget 管理器实例渲染相应的模型,该模型返回一个 JavaScript promise。渲染器创建一个容器 lumino widget,它同步返回给 OutputArea,然后当 promise 解析时,用渲染的 ipywidget 填充容器。