UNPKG

47.6 kBJavaScriptView Raw
1/* -----------------------------------------------------------------------------
2| Copyright (c) Jupyter Development Team.
3| Distributed under the terms of the Modified BSD License.
4|----------------------------------------------------------------------------*/
5import { marked } from 'marked';
6import { AttachmentsResolver } from '@jupyterlab/attachments';
7import { ActivityMonitor, URLExt } from '@jupyterlab/coreutils';
8import { OutputArea, OutputPrompt, SimplifiedOutputArea, Stdin } from '@jupyterlab/outputarea';
9import { imageRendererFactory, MimeModel } from '@jupyterlab/rendermime';
10import { addIcon } from '@jupyterlab/ui-components';
11import { PromiseDelegate, UUID } from '@lumino/coreutils';
12import { filter, some, toArray } from '@lumino/algorithm';
13import { Debouncer } from '@lumino/polling';
14import { Signal } from '@lumino/signaling';
15import { Panel, PanelLayout, Widget } from '@lumino/widgets';
16import { InputCollapser, OutputCollapser } from './collapser';
17import { CellFooter, CellHeader } from './headerfooter';
18import { InputArea, InputPrompt } from './inputarea';
19import { InputPlaceholder, OutputPlaceholder } from './placeholder';
20import { ResizeHandle } from './resizeHandle';
21/**
22 * The CSS class added to cell widgets.
23 */
24const CELL_CLASS = 'jp-Cell';
25/**
26 * The CSS class added to the cell header.
27 */
28const CELL_HEADER_CLASS = 'jp-Cell-header';
29/**
30 * The CSS class added to the cell footer.
31 */
32const CELL_FOOTER_CLASS = 'jp-Cell-footer';
33/**
34 * The CSS class added to the cell input wrapper.
35 */
36const CELL_INPUT_WRAPPER_CLASS = 'jp-Cell-inputWrapper';
37/**
38 * The CSS class added to the cell output wrapper.
39 */
40const CELL_OUTPUT_WRAPPER_CLASS = 'jp-Cell-outputWrapper';
41/**
42 * The CSS class added to the cell input area.
43 */
44const CELL_INPUT_AREA_CLASS = 'jp-Cell-inputArea';
45/**
46 * The CSS class added to the cell output area.
47 */
48const CELL_OUTPUT_AREA_CLASS = 'jp-Cell-outputArea';
49/**
50 * The CSS class added to the cell input collapser.
51 */
52const CELL_INPUT_COLLAPSER_CLASS = 'jp-Cell-inputCollapser';
53/**
54 * The CSS class added to the cell output collapser.
55 */
56const CELL_OUTPUT_COLLAPSER_CLASS = 'jp-Cell-outputCollapser';
57/**
58 * The class name added to the cell when readonly.
59 */
60const READONLY_CLASS = 'jp-mod-readOnly';
61/**
62 * The class name added to the cell when dirty.
63 */
64const DIRTY_CLASS = 'jp-mod-dirty';
65/**
66 * The class name added to code cells.
67 */
68const CODE_CELL_CLASS = 'jp-CodeCell';
69/**
70 * The class name added to markdown cells.
71 */
72const MARKDOWN_CELL_CLASS = 'jp-MarkdownCell';
73/**
74 * The class name added to rendered markdown output widgets.
75 */
76const MARKDOWN_OUTPUT_CLASS = 'jp-MarkdownOutput';
77export const MARKDOWN_HEADING_COLLAPSED = 'jp-MarkdownHeadingCollapsed';
78const HEADING_COLLAPSER_CLASS = 'jp-collapseHeadingButton';
79const SHOW_HIDDEN_CELLS_CLASS = 'jp-showHiddenCellsButton';
80/**
81 * The class name added to raw cells.
82 */
83const RAW_CELL_CLASS = 'jp-RawCell';
84/**
85 * The class name added to a rendered input area.
86 */
87const RENDERED_CLASS = 'jp-mod-rendered';
88const NO_OUTPUTS_CLASS = 'jp-mod-noOutputs';
89/**
90 * The text applied to an empty markdown cell.
91 */
92const DEFAULT_MARKDOWN_TEXT = 'Type Markdown and LaTeX: $ α^2 $';
93/**
94 * The timeout to wait for change activity to have ceased before rendering.
95 */
96const RENDER_TIMEOUT = 1000;
97/**
98 * The mime type for a rich contents drag object.
99 */
100const CONTENTS_MIME_RICH = 'application/x-jupyter-icontentsrich';
101/** ****************************************************************************
102 * Cell
103 ******************************************************************************/
104/**
105 * A base cell widget.
106 */
107export class Cell extends Widget {
108 /**
109 * Construct a new base cell widget.
110 */
111 constructor(options) {
112 super();
113 this._displayChanged = new Signal(this);
114 this._readOnly = false;
115 this._inputHidden = false;
116 this._syncCollapse = false;
117 this._syncEditable = false;
118 this._resizeDebouncer = new Debouncer(() => {
119 this._displayChanged.emit();
120 }, 0);
121 this.addClass(CELL_CLASS);
122 const model = (this._model = options.model);
123 const contentFactory = (this.contentFactory =
124 options.contentFactory || Cell.defaultContentFactory);
125 this.layout = new PanelLayout();
126 // Header
127 const header = contentFactory.createCellHeader();
128 header.addClass(CELL_HEADER_CLASS);
129 this.layout.addWidget(header);
130 // Input
131 const inputWrapper = (this._inputWrapper = new Panel());
132 inputWrapper.addClass(CELL_INPUT_WRAPPER_CLASS);
133 const inputCollapser = new InputCollapser();
134 inputCollapser.addClass(CELL_INPUT_COLLAPSER_CLASS);
135 const input = (this._input = new InputArea({
136 model,
137 contentFactory,
138 updateOnShow: options.updateEditorOnShow,
139 placeholder: options.placeholder
140 }));
141 input.addClass(CELL_INPUT_AREA_CLASS);
142 inputWrapper.addWidget(inputCollapser);
143 inputWrapper.addWidget(input);
144 this.layout.addWidget(inputWrapper);
145 this._inputPlaceholder = new InputPlaceholder(() => {
146 this.inputHidden = !this.inputHidden;
147 });
148 // Footer
149 const footer = this.contentFactory.createCellFooter();
150 footer.addClass(CELL_FOOTER_CLASS);
151 this.layout.addWidget(footer);
152 // Editor settings
153 if (options.editorConfig) {
154 this.editor.setOptions(Object.assign({}, options.editorConfig));
155 }
156 model.metadata.changed.connect(this.onMetadataChanged, this);
157 }
158 /**
159 * Initialize view state from model.
160 *
161 * #### Notes
162 * Should be called after construction. For convenience, returns this, so it
163 * can be chained in the construction, like `new Foo().initializeState();`
164 */
165 initializeState() {
166 this.loadCollapseState();
167 this.loadEditableState();
168 return this;
169 }
170 /**
171 * Signal to indicate that widget has changed visibly (in size, in type, etc)
172 */
173 get displayChanged() {
174 return this._displayChanged;
175 }
176 /**
177 * Get the prompt node used by the cell.
178 */
179 get promptNode() {
180 if (!this._inputHidden) {
181 return this._input.promptNode;
182 }
183 else {
184 return this._inputPlaceholder.node
185 .firstElementChild;
186 }
187 }
188 /**
189 * Get the CodeEditorWrapper used by the cell.
190 */
191 get editorWidget() {
192 return this._input.editorWidget;
193 }
194 /**
195 * Get the CodeEditor used by the cell.
196 */
197 get editor() {
198 return this._input.editor;
199 }
200 /**
201 * Get the model used by the cell.
202 */
203 get model() {
204 return this._model;
205 }
206 /**
207 * Get the input area for the cell.
208 */
209 get inputArea() {
210 return this._input;
211 }
212 /**
213 * The read only state of the cell.
214 */
215 get readOnly() {
216 return this._readOnly;
217 }
218 set readOnly(value) {
219 if (value === this._readOnly) {
220 return;
221 }
222 this._readOnly = value;
223 if (this.syncEditable) {
224 this.saveEditableState();
225 }
226 this.update();
227 }
228 /**
229 * Save view editable state to model
230 */
231 saveEditableState() {
232 const { metadata } = this.model;
233 const current = metadata.get('editable');
234 if ((this.readOnly && current === false) ||
235 (!this.readOnly && current === undefined)) {
236 return;
237 }
238 if (this.readOnly) {
239 this.model.metadata.set('editable', false);
240 }
241 else {
242 this.model.metadata.delete('editable');
243 }
244 }
245 /**
246 * Load view editable state from model.
247 */
248 loadEditableState() {
249 this.readOnly = this.model.metadata.get('editable') === false;
250 }
251 /**
252 * A promise that resolves when the widget renders for the first time.
253 */
254 get ready() {
255 return Promise.resolve(undefined);
256 }
257 /**
258 * Set the prompt for the widget.
259 */
260 setPrompt(value) {
261 this._input.setPrompt(value);
262 }
263 /**
264 * The view state of input being hidden.
265 */
266 get inputHidden() {
267 return this._inputHidden;
268 }
269 set inputHidden(value) {
270 if (this._inputHidden === value) {
271 return;
272 }
273 const layout = this._inputWrapper.layout;
274 if (value) {
275 this._input.parent = null;
276 layout.addWidget(this._inputPlaceholder);
277 }
278 else {
279 this._inputPlaceholder.parent = null;
280 layout.addWidget(this._input);
281 }
282 this._inputHidden = value;
283 if (this.syncCollapse) {
284 this.saveCollapseState();
285 }
286 this.handleInputHidden(value);
287 }
288 /**
289 * Save view collapse state to model
290 */
291 saveCollapseState() {
292 const jupyter = Object.assign({}, this.model.metadata.get('jupyter'));
293 if ((this.inputHidden && jupyter.source_hidden === true) ||
294 (!this.inputHidden && jupyter.source_hidden === undefined)) {
295 return;
296 }
297 if (this.inputHidden) {
298 jupyter.source_hidden = true;
299 }
300 else {
301 delete jupyter.source_hidden;
302 }
303 if (Object.keys(jupyter).length === 0) {
304 this.model.metadata.delete('jupyter');
305 }
306 else {
307 this.model.metadata.set('jupyter', jupyter);
308 }
309 }
310 /**
311 * Revert view collapse state from model.
312 */
313 loadCollapseState() {
314 const jupyter = this.model.metadata.get('jupyter') || {};
315 this.inputHidden = !!jupyter.source_hidden;
316 }
317 /**
318 * Handle the input being hidden.
319 *
320 * #### Notes
321 * This is called by the `inputHidden` setter so that subclasses
322 * can perform actions upon the input being hidden without accessing
323 * private state.
324 */
325 handleInputHidden(value) {
326 return;
327 }
328 /**
329 * Whether to sync the collapse state to the cell model.
330 */
331 get syncCollapse() {
332 return this._syncCollapse;
333 }
334 set syncCollapse(value) {
335 if (this._syncCollapse === value) {
336 return;
337 }
338 this._syncCollapse = value;
339 if (value) {
340 this.loadCollapseState();
341 }
342 }
343 /**
344 * Whether to sync the editable state to the cell model.
345 */
346 get syncEditable() {
347 return this._syncEditable;
348 }
349 set syncEditable(value) {
350 if (this._syncEditable === value) {
351 return;
352 }
353 this._syncEditable = value;
354 if (value) {
355 this.loadEditableState();
356 }
357 }
358 /**
359 * Clone the cell, using the same model.
360 */
361 clone() {
362 const constructor = this.constructor;
363 return new constructor({
364 model: this.model,
365 contentFactory: this.contentFactory,
366 placeholder: false
367 });
368 }
369 /**
370 * Dispose of the resources held by the widget.
371 */
372 dispose() {
373 // Do nothing if already disposed.
374 if (this.isDisposed) {
375 return;
376 }
377 this._resizeDebouncer.dispose();
378 this._input = null;
379 this._model = null;
380 this._inputWrapper = null;
381 this._inputPlaceholder = null;
382 super.dispose();
383 }
384 /**
385 * Handle `after-attach` messages.
386 */
387 onAfterAttach(msg) {
388 this.update();
389 }
390 /**
391 * Handle `'activate-request'` messages.
392 */
393 onActivateRequest(msg) {
394 this.editor.focus();
395 }
396 /**
397 * Handle `fit-request` messages.
398 */
399 onFitRequest(msg) {
400 // need this for for when a theme changes font size
401 this.editor.refresh();
402 }
403 /**
404 * Handle `resize` messages.
405 */
406 onResize(msg) {
407 void this._resizeDebouncer.invoke();
408 }
409 /**
410 * Handle `update-request` messages.
411 */
412 onUpdateRequest(msg) {
413 if (!this._model) {
414 return;
415 }
416 // Handle read only state.
417 if (this.editor.getOption('readOnly') !== this._readOnly) {
418 this.editor.setOption('readOnly', this._readOnly);
419 this.toggleClass(READONLY_CLASS, this._readOnly);
420 }
421 }
422 /**
423 * Handle changes in the metadata.
424 */
425 onMetadataChanged(model, args) {
426 switch (args.key) {
427 case 'jupyter':
428 if (this.syncCollapse) {
429 this.loadCollapseState();
430 }
431 break;
432 case 'editable':
433 if (this.syncEditable) {
434 this.loadEditableState();
435 }
436 break;
437 default:
438 break;
439 }
440 }
441}
442/**
443 * The namespace for the `Cell` class statics.
444 */
445(function (Cell) {
446 /**
447 * The default implementation of an `IContentFactory`.
448 *
449 * This includes a CodeMirror editor factory to make it easy to use out of the box.
450 */
451 class ContentFactory {
452 /**
453 * Create a content factory for a cell.
454 */
455 constructor(options = {}) {
456 this._editorFactory =
457 options.editorFactory || InputArea.defaultEditorFactory;
458 }
459 /**
460 * The readonly editor factory that create code editors
461 */
462 get editorFactory() {
463 return this._editorFactory;
464 }
465 /**
466 * Create a new cell header for the parent widget.
467 */
468 createCellHeader() {
469 return new CellHeader();
470 }
471 /**
472 * Create a new cell header for the parent widget.
473 */
474 createCellFooter() {
475 return new CellFooter();
476 }
477 /**
478 * Create an input prompt.
479 */
480 createInputPrompt() {
481 return new InputPrompt();
482 }
483 /**
484 * Create the output prompt for the widget.
485 */
486 createOutputPrompt() {
487 return new OutputPrompt();
488 }
489 /**
490 * Create an stdin widget.
491 */
492 createStdin(options) {
493 return new Stdin(options);
494 }
495 }
496 Cell.ContentFactory = ContentFactory;
497 /**
498 * The default content factory for cells.
499 */
500 Cell.defaultContentFactory = new ContentFactory();
501})(Cell || (Cell = {}));
502/** ****************************************************************************
503 * CodeCell
504 ******************************************************************************/
505/**
506 * A widget for a code cell.
507 */
508export class CodeCell extends Cell {
509 /**
510 * Construct a code cell widget.
511 */
512 constructor(options) {
513 super(options);
514 this._outputHidden = false;
515 this._syncScrolled = false;
516 this._savingMetadata = false;
517 this.addClass(CODE_CELL_CLASS);
518 // Only save options not handled by parent constructor.
519 const rendermime = (this._rendermime = options.rendermime);
520 const contentFactory = this.contentFactory;
521 const model = this.model;
522 if (!options.placeholder) {
523 // Insert the output before the cell footer.
524 const outputWrapper = (this._outputWrapper = new Panel());
525 outputWrapper.addClass(CELL_OUTPUT_WRAPPER_CLASS);
526 const outputCollapser = new OutputCollapser();
527 outputCollapser.addClass(CELL_OUTPUT_COLLAPSER_CLASS);
528 const output = (this._output = new OutputArea({
529 model: model.outputs,
530 rendermime,
531 contentFactory: contentFactory,
532 maxNumberOutputs: options.maxNumberOutputs
533 }));
534 output.addClass(CELL_OUTPUT_AREA_CLASS);
535 // Set a CSS if there are no outputs, and connect a signal for future
536 // changes to the number of outputs. This is for conditional styling
537 // if there are no outputs.
538 if (model.outputs.length === 0) {
539 this.addClass(NO_OUTPUTS_CLASS);
540 }
541 output.outputLengthChanged.connect(this._outputLengthHandler, this);
542 outputWrapper.addWidget(outputCollapser);
543 outputWrapper.addWidget(output);
544 this.layout.insertWidget(2, new ResizeHandle(this.node));
545 this.layout.insertWidget(3, outputWrapper);
546 if (model.isDirty) {
547 this.addClass(DIRTY_CLASS);
548 }
549 this._outputPlaceholder = new OutputPlaceholder(() => {
550 this.outputHidden = !this.outputHidden;
551 });
552 }
553 model.stateChanged.connect(this.onStateChanged, this);
554 }
555 /**
556 * Initialize view state from model.
557 *
558 * #### Notes
559 * Should be called after construction. For convenience, returns this, so it
560 * can be chained in the construction, like `new Foo().initializeState();`
561 */
562 initializeState() {
563 super.initializeState();
564 this.loadScrolledState();
565 this.setPrompt(`${this.model.executionCount || ''}`);
566 return this;
567 }
568 /**
569 * Get the output area for the cell.
570 */
571 get outputArea() {
572 return this._output;
573 }
574 /**
575 * The view state of output being collapsed.
576 */
577 get outputHidden() {
578 return this._outputHidden;
579 }
580 set outputHidden(value) {
581 if (this._outputHidden === value) {
582 return;
583 }
584 const layout = this._outputWrapper.layout;
585 if (value) {
586 layout.removeWidget(this._output);
587 layout.addWidget(this._outputPlaceholder);
588 if (this.inputHidden && !this._outputWrapper.isHidden) {
589 this._outputWrapper.hide();
590 }
591 }
592 else {
593 if (this._outputWrapper.isHidden) {
594 this._outputWrapper.show();
595 }
596 layout.removeWidget(this._outputPlaceholder);
597 layout.addWidget(this._output);
598 }
599 this._outputHidden = value;
600 if (this.syncCollapse) {
601 this.saveCollapseState();
602 }
603 }
604 /**
605 * Save view collapse state to model
606 */
607 saveCollapseState() {
608 // Because collapse state for a code cell involves two different pieces of
609 // metadata (the `collapsed` and `jupyter` metadata keys), we block reacting
610 // to changes in metadata until we have fully committed our changes.
611 // Otherwise setting one key can trigger a write to the other key to
612 // maintain the synced consistency.
613 this._savingMetadata = true;
614 try {
615 super.saveCollapseState();
616 const metadata = this.model.metadata;
617 const collapsed = this.model.metadata.get('collapsed');
618 if ((this.outputHidden && collapsed === true) ||
619 (!this.outputHidden && collapsed === undefined)) {
620 return;
621 }
622 // Do not set jupyter.outputs_hidden since it is redundant. See
623 // and https://github.com/jupyter/nbformat/issues/137
624 if (this.outputHidden) {
625 metadata.set('collapsed', true);
626 }
627 else {
628 metadata.delete('collapsed');
629 }
630 }
631 finally {
632 this._savingMetadata = false;
633 }
634 }
635 /**
636 * Revert view collapse state from model.
637 *
638 * We consider the `collapsed` metadata key as the source of truth for outputs
639 * being hidden.
640 */
641 loadCollapseState() {
642 super.loadCollapseState();
643 this.outputHidden = !!this.model.metadata.get('collapsed');
644 }
645 /**
646 * Whether the output is in a scrolled state?
647 */
648 get outputsScrolled() {
649 return this._outputsScrolled;
650 }
651 set outputsScrolled(value) {
652 this.toggleClass('jp-mod-outputsScrolled', value);
653 this._outputsScrolled = value;
654 if (this.syncScrolled) {
655 this.saveScrolledState();
656 }
657 }
658 /**
659 * Save view collapse state to model
660 */
661 saveScrolledState() {
662 const { metadata } = this.model;
663 const current = metadata.get('scrolled');
664 if ((this.outputsScrolled && current === true) ||
665 (!this.outputsScrolled && current === undefined)) {
666 return;
667 }
668 if (this.outputsScrolled) {
669 metadata.set('scrolled', true);
670 }
671 else {
672 metadata.delete('scrolled');
673 }
674 }
675 /**
676 * Revert view collapse state from model.
677 */
678 loadScrolledState() {
679 const metadata = this.model.metadata;
680 // We don't have the notion of 'auto' scrolled, so we make it false.
681 if (metadata.get('scrolled') === 'auto') {
682 this.outputsScrolled = false;
683 }
684 else {
685 this.outputsScrolled = !!metadata.get('scrolled');
686 }
687 }
688 /**
689 * Whether to sync the scrolled state to the cell model.
690 */
691 get syncScrolled() {
692 return this._syncScrolled;
693 }
694 set syncScrolled(value) {
695 if (this._syncScrolled === value) {
696 return;
697 }
698 this._syncScrolled = value;
699 if (value) {
700 this.loadScrolledState();
701 }
702 }
703 /**
704 * Handle the input being hidden.
705 *
706 * #### Notes
707 * This method is called by the case cell implementation and is
708 * subclasses here so the code cell can watch to see when input
709 * is hidden without accessing private state.
710 */
711 handleInputHidden(value) {
712 if (!value && this._outputWrapper.isHidden) {
713 this._outputWrapper.show();
714 }
715 else if (value && !this._outputWrapper.isHidden && this._outputHidden) {
716 this._outputWrapper.hide();
717 }
718 }
719 /**
720 * Clone the cell, using the same model.
721 */
722 clone() {
723 const constructor = this.constructor;
724 return new constructor({
725 model: this.model,
726 contentFactory: this.contentFactory,
727 rendermime: this._rendermime,
728 placeholder: false
729 });
730 }
731 /**
732 * Clone the OutputArea alone, returning a simplified output area, using the same model.
733 */
734 cloneOutputArea() {
735 return new SimplifiedOutputArea({
736 model: this.model.outputs,
737 contentFactory: this.contentFactory,
738 rendermime: this._rendermime
739 });
740 }
741 /**
742 * Dispose of the resources used by the widget.
743 */
744 dispose() {
745 if (this.isDisposed) {
746 return;
747 }
748 this._output.outputLengthChanged.disconnect(this._outputLengthHandler, this);
749 this._rendermime = null;
750 this._output = null;
751 this._outputWrapper = null;
752 this._outputPlaceholder = null;
753 super.dispose();
754 }
755 /**
756 * Handle changes in the model.
757 */
758 onStateChanged(model, args) {
759 switch (args.name) {
760 case 'executionCount':
761 this.setPrompt(`${model.executionCount || ''}`);
762 break;
763 case 'isDirty':
764 if (model.isDirty) {
765 this.addClass(DIRTY_CLASS);
766 }
767 else {
768 this.removeClass(DIRTY_CLASS);
769 }
770 break;
771 default:
772 break;
773 }
774 }
775 /**
776 * Handle changes in the metadata.
777 */
778 onMetadataChanged(model, args) {
779 if (this._savingMetadata) {
780 // We are in middle of a metadata transaction, so don't react to it.
781 return;
782 }
783 switch (args.key) {
784 case 'scrolled':
785 if (this.syncScrolled) {
786 this.loadScrolledState();
787 }
788 break;
789 case 'collapsed':
790 if (this.syncCollapse) {
791 this.loadCollapseState();
792 }
793 break;
794 default:
795 break;
796 }
797 super.onMetadataChanged(model, args);
798 }
799 /**
800 * Handle changes in the number of outputs in the output area.
801 */
802 _outputLengthHandler(sender, args) {
803 const force = args === 0 ? true : false;
804 this.toggleClass(NO_OUTPUTS_CLASS, force);
805 }
806}
807/**
808 * The namespace for the `CodeCell` class statics.
809 */
810(function (CodeCell) {
811 /**
812 * Execute a cell given a client session.
813 */
814 async function execute(cell, sessionContext, metadata) {
815 var _a;
816 const model = cell.model;
817 const code = model.value.text;
818 if (!code.trim() || !((_a = sessionContext.session) === null || _a === void 0 ? void 0 : _a.kernel)) {
819 model.clearExecution();
820 return;
821 }
822 const cellId = { cellId: model.id };
823 metadata = Object.assign(Object.assign(Object.assign({}, model.metadata.toJSON()), metadata), cellId);
824 const { recordTiming } = metadata;
825 model.clearExecution();
826 cell.outputHidden = false;
827 cell.setPrompt('*');
828 model.trusted = true;
829 let future;
830 try {
831 const msgPromise = OutputArea.execute(code, cell.outputArea, sessionContext, metadata);
832 // cell.outputArea.future assigned synchronously in `execute`
833 if (recordTiming) {
834 const recordTimingHook = (msg) => {
835 let label;
836 switch (msg.header.msg_type) {
837 case 'status':
838 label = `status.${msg.content.execution_state}`;
839 break;
840 case 'execute_input':
841 label = 'execute_input';
842 break;
843 default:
844 return true;
845 }
846 // If the data is missing, estimate it to now
847 // Date was added in 5.1: https://jupyter-client.readthedocs.io/en/stable/messaging.html#message-header
848 const value = msg.header.date || new Date().toISOString();
849 const timingInfo = Object.assign({}, model.metadata.get('execution'));
850 timingInfo[`iopub.${label}`] = value;
851 model.metadata.set('execution', timingInfo);
852 return true;
853 };
854 cell.outputArea.future.registerMessageHook(recordTimingHook);
855 }
856 else {
857 model.metadata.delete('execution');
858 }
859 // Save this execution's future so we can compare in the catch below.
860 future = cell.outputArea.future;
861 const msg = (await msgPromise);
862 model.executionCount = msg.content.execution_count;
863 if (recordTiming) {
864 const timingInfo = Object.assign({}, model.metadata.get('execution'));
865 const started = msg.metadata.started;
866 // Started is not in the API, but metadata IPyKernel sends
867 if (started) {
868 timingInfo['shell.execute_reply.started'] = started;
869 }
870 // Per above, the 5.0 spec does not assume date, so we estimate is required
871 const finished = msg.header.date;
872 timingInfo['shell.execute_reply'] =
873 finished || new Date().toISOString();
874 model.metadata.set('execution', timingInfo);
875 }
876 return msg;
877 }
878 catch (e) {
879 // If we started executing, and the cell is still indicating this
880 // execution, clear the prompt.
881 if (future && !cell.isDisposed && cell.outputArea.future === future) {
882 cell.setPrompt('');
883 }
884 throw e;
885 }
886 }
887 CodeCell.execute = execute;
888})(CodeCell || (CodeCell = {}));
889/**
890 * `AttachmentsCell` - A base class for a cell widget that allows
891 * attachments to be drag/drop'd or pasted onto it
892 */
893export class AttachmentsCell extends Cell {
894 /**
895 * Handle the DOM events for the widget.
896 *
897 * @param event - The DOM event sent to the widget.
898 *
899 * #### Notes
900 * This method implements the DOM `EventListener` interface and is
901 * called in response to events on the notebook panel's node. It should
902 * not be called directly by user code.
903 */
904 handleEvent(event) {
905 switch (event.type) {
906 case 'paste':
907 this._evtPaste(event);
908 break;
909 case 'dragenter':
910 event.preventDefault();
911 break;
912 case 'dragover':
913 event.preventDefault();
914 break;
915 case 'drop':
916 this._evtNativeDrop(event);
917 break;
918 case 'lm-dragover':
919 this._evtDragOver(event);
920 break;
921 case 'lm-drop':
922 this._evtDrop(event);
923 break;
924 default:
925 break;
926 }
927 }
928 /**
929 * Handle `after-attach` messages for the widget.
930 */
931 onAfterAttach(msg) {
932 super.onAfterAttach(msg);
933 const node = this.node;
934 node.addEventListener('lm-dragover', this);
935 node.addEventListener('lm-drop', this);
936 node.addEventListener('dragenter', this);
937 node.addEventListener('dragover', this);
938 node.addEventListener('drop', this);
939 node.addEventListener('paste', this);
940 }
941 /**
942 * A message handler invoked on a `'before-detach'`
943 * message
944 */
945 onBeforeDetach(msg) {
946 const node = this.node;
947 node.removeEventListener('drop', this);
948 node.removeEventListener('dragover', this);
949 node.removeEventListener('dragenter', this);
950 node.removeEventListener('paste', this);
951 node.removeEventListener('lm-dragover', this);
952 node.removeEventListener('lm-drop', this);
953 }
954 _evtDragOver(event) {
955 const supportedMimeType = some(imageRendererFactory.mimeTypes, mimeType => {
956 if (!event.mimeData.hasData(CONTENTS_MIME_RICH)) {
957 return false;
958 }
959 const data = event.mimeData.getData(CONTENTS_MIME_RICH);
960 return data.model.mimetype === mimeType;
961 });
962 if (!supportedMimeType) {
963 return;
964 }
965 event.preventDefault();
966 event.stopPropagation();
967 event.dropAction = event.proposedAction;
968 }
969 /**
970 * Handle the `paste` event for the widget
971 */
972 _evtPaste(event) {
973 if (event.clipboardData) {
974 const items = event.clipboardData.items;
975 for (let i = 0; i < items.length; i++) {
976 if (items[i].type === 'text/plain') {
977 // Skip if this text is the path to a file
978 if (i < items.length - 1 && items[i + 1].kind === 'file') {
979 continue;
980 }
981 items[i].getAsString(text => {
982 var _a, _b;
983 (_b = (_a = this.editor).replaceSelection) === null || _b === void 0 ? void 0 : _b.call(_a, text);
984 });
985 }
986 this._attachFiles(event.clipboardData.items);
987 }
988 }
989 event.preventDefault();
990 }
991 /**
992 * Handle the `drop` event for the widget
993 */
994 _evtNativeDrop(event) {
995 if (event.dataTransfer) {
996 this._attachFiles(event.dataTransfer.items);
997 }
998 event.preventDefault();
999 }
1000 /**
1001 * Handle the `'lm-drop'` event for the widget.
1002 */
1003 _evtDrop(event) {
1004 const supportedMimeTypes = toArray(filter(event.mimeData.types(), mimeType => {
1005 if (mimeType === CONTENTS_MIME_RICH) {
1006 const data = event.mimeData.getData(CONTENTS_MIME_RICH);
1007 return (imageRendererFactory.mimeTypes.indexOf(data.model.mimetype) !== -1);
1008 }
1009 return imageRendererFactory.mimeTypes.indexOf(mimeType) !== -1;
1010 }));
1011 if (supportedMimeTypes.length === 0) {
1012 return;
1013 }
1014 event.preventDefault();
1015 event.stopPropagation();
1016 if (event.proposedAction === 'none') {
1017 event.dropAction = 'none';
1018 return;
1019 }
1020 event.dropAction = 'copy';
1021 for (const mimeType of supportedMimeTypes) {
1022 if (mimeType === CONTENTS_MIME_RICH) {
1023 const { model, withContent } = event.mimeData.getData(CONTENTS_MIME_RICH);
1024 if (model.type === 'file') {
1025 const URI = this._generateURI(model.name);
1026 this.updateCellSourceWithAttachment(model.name, URI);
1027 void withContent().then(fullModel => {
1028 this.model.attachments.set(URI, {
1029 [fullModel.mimetype]: fullModel.content
1030 });
1031 });
1032 }
1033 }
1034 else {
1035 // Pure mimetype, no useful name to infer
1036 const URI = this._generateURI();
1037 this.model.attachments.set(URI, {
1038 [mimeType]: event.mimeData.getData(mimeType)
1039 });
1040 this.updateCellSourceWithAttachment(URI, URI);
1041 }
1042 }
1043 }
1044 /**
1045 * Attaches all DataTransferItems (obtained from
1046 * clipboard or native drop events) to the cell
1047 */
1048 _attachFiles(items) {
1049 for (let i = 0; i < items.length; i++) {
1050 const item = items[i];
1051 if (item.kind === 'file') {
1052 const blob = item.getAsFile();
1053 if (blob) {
1054 this._attachFile(blob);
1055 }
1056 }
1057 }
1058 }
1059 /**
1060 * Takes in a file object and adds it to
1061 * the cell attachments
1062 */
1063 _attachFile(blob) {
1064 const reader = new FileReader();
1065 reader.onload = evt => {
1066 const { href, protocol } = URLExt.parse(reader.result);
1067 if (protocol !== 'data:') {
1068 return;
1069 }
1070 const dataURIRegex = /([\w+\/\+]+)?(?:;(charset=[\w\d-]*|base64))?,(.*)/;
1071 const matches = dataURIRegex.exec(href);
1072 if (!matches || matches.length !== 4) {
1073 return;
1074 }
1075 const mimeType = matches[1];
1076 const encodedData = matches[3];
1077 const bundle = { [mimeType]: encodedData };
1078 const URI = this._generateURI(blob.name);
1079 if (mimeType.startsWith('image/')) {
1080 this.model.attachments.set(URI, bundle);
1081 this.updateCellSourceWithAttachment(blob.name, URI);
1082 }
1083 };
1084 reader.onerror = evt => {
1085 console.error(`Failed to attach ${blob.name}` + evt);
1086 };
1087 reader.readAsDataURL(blob);
1088 }
1089 /**
1090 * Generates a unique URI for a file
1091 * while preserving the file extension.
1092 */
1093 _generateURI(name = '') {
1094 const lastIndex = name.lastIndexOf('.');
1095 return lastIndex !== -1
1096 ? UUID.uuid4().concat(name.substring(lastIndex))
1097 : UUID.uuid4();
1098 }
1099}
1100/** ****************************************************************************
1101 * MarkdownCell
1102 ******************************************************************************/
1103/**
1104 * A widget for a Markdown cell.
1105 *
1106 * #### Notes
1107 * Things get complicated if we want the rendered text to update
1108 * any time the text changes, the text editor model changes,
1109 * or the input area model changes. We don't support automatically
1110 * updating the rendered text in all of these cases.
1111 */
1112export class MarkdownCell extends AttachmentsCell {
1113 /**
1114 * Construct a Markdown cell widget.
1115 */
1116 constructor(options) {
1117 var _a, _b, _c;
1118 super(options);
1119 this._toggleCollapsedSignal = new Signal(this);
1120 this._renderer = null;
1121 this._rendered = true;
1122 this._prevText = '';
1123 this._ready = new PromiseDelegate();
1124 this._showEditorForReadOnlyMarkdown = true;
1125 this.addClass(MARKDOWN_CELL_CLASS);
1126 // Ensure we can resolve attachments:
1127 this._rendermime = options.rendermime.clone({
1128 resolver: new AttachmentsResolver({
1129 parent: (_a = options.rendermime.resolver) !== null && _a !== void 0 ? _a : undefined,
1130 model: this.model.attachments
1131 })
1132 });
1133 // Stop codemirror handling paste
1134 this.editor.setOption('handlePaste', false);
1135 // Check if heading cell is set to be collapsed
1136 this._headingCollapsed = ((_b = this.model.metadata.get(MARKDOWN_HEADING_COLLAPSED)) !== null && _b !== void 0 ? _b : false);
1137 // Throttle the rendering rate of the widget.
1138 this._monitor = new ActivityMonitor({
1139 signal: this.model.contentChanged,
1140 timeout: RENDER_TIMEOUT
1141 });
1142 this._monitor.activityStopped.connect(() => {
1143 if (this._rendered) {
1144 this.update();
1145 }
1146 }, this);
1147 void this._updateRenderedInput().then(() => {
1148 this._ready.resolve(void 0);
1149 });
1150 this.renderCollapseButtons(this._renderer);
1151 this.renderInput(this._renderer);
1152 this._showEditorForReadOnlyMarkdown = (_c = options.showEditorForReadOnlyMarkdown) !== null && _c !== void 0 ? _c : MarkdownCell.defaultShowEditorForReadOnlyMarkdown;
1153 }
1154 /**
1155 * A promise that resolves when the widget renders for the first time.
1156 */
1157 get ready() {
1158 return this._ready.promise;
1159 }
1160 /**
1161 * Text that represents the heading if cell is a heading.
1162 * Returns empty string if not a heading.
1163 */
1164 get headingInfo() {
1165 let text = this.model.value.text;
1166 const lines = marked.lexer(text);
1167 let line;
1168 for (line of lines) {
1169 if (line.type === 'heading') {
1170 return { text: line.text, level: line.depth };
1171 }
1172 else if (line.type === 'html') {
1173 let match = line.raw.match(/<h([1-6])(.*?)>(.*?)<\/h\1>/);
1174 if (match === null || match === void 0 ? void 0 : match[3]) {
1175 return { text: match[3], level: parseInt(match[1]) };
1176 }
1177 return { text: '', level: -1 };
1178 }
1179 }
1180 return { text: '', level: -1 };
1181 }
1182 get headingCollapsed() {
1183 return this._headingCollapsed;
1184 }
1185 set headingCollapsed(value) {
1186 this._headingCollapsed = value;
1187 if (value) {
1188 this.model.metadata.set(MARKDOWN_HEADING_COLLAPSED, value);
1189 }
1190 else if (this.model.metadata.has(MARKDOWN_HEADING_COLLAPSED)) {
1191 this.model.metadata.delete(MARKDOWN_HEADING_COLLAPSED);
1192 }
1193 const collapseButton = this.inputArea.promptNode.getElementsByClassName(HEADING_COLLAPSER_CLASS)[0];
1194 if (collapseButton) {
1195 if (value) {
1196 collapseButton.classList.add('jp-mod-collapsed');
1197 }
1198 else {
1199 collapseButton.classList.remove('jp-mod-collapsed');
1200 }
1201 }
1202 this.renderCollapseButtons(this._renderer);
1203 }
1204 get numberChildNodes() {
1205 return this._numberChildNodes;
1206 }
1207 set numberChildNodes(value) {
1208 this._numberChildNodes = value;
1209 this.renderCollapseButtons(this._renderer);
1210 }
1211 get toggleCollapsedSignal() {
1212 return this._toggleCollapsedSignal;
1213 }
1214 /**
1215 * Whether the cell is rendered.
1216 */
1217 get rendered() {
1218 return this._rendered;
1219 }
1220 set rendered(value) {
1221 // Show cell as rendered when cell is not editable
1222 if (this.readOnly && this._showEditorForReadOnlyMarkdown === false) {
1223 value = true;
1224 }
1225 if (value === this._rendered) {
1226 return;
1227 }
1228 this._rendered = value;
1229 this._handleRendered();
1230 // Refreshing an editor can be really expensive, so we don't call it from
1231 // _handleRendered, since _handledRendered is also called on every update
1232 // request.
1233 if (!this._rendered) {
1234 this.editor.refresh();
1235 }
1236 // If the rendered state changed, raise an event.
1237 this._displayChanged.emit();
1238 }
1239 /*
1240 * Whether the Markdown editor is visible in read-only mode.
1241 */
1242 get showEditorForReadOnly() {
1243 return this._showEditorForReadOnlyMarkdown;
1244 }
1245 set showEditorForReadOnly(value) {
1246 this._showEditorForReadOnlyMarkdown = value;
1247 if (value === false) {
1248 this.rendered = true;
1249 }
1250 }
1251 dispose() {
1252 if (this.isDisposed) {
1253 return;
1254 }
1255 this._monitor.dispose();
1256 super.dispose();
1257 }
1258 maybeCreateCollapseButton() {
1259 if (this.headingInfo.level > 0 &&
1260 this.inputArea.promptNode.getElementsByClassName(HEADING_COLLAPSER_CLASS)
1261 .length == 0) {
1262 let collapseButton = this.inputArea.promptNode.appendChild(document.createElement('button'));
1263 collapseButton.className = `jp-Button ${HEADING_COLLAPSER_CLASS}`;
1264 collapseButton.setAttribute('data-heading-level', this.headingInfo.level.toString());
1265 if (this._headingCollapsed) {
1266 collapseButton.classList.add('jp-mod-collapsed');
1267 }
1268 else {
1269 collapseButton.classList.remove('jp-mod-collapsed');
1270 }
1271 collapseButton.onclick = (event) => {
1272 this.headingCollapsed = !this.headingCollapsed;
1273 this._toggleCollapsedSignal.emit(this._headingCollapsed);
1274 };
1275 }
1276 }
1277 maybeCreateOrUpdateExpandButton() {
1278 var _a, _b;
1279 const expandButton = this.node.getElementsByClassName(SHOW_HIDDEN_CELLS_CLASS);
1280 // Create the "show hidden" button if not already created
1281 if (this.headingCollapsed &&
1282 expandButton.length === 0 &&
1283 this._numberChildNodes > 0) {
1284 const numberChildNodes = document.createElement('button');
1285 numberChildNodes.className = `bp3-button bp3-minimal jp-Button ${SHOW_HIDDEN_CELLS_CLASS}`;
1286 addIcon.render(numberChildNodes);
1287 const numberChildNodesText = document.createElement('div');
1288 numberChildNodesText.nodeValue = `${this._numberChildNodes} cell${this._numberChildNodes > 1 ? 's' : ''} hidden`;
1289 numberChildNodes.appendChild(numberChildNodesText);
1290 numberChildNodes.onclick = () => {
1291 this.headingCollapsed = false;
1292 this._toggleCollapsedSignal.emit(this._headingCollapsed);
1293 };
1294 this.node.appendChild(numberChildNodes);
1295 }
1296 else if (((_b = (_a = expandButton === null || expandButton === void 0 ? void 0 : expandButton[0]) === null || _a === void 0 ? void 0 : _a.childNodes) === null || _b === void 0 ? void 0 : _b.length) > 1) {
1297 // If the heading is collapsed, update text
1298 if (this._headingCollapsed) {
1299 expandButton[0].childNodes[1].textContent = `${this._numberChildNodes} cell${this._numberChildNodes > 1 ? 's' : ''} hidden`;
1300 // If the heading isn't collapsed, remove the button
1301 }
1302 else {
1303 for (const el of Array.from(expandButton)) {
1304 this.node.removeChild(el);
1305 }
1306 }
1307 }
1308 }
1309 /**
1310 * Render the collapse button for heading cells,
1311 * and for collapsed heading cells render the "expand hidden cells"
1312 * button.
1313 */
1314 renderCollapseButtons(widget) {
1315 this.node.classList.toggle(MARKDOWN_HEADING_COLLAPSED, this._headingCollapsed);
1316 this.maybeCreateCollapseButton();
1317 this.maybeCreateOrUpdateExpandButton();
1318 }
1319 /**
1320 * Render an input instead of the text editor.
1321 */
1322 renderInput(widget) {
1323 this.addClass(RENDERED_CLASS);
1324 this.renderCollapseButtons(widget);
1325 this.inputArea.renderInput(widget);
1326 }
1327 /**
1328 * Show the text editor instead of rendered input.
1329 */
1330 showEditor() {
1331 this.removeClass(RENDERED_CLASS);
1332 this.inputArea.showEditor();
1333 }
1334 /*
1335 * Handle `update-request` messages.
1336 */
1337 onUpdateRequest(msg) {
1338 // Make sure we are properly rendered.
1339 this._handleRendered();
1340 super.onUpdateRequest(msg);
1341 }
1342 /**
1343 * Modify the cell source to include a reference to the attachment.
1344 */
1345 updateCellSourceWithAttachment(attachmentName, URI) {
1346 var _a, _b;
1347 const textToBeAppended = `![${attachmentName}](attachment:${URI !== null && URI !== void 0 ? URI : attachmentName})`;
1348 (_b = (_a = this.editor).replaceSelection) === null || _b === void 0 ? void 0 : _b.call(_a, textToBeAppended);
1349 }
1350 /**
1351 * Handle the rendered state.
1352 */
1353 _handleRendered() {
1354 if (!this._rendered) {
1355 this.showEditor();
1356 }
1357 else {
1358 // TODO: It would be nice for the cell to provide a way for
1359 // its consumers to hook into when the rendering is done.
1360 void this._updateRenderedInput();
1361 this.renderInput(this._renderer);
1362 }
1363 }
1364 /**
1365 * Update the rendered input.
1366 */
1367 _updateRenderedInput() {
1368 const model = this.model;
1369 const text = (model && model.value.text) || DEFAULT_MARKDOWN_TEXT;
1370 // Do not re-render if the text has not changed.
1371 if (text !== this._prevText) {
1372 const mimeModel = new MimeModel({ data: { 'text/markdown': text } });
1373 if (!this._renderer) {
1374 this._renderer = this._rendermime.createRenderer('text/markdown');
1375 this._renderer.addClass(MARKDOWN_OUTPUT_CLASS);
1376 }
1377 this._prevText = text;
1378 return this._renderer.renderModel(mimeModel);
1379 }
1380 return Promise.resolve(void 0);
1381 }
1382 /**
1383 * Clone the cell, using the same model.
1384 */
1385 clone() {
1386 const constructor = this.constructor;
1387 return new constructor({
1388 model: this.model,
1389 contentFactory: this.contentFactory,
1390 rendermime: this._rendermime,
1391 placeholder: false
1392 });
1393 }
1394}
1395/**
1396 * The namespace for the `CodeCell` class statics.
1397 */
1398(function (MarkdownCell) {
1399 /**
1400 * Default value for showEditorForReadOnlyMarkdown.
1401 */
1402 MarkdownCell.defaultShowEditorForReadOnlyMarkdown = true;
1403})(MarkdownCell || (MarkdownCell = {}));
1404/** ****************************************************************************
1405 * RawCell
1406 ******************************************************************************/
1407/**
1408 * A widget for a raw cell.
1409 */
1410export class RawCell extends Cell {
1411 /**
1412 * Construct a raw cell widget.
1413 */
1414 constructor(options) {
1415 super(options);
1416 this.addClass(RAW_CELL_CLASS);
1417 }
1418 /**
1419 * Clone the cell, using the same model.
1420 */
1421 clone() {
1422 const constructor = this.constructor;
1423 return new constructor({
1424 model: this.model,
1425 contentFactory: this.contentFactory,
1426 placeholder: false
1427 });
1428 }
1429}
1430//# sourceMappingURL=widget.js.map
\No newline at end of file