UNPKG

47.4 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._input = null;
378 this._model = null;
379 this._inputWrapper = null;
380 this._inputPlaceholder = null;
381 super.dispose();
382 }
383 /**
384 * Handle `after-attach` messages.
385 */
386 onAfterAttach(msg) {
387 this.update();
388 }
389 /**
390 * Handle `'activate-request'` messages.
391 */
392 onActivateRequest(msg) {
393 this.editor.focus();
394 }
395 /**
396 * Handle `fit-request` messages.
397 */
398 onFitRequest(msg) {
399 // need this for for when a theme changes font size
400 this.editor.refresh();
401 }
402 /**
403 * Handle `resize` messages.
404 */
405 onResize(msg) {
406 void this._resizeDebouncer.invoke();
407 }
408 /**
409 * Handle `update-request` messages.
410 */
411 onUpdateRequest(msg) {
412 if (!this._model) {
413 return;
414 }
415 // Handle read only state.
416 if (this.editor.getOption('readOnly') !== this._readOnly) {
417 this.editor.setOption('readOnly', this._readOnly);
418 this.toggleClass(READONLY_CLASS, this._readOnly);
419 }
420 }
421 /**
422 * Handle changes in the metadata.
423 */
424 onMetadataChanged(model, args) {
425 switch (args.key) {
426 case 'jupyter':
427 if (this.syncCollapse) {
428 this.loadCollapseState();
429 }
430 break;
431 case 'editable':
432 if (this.syncEditable) {
433 this.loadEditableState();
434 }
435 break;
436 default:
437 break;
438 }
439 }
440}
441/**
442 * The namespace for the `Cell` class statics.
443 */
444(function (Cell) {
445 /**
446 * The default implementation of an `IContentFactory`.
447 *
448 * This includes a CodeMirror editor factory to make it easy to use out of the box.
449 */
450 class ContentFactory {
451 /**
452 * Create a content factory for a cell.
453 */
454 constructor(options = {}) {
455 this._editorFactory =
456 options.editorFactory || InputArea.defaultEditorFactory;
457 }
458 /**
459 * The readonly editor factory that create code editors
460 */
461 get editorFactory() {
462 return this._editorFactory;
463 }
464 /**
465 * Create a new cell header for the parent widget.
466 */
467 createCellHeader() {
468 return new CellHeader();
469 }
470 /**
471 * Create a new cell header for the parent widget.
472 */
473 createCellFooter() {
474 return new CellFooter();
475 }
476 /**
477 * Create an input prompt.
478 */
479 createInputPrompt() {
480 return new InputPrompt();
481 }
482 /**
483 * Create the output prompt for the widget.
484 */
485 createOutputPrompt() {
486 return new OutputPrompt();
487 }
488 /**
489 * Create an stdin widget.
490 */
491 createStdin(options) {
492 return new Stdin(options);
493 }
494 }
495 Cell.ContentFactory = ContentFactory;
496 /**
497 * The default content factory for cells.
498 */
499 Cell.defaultContentFactory = new ContentFactory();
500})(Cell || (Cell = {}));
501/** ****************************************************************************
502 * CodeCell
503 ******************************************************************************/
504/**
505 * A widget for a code cell.
506 */
507export class CodeCell extends Cell {
508 /**
509 * Construct a code cell widget.
510 */
511 constructor(options) {
512 super(options);
513 this._outputHidden = false;
514 this._syncScrolled = false;
515 this._savingMetadata = false;
516 this.addClass(CODE_CELL_CLASS);
517 // Only save options not handled by parent constructor.
518 const rendermime = (this._rendermime = options.rendermime);
519 const contentFactory = this.contentFactory;
520 const model = this.model;
521 if (!options.placeholder) {
522 // Insert the output before the cell footer.
523 const outputWrapper = (this._outputWrapper = new Panel());
524 outputWrapper.addClass(CELL_OUTPUT_WRAPPER_CLASS);
525 const outputCollapser = new OutputCollapser();
526 outputCollapser.addClass(CELL_OUTPUT_COLLAPSER_CLASS);
527 const output = (this._output = new OutputArea({
528 model: model.outputs,
529 rendermime,
530 contentFactory: contentFactory,
531 maxNumberOutputs: options.maxNumberOutputs
532 }));
533 output.addClass(CELL_OUTPUT_AREA_CLASS);
534 // Set a CSS if there are no outputs, and connect a signal for future
535 // changes to the number of outputs. This is for conditional styling
536 // if there are no outputs.
537 if (model.outputs.length === 0) {
538 this.addClass(NO_OUTPUTS_CLASS);
539 }
540 output.outputLengthChanged.connect(this._outputLengthHandler, this);
541 outputWrapper.addWidget(outputCollapser);
542 outputWrapper.addWidget(output);
543 this.layout.insertWidget(2, new ResizeHandle(this.node));
544 this.layout.insertWidget(3, outputWrapper);
545 if (model.isDirty) {
546 this.addClass(DIRTY_CLASS);
547 }
548 this._outputPlaceholder = new OutputPlaceholder(() => {
549 this.outputHidden = !this.outputHidden;
550 });
551 }
552 model.stateChanged.connect(this.onStateChanged, this);
553 }
554 /**
555 * Initialize view state from model.
556 *
557 * #### Notes
558 * Should be called after construction. For convenience, returns this, so it
559 * can be chained in the construction, like `new Foo().initializeState();`
560 */
561 initializeState() {
562 super.initializeState();
563 this.loadScrolledState();
564 this.setPrompt(`${this.model.executionCount || ''}`);
565 return this;
566 }
567 /**
568 * Get the output area for the cell.
569 */
570 get outputArea() {
571 return this._output;
572 }
573 /**
574 * The view state of output being collapsed.
575 */
576 get outputHidden() {
577 return this._outputHidden;
578 }
579 set outputHidden(value) {
580 if (this._outputHidden === value) {
581 return;
582 }
583 const layout = this._outputWrapper.layout;
584 if (value) {
585 layout.removeWidget(this._output);
586 layout.addWidget(this._outputPlaceholder);
587 if (this.inputHidden && !this._outputWrapper.isHidden) {
588 this._outputWrapper.hide();
589 }
590 }
591 else {
592 if (this._outputWrapper.isHidden) {
593 this._outputWrapper.show();
594 }
595 layout.removeWidget(this._outputPlaceholder);
596 layout.addWidget(this._output);
597 }
598 this._outputHidden = value;
599 if (this.syncCollapse) {
600 this.saveCollapseState();
601 }
602 }
603 /**
604 * Save view collapse state to model
605 */
606 saveCollapseState() {
607 // Because collapse state for a code cell involves two different pieces of
608 // metadata (the `collapsed` and `jupyter` metadata keys), we block reacting
609 // to changes in metadata until we have fully committed our changes.
610 // Otherwise setting one key can trigger a write to the other key to
611 // maintain the synced consistency.
612 this._savingMetadata = true;
613 try {
614 super.saveCollapseState();
615 const metadata = this.model.metadata;
616 const collapsed = this.model.metadata.get('collapsed');
617 if ((this.outputHidden && collapsed === true) ||
618 (!this.outputHidden && collapsed === undefined)) {
619 return;
620 }
621 // Do not set jupyter.outputs_hidden since it is redundant. See
622 // and https://github.com/jupyter/nbformat/issues/137
623 if (this.outputHidden) {
624 metadata.set('collapsed', true);
625 }
626 else {
627 metadata.delete('collapsed');
628 }
629 }
630 finally {
631 this._savingMetadata = false;
632 }
633 }
634 /**
635 * Revert view collapse state from model.
636 *
637 * We consider the `collapsed` metadata key as the source of truth for outputs
638 * being hidden.
639 */
640 loadCollapseState() {
641 super.loadCollapseState();
642 this.outputHidden = !!this.model.metadata.get('collapsed');
643 }
644 /**
645 * Whether the output is in a scrolled state?
646 */
647 get outputsScrolled() {
648 return this._outputsScrolled;
649 }
650 set outputsScrolled(value) {
651 this.toggleClass('jp-mod-outputsScrolled', value);
652 this._outputsScrolled = value;
653 if (this.syncScrolled) {
654 this.saveScrolledState();
655 }
656 }
657 /**
658 * Save view collapse state to model
659 */
660 saveScrolledState() {
661 const { metadata } = this.model;
662 const current = metadata.get('scrolled');
663 if ((this.outputsScrolled && current === true) ||
664 (!this.outputsScrolled && current === undefined)) {
665 return;
666 }
667 if (this.outputsScrolled) {
668 metadata.set('scrolled', true);
669 }
670 else {
671 metadata.delete('scrolled');
672 }
673 }
674 /**
675 * Revert view collapse state from model.
676 */
677 loadScrolledState() {
678 const metadata = this.model.metadata;
679 // We don't have the notion of 'auto' scrolled, so we make it false.
680 if (metadata.get('scrolled') === 'auto') {
681 this.outputsScrolled = false;
682 }
683 else {
684 this.outputsScrolled = !!metadata.get('scrolled');
685 }
686 }
687 /**
688 * Whether to sync the scrolled state to the cell model.
689 */
690 get syncScrolled() {
691 return this._syncScrolled;
692 }
693 set syncScrolled(value) {
694 if (this._syncScrolled === value) {
695 return;
696 }
697 this._syncScrolled = value;
698 if (value) {
699 this.loadScrolledState();
700 }
701 }
702 /**
703 * Handle the input being hidden.
704 *
705 * #### Notes
706 * This method is called by the case cell implementation and is
707 * subclasses here so the code cell can watch to see when input
708 * is hidden without accessing private state.
709 */
710 handleInputHidden(value) {
711 if (!value && this._outputWrapper.isHidden) {
712 this._outputWrapper.show();
713 }
714 else if (value && !this._outputWrapper.isHidden && this._outputHidden) {
715 this._outputWrapper.hide();
716 }
717 }
718 /**
719 * Clone the cell, using the same model.
720 */
721 clone() {
722 const constructor = this.constructor;
723 return new constructor({
724 model: this.model,
725 contentFactory: this.contentFactory,
726 rendermime: this._rendermime,
727 placeholder: false
728 });
729 }
730 /**
731 * Clone the OutputArea alone, returning a simplified output area, using the same model.
732 */
733 cloneOutputArea() {
734 return new SimplifiedOutputArea({
735 model: this.model.outputs,
736 contentFactory: this.contentFactory,
737 rendermime: this._rendermime
738 });
739 }
740 /**
741 * Dispose of the resources used by the widget.
742 */
743 dispose() {
744 if (this.isDisposed) {
745 return;
746 }
747 this._output.outputLengthChanged.disconnect(this._outputLengthHandler, this);
748 this._rendermime = null;
749 this._output = null;
750 this._outputWrapper = null;
751 this._outputPlaceholder = null;
752 super.dispose();
753 }
754 /**
755 * Handle changes in the model.
756 */
757 onStateChanged(model, args) {
758 switch (args.name) {
759 case 'executionCount':
760 this.setPrompt(`${model.executionCount || ''}`);
761 break;
762 case 'isDirty':
763 if (model.isDirty) {
764 this.addClass(DIRTY_CLASS);
765 }
766 else {
767 this.removeClass(DIRTY_CLASS);
768 }
769 break;
770 default:
771 break;
772 }
773 }
774 /**
775 * Handle changes in the metadata.
776 */
777 onMetadataChanged(model, args) {
778 if (this._savingMetadata) {
779 // We are in middle of a metadata transaction, so don't react to it.
780 return;
781 }
782 switch (args.key) {
783 case 'scrolled':
784 if (this.syncScrolled) {
785 this.loadScrolledState();
786 }
787 break;
788 case 'collapsed':
789 if (this.syncCollapse) {
790 this.loadCollapseState();
791 }
792 break;
793 default:
794 break;
795 }
796 super.onMetadataChanged(model, args);
797 }
798 /**
799 * Handle changes in the number of outputs in the output area.
800 */
801 _outputLengthHandler(sender, args) {
802 const force = args === 0 ? true : false;
803 this.toggleClass(NO_OUTPUTS_CLASS, force);
804 }
805}
806/**
807 * The namespace for the `CodeCell` class statics.
808 */
809(function (CodeCell) {
810 /**
811 * Execute a cell given a client session.
812 */
813 async function execute(cell, sessionContext, metadata) {
814 var _a;
815 const model = cell.model;
816 const code = model.value.text;
817 if (!code.trim() || !((_a = sessionContext.session) === null || _a === void 0 ? void 0 : _a.kernel)) {
818 model.clearExecution();
819 return;
820 }
821 const cellId = { cellId: model.id };
822 metadata = Object.assign(Object.assign(Object.assign({}, model.metadata.toJSON()), metadata), cellId);
823 const { recordTiming } = metadata;
824 model.clearExecution();
825 cell.outputHidden = false;
826 cell.setPrompt('*');
827 model.trusted = true;
828 let future;
829 try {
830 const msgPromise = OutputArea.execute(code, cell.outputArea, sessionContext, metadata);
831 // cell.outputArea.future assigned synchronously in `execute`
832 if (recordTiming) {
833 const recordTimingHook = (msg) => {
834 let label;
835 switch (msg.header.msg_type) {
836 case 'status':
837 label = `status.${msg.content.execution_state}`;
838 break;
839 case 'execute_input':
840 label = 'execute_input';
841 break;
842 default:
843 return true;
844 }
845 // If the data is missing, estimate it to now
846 // Date was added in 5.1: https://jupyter-client.readthedocs.io/en/stable/messaging.html#message-header
847 const value = msg.header.date || new Date().toISOString();
848 const timingInfo = Object.assign({}, model.metadata.get('execution'));
849 timingInfo[`iopub.${label}`] = value;
850 model.metadata.set('execution', timingInfo);
851 return true;
852 };
853 cell.outputArea.future.registerMessageHook(recordTimingHook);
854 }
855 else {
856 model.metadata.delete('execution');
857 }
858 // Save this execution's future so we can compare in the catch below.
859 future = cell.outputArea.future;
860 const msg = (await msgPromise);
861 model.executionCount = msg.content.execution_count;
862 if (recordTiming) {
863 const timingInfo = Object.assign({}, model.metadata.get('execution'));
864 const started = msg.metadata.started;
865 // Started is not in the API, but metadata IPyKernel sends
866 if (started) {
867 timingInfo['shell.execute_reply.started'] = started;
868 }
869 // Per above, the 5.0 spec does not assume date, so we estimate is required
870 const finished = msg.header.date;
871 timingInfo['shell.execute_reply'] =
872 finished || new Date().toISOString();
873 model.metadata.set('execution', timingInfo);
874 }
875 return msg;
876 }
877 catch (e) {
878 // If we started executing, and the cell is still indicating this
879 // execution, clear the prompt.
880 if (future && !cell.isDisposed && cell.outputArea.future === future) {
881 cell.setPrompt('');
882 }
883 throw e;
884 }
885 }
886 CodeCell.execute = execute;
887})(CodeCell || (CodeCell = {}));
888/**
889 * `AttachmentsCell` - A base class for a cell widget that allows
890 * attachments to be drag/drop'd or pasted onto it
891 */
892export class AttachmentsCell extends Cell {
893 /**
894 * Handle the DOM events for the widget.
895 *
896 * @param event - The DOM event sent to the widget.
897 *
898 * #### Notes
899 * This method implements the DOM `EventListener` interface and is
900 * called in response to events on the notebook panel's node. It should
901 * not be called directly by user code.
902 */
903 handleEvent(event) {
904 switch (event.type) {
905 case 'paste':
906 this._evtPaste(event);
907 break;
908 case 'dragenter':
909 event.preventDefault();
910 break;
911 case 'dragover':
912 event.preventDefault();
913 break;
914 case 'drop':
915 this._evtNativeDrop(event);
916 break;
917 case 'lm-dragover':
918 this._evtDragOver(event);
919 break;
920 case 'lm-drop':
921 this._evtDrop(event);
922 break;
923 default:
924 break;
925 }
926 }
927 /**
928 * Handle `after-attach` messages for the widget.
929 */
930 onAfterAttach(msg) {
931 super.onAfterAttach(msg);
932 const node = this.node;
933 node.addEventListener('lm-dragover', this);
934 node.addEventListener('lm-drop', this);
935 node.addEventListener('dragenter', this);
936 node.addEventListener('dragover', this);
937 node.addEventListener('drop', this);
938 node.addEventListener('paste', this);
939 }
940 /**
941 * A message handler invoked on a `'before-detach'`
942 * message
943 */
944 onBeforeDetach(msg) {
945 const node = this.node;
946 node.removeEventListener('drop', this);
947 node.removeEventListener('dragover', this);
948 node.removeEventListener('dragenter', this);
949 node.removeEventListener('paste', this);
950 node.removeEventListener('lm-dragover', this);
951 node.removeEventListener('lm-drop', this);
952 }
953 _evtDragOver(event) {
954 const supportedMimeType = some(imageRendererFactory.mimeTypes, mimeType => {
955 if (!event.mimeData.hasData(CONTENTS_MIME_RICH)) {
956 return false;
957 }
958 const data = event.mimeData.getData(CONTENTS_MIME_RICH);
959 return data.model.mimetype === mimeType;
960 });
961 if (!supportedMimeType) {
962 return;
963 }
964 event.preventDefault();
965 event.stopPropagation();
966 event.dropAction = event.proposedAction;
967 }
968 /**
969 * Handle the `paste` event for the widget
970 */
971 _evtPaste(event) {
972 if (event.clipboardData) {
973 const items = event.clipboardData.items;
974 for (let i = 0; i < items.length; i++) {
975 if (items[i].type === 'text/plain') {
976 // Skip if this text is the path to a file
977 if (i < items.length - 1 && items[i + 1].kind === 'file') {
978 continue;
979 }
980 items[i].getAsString(text => {
981 var _a, _b;
982 (_b = (_a = this.editor).replaceSelection) === null || _b === void 0 ? void 0 : _b.call(_a, text);
983 });
984 }
985 this._attachFiles(event.clipboardData.items);
986 }
987 }
988 event.preventDefault();
989 }
990 /**
991 * Handle the `drop` event for the widget
992 */
993 _evtNativeDrop(event) {
994 if (event.dataTransfer) {
995 this._attachFiles(event.dataTransfer.items);
996 }
997 event.preventDefault();
998 }
999 /**
1000 * Handle the `'lm-drop'` event for the widget.
1001 */
1002 _evtDrop(event) {
1003 const supportedMimeTypes = toArray(filter(event.mimeData.types(), mimeType => {
1004 if (mimeType === CONTENTS_MIME_RICH) {
1005 const data = event.mimeData.getData(CONTENTS_MIME_RICH);
1006 return (imageRendererFactory.mimeTypes.indexOf(data.model.mimetype) !== -1);
1007 }
1008 return imageRendererFactory.mimeTypes.indexOf(mimeType) !== -1;
1009 }));
1010 if (supportedMimeTypes.length === 0) {
1011 return;
1012 }
1013 event.preventDefault();
1014 event.stopPropagation();
1015 if (event.proposedAction === 'none') {
1016 event.dropAction = 'none';
1017 return;
1018 }
1019 event.dropAction = 'copy';
1020 for (const mimeType of supportedMimeTypes) {
1021 if (mimeType === CONTENTS_MIME_RICH) {
1022 const { model, withContent } = event.mimeData.getData(CONTENTS_MIME_RICH);
1023 if (model.type === 'file') {
1024 const URI = this._generateURI(model.name);
1025 this.updateCellSourceWithAttachment(model.name, URI);
1026 void withContent().then(fullModel => {
1027 this.model.attachments.set(URI, {
1028 [fullModel.mimetype]: fullModel.content
1029 });
1030 });
1031 }
1032 }
1033 else {
1034 // Pure mimetype, no useful name to infer
1035 const URI = this._generateURI();
1036 this.model.attachments.set(URI, {
1037 [mimeType]: event.mimeData.getData(mimeType)
1038 });
1039 this.updateCellSourceWithAttachment(URI, URI);
1040 }
1041 }
1042 }
1043 /**
1044 * Attaches all DataTransferItems (obtained from
1045 * clipboard or native drop events) to the cell
1046 */
1047 _attachFiles(items) {
1048 for (let i = 0; i < items.length; i++) {
1049 const item = items[i];
1050 if (item.kind === 'file') {
1051 const blob = item.getAsFile();
1052 if (blob) {
1053 this._attachFile(blob);
1054 }
1055 }
1056 }
1057 }
1058 /**
1059 * Takes in a file object and adds it to
1060 * the cell attachments
1061 */
1062 _attachFile(blob) {
1063 const reader = new FileReader();
1064 reader.onload = evt => {
1065 const { href, protocol } = URLExt.parse(reader.result);
1066 if (protocol !== 'data:') {
1067 return;
1068 }
1069 const dataURIRegex = /([\w+\/\+]+)?(?:;(charset=[\w\d-]*|base64))?,(.*)/;
1070 const matches = dataURIRegex.exec(href);
1071 if (!matches || matches.length !== 4) {
1072 return;
1073 }
1074 const mimeType = matches[1];
1075 const encodedData = matches[3];
1076 const bundle = { [mimeType]: encodedData };
1077 const URI = this._generateURI(blob.name);
1078 if (mimeType.startsWith('image/')) {
1079 this.model.attachments.set(URI, bundle);
1080 this.updateCellSourceWithAttachment(blob.name, URI);
1081 }
1082 };
1083 reader.onerror = evt => {
1084 console.error(`Failed to attach ${blob.name}` + evt);
1085 };
1086 reader.readAsDataURL(blob);
1087 }
1088 /**
1089 * Generates a unique URI for a file
1090 * while preserving the file extension.
1091 */
1092 _generateURI(name = '') {
1093 const lastIndex = name.lastIndexOf('.');
1094 return lastIndex !== -1
1095 ? UUID.uuid4().concat(name.substring(lastIndex))
1096 : UUID.uuid4();
1097 }
1098}
1099/** ****************************************************************************
1100 * MarkdownCell
1101 ******************************************************************************/
1102/**
1103 * A widget for a Markdown cell.
1104 *
1105 * #### Notes
1106 * Things get complicated if we want the rendered text to update
1107 * any time the text changes, the text editor model changes,
1108 * or the input area model changes. We don't support automatically
1109 * updating the rendered text in all of these cases.
1110 */
1111export class MarkdownCell extends AttachmentsCell {
1112 /**
1113 * Construct a Markdown cell widget.
1114 */
1115 constructor(options) {
1116 var _a, _b, _c;
1117 super(options);
1118 this._toggleCollapsedSignal = new Signal(this);
1119 this._renderer = null;
1120 this._rendered = true;
1121 this._prevText = '';
1122 this._ready = new PromiseDelegate();
1123 this._showEditorForReadOnlyMarkdown = true;
1124 this.addClass(MARKDOWN_CELL_CLASS);
1125 // Ensure we can resolve attachments:
1126 this._rendermime = options.rendermime.clone({
1127 resolver: new AttachmentsResolver({
1128 parent: (_a = options.rendermime.resolver) !== null && _a !== void 0 ? _a : undefined,
1129 model: this.model.attachments
1130 })
1131 });
1132 // Stop codemirror handling paste
1133 this.editor.setOption('handlePaste', false);
1134 // Check if heading cell is set to be collapsed
1135 this._headingCollapsed = ((_b = this.model.metadata.get(MARKDOWN_HEADING_COLLAPSED)) !== null && _b !== void 0 ? _b : false);
1136 // Throttle the rendering rate of the widget.
1137 this._monitor = new ActivityMonitor({
1138 signal: this.model.contentChanged,
1139 timeout: RENDER_TIMEOUT
1140 });
1141 this._monitor.activityStopped.connect(() => {
1142 if (this._rendered) {
1143 this.update();
1144 }
1145 }, this);
1146 void this._updateRenderedInput().then(() => {
1147 this._ready.resolve(void 0);
1148 });
1149 this.renderCollapseButtons(this._renderer);
1150 this.renderInput(this._renderer);
1151 this._showEditorForReadOnlyMarkdown = (_c = options.showEditorForReadOnlyMarkdown) !== null && _c !== void 0 ? _c : MarkdownCell.defaultShowEditorForReadOnlyMarkdown;
1152 }
1153 /**
1154 * A promise that resolves when the widget renders for the first time.
1155 */
1156 get ready() {
1157 return this._ready.promise;
1158 }
1159 /**
1160 * Text that represents the heading if cell is a heading.
1161 * Returns empty string if not a heading.
1162 */
1163 get headingInfo() {
1164 let text = this.model.value.text;
1165 const lines = marked.lexer(text);
1166 let line;
1167 for (line of lines) {
1168 if (line.type === 'heading') {
1169 return { text: line.text, level: line.depth };
1170 }
1171 else if (line.type === 'html') {
1172 let match = line.raw.match(/<h([1-6])(.*?)>(.*?)<\/h\1>/);
1173 if (match === null || match === void 0 ? void 0 : match[3]) {
1174 return { text: match[3], level: parseInt(match[1]) };
1175 }
1176 return { text: '', level: -1 };
1177 }
1178 }
1179 return { text: '', level: -1 };
1180 }
1181 get headingCollapsed() {
1182 return this._headingCollapsed;
1183 }
1184 set headingCollapsed(value) {
1185 this._headingCollapsed = value;
1186 if (value) {
1187 this.model.metadata.set(MARKDOWN_HEADING_COLLAPSED, value);
1188 }
1189 else if (this.model.metadata.has(MARKDOWN_HEADING_COLLAPSED)) {
1190 this.model.metadata.delete(MARKDOWN_HEADING_COLLAPSED);
1191 }
1192 const collapseButton = this.inputArea.promptNode.getElementsByClassName(HEADING_COLLAPSER_CLASS)[0];
1193 if (collapseButton) {
1194 if (value) {
1195 collapseButton.classList.add('jp-mod-collapsed');
1196 }
1197 else {
1198 collapseButton.classList.remove('jp-mod-collapsed');
1199 }
1200 }
1201 this.renderCollapseButtons(this._renderer);
1202 }
1203 get numberChildNodes() {
1204 return this._numberChildNodes;
1205 }
1206 set numberChildNodes(value) {
1207 this._numberChildNodes = value;
1208 this.renderCollapseButtons(this._renderer);
1209 }
1210 get toggleCollapsedSignal() {
1211 return this._toggleCollapsedSignal;
1212 }
1213 /**
1214 * Whether the cell is rendered.
1215 */
1216 get rendered() {
1217 return this._rendered;
1218 }
1219 set rendered(value) {
1220 // Show cell as rendered when cell is not editable
1221 if (this.readOnly && this._showEditorForReadOnlyMarkdown === false) {
1222 value = true;
1223 }
1224 if (value === this._rendered) {
1225 return;
1226 }
1227 this._rendered = value;
1228 this._handleRendered();
1229 // Refreshing an editor can be really expensive, so we don't call it from
1230 // _handleRendered, since _handledRendered is also called on every update
1231 // request.
1232 if (!this._rendered) {
1233 this.editor.refresh();
1234 }
1235 // If the rendered state changed, raise an event.
1236 this._displayChanged.emit();
1237 }
1238 /*
1239 * Whether the Markdown editor is visible in read-only mode.
1240 */
1241 get showEditorForReadOnly() {
1242 return this._showEditorForReadOnlyMarkdown;
1243 }
1244 set showEditorForReadOnly(value) {
1245 this._showEditorForReadOnlyMarkdown = value;
1246 if (value === false) {
1247 this.rendered = true;
1248 }
1249 }
1250 maybeCreateCollapseButton() {
1251 if (this.headingInfo.level > 0 &&
1252 this.inputArea.promptNode.getElementsByClassName(HEADING_COLLAPSER_CLASS)
1253 .length == 0) {
1254 let collapseButton = this.inputArea.promptNode.appendChild(document.createElement('button'));
1255 collapseButton.className = `jp-Button ${HEADING_COLLAPSER_CLASS}`;
1256 collapseButton.setAttribute('data-heading-level', this.headingInfo.level.toString());
1257 if (this._headingCollapsed) {
1258 collapseButton.classList.add('jp-mod-collapsed');
1259 }
1260 else {
1261 collapseButton.classList.remove('jp-mod-collapsed');
1262 }
1263 collapseButton.onclick = (event) => {
1264 this.headingCollapsed = !this.headingCollapsed;
1265 this._toggleCollapsedSignal.emit(this._headingCollapsed);
1266 };
1267 }
1268 }
1269 maybeCreateOrUpdateExpandButton() {
1270 var _a, _b;
1271 const expandButton = this.node.getElementsByClassName(SHOW_HIDDEN_CELLS_CLASS);
1272 // Create the "show hidden" button if not already created
1273 if (this.headingCollapsed &&
1274 expandButton.length === 0 &&
1275 this._numberChildNodes > 0) {
1276 const numberChildNodes = document.createElement('button');
1277 numberChildNodes.className = `bp3-button bp3-minimal jp-Button ${SHOW_HIDDEN_CELLS_CLASS}`;
1278 addIcon.render(numberChildNodes);
1279 const numberChildNodesText = document.createElement('div');
1280 numberChildNodesText.nodeValue = `${this._numberChildNodes} cell${this._numberChildNodes > 1 ? 's' : ''} hidden`;
1281 numberChildNodes.appendChild(numberChildNodesText);
1282 numberChildNodes.onclick = () => {
1283 this.headingCollapsed = false;
1284 this._toggleCollapsedSignal.emit(this._headingCollapsed);
1285 };
1286 this.node.appendChild(numberChildNodes);
1287 }
1288 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) {
1289 // If the heading is collapsed, update text
1290 if (this._headingCollapsed) {
1291 expandButton[0].childNodes[1].textContent = `${this._numberChildNodes} cell${this._numberChildNodes > 1 ? 's' : ''} hidden`;
1292 // If the heading isn't collapsed, remove the button
1293 }
1294 else {
1295 for (const el of expandButton) {
1296 this.node.removeChild(el);
1297 }
1298 }
1299 }
1300 }
1301 /**
1302 * Render the collapse button for heading cells,
1303 * and for collapsed heading cells render the "expand hidden cells"
1304 * button.
1305 */
1306 renderCollapseButtons(widget) {
1307 this.node.classList.toggle(MARKDOWN_HEADING_COLLAPSED, this._headingCollapsed);
1308 this.maybeCreateCollapseButton();
1309 this.maybeCreateOrUpdateExpandButton();
1310 }
1311 /**
1312 * Render an input instead of the text editor.
1313 */
1314 renderInput(widget) {
1315 this.addClass(RENDERED_CLASS);
1316 this.renderCollapseButtons(widget);
1317 this.inputArea.renderInput(widget);
1318 }
1319 /**
1320 * Show the text editor instead of rendered input.
1321 */
1322 showEditor() {
1323 this.removeClass(RENDERED_CLASS);
1324 this.inputArea.showEditor();
1325 }
1326 /*
1327 * Handle `update-request` messages.
1328 */
1329 onUpdateRequest(msg) {
1330 // Make sure we are properly rendered.
1331 this._handleRendered();
1332 super.onUpdateRequest(msg);
1333 }
1334 /**
1335 * Modify the cell source to include a reference to the attachment.
1336 */
1337 updateCellSourceWithAttachment(attachmentName, URI) {
1338 var _a, _b;
1339 const textToBeAppended = `![${attachmentName}](attachment:${URI !== null && URI !== void 0 ? URI : attachmentName})`;
1340 (_b = (_a = this.editor).replaceSelection) === null || _b === void 0 ? void 0 : _b.call(_a, textToBeAppended);
1341 }
1342 /**
1343 * Handle the rendered state.
1344 */
1345 _handleRendered() {
1346 if (!this._rendered) {
1347 this.showEditor();
1348 }
1349 else {
1350 // TODO: It would be nice for the cell to provide a way for
1351 // its consumers to hook into when the rendering is done.
1352 void this._updateRenderedInput();
1353 this.renderInput(this._renderer);
1354 }
1355 }
1356 /**
1357 * Update the rendered input.
1358 */
1359 _updateRenderedInput() {
1360 const model = this.model;
1361 const text = (model && model.value.text) || DEFAULT_MARKDOWN_TEXT;
1362 // Do not re-render if the text has not changed.
1363 if (text !== this._prevText) {
1364 const mimeModel = new MimeModel({ data: { 'text/markdown': text } });
1365 if (!this._renderer) {
1366 this._renderer = this._rendermime.createRenderer('text/markdown');
1367 this._renderer.addClass(MARKDOWN_OUTPUT_CLASS);
1368 }
1369 this._prevText = text;
1370 return this._renderer.renderModel(mimeModel);
1371 }
1372 return Promise.resolve(void 0);
1373 }
1374 /**
1375 * Clone the cell, using the same model.
1376 */
1377 clone() {
1378 const constructor = this.constructor;
1379 return new constructor({
1380 model: this.model,
1381 contentFactory: this.contentFactory,
1382 rendermime: this._rendermime,
1383 placeholder: false
1384 });
1385 }
1386}
1387/**
1388 * The namespace for the `CodeCell` class statics.
1389 */
1390(function (MarkdownCell) {
1391 /**
1392 * Default value for showEditorForReadOnlyMarkdown.
1393 */
1394 MarkdownCell.defaultShowEditorForReadOnlyMarkdown = true;
1395})(MarkdownCell || (MarkdownCell = {}));
1396/** ****************************************************************************
1397 * RawCell
1398 ******************************************************************************/
1399/**
1400 * A widget for a raw cell.
1401 */
1402export class RawCell extends Cell {
1403 /**
1404 * Construct a raw cell widget.
1405 */
1406 constructor(options) {
1407 super(options);
1408 this.addClass(RAW_CELL_CLASS);
1409 }
1410 /**
1411 * Clone the cell, using the same model.
1412 */
1413 clone() {
1414 const constructor = this.constructor;
1415 return new constructor({
1416 model: this.model,
1417 contentFactory: this.contentFactory,
1418 placeholder: false
1419 });
1420 }
1421}
1422//# sourceMappingURL=widget.js.map
\No newline at end of file