UNPKG

22.5 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 '@jupyterlab/shared-models';
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 var _a;
236 if (change.metadataChange) {
237 const newValue = (_a = change.metadataChange) === null || _a === void 0 ? void 0 : _a.newValue;
238 if (newValue) {
239 this._updateModelDBMetadata(newValue);
240 }
241 }
242 });
243 }
244 _updateModelDBMetadata(metadata) {
245 Object.keys(metadata).map(key => {
246 switch (key) {
247 case 'collapsed':
248 this.metadata.set('collapsed', metadata.jupyter);
249 break;
250 case 'jupyter':
251 this.metadata.set('jupyter', metadata.jupyter);
252 break;
253 case 'name':
254 this.metadata.set('name', metadata.name);
255 break;
256 case 'scrolled':
257 this.metadata.set('scrolled', metadata.scrolled);
258 break;
259 case 'tags':
260 this.metadata.set('tags', metadata.tags);
261 break;
262 case 'trusted':
263 this.metadata.set('trusted', metadata.trusted);
264 break;
265 default:
266 // The default is applied for custom metadata that are not
267 // defined in the official nbformat but which are defined
268 // by the user.
269 this.metadata.set(key, metadata[key]);
270 }
271 });
272 }
273 /**
274 * Handle a change to the observable value.
275 */
276 onGenericChange() {
277 this.contentChanged.emit(void 0);
278 }
279}
280/**
281 * A base implementation for cell models with attachments.
282 */
283export class AttachmentsCellModel extends CellModel {
284 /**
285 * Construct a new cell with optional attachments.
286 */
287 constructor(options) {
288 super(options);
289 const factory = options.contentFactory || AttachmentsCellModel.defaultContentFactory;
290 let attachments;
291 const cell = options.cell;
292 if (cell && (cell.cell_type === 'raw' || cell.cell_type === 'markdown')) {
293 attachments = cell
294 .attachments;
295 }
296 this._attachments = factory.createAttachmentsModel({
297 values: attachments,
298 modelDB: this.modelDB
299 });
300 this._attachments.stateChanged.connect(this.onGenericChange, this);
301 }
302 /**
303 * Get the attachments of the model.
304 */
305 get attachments() {
306 return this._attachments;
307 }
308 /**
309 * Serialize the model to JSON.
310 */
311 toJSON() {
312 const cell = super.toJSON();
313 if (this.attachments.length) {
314 cell.attachments = this.attachments.toJSON();
315 }
316 return cell;
317 }
318}
319/**
320 * The namespace for `AttachmentsCellModel` statics.
321 */
322(function (AttachmentsCellModel) {
323 /**
324 * The default implementation of an `IContentFactory`.
325 */
326 class ContentFactory {
327 /**
328 * Create an attachments model.
329 */
330 createAttachmentsModel(options) {
331 return new AttachmentsModel(options);
332 }
333 }
334 AttachmentsCellModel.ContentFactory = ContentFactory;
335 /**
336 * The shared `ContentFactory` instance.
337 */
338 AttachmentsCellModel.defaultContentFactory = new ContentFactory();
339})(AttachmentsCellModel || (AttachmentsCellModel = {}));
340/**
341 * An implementation of a raw cell model.
342 */
343export class RawCellModel extends AttachmentsCellModel {
344 /**
345 * The type of the cell.
346 */
347 get type() {
348 return 'raw';
349 }
350 /**
351 * Serialize the model to JSON.
352 */
353 toJSON() {
354 const cell = super.toJSON();
355 cell.id = this.id;
356 return cell;
357 }
358}
359/**
360 * An implementation of a markdown cell model.
361 */
362export class MarkdownCellModel extends AttachmentsCellModel {
363 /**
364 * Construct a markdown cell model from optional cell content.
365 */
366 constructor(options) {
367 super(options);
368 // Use the Github-flavored markdown mode.
369 this.mimeType = 'text/x-ipythongfm';
370 }
371 /**
372 * The type of the cell.
373 */
374 get type() {
375 return 'markdown';
376 }
377 /**
378 * Serialize the model to JSON.
379 */
380 toJSON() {
381 const cell = super.toJSON();
382 cell.id = this.id;
383 return cell;
384 }
385}
386/**
387 * An implementation of a code cell Model.
388 */
389export class CodeCellModel extends CellModel {
390 /**
391 * Construct a new code cell with optional original cell content.
392 */
393 constructor(options) {
394 var _a;
395 super(options);
396 this._executedCode = '';
397 this._isDirty = false;
398 const factory = options.contentFactory || CodeCellModel.defaultContentFactory;
399 const trusted = this.trusted;
400 const cell = options.cell;
401 let outputs = [];
402 const executionCount = this.modelDB.createValue('executionCount');
403 if (!executionCount.get()) {
404 if (cell && cell.cell_type === 'code') {
405 executionCount.set(cell.execution_count || null);
406 outputs = (_a = cell.outputs) !== null && _a !== void 0 ? _a : [];
407 // If execution count is not null presume the input code was the latest executed
408 // TODO load from the notebook file when the dirty state is stored in it
409 if (cell.execution_count != null) {
410 // True if execution_count is null or undefined
411 this._executedCode = this.value.text.trim();
412 }
413 }
414 else {
415 executionCount.set(null);
416 }
417 }
418 this.value.changed.connect(this._onValueChanged, this);
419 executionCount.changed.connect(this._onExecutionCountChanged, this);
420 globalModelDBMutex(() => {
421 const sharedCell = this.sharedModel;
422 sharedCell.setOutputs(outputs);
423 });
424 this._outputs = factory.createOutputArea({ trusted, values: outputs });
425 this._outputs.changed.connect(this.onGenericChange, this);
426 this._outputs.changed.connect(this.onModelDBOutputsChange, this);
427 // We keep `collapsed` and `jupyter.outputs_hidden` metadata in sync, since
428 // they are redundant in nbformat 4.4. See
429 // https://github.com/jupyter/nbformat/issues/137
430 this.metadata.changed.connect(Private.collapseChanged, this);
431 // Sync `collapsed` and `jupyter.outputs_hidden` for the first time, giving
432 // preference to `collapsed`.
433 if (this.metadata.has('collapsed')) {
434 const collapsed = this.metadata.get('collapsed');
435 Private.collapseChanged(this.metadata, {
436 type: 'change',
437 key: 'collapsed',
438 oldValue: collapsed,
439 newValue: collapsed
440 });
441 }
442 else if (this.metadata.has('jupyter')) {
443 const jupyter = this.metadata.get('jupyter');
444 if (jupyter.hasOwnProperty('outputs_hidden')) {
445 Private.collapseChanged(this.metadata, {
446 type: 'change',
447 key: 'jupyter',
448 oldValue: jupyter,
449 newValue: jupyter
450 });
451 }
452 }
453 }
454 switchSharedModel(sharedModel, reinitialize) {
455 if (reinitialize) {
456 this.clearExecution();
457 sharedModel.getOutputs().forEach(output => this._outputs.add(output));
458 }
459 super.switchSharedModel(sharedModel, reinitialize);
460 }
461 /**
462 * The type of the cell.
463 */
464 get type() {
465 return 'code';
466 }
467 /**
468 * The execution count of the cell.
469 */
470 get executionCount() {
471 return this.modelDB.has('executionCount')
472 ? this.modelDB.getValue('executionCount')
473 : null;
474 }
475 set executionCount(newValue) {
476 const oldValue = this.executionCount;
477 if (newValue === oldValue) {
478 return;
479 }
480 this.modelDB.setValue('executionCount', newValue || null);
481 }
482 /**
483 * Whether the cell is dirty or not.
484 *
485 * A cell is dirty if it is output is not empty and does not
486 * result of the input code execution.
487 */
488 get isDirty() {
489 // Test could be done dynamically with this._executedCode
490 // but for performance reason, the diff status is stored in a boolean.
491 return this._isDirty;
492 }
493 /**
494 * Set whether the cell is dirty or not.
495 */
496 _setDirty(v) {
497 if (v !== this._isDirty) {
498 if (!v) {
499 this._executedCode = this.value.text.trim();
500 }
501 this._isDirty = v;
502 this.stateChanged.emit({
503 name: 'isDirty',
504 oldValue: !v,
505 newValue: v
506 });
507 }
508 }
509 clearExecution() {
510 this.outputs.clear();
511 this.executionCount = null;
512 this._setDirty(false);
513 this.metadata.delete('execution');
514 }
515 /**
516 * The cell outputs.
517 */
518 get outputs() {
519 return this._outputs;
520 }
521 /**
522 * Dispose of the resources held by the model.
523 */
524 dispose() {
525 if (this.isDisposed) {
526 return;
527 }
528 this._outputs.dispose();
529 this._outputs = null;
530 super.dispose();
531 }
532 /**
533 * Serialize the model to JSON.
534 */
535 toJSON() {
536 const cell = super.toJSON();
537 cell.execution_count = this.executionCount || null;
538 cell.outputs = this.outputs.toJSON();
539 cell.id = this.id;
540 return cell;
541 }
542 /**
543 * Handle a change to the trusted state.
544 */
545 onTrustedChanged(trusted, args) {
546 if (this._outputs) {
547 this._outputs.trusted = args.newValue;
548 }
549 this.stateChanged.emit({
550 name: 'trusted',
551 oldValue: args.oldValue,
552 newValue: args.newValue
553 });
554 }
555 /**
556 * Handle a change to the cell outputs modelDB and reflect it in the shared model.
557 */
558 onModelDBOutputsChange(sender, event) {
559 const codeCell = this.sharedModel;
560 globalModelDBMutex(() => {
561 switch (event.type) {
562 case 'add': {
563 const outputs = event.newValues.map(output => output.toJSON());
564 codeCell.updateOutputs(event.newIndex, event.newIndex + outputs.length, outputs);
565 break;
566 }
567 case 'set': {
568 const newValues = event.newValues.map(output => output.toJSON());
569 codeCell.updateOutputs(event.oldIndex, event.oldIndex + newValues.length, newValues);
570 break;
571 }
572 case 'remove':
573 codeCell.updateOutputs(event.oldIndex, event.oldValues.length);
574 break;
575 default:
576 throw new Error(`Invalid event type: ${event.type}`);
577 }
578 });
579 }
580 /**
581 * Handle a change to the code cell value.
582 */
583 _onValueChanged() {
584 if (this.executionCount !== null) {
585 this._setDirty(this._executedCode !== this.value.text.trim());
586 }
587 }
588 /**
589 * Handle a change to the output shared model and reflect it in modelDB.
590 * We update the modeldb metadata when the nbcell changes.
591 *
592 * This method overrides the CellModel protected _onSharedModelChanged
593 * so we first call super._onSharedModelChanged
594 *
595 * @override CellModel._onSharedModelChanged
596 */
597 _onSharedModelChanged(sender, change) {
598 super._onSharedModelChanged(sender, change);
599 globalModelDBMutex(() => {
600 if (change.outputsChange) {
601 this.clearExecution();
602 sender.getOutputs().forEach(output => this._outputs.add(output));
603 }
604 if (change.executionCountChange) {
605 this.executionCount = change.executionCountChange.newValue
606 ? change.executionCountChange.newValue
607 : null;
608 }
609 });
610 }
611 /**
612 * Handle a change to the execution count.
613 */
614 _onExecutionCountChanged(count, args) {
615 const codeCell = this.sharedModel;
616 globalModelDBMutex(() => {
617 codeCell.execution_count = args.newValue
618 ? args.newValue
619 : null;
620 });
621 this.contentChanged.emit(void 0);
622 this.stateChanged.emit({
623 name: 'executionCount',
624 oldValue: args.oldValue,
625 newValue: args.newValue
626 });
627 if (args.newValue && this.isDirty) {
628 this._setDirty(false);
629 }
630 }
631}
632/**
633 * The namespace for `CodeCellModel` statics.
634 */
635(function (CodeCellModel) {
636 /**
637 * The default implementation of an `IContentFactory`.
638 */
639 class ContentFactory {
640 /**
641 * Create an output area.
642 */
643 createOutputArea(options) {
644 return new OutputAreaModel(options);
645 }
646 }
647 CodeCellModel.ContentFactory = ContentFactory;
648 /**
649 * The shared `ContentFactory` instance.
650 */
651 CodeCellModel.defaultContentFactory = new ContentFactory();
652})(CodeCellModel || (CodeCellModel = {}));
653var Private;
654(function (Private) {
655 function collapseChanged(metadata, args) {
656 if (args.key === 'collapsed') {
657 const jupyter = (metadata.get('jupyter') || {});
658 const { outputs_hidden } = jupyter, newJupyter = __rest(jupyter, ["outputs_hidden"]);
659 if (outputs_hidden !== args.newValue) {
660 if (args.newValue !== undefined) {
661 newJupyter['outputs_hidden'] = args.newValue;
662 }
663 if (Object.keys(newJupyter).length === 0) {
664 metadata.delete('jupyter');
665 }
666 else {
667 metadata.set('jupyter', newJupyter);
668 }
669 }
670 }
671 else if (args.key === 'jupyter') {
672 const jupyter = (args.newValue || {});
673 if (jupyter.hasOwnProperty('outputs_hidden')) {
674 metadata.set('collapsed', jupyter.outputs_hidden);
675 }
676 else {
677 metadata.delete('collapsed');
678 }
679 }
680 }
681 Private.collapseChanged = collapseChanged;
682})(Private || (Private = {}));
683//# sourceMappingURL=model.js.map
\No newline at end of file