UNPKG

23.9 kBJavaScriptView Raw
1/* -----------------------------------------------------------------------------
2| Copyright (c) Jupyter Development Team.
3| Distributed under the terms of the Modified BSD License.
4|----------------------------------------------------------------------------*/
5var __rest = (this && this.__rest) || function (s, e) {
6 var t = {};
7 for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
8 t[p] = s[p];
9 if (s != null && typeof Object.getOwnPropertySymbols === "function")
10 for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
11 if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
12 t[p[i]] = s[p[i]];
13 }
14 return t;
15};
16import { JSONExt } from '@lumino/coreutils';
17import { Signal } from '@lumino/signaling';
18import { AttachmentsModel } from '@jupyterlab/attachments';
19import { CodeEditor } from '@jupyterlab/codeeditor';
20import * as models from '@jupyter/ydoc';
21import { UUID } from '@lumino/coreutils';
22import { OutputAreaModel } from '@jupyterlab/outputarea';
23const globalModelDBMutex = models.createMutex();
24export function isCodeCellModel(model) {
25 return model.type === 'code';
26}
27export function isMarkdownCellModel(model) {
28 return model.type === 'markdown';
29}
30export function isRawCellModel(model) {
31 return model.type === 'raw';
32}
33/**
34 * An implementation of the cell model.
35 */
36export class CellModel extends CodeEditor.Model {
37 /**
38 * Construct a cell model from optional cell content.
39 */
40 constructor(options) {
41 var _a;
42 super({
43 modelDB: options.modelDB,
44 id: options.id || ((_a = options.cell) === null || _a === void 0 ? void 0 : _a.id) || UUID.uuid4()
45 });
46 /**
47 * A signal emitted when the state of the model changes.
48 */
49 this.contentChanged = new Signal(this);
50 /**
51 * A signal emitted when a model state changes.
52 */
53 this.stateChanged = new Signal(this);
54 this.value.changed.connect(this.onGenericChange, this);
55 const cellType = this.modelDB.createValue('type');
56 cellType.set(this.type);
57 const observableMetadata = this.modelDB.createMap('metadata');
58 observableMetadata.changed.connect(this.onModelDBMetadataChange, this);
59 observableMetadata.changed.connect(this.onGenericChange, this);
60 const cell = options.cell;
61 const trusted = this.modelDB.createValue('trusted');
62 trusted.changed.connect(this.onTrustedChanged, this);
63 if (!cell) {
64 trusted.set(false);
65 return;
66 }
67 trusted.set(!!cell.metadata['trusted']);
68 delete cell.metadata['trusted'];
69 // Set the text value, normalizing line endings to \n
70 if (Array.isArray(cell.source)) {
71 this.value.text = cell.source
72 .map(s => s.replace(/\r\n/g, '\n').replace(/\r/g, '\n'))
73 .join('');
74 }
75 else {
76 this.value.text = cell.source.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
77 }
78 const metadata = JSONExt.deepCopy(cell.metadata);
79 if (this.type !== 'raw') {
80 delete metadata['format'];
81 }
82 if (this.type !== 'code') {
83 delete metadata['collapsed'];
84 delete metadata['scrolled'];
85 }
86 for (const key in metadata) {
87 observableMetadata.set(key, metadata[key]);
88 }
89 }
90 /**
91 * The type of cell.
92 */
93 get type() {
94 // This getter really should be abstract, but our current constructor
95 // depends on .type working
96 return 'raw';
97 }
98 /**
99 * The id for the cell.
100 */
101 get id() {
102 return this.sharedModel.getId();
103 }
104 /**
105 * The metadata associated with the cell.
106 */
107 get metadata() {
108 return this.modelDB.get('metadata');
109 }
110 /**
111 * Get the trusted state of the model.
112 */
113 get trusted() {
114 return this.modelDB.getValue('trusted');
115 }
116 /**
117 * Set the trusted state of the model.
118 */
119 set trusted(newValue) {
120 const oldValue = this.trusted;
121 if (oldValue === newValue) {
122 return;
123 }
124 this.modelDB.setValue('trusted', newValue);
125 }
126 /**
127 * Serialize the model to JSON.
128 */
129 toJSON() {
130 const metadata = Object.create(null);
131 for (const key of this.metadata.keys()) {
132 const value = JSON.parse(JSON.stringify(this.metadata.get(key)));
133 metadata[key] = value;
134 }
135 if (this.trusted) {
136 metadata['trusted'] = true;
137 }
138 return {
139 cell_type: this.type,
140 source: this.value.text,
141 metadata
142 };
143 }
144 /**
145 * Handle a change to the trusted state.
146 *
147 * The default implementation is a no-op.
148 */
149 onTrustedChanged(trusted, args) {
150 /* no-op */
151 }
152 /**
153 * When we initialize a cell model, we create a standalone model that cannot be shared in a YNotebook.
154 * Call this function to re-initialize the local representation based on a fresh shared model (e.g. models.YFile or models.YCodeCell).
155 *
156 * @param sharedModel
157 * @param reinitialize Whether to reinitialize the shared model.
158 */
159 switchSharedModel(sharedModel, reinitialize) {
160 if (reinitialize) {
161 const newValue = sharedModel.getMetadata();
162 if (newValue) {
163 this._updateModelDBMetadata(newValue);
164 }
165 }
166 super.switchSharedModel(sharedModel, reinitialize);
167 }
168 /**
169 * Handle a change to the cell metadata modelDB and reflect it in the shared model.
170 */
171 onModelDBMetadataChange(sender, event) {
172 const metadata = this.sharedModel.getMetadata();
173 globalModelDBMutex(() => {
174 switch (event.type) {
175 case 'add':
176 this._changeCellMetadata(metadata, event);
177 break;
178 case 'change':
179 this._changeCellMetadata(metadata, event);
180 break;
181 case 'remove':
182 delete metadata[event.key];
183 break;
184 default:
185 throw new Error(`Invalid event type: ${event.type}`);
186 }
187 this.sharedModel.setMetadata(metadata);
188 });
189 }
190 /**
191 * Change the cell metadata for a given event.
192 *
193 * @param metadata The cell metadata.
194 * @param event The event to handle.
195 */
196 _changeCellMetadata(metadata, event) {
197 switch (event.key) {
198 case 'jupyter':
199 metadata.jupyter = event.newValue;
200 break;
201 case 'collapsed':
202 metadata.collapsed = event.newValue;
203 break;
204 case 'name':
205 metadata.name = event.newValue;
206 break;
207 case 'scrolled':
208 metadata.scrolled = event.newValue;
209 break;
210 case 'tags':
211 metadata.tags = event.newValue;
212 break;
213 case 'trusted':
214 metadata.trusted = event.newValue;
215 break;
216 default:
217 // The default is applied for custom metadata that are not
218 // defined in the official nbformat but which are defined
219 // by the user.
220 metadata[event.key] = event.newValue;
221 }
222 }
223 /**
224 * Handle a change to the cell shared model and reflect it in modelDB.
225 * We update the modeldb metadata when the shared model changes.
226 *
227 * This method overrides the CodeEditor protected _onSharedModelChanged
228 * so we first call super._onSharedModelChanged
229 *
230 * @override CodeEditor._onSharedModelChanged
231 */
232 _onSharedModelChanged(sender, change) {
233 super._onSharedModelChanged(sender, change);
234 globalModelDBMutex(() => {
235 if (change.metadataChange) {
236 const newValue = this.sharedModel.getMetadata();
237 if (newValue) {
238 this._updateModelDBMetadata(newValue);
239 }
240 }
241 });
242 }
243 _updateModelDBMetadata(metadata) {
244 this.metadata.clear();
245 Object.entries(metadata).forEach(([key, value]) => {
246 switch (key) {
247 case 'trusted':
248 this.metadata.set('trusted', value);
249 this.trusted = value;
250 break;
251 default:
252 // The default is applied for custom metadata that are not
253 // defined in the official nbformat but which are defined
254 // by the user.
255 this.metadata.set(key, value);
256 }
257 });
258 }
259 /**
260 * Handle a change to the observable value.
261 */
262 onGenericChange() {
263 this.contentChanged.emit(void 0);
264 }
265}
266/**
267 * A base implementation for cell models with attachments.
268 */
269export class AttachmentsCellModel extends CellModel {
270 /**
271 * Construct a new cell with optional attachments.
272 */
273 constructor(options) {
274 super(options);
275 const factory = options.contentFactory || AttachmentsCellModel.defaultContentFactory;
276 let attachments;
277 const cell = options.cell;
278 if (cell && (cell.cell_type === 'raw' || cell.cell_type === 'markdown')) {
279 attachments = cell
280 .attachments;
281 }
282 this._attachments = factory.createAttachmentsModel({
283 values: attachments,
284 modelDB: this.modelDB
285 });
286 this._attachments.stateChanged.connect(this.onGenericChange, this);
287 this._attachments.changed.connect(this.onModelDBAttachmentsChange, this);
288 }
289 /**
290 * Get the attachments of the model.
291 */
292 get attachments() {
293 return this._attachments;
294 }
295 /**
296 * Serialize the model to JSON.
297 */
298 toJSON() {
299 const cell = super.toJSON();
300 if (this.attachments.length) {
301 cell.attachments = this.attachments.toJSON();
302 }
303 return cell;
304 }
305 switchSharedModel(sharedModel, reinitialize) {
306 if (reinitialize) {
307 const attachments = sharedModel.getAttachments();
308 this._attachments.fromJSON(attachments !== null && attachments !== void 0 ? attachments : {});
309 }
310 super.switchSharedModel(sharedModel, reinitialize);
311 }
312 /**
313 * Handle a change to the cell outputs modelDB and reflect it in the shared model.
314 */
315 onModelDBAttachmentsChange(sender, event) {
316 const sharedModel = this.sharedModel;
317 globalModelDBMutex(() => {
318 switch (event.type) {
319 case 'add':
320 case 'change':
321 case 'remove':
322 sharedModel.setAttachments(sender.toJSON());
323 break;
324 default:
325 throw new Error(`Invalid event type: ${event.type}`);
326 }
327 });
328 }
329 /**
330 * Handle a change to the output shared model and reflect it in modelDB.
331 * We update the modeldb metadata when the nbcell changes.
332 *
333 * This method overrides the CellModel protected _onSharedModelChanged
334 * so we first call super._onSharedModelChanged
335 *
336 * @override CellModel._onSharedModelChanged
337 */
338 _onSharedModelChanged(sender, change) {
339 super._onSharedModelChanged(sender, change);
340 const sharedModel = this.sharedModel;
341 globalModelDBMutex(() => {
342 if (change.attachmentsChange) {
343 const attachments = sharedModel.getAttachments();
344 this._attachments.fromJSON(attachments !== null && attachments !== void 0 ? attachments : {});
345 }
346 });
347 }
348}
349/**
350 * The namespace for `AttachmentsCellModel` statics.
351 */
352(function (AttachmentsCellModel) {
353 /**
354 * The default implementation of an `IContentFactory`.
355 */
356 class ContentFactory {
357 /**
358 * Create an attachments model.
359 */
360 createAttachmentsModel(options) {
361 return new AttachmentsModel(options);
362 }
363 }
364 AttachmentsCellModel.ContentFactory = ContentFactory;
365 /**
366 * The shared `ContentFactory` instance.
367 */
368 AttachmentsCellModel.defaultContentFactory = new ContentFactory();
369})(AttachmentsCellModel || (AttachmentsCellModel = {}));
370/**
371 * An implementation of a raw cell model.
372 */
373export class RawCellModel extends AttachmentsCellModel {
374 /**
375 * The type of the cell.
376 */
377 get type() {
378 return 'raw';
379 }
380 /**
381 * Serialize the model to JSON.
382 */
383 toJSON() {
384 const cell = super.toJSON();
385 cell.id = this.id;
386 return cell;
387 }
388}
389/**
390 * An implementation of a markdown cell model.
391 */
392export class MarkdownCellModel extends AttachmentsCellModel {
393 /**
394 * Construct a markdown cell model from optional cell content.
395 */
396 constructor(options) {
397 super(options);
398 // Use the Github-flavored markdown mode.
399 this.mimeType = 'text/x-ipythongfm';
400 }
401 /**
402 * The type of the cell.
403 */
404 get type() {
405 return 'markdown';
406 }
407 /**
408 * Serialize the model to JSON.
409 */
410 toJSON() {
411 const cell = super.toJSON();
412 cell.id = this.id;
413 return cell;
414 }
415}
416/**
417 * An implementation of a code cell Model.
418 */
419export class CodeCellModel extends CellModel {
420 /**
421 * Construct a new code cell with optional original cell content.
422 */
423 constructor(options) {
424 var _a;
425 super(options);
426 this._executedCode = '';
427 this._isDirty = false;
428 const factory = options.contentFactory || CodeCellModel.defaultContentFactory;
429 const trusted = this.trusted;
430 const cell = options.cell;
431 let outputs = [];
432 const executionCount = this.modelDB.createValue('executionCount');
433 if (!executionCount.get()) {
434 if (cell && cell.cell_type === 'code') {
435 executionCount.set(cell.execution_count || null);
436 outputs = (_a = cell.outputs) !== null && _a !== void 0 ? _a : [];
437 // If execution count is not null presume the input code was the latest executed
438 // TODO load from the notebook file when the dirty state is stored in it
439 if (cell.execution_count != null) {
440 // True if execution_count is null or undefined
441 this._executedCode = this.value.text.trim();
442 }
443 }
444 else {
445 executionCount.set(null);
446 }
447 }
448 this.value.changed.connect(this._onValueChanged, this);
449 executionCount.changed.connect(this._onExecutionCountChanged, this);
450 globalModelDBMutex(() => {
451 const sharedCell = this.sharedModel;
452 sharedCell.setOutputs(outputs);
453 });
454 this._outputs = factory.createOutputArea({ trusted, values: outputs });
455 this._outputs.changed.connect(this.onGenericChange, this);
456 this._outputs.changed.connect(this.onModelDBOutputsChange, this);
457 // We keep `collapsed` and `jupyter.outputs_hidden` metadata in sync, since
458 // they are redundant in nbformat 4.4. See
459 // https://github.com/jupyter/nbformat/issues/137
460 this.metadata.changed.connect(Private.collapseChanged, this);
461 // Sync `collapsed` and `jupyter.outputs_hidden` for the first time, giving
462 // preference to `collapsed`.
463 if (this.metadata.has('collapsed')) {
464 const collapsed = this.metadata.get('collapsed');
465 Private.collapseChanged(this.metadata, {
466 type: 'change',
467 key: 'collapsed',
468 oldValue: collapsed,
469 newValue: collapsed
470 });
471 }
472 else if (this.metadata.has('jupyter')) {
473 const jupyter = this.metadata.get('jupyter');
474 if (jupyter.hasOwnProperty('outputs_hidden')) {
475 Private.collapseChanged(this.metadata, {
476 type: 'change',
477 key: 'jupyter',
478 oldValue: jupyter,
479 newValue: jupyter
480 });
481 }
482 }
483 }
484 switchSharedModel(sharedModel, reinitialize) {
485 if (reinitialize) {
486 this.clearExecution();
487 sharedModel.getOutputs().forEach(output => this._outputs.add(output));
488 }
489 super.switchSharedModel(sharedModel, reinitialize);
490 }
491 /**
492 * The type of the cell.
493 */
494 get type() {
495 return 'code';
496 }
497 /**
498 * The execution count of the cell.
499 */
500 get executionCount() {
501 return this.modelDB.has('executionCount')
502 ? this.modelDB.getValue('executionCount')
503 : null;
504 }
505 set executionCount(newValue) {
506 const oldValue = this.executionCount;
507 if (newValue === oldValue) {
508 return;
509 }
510 this.modelDB.setValue('executionCount', newValue || null);
511 }
512 /**
513 * Whether the cell is dirty or not.
514 *
515 * A cell is dirty if it is output is not empty and does not
516 * result of the input code execution.
517 */
518 get isDirty() {
519 // Test could be done dynamically with this._executedCode
520 // but for performance reason, the diff status is stored in a boolean.
521 return this._isDirty;
522 }
523 /**
524 * Set whether the cell is dirty or not.
525 */
526 _setDirty(v) {
527 if (v !== this._isDirty) {
528 if (!v) {
529 this._executedCode = this.value.text.trim();
530 }
531 this._isDirty = v;
532 this.stateChanged.emit({
533 name: 'isDirty',
534 oldValue: !v,
535 newValue: v
536 });
537 }
538 }
539 clearExecution() {
540 this.outputs.clear();
541 this.executionCount = null;
542 this._setDirty(false);
543 this.metadata.delete('execution');
544 }
545 /**
546 * The cell outputs.
547 */
548 get outputs() {
549 return this._outputs;
550 }
551 /**
552 * Dispose of the resources held by the model.
553 */
554 dispose() {
555 if (this.isDisposed) {
556 return;
557 }
558 this._outputs.dispose();
559 this._outputs = null;
560 super.dispose();
561 }
562 /**
563 * Serialize the model to JSON.
564 */
565 toJSON() {
566 const cell = super.toJSON();
567 cell.execution_count = this.executionCount || null;
568 cell.outputs = this.outputs.toJSON();
569 cell.id = this.id;
570 return cell;
571 }
572 /**
573 * Handle a change to the trusted state.
574 */
575 onTrustedChanged(trusted, args) {
576 const newTrusted = args.newValue;
577 if (this._outputs) {
578 this._outputs.trusted = newTrusted;
579 }
580 if (newTrusted) {
581 const codeCell = this.sharedModel;
582 const metadata = codeCell.getMetadata();
583 metadata.trusted = true;
584 codeCell.setMetadata(metadata);
585 }
586 this.stateChanged.emit({
587 name: 'trusted',
588 oldValue: args.oldValue,
589 newValue: newTrusted
590 });
591 }
592 /**
593 * Handle a change to the cell outputs modelDB and reflect it in the shared model.
594 */
595 onModelDBOutputsChange(sender, event) {
596 const codeCell = this.sharedModel;
597 globalModelDBMutex(() => {
598 switch (event.type) {
599 case 'add': {
600 const outputs = event.newValues.map(output => output.toJSON());
601 codeCell.updateOutputs(event.newIndex, event.newIndex + outputs.length, outputs);
602 break;
603 }
604 case 'set': {
605 const newValues = event.newValues.map(output => output.toJSON());
606 codeCell.updateOutputs(event.oldIndex, event.oldIndex + newValues.length, newValues);
607 break;
608 }
609 case 'remove':
610 codeCell.updateOutputs(event.oldIndex, event.oldValues.length);
611 break;
612 default:
613 throw new Error(`Invalid event type: ${event.type}`);
614 }
615 });
616 }
617 /**
618 * Handle a change to the code cell value.
619 */
620 _onValueChanged() {
621 if (this.executionCount !== null) {
622 this._setDirty(this._executedCode !== this.value.text.trim());
623 }
624 }
625 /**
626 * Handle a change to the output shared model and reflect it in modelDB.
627 * We update the modeldb metadata when the nbcell changes.
628 *
629 * This method overrides the CellModel protected _onSharedModelChanged
630 * so we first call super._onSharedModelChanged
631 *
632 * @override CellModel._onSharedModelChanged
633 */
634 _onSharedModelChanged(sender, change) {
635 super._onSharedModelChanged(sender, change);
636 globalModelDBMutex(() => {
637 if (change.outputsChange) {
638 this.clearExecution();
639 sender.getOutputs().forEach(output => this._outputs.add(output));
640 }
641 if (change.executionCountChange) {
642 this.executionCount = change.executionCountChange.newValue
643 ? change.executionCountChange.newValue
644 : null;
645 }
646 });
647 }
648 /**
649 * Handle a change to the execution count.
650 */
651 _onExecutionCountChanged(count, args) {
652 const codeCell = this.sharedModel;
653 globalModelDBMutex(() => {
654 codeCell.execution_count = args.newValue
655 ? args.newValue
656 : null;
657 });
658 this.contentChanged.emit(void 0);
659 this.stateChanged.emit({
660 name: 'executionCount',
661 oldValue: args.oldValue,
662 newValue: args.newValue
663 });
664 if (args.newValue && this.isDirty) {
665 this._setDirty(false);
666 }
667 }
668}
669/**
670 * The namespace for `CodeCellModel` statics.
671 */
672(function (CodeCellModel) {
673 /**
674 * The default implementation of an `IContentFactory`.
675 */
676 class ContentFactory {
677 /**
678 * Create an output area.
679 */
680 createOutputArea(options) {
681 return new OutputAreaModel(options);
682 }
683 }
684 CodeCellModel.ContentFactory = ContentFactory;
685 /**
686 * The shared `ContentFactory` instance.
687 */
688 CodeCellModel.defaultContentFactory = new ContentFactory();
689})(CodeCellModel || (CodeCellModel = {}));
690var Private;
691(function (Private) {
692 function collapseChanged(metadata, args) {
693 if (args.key === 'collapsed') {
694 const jupyter = (metadata.get('jupyter') || {});
695 const { outputs_hidden } = jupyter, newJupyter = __rest(jupyter, ["outputs_hidden"]);
696 if (outputs_hidden !== args.newValue) {
697 if (args.newValue !== undefined) {
698 newJupyter['outputs_hidden'] = args.newValue;
699 }
700 if (Object.keys(newJupyter).length === 0) {
701 metadata.delete('jupyter');
702 }
703 else {
704 metadata.set('jupyter', newJupyter);
705 }
706 }
707 }
708 else if (args.key === 'jupyter') {
709 const jupyter = (args.newValue || {});
710 if (jupyter.hasOwnProperty('outputs_hidden')) {
711 metadata.set('collapsed', jupyter.outputs_hidden);
712 }
713 else {
714 metadata.delete('collapsed');
715 }
716 }
717 }
718 Private.collapseChanged = collapseChanged;
719})(Private || (Private = {}));
720//# sourceMappingURL=model.js.map
\No newline at end of file