在 JupyterLab Extensions 中访问笔记本单元元数据和 HTML class 属性

Accessing notebook cell metadata and HTML class attributes in JupyterLab Extensions

在最小可行的 JupyterLab 扩展中,例如使用 JupyterLab Plugin Playground 进行测试,我如何添加一个工具栏按钮来切换 HTML 关联的特定 class 属性使用一个或多个选定的笔记本单元格(代码单元格或降价单元格)?

进一步概括示例:

作为起点,以下代码(取自 JupyterLab extension examples)应向工具栏添加一个按钮:


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

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

import { ToolbarButton } from '@jupyterlab/apputils';

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

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

/**
 * The plugin registration information.
 */
const plugin: JupyterFrontEndPlugin<void> = {
  activate,
  id: 'toolbar-button',
  autoStart: true,
};

/**
 * A notebook widget extension that adds a button to the toolbar.
 */
export class ButtonExtension
  implements DocumentRegistry.IWidgetExtension<NotebookPanel, INotebookModel>
{
  /**
   * Create a new extension for the notebook panel widget.
   *
   * @param panel Notebook panel
   * @param context Notebook context
   * @returns Disposable on the added button
   */
  createNew(
    panel: NotebookPanel,
    context: DocumentRegistry.IContext<INotebookModel>
  ): IDisposable {
    const myButtonAction = () => {
      // Perform some action
    };

    const button = new ToolbarButton({
      className: 'my-action-button',
      label: 'My Button',
      onClick: myButtonAction,
      tooltip: 'Perform My Button action',
    });

    panel.toolbar.insertItem(10, 'myNewAction', button);
    return new DisposableDelegate(() => {
      button.dispose();
    });
  }
}

/**
 * Activate the extension.
 *
 * @param app Main application object
 */
function activate(app: JupyterFrontEnd): void {
  app.docRegistry.addWidgetExtension('Notebook', new ButtonExtension());
}

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

您应该首先获取 Notebook class 实例的句柄(这是您已经可用的笔记本 panel 的内容):

// in anonymous function assigned to myButtonAction
const notebook = panel.content;

笔记本实例为您提供活动的 Cell 小部件:

const activeCell = notebook.activeCell;

单元格小部件有两个有趣的属性:model 允许您访问元数据,node 允许操作 DOM 结构。

例如,如果单元格是降价单元格 (=ICellModel has .type (CellType) 等于 'markdown',则可以切换单元格节点的 class:

if (activeCell.model.type === 'markdown') {
   activeCell.node.classList.toggle('someClass');
}

元数据存储在cell.model.metadata

对于单元格的选择,以下内容应该有效:

const {head, anchor} = notebook.getContiguousSelection();
if (head === null || anchor === null) {
  // no selection
  return;
}
const start = head > anchor ? anchor : head;
const end = head > anchor ? head : anchor;
for (let cellIndex = start; cellIndex <= end; cellIndex++) {
  const cell = notebook.widgets[cellIndex];
  if (cell.model.type === 'code') {
      cell.node.classList.toggle('someOtherClass');
  }
}

然而,这种方法存在一个问题,因为当笔记本在单独的视图中打开,或者只是重新加载时,classes 将消失(因为它们只在 DOM 节点)。如果你需要坚持,我会推荐:

  • 使用按钮仅写入单元格元数据
  • 添加一个单独的回调函数来监听笔记本型号的任何变化,大致(未测试!):
    // in createNew()
    const notebook = panel.content;
    notebook.modelChanged.connect((notebook) => {
      // iterate cells and toggle DOM classes as needed, e.g.
      for (const cell of notebook.widgets) {
        if (cell.model.metadata.get('someMetaData')) {
          cell.node.classList.toggle('someOtherClass');
        }
      }
    });
    
    这也应该(原则上)与协作编辑一起工作。