@jupyterlab/cells
Version:
JupyterLab - Notebook Cells
1,483 lines (1,482 loc) • 64.1 kB
JavaScript
/* -----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified BSD License.
|----------------------------------------------------------------------------*/
import { EditorView } from '@codemirror/view';
import { AttachmentsResolver } from '@jupyterlab/attachments';
import { ActivityMonitor, URLExt } from '@jupyterlab/coreutils';
import { OutputArea, OutputPrompt, SimplifiedOutputArea, Stdin } from '@jupyterlab/outputarea';
import { imageRendererFactory, MimeModel } from '@jupyterlab/rendermime';
import { TableOfContentsUtils } from '@jupyterlab/toc';
import { nullTranslator } from '@jupyterlab/translation';
import { addIcon } from '@jupyterlab/ui-components';
import { PromiseDelegate, UUID } from '@lumino/coreutils';
import { some } from '@lumino/algorithm';
import { MessageLoop } from '@lumino/messaging';
import { Debouncer } from '@lumino/polling';
import { Signal } from '@lumino/signaling';
import { Panel, PanelLayout, Widget } from '@lumino/widgets';
import { InputCollapser, OutputCollapser } from './collapser';
import { CellFooter, CellHeader } from './headerfooter';
import { InputArea, InputPrompt } from './inputarea';
import { InputPlaceholder, OutputPlaceholder } from './placeholder';
import { ResizeHandle } from './resizeHandle';
/**
* The CSS class added to cell widgets.
*/
const CELL_CLASS = 'jp-Cell';
/**
* The CSS class added to the cell header.
*/
const CELL_HEADER_CLASS = 'jp-Cell-header';
/**
* The CSS class added to the cell footer.
*/
const CELL_FOOTER_CLASS = 'jp-Cell-footer';
/**
* The CSS class added to the cell input wrapper.
*/
const CELL_INPUT_WRAPPER_CLASS = 'jp-Cell-inputWrapper';
/**
* The CSS class added to the cell output wrapper.
*/
const CELL_OUTPUT_WRAPPER_CLASS = 'jp-Cell-outputWrapper';
/**
* The CSS class added to the cell input area.
*/
const CELL_INPUT_AREA_CLASS = 'jp-Cell-inputArea';
/**
* The CSS class added to the cell output area.
*/
const CELL_OUTPUT_AREA_CLASS = 'jp-Cell-outputArea';
/**
* The CSS class added to the cell input collapser.
*/
const CELL_INPUT_COLLAPSER_CLASS = 'jp-Cell-inputCollapser';
/**
* The CSS class added to the cell output collapser.
*/
const CELL_OUTPUT_COLLAPSER_CLASS = 'jp-Cell-outputCollapser';
/**
* The class name added to the cell when dirty.
*/
const DIRTY_CLASS = 'jp-mod-dirty';
/**
* The class name added to code cells.
*/
const CODE_CELL_CLASS = 'jp-CodeCell';
/**
* The class name added to markdown cells.
*/
const MARKDOWN_CELL_CLASS = 'jp-MarkdownCell';
/**
* The class name added to rendered markdown output widgets.
*/
const MARKDOWN_OUTPUT_CLASS = 'jp-MarkdownOutput';
const MARKDOWN_HEADING_COLLAPSED = 'jp-MarkdownHeadingCollapsed';
const HEADING_COLLAPSER_CLASS = 'jp-collapseHeadingButton';
const SHOW_HIDDEN_CELLS_CLASS = 'jp-showHiddenCellsButton';
/**
* The class name added to raw cells.
*/
const RAW_CELL_CLASS = 'jp-RawCell';
/**
* The class name added to a rendered input area.
*/
const RENDERED_CLASS = 'jp-mod-rendered';
const NO_OUTPUTS_CLASS = 'jp-mod-noOutputs';
/**
* The text applied to an empty markdown cell.
*/
const DEFAULT_MARKDOWN_TEXT = 'Type Markdown and LaTeX: $ α^2 $';
/**
* The timeout to wait for change activity to have ceased before rendering.
*/
const RENDER_TIMEOUT = 1000;
/**
* The mime type for a rich contents drag object.
*/
const CONTENTS_MIME_RICH = 'application/x-jupyter-icontentsrich';
/** ****************************************************************************
* Cell
******************************************************************************/
/**
* A base cell widget.
*/
export class Cell extends Widget {
/**
* Construct a new base cell widget.
*/
constructor(options) {
var _a, _b, _c, _d;
super();
this.prompt = '';
this._displayChanged = new Signal(this);
this._editorConfig = {};
this._inputHidden = false;
this._inViewportChanged = new Signal(this);
this._readOnly = false;
this._ready = new PromiseDelegate();
this._resizeDebouncer = new Debouncer(() => {
this._displayChanged.emit();
}, 0);
this._syncCollapse = false;
this._syncEditable = false;
this.addClass(CELL_CLASS);
const model = (this._model = options.model);
this.contentFactory = options.contentFactory;
this.layout = (_a = options.layout) !== null && _a !== void 0 ? _a : new PanelLayout();
// Set up translator for aria labels
this.translator = (_b = options.translator) !== null && _b !== void 0 ? _b : nullTranslator;
this._editorConfig = (_c = options.editorConfig) !== null && _c !== void 0 ? _c : {};
this._placeholder = true;
this._inViewport = false;
this.placeholder = (_d = options.placeholder) !== null && _d !== void 0 ? _d : true;
model.metadataChanged.connect(this.onMetadataChanged, this);
}
/**
* Initialize view state from model.
*
* #### Notes
* Should be called after construction. For convenience, returns this, so it
* can be chained in the construction, like `new Foo().initializeState();`
*/
initializeState() {
this.loadCollapseState();
this.loadEditableState();
return this;
}
/**
* Signal to indicate that widget has changed visibly (in size, in type, etc)
*/
get displayChanged() {
return this._displayChanged;
}
/**
* Whether the cell is in viewport or not.
*/
get inViewport() {
return this._inViewport;
}
set inViewport(v) {
if (this._inViewport !== v) {
this._inViewport = v;
this._inViewportChanged.emit(this._inViewport);
}
}
/**
* Will emit true just after the node is attached to the DOM
* Will emit false just before the node is detached of the DOM
*/
get inViewportChanged() {
return this._inViewportChanged;
}
/**
* Whether the cell is a placeholder not yet fully rendered or not.
*/
get placeholder() {
return this._placeholder;
}
set placeholder(v) {
if (this._placeholder !== v && v === false) {
this.initializeDOM();
this._placeholder = v;
this._ready.resolve();
}
}
/**
* Get the prompt node used by the cell.
*/
get promptNode() {
if (this.placeholder) {
return null;
}
if (!this._inputHidden) {
return this._input.promptNode;
}
else {
return this._inputPlaceholder.node
.firstElementChild;
}
}
/**
* Get the CodeEditorWrapper used by the cell.
*/
get editorWidget() {
var _a, _b;
return (_b = (_a = this._input) === null || _a === void 0 ? void 0 : _a.editorWidget) !== null && _b !== void 0 ? _b : null;
}
/**
* Get the CodeEditor used by the cell.
*/
get editor() {
var _a, _b;
return (_b = (_a = this._input) === null || _a === void 0 ? void 0 : _a.editor) !== null && _b !== void 0 ? _b : null;
}
/**
* Editor configuration
*/
get editorConfig() {
return this._editorConfig;
}
/**
* Cell headings
*/
get headings() {
return new Array();
}
/**
* Get the model used by the cell.
*/
get model() {
return this._model;
}
/**
* Get the input area for the cell.
*/
get inputArea() {
return this._input;
}
/**
* The read only state of the cell.
*/
get readOnly() {
return this._readOnly;
}
set readOnly(value) {
if (value === this._readOnly) {
return;
}
this._readOnly = value;
if (this.syncEditable) {
this.saveEditableState();
}
this.update();
}
/**
* Whether the cell is a placeholder that defer rendering
*
* #### Notes
* You can wait for the promise `Cell.ready` to wait for the
* cell to be rendered.
*/
isPlaceholder() {
return this.placeholder;
}
/**
* Save view editable state to model
*/
saveEditableState() {
const { sharedModel } = this.model;
const current = sharedModel.getMetadata('editable');
if ((this.readOnly && current === false) ||
(!this.readOnly && current === undefined)) {
return;
}
if (this.readOnly) {
sharedModel.setMetadata('editable', false);
}
else {
sharedModel.deleteMetadata('editable');
}
}
/**
* Load view editable state from model.
*/
loadEditableState() {
this.readOnly =
this.model.sharedModel.getMetadata('editable') ===
false;
}
/**
* A promise that resolves when the widget renders for the first time.
*/
get ready() {
return this._ready.promise;
}
/**
* Set the prompt for the widget.
*/
setPrompt(value) {
var _a;
this.prompt = value;
(_a = this._input) === null || _a === void 0 ? void 0 : _a.setPrompt(value);
}
/**
* The view state of input being hidden.
*/
get inputHidden() {
return this._inputHidden;
}
set inputHidden(value) {
var _a;
if (this._inputHidden === value) {
return;
}
if (!this.placeholder) {
const layout = this._inputWrapper.layout;
if (value) {
this._input.parent = null;
if (this._inputPlaceholder) {
this._inputPlaceholder.text = (_a = this.model.sharedModel
.getSource()
.split('\n')) === null || _a === void 0 ? void 0 : _a[0];
}
layout.addWidget(this._inputPlaceholder);
}
else {
this._inputPlaceholder.parent = null;
layout.addWidget(this._input);
}
}
this._inputHidden = value;
if (this.syncCollapse) {
this.saveCollapseState();
}
this.handleInputHidden(value);
}
/**
* Save view collapse state to model
*/
saveCollapseState() {
const jupyter = { ...this.model.getMetadata('jupyter') };
if ((this.inputHidden && jupyter.source_hidden === true) ||
(!this.inputHidden && jupyter.source_hidden === undefined)) {
return;
}
if (this.inputHidden) {
jupyter.source_hidden = true;
}
else {
delete jupyter.source_hidden;
}
if (Object.keys(jupyter).length === 0) {
this.model.deleteMetadata('jupyter');
}
else {
this.model.setMetadata('jupyter', jupyter);
}
}
/**
* Revert view collapse state from model.
*/
loadCollapseState() {
var _a;
const jupyter = (_a = this.model.getMetadata('jupyter')) !== null && _a !== void 0 ? _a : {};
this.inputHidden = !!jupyter.source_hidden;
}
/**
* Handle the input being hidden.
*
* #### Notes
* This is called by the `inputHidden` setter so that subclasses
* can perform actions upon the input being hidden without accessing
* private state.
*/
handleInputHidden(value) {
return;
}
/**
* Whether to sync the collapse state to the cell model.
*/
get syncCollapse() {
return this._syncCollapse;
}
set syncCollapse(value) {
if (this._syncCollapse === value) {
return;
}
this._syncCollapse = value;
if (value) {
this.loadCollapseState();
}
}
/**
* Whether to sync the editable state to the cell model.
*/
get syncEditable() {
return this._syncEditable;
}
set syncEditable(value) {
if (this._syncEditable === value) {
return;
}
this._syncEditable = value;
if (value) {
this.loadEditableState();
}
}
/**
* Clone the cell, using the same model.
*/
clone() {
const constructor = this.constructor;
return new constructor({
model: this.model,
contentFactory: this.contentFactory,
placeholder: false,
translator: this.translator
});
}
/**
* Dispose of the resources held by the widget.
*/
dispose() {
// Do nothing if already disposed.
if (this.isDisposed) {
return;
}
this._resizeDebouncer.dispose();
this._input = null;
this._model = null;
this._inputWrapper = null;
this._inputPlaceholder = null;
super.dispose();
}
/**
* Update the editor configuration with the partial provided dictionary.
*
* @param v Partial editor configuration
*/
updateEditorConfig(v) {
this._editorConfig = { ...this._editorConfig, ...v };
if (this.editor) {
this.editor.setOptions(this._editorConfig);
}
}
/**
* Create children widgets.
*/
initializeDOM() {
if (!this.placeholder) {
return;
}
const contentFactory = this.contentFactory;
const model = this._model;
// Header
const header = contentFactory.createCellHeader();
header.addClass(CELL_HEADER_CLASS);
this.layout.addWidget(header);
// Input
const inputWrapper = (this._inputWrapper = new Panel());
inputWrapper.addClass(CELL_INPUT_WRAPPER_CLASS);
const inputCollapser = new InputCollapser();
inputCollapser.addClass(CELL_INPUT_COLLAPSER_CLASS);
const input = (this._input = new InputArea({
model,
contentFactory,
editorOptions: this.getEditorOptions()
}));
input.addClass(CELL_INPUT_AREA_CLASS);
inputWrapper.addWidget(inputCollapser);
inputWrapper.addWidget(input);
this.layout.addWidget(inputWrapper);
this._inputPlaceholder = new InputPlaceholder({
callback: () => {
this.inputHidden = !this.inputHidden;
},
text: input.model.sharedModel.getSource().split('\n')[0],
translator: this.translator
});
input.model.contentChanged.connect((sender, args) => {
var _a;
if (this._inputPlaceholder && this.inputHidden) {
this._inputPlaceholder.text = (_a = sender.sharedModel
.getSource()
.split('\n')) === null || _a === void 0 ? void 0 : _a[0];
}
});
if (this.inputHidden) {
input.parent = null;
inputWrapper.layout.addWidget(this._inputPlaceholder);
}
// Footer
const footer = this.contentFactory.createCellFooter();
footer.addClass(CELL_FOOTER_CLASS);
this.layout.addWidget(footer);
}
/**
* Get the editor options at initialization.
*
* @returns Editor options
*/
getEditorOptions() {
return { config: this.editorConfig };
}
/**
* Handle `before-attach` messages.
*/
onBeforeAttach(msg) {
if (this.placeholder) {
this.placeholder = false;
}
}
/**
* Handle `after-attach` messages.
*/
onAfterAttach(msg) {
this.update();
}
/**
* Handle `'activate-request'` messages.
*/
onActivateRequest(msg) {
var _a;
(_a = this.editor) === null || _a === void 0 ? void 0 : _a.focus();
}
/**
* Handle `resize` messages.
*/
onResize(msg) {
void this._resizeDebouncer.invoke();
}
/**
* Handle `update-request` messages.
*/
onUpdateRequest(msg) {
var _a, _b;
if (!this._model) {
return;
}
// Handle read only state.
if (((_a = this.editor) === null || _a === void 0 ? void 0 : _a.getOption('readOnly')) !== this._readOnly) {
(_b = this.editor) === null || _b === void 0 ? void 0 : _b.setOption('readOnly', this._readOnly);
}
}
onContentChanged() {
var _a;
if (this.inputHidden && this._inputPlaceholder) {
this._inputPlaceholder.text = (_a = this.model.sharedModel
.getSource()
.split('\n')) === null || _a === void 0 ? void 0 : _a[0];
}
}
/**
* Handle changes in the metadata.
*/
onMetadataChanged(model, args) {
switch (args.key) {
case 'jupyter':
if (this.syncCollapse) {
this.loadCollapseState();
}
break;
case 'editable':
if (this.syncEditable) {
this.loadEditableState();
}
break;
default:
break;
}
}
}
/**
* The namespace for the `Cell` class statics.
*/
(function (Cell) {
/**
* Type of headings
*/
let HeadingType;
(function (HeadingType) {
/**
* Heading from HTML output
*/
HeadingType[HeadingType["HTML"] = 0] = "HTML";
/**
* Heading from Markdown cell or Markdown output
*/
HeadingType[HeadingType["Markdown"] = 1] = "Markdown";
})(HeadingType = Cell.HeadingType || (Cell.HeadingType = {}));
/**
* The default implementation of an `IContentFactory`.
*
* This includes a CodeMirror editor factory to make it easy to use out of the box.
*/
class ContentFactory {
/**
* Create a content factory for a cell.
*/
constructor(options) {
this._editorFactory = options.editorFactory;
}
/**
* The readonly editor factory that create code editors
*/
get editorFactory() {
return this._editorFactory;
}
/**
* Create a new cell header for the parent widget.
*/
createCellHeader() {
return new CellHeader();
}
/**
* Create a new cell footer for the parent widget.
*/
createCellFooter() {
return new CellFooter();
}
/**
* Create an input prompt.
*/
createInputPrompt() {
return new InputPrompt();
}
/**
* Create the output prompt for the widget.
*/
createOutputPrompt() {
return new OutputPrompt();
}
/**
* Create an stdin widget.
*/
createStdin(options) {
return new Stdin(options);
}
}
Cell.ContentFactory = ContentFactory;
})(Cell || (Cell = {}));
/** ****************************************************************************
* CodeCell
******************************************************************************/
/**
* Code cell layout
*
* It will not detached the output area when the cell is detached.
*/
export class CodeCellLayout extends PanelLayout {
/**
* A message handler invoked on a `'before-attach'` message.
*
* #### Notes
* The default implementation of this method forwards the message
* to all widgets. It assumes all widget nodes are attached to the
* parent widget node.
*
* This may be reimplemented by subclasses as needed.
*/
onBeforeAttach(msg) {
let beforeOutputArea = true;
const outputAreaWrapper = this.parent.node.firstElementChild;
for (const widget of this) {
if (outputAreaWrapper) {
if (widget.node === outputAreaWrapper) {
beforeOutputArea = false;
}
else {
MessageLoop.sendMessage(widget, msg);
if (beforeOutputArea) {
this.parent.node.insertBefore(widget.node, outputAreaWrapper);
}
else {
this.parent.node.appendChild(widget.node);
}
// Force setting isVisible to true as it requires the parent widget to be
// visible. But that flag will be set only during the `onAfterAttach` call.
if (!this.parent.isHidden) {
widget.setFlag(Widget.Flag.IsVisible);
}
// Not called in NotebookWindowedLayout to avoid outputArea
// widgets unwanted update or reset.
MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach);
}
}
}
}
/**
* A message handler invoked on an `'after-detach'` message.
*
* #### Notes
* The default implementation of this method forwards the message
* to all widgets. It assumes all widget nodes are attached to the
* parent widget node.
*
* This may be reimplemented by subclasses as needed.
*/
onAfterDetach(msg) {
for (const widget of this) {
// TODO we could improve this further by removing outputs based
// on their mime type (for example plain/text or markdown could safely be detached)
// If the cell is out of the view port, its children are already detached -> skip detaching
if (!widget.hasClass(CELL_OUTPUT_WRAPPER_CLASS) &&
widget.node.isConnected) {
// Not called in NotebookWindowedLayout for windowed notebook
MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach);
this.parent.node.removeChild(widget.node);
MessageLoop.sendMessage(widget, msg);
}
}
}
}
/**
* A widget for a code cell.
*/
export class CodeCell extends Cell {
/**
* Construct a code cell widget.
*/
constructor(options) {
var _a;
super({ layout: new CodeCellLayout(), ...options, placeholder: true });
this._headingsCache = null;
this._outputHidden = false;
this._outputWrapper = null;
this._outputPlaceholder = null;
this._syncScrolled = false;
this.addClass(CODE_CELL_CLASS);
const trans = this.translator.load('jupyterlab');
// Only save options not handled by parent constructor.
const rendermime = (this._rendermime = options.rendermime);
const contentFactory = this.contentFactory;
const model = this.model;
this.maxNumberOutputs = options.maxNumberOutputs;
// Note that modifying the below label warrants one to also modify
// the same in this._outputLengthHandler. Ideally, this label must
// have been a constant and used in both places but it is not done
// so because of limitations in the translation manager.
const ariaLabel = model.outputs.length === 0
? trans.__('Code Cell Content')
: trans.__('Code Cell Content with Output');
this.node.setAttribute('aria-label', ariaLabel);
const output = (this._output = new OutputArea({
model: this.model.outputs,
rendermime,
contentFactory: contentFactory,
maxNumberOutputs: this.maxNumberOutputs,
translator: this.translator,
promptOverlay: true,
inputHistoryScope: options.inputHistoryScope
}));
output.addClass(CELL_OUTPUT_AREA_CLASS);
output.toggleScrolling.connect(() => {
this.outputsScrolled = !this.outputsScrolled;
});
// Defer setting placeholder as OutputArea must be instantiated before initializing the DOM
this.placeholder = (_a = options.placeholder) !== null && _a !== void 0 ? _a : true;
model.outputs.changed.connect(this.onOutputChanged, this);
model.outputs.stateChanged.connect(this.onOutputChanged, this);
model.stateChanged.connect(this.onStateChanged, this);
}
/**
* Create children widgets.
*/
initializeDOM() {
if (!this.placeholder) {
return;
}
super.initializeDOM();
this.setPrompt(this.prompt);
// Insert the output before the cell footer.
const outputWrapper = (this._outputWrapper = new Panel());
outputWrapper.addClass(CELL_OUTPUT_WRAPPER_CLASS);
const outputCollapser = new OutputCollapser();
outputCollapser.addClass(CELL_OUTPUT_COLLAPSER_CLASS);
outputWrapper.addWidget(outputCollapser);
// Set a CSS if there are no outputs, and connect a signal for future
// changes to the number of outputs. This is for conditional styling
// if there are no outputs.
if (this.model.outputs.length === 0) {
this.addClass(NO_OUTPUTS_CLASS);
}
this._output.outputLengthChanged.connect(this._outputLengthHandler, this);
outputWrapper.addWidget(this._output);
const layout = this.layout;
layout.insertWidget(layout.widgets.length - 1, new ResizeHandle(this.node));
layout.insertWidget(layout.widgets.length - 1, outputWrapper);
if (this.model.isDirty) {
this.addClass(DIRTY_CLASS);
}
this._outputPlaceholder = new OutputPlaceholder({
callback: () => {
this.outputHidden = !this.outputHidden;
},
text: this.getOutputPlaceholderText(),
translator: this.translator
});
const layoutWrapper = outputWrapper.layout;
if (this.outputHidden) {
layoutWrapper.removeWidget(this._output);
layoutWrapper.addWidget(this._outputPlaceholder);
if (this.inputHidden && !outputWrapper.isHidden) {
this._outputWrapper.hide();
}
}
const trans = this.translator.load('jupyterlab');
const ariaLabel = this.model.outputs.length === 0
? trans.__('Code Cell Content')
: trans.__('Code Cell Content with Output');
this.node.setAttribute('aria-label', ariaLabel);
}
getOutputPlaceholderText() {
var _a;
const firstOutput = this.model.outputs.get(0);
const outputData = firstOutput === null || firstOutput === void 0 ? void 0 : firstOutput.data;
if (!outputData) {
return undefined;
}
const supportedOutputTypes = [
'text/html',
'image/svg+xml',
'application/pdf',
'text/markdown',
'text/plain',
'application/vnd.jupyter.stderr',
'application/vnd.jupyter.stdout',
'text'
];
const preferredOutput = supportedOutputTypes.find(mt => {
const data = firstOutput.data[mt];
return (Array.isArray(data) ? typeof data[0] : typeof data) === 'string';
});
const dataToDisplay = firstOutput.data[preferredOutput !== null && preferredOutput !== void 0 ? preferredOutput : ''];
if (dataToDisplay !== undefined) {
return (_a = (Array.isArray(dataToDisplay)
? dataToDisplay
: dataToDisplay === null || dataToDisplay === void 0 ? void 0 : dataToDisplay.split('\n'))) === null || _a === void 0 ? void 0 : _a.find(part => part !== '');
}
return undefined;
}
/**
* Initialize view state from model.
*
* #### Notes
* Should be called after construction. For convenience, returns this, so it
* can be chained in the construction, like `new Foo().initializeState();`
*/
initializeState() {
super.initializeState();
this.loadScrolledState();
this.setPrompt(`${this.model.executionCount || ''}`);
return this;
}
get headings() {
if (!this._headingsCache) {
const headings = [];
// Iterate over the code cell outputs to check for Markdown or HTML from which we can generate ToC headings...
const outputs = this.model.outputs;
for (let j = 0; j < outputs.length; j++) {
const m = outputs.get(j);
let htmlType = null;
let mdType = null;
Object.keys(m.data).forEach(t => {
if (!mdType && TableOfContentsUtils.Markdown.isMarkdown(t)) {
mdType = t;
}
else if (!htmlType && TableOfContentsUtils.isHTML(t)) {
htmlType = t;
}
});
// Parse HTML output
if (htmlType) {
headings.push(...TableOfContentsUtils.getHTMLHeadings(this._rendermime.sanitizer.sanitize(m.data[htmlType])).map(heading => {
return {
...heading,
outputIndex: j,
type: Cell.HeadingType.HTML
};
}));
}
else if (mdType) {
headings.push(...TableOfContentsUtils.Markdown.getHeadings(m.data[mdType]).map(heading => {
return {
...heading,
outputIndex: j,
type: Cell.HeadingType.Markdown
};
}));
}
}
this._headingsCache = headings;
}
return [...this._headingsCache];
}
/**
* Get the output area for the cell.
*/
get outputArea() {
return this._output;
}
/**
* The view state of output being collapsed.
*/
get outputHidden() {
return this._outputHidden;
}
set outputHidden(value) {
var _a;
if (this._outputHidden === value) {
return;
}
if (!this.placeholder) {
const layout = this._outputWrapper.layout;
if (value) {
layout.removeWidget(this._output);
layout.addWidget(this._outputPlaceholder);
if (this.inputHidden && !this._outputWrapper.isHidden) {
this._outputWrapper.hide();
}
if (this._outputPlaceholder) {
this._outputPlaceholder.text = (_a = this.getOutputPlaceholderText()) !== null && _a !== void 0 ? _a : '';
}
}
else {
if (this._outputWrapper.isHidden) {
this._outputWrapper.show();
}
layout.removeWidget(this._outputPlaceholder);
layout.addWidget(this._output);
}
}
this._outputHidden = value;
if (this.syncCollapse) {
this.saveCollapseState();
}
}
/**
* Save view collapse state to model
*/
saveCollapseState() {
// Because collapse state for a code cell involves two different pieces of
// metadata (the `collapsed` and `jupyter` metadata keys), we block reacting
// to changes in metadata until we have fully committed our changes.
// Otherwise setting one key can trigger a write to the other key to
// maintain the synced consistency.
this.model.sharedModel.transact(() => {
super.saveCollapseState();
const collapsed = this.model.getMetadata('collapsed');
if ((this.outputHidden && collapsed === true) ||
(!this.outputHidden && collapsed === undefined)) {
return;
}
// Do not set jupyter.outputs_hidden since it is redundant. See
// and https://github.com/jupyter/nbformat/issues/137
if (this.outputHidden) {
this.model.setMetadata('collapsed', true);
}
else {
this.model.deleteMetadata('collapsed');
}
}, false);
}
/**
* Revert view collapse state from model.
*
* We consider the `collapsed` metadata key as the source of truth for outputs
* being hidden.
*/
loadCollapseState() {
super.loadCollapseState();
this.outputHidden = !!this.model.getMetadata('collapsed');
}
/**
* Whether the output is in a scrolled state?
*/
get outputsScrolled() {
return this._outputsScrolled;
}
set outputsScrolled(value) {
this.toggleClass('jp-mod-outputsScrolled', value);
this._outputsScrolled = value;
if (this.syncScrolled) {
this.saveScrolledState();
}
}
/**
* Save view collapse state to model
*/
saveScrolledState() {
const current = this.model.getMetadata('scrolled');
if ((this.outputsScrolled && current === true) ||
(!this.outputsScrolled && current === undefined)) {
return;
}
if (this.outputsScrolled) {
this.model.setMetadata('scrolled', true);
}
else {
this.model.deleteMetadata('scrolled');
}
}
/**
* Revert view collapse state from model.
*/
loadScrolledState() {
// We don't have the notion of 'auto' scrolled, so we make it false.
if (this.model.getMetadata('scrolled') === 'auto') {
this.outputsScrolled = false;
}
else {
this.outputsScrolled = !!this.model.getMetadata('scrolled');
}
}
/**
* Whether to sync the scrolled state to the cell model.
*/
get syncScrolled() {
return this._syncScrolled;
}
set syncScrolled(value) {
if (this._syncScrolled === value) {
return;
}
this._syncScrolled = value;
if (value) {
this.loadScrolledState();
}
}
/**
* Handle the input being hidden.
*
* #### Notes
* This method is called by the case cell implementation and is
* subclasses here so the code cell can watch to see when input
* is hidden without accessing private state.
*/
handleInputHidden(value) {
if (this.placeholder) {
return;
}
if (!value && this._outputWrapper.isHidden) {
this._outputWrapper.show();
}
else if (value && !this._outputWrapper.isHidden && this._outputHidden) {
this._outputWrapper.hide();
}
}
/**
* Clone the cell, using the same model.
*/
clone() {
const constructor = this.constructor;
return new constructor({
model: this.model,
contentFactory: this.contentFactory,
rendermime: this._rendermime,
placeholder: false,
translator: this.translator
});
}
/**
* Clone the OutputArea alone, returning a simplified output area, using the same model.
*/
cloneOutputArea() {
return new SimplifiedOutputArea({
model: this.model.outputs,
contentFactory: this.contentFactory,
rendermime: this._rendermime
});
}
/**
* Dispose of the resources used by the widget.
*/
dispose() {
if (this.isDisposed) {
return;
}
this._output.outputLengthChanged.disconnect(this._outputLengthHandler, this);
this._rendermime = null;
this._output = null;
this._outputWrapper = null;
this._outputPlaceholder = null;
super.dispose();
}
/**
* Handle changes in the model.
*/
onStateChanged(model, args) {
switch (args.name) {
case 'executionCount':
this.setPrompt(`${model.executionCount || ''}`);
break;
case 'isDirty':
if (model.isDirty) {
this.addClass(DIRTY_CLASS);
}
else {
this.removeClass(DIRTY_CLASS);
}
break;
default:
break;
}
}
/**
* Callback on output changes
*/
onOutputChanged() {
var _a;
this._headingsCache = null;
if (this._outputPlaceholder && this.outputHidden) {
this._outputPlaceholder.text = (_a = this.getOutputPlaceholderText()) !== null && _a !== void 0 ? _a : '';
}
}
/**
* Handle changes in the metadata.
*/
onMetadataChanged(model, args) {
switch (args.key) {
case 'scrolled':
if (this.syncScrolled) {
this.loadScrolledState();
}
break;
case 'collapsed':
if (this.syncCollapse) {
this.loadCollapseState();
}
break;
default:
break;
}
super.onMetadataChanged(model, args);
}
/**
* Handle changes in the number of outputs in the output area.
*/
_outputLengthHandler(sender, args) {
const force = args === 0 ? true : false;
this.toggleClass(NO_OUTPUTS_CLASS, force);
const trans = this.translator.load('jupyterlab');
const ariaLabel = force
? trans.__('Code Cell Content')
: trans.__('Code Cell Content with Output');
this.node.setAttribute('aria-label', ariaLabel);
}
}
/**
* The namespace for the `CodeCell` class statics.
*/
(function (CodeCell) {
/**
* Execute a cell given a client session.
*/
async function execute(cell, sessionContext, metadata) {
var _a;
const model = cell.model;
const code = model.sharedModel.getSource();
if (!code.trim() || !((_a = sessionContext.session) === null || _a === void 0 ? void 0 : _a.kernel)) {
model.sharedModel.transact(() => {
model.clearExecution();
}, false);
return;
}
const cellId = { cellId: model.sharedModel.getId() };
metadata = {
...model.metadata,
...metadata,
...cellId
};
const { recordTiming } = metadata;
model.sharedModel.transact(() => {
model.clearExecution();
cell.outputHidden = false;
}, false);
cell.setPrompt('*');
model.trusted = true;
let future;
try {
const msgPromise = OutputArea.execute(code, cell.outputArea, sessionContext, metadata);
// cell.outputArea.future assigned synchronously in `execute`
if (recordTiming) {
const recordTimingHook = (msg) => {
let label;
switch (msg.header.msg_type) {
case 'status':
label = `status.${msg.content.execution_state}`;
break;
case 'execute_input':
label = 'execute_input';
break;
default:
return true;
}
// If the data is missing, estimate it to now
// Date was added in 5.1: https://jupyter-client.readthedocs.io/en/stable/messaging.html#message-header
const value = msg.header.date || new Date().toISOString();
const timingInfo = Object.assign({}, model.getMetadata('execution'));
timingInfo[`iopub.${label}`] = value;
model.setMetadata('execution', timingInfo);
return true;
};
cell.outputArea.future.registerMessageHook(recordTimingHook);
}
else {
model.deleteMetadata('execution');
}
// Save this execution's future so we can compare in the catch below.
future = cell.outputArea.future;
const msg = (await msgPromise);
model.executionCount = msg.content.execution_count;
if (recordTiming) {
const timingInfo = Object.assign({}, model.getMetadata('execution'));
const started = msg.metadata.started;
// Started is not in the API, but metadata IPyKernel sends
if (started) {
timingInfo['shell.execute_reply.started'] = started;
}
// Per above, the 5.0 spec does not assume date, so we estimate is required
const finished = msg.header.date;
timingInfo['shell.execute_reply'] =
finished || new Date().toISOString();
model.setMetadata('execution', timingInfo);
}
return msg;
}
catch (e) {
// If we started executing, and the cell is still indicating this
// execution, clear the prompt.
if (future && !cell.isDisposed && cell.outputArea.future === future) {
cell.setPrompt('');
}
throw e;
}
}
CodeCell.execute = execute;
})(CodeCell || (CodeCell = {}));
/**
* `AttachmentsCell` - A base class for a cell widget that allows
* attachments to be drag/drop'd or pasted onto it
*/
export class AttachmentsCell extends Cell {
/**
* Handle the DOM events for the widget.
*
* @param event - The DOM event sent to the widget.
*
* #### Notes
* This method implements the DOM `EventListener` interface and is
* called in response to events on the notebook panel's node. It should
* not be called directly by user code.
*/
handleEvent(event) {
switch (event.type) {
case 'lm-dragover':
this._evtDragOver(event);
break;
case 'lm-drop':
this._evtDrop(event);
break;
default:
break;
}
}
/**
* Get the editor options at initialization.
*
* @returns Editor options
*/
getEditorOptions() {
var _a, _b;
const base = (_a = super.getEditorOptions()) !== null && _a !== void 0 ? _a : {};
base.extensions = [
...((_b = base.extensions) !== null && _b !== void 0 ? _b : []),
EditorView.domEventHandlers({
dragenter: (event) => {
event.preventDefault();
},
dragover: (event) => {
event.preventDefault();
},
drop: (event) => {
this._evtNativeDrop(event);
},
paste: (event) => {
this._evtPaste(event);
}
})
];
return base;
}
/**
* Handle `after-attach` messages for the widget.
*/
onAfterAttach(msg) {
super.onAfterAttach(msg);
const node = this.node;
node.addEventListener('lm-dragover', this);
node.addEventListener('lm-drop', this);
}
/**
* A message handler invoked on a `'before-detach'`
* message
*/
onBeforeDetach(msg) {
const node = this.node;
node.removeEventListener('lm-dragover', this);
node.removeEventListener('lm-drop', this);
super.onBeforeDetach(msg);
}
_evtDragOver(event) {
const supportedMimeType = some(imageRendererFactory.mimeTypes, mimeType => {
if (!event.mimeData.hasData(CONTENTS_MIME_RICH)) {
return false;
}
const data = event.mimeData.getData(CONTENTS_MIME_RICH);
return data.model.mimetype === mimeType;
});
if (!supportedMimeType) {
return;
}
event.preventDefault();
event.stopPropagation();
event.dropAction = event.proposedAction;
}
/**
* Handle the `paste` event for the widget
*/
_evtPaste(event) {
if (event.clipboardData) {
const items = event.clipboardData.items;
for (let i = 0; i < items.length; i++) {
if (items[i].type === 'text/plain') {
// Skip if this text is the path to a file
if (i < items.length - 1 && items[i + 1].kind === 'file') {
continue;
}
items[i].getAsString(text => {
var _a, _b;
(_b = (_a = this.editor).replaceSelection) === null || _b === void 0 ? void 0 : _b.call(_a, text);
});
}
this._attachFiles(event.clipboardData.items);
}
}
event.preventDefault();
}
/**
* Handle the `drop` event for the widget
*/
_evtNativeDrop(event) {
if (event.dataTransfer) {
this._attachFiles(event.dataTransfer.items);
}
event.preventDefault();
}
/**
* Handle the `'lm-drop'` event for the widget.
*/
_evtDrop(event) {
const supportedMimeTypes = event.mimeData.types().filter(mimeType => {
if (mimeType === CONTENTS_MIME_RICH) {
const data = event.mimeData.getData(CONTENTS_MIME_RICH);
return (imageRendererFactory.mimeTypes.indexOf(data.model.mimetype) !== -1);
}
return imageRendererFactory.mimeTypes.indexOf(mimeType) !== -1;
});
if (supportedMimeTypes.length === 0) {
return;
}
event.preventDefault();
event.stopPropagation();
if (event.proposedAction === 'none') {
event.dropAction = 'none';
return;
}
event.dropAction = 'copy';
for (const mimeType of supportedMimeTypes) {
if (mimeType === CONTENTS_MIME_RICH) {
const { model, withContent } = event.mimeData.getData(CONTENTS_MIME_RICH);
if (model.type === 'file') {
const URI = this._generateURI(model.name);
this.updateCellSourceWithAttachment(model.name, URI);
void withContent().then(fullModel => {
this.model.attachments.set(URI, {
[fullModel.mimetype]: fullModel.content
});
});
}
}
else {
// Pure mimetype, no useful name to infer
const URI = this._generateURI();
this.model.attachments.set(URI, {
[mimeType]: event.mimeData.getData(mimeType)
});
this.updateCellSourceWithAttachment(URI, URI);
}
}
}
/**
* Attaches all DataTransferItems (obtained from
* clipboard or native drop events) to the cell
*/
_attachFiles(items) {
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.kind === 'file') {
const blob = item.getAsFile();
if (blob) {
this._attachFile(blob);
}
}
}
}
/**
* Takes in a file object and adds it to
* the cell attachments
*/
_attachFile(blob) {
const reader = new FileReader();
reader.onload = evt => {
const { href, protocol } = URLExt.parse(reader.result);
if (protocol !== 'data:') {
return;
}
const dataURIRegex = /([\w+\/\+]+)?(?:;(charset=[\w\d-]*|base64))?,(.*)/;
const matches = dataURIRegex.exec(href);
if (!matches || matches.length !== 4) {
return;
}
const mimeType = matches[1];
const encodedData = matches[3];
const bundle = { [mimeType]: encodedData };
const URI = this._generateURI(blob.name);
if (mimeType.startsWith('image/')) {
this.model.attachments.set(URI, bundle);
this.updateCellSourceWithAttachment(blob.name, URI);
}
};
reader.onerror = evt => {
console.error(`Failed to attach ${blob.name}` + evt);
};
reader.readAsDataURL(blob);
}
/**
* Generates a unique URI for a file
* while preserving the file extension.
*/
_generateURI(name = '') {
const lastIndex = name.lastIndexOf('.');
return lastIndex !== -1
? UUID.uuid4().concat(name.substring(lastIndex))
: UUID.uuid4();
}
}
/** ****************************************************************************
* MarkdownCell
******************************************************************************/
/**
* A widget for a Markdown cell.
*
* #### Notes
* Things get complicated if we want the rendered text to update
* any time the text changes, the text editor model changes,
* or the input area model changes. We don't support automatically
* updating the rendered text in all of these cases.
*/
export class MarkdownCell extends AttachmentsCell {
/**
* Construct a Markdown cell widget.
*/
constructor(options) {
var _a, _b, _c, _d;
super({ ...options, placeholder: true });
this._headingsCache = null;
this._headingCollapsedChanged = new Signal(this);
this._prevText = '';
this._rendered = true;
this._renderedChanged = new Signal(this);