UNPKG

@jupyterlab/cells

Version:
1,483 lines (1,482 loc) 64.1 kB
/* ----------------------------------------------------------------------------- | 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);