UNPKG

13.3 kBPlain TextView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3
4import { Dialog, showDialog } from '@jupyterlab/apputils';
5import { ICellModel } from '@jupyterlab/cells';
6import { IChangedArgs } from '@jupyterlab/coreutils';
7import { DocumentRegistry } from '@jupyterlab/docregistry';
8import * as nbformat from '@jupyterlab/nbformat';
9import { IObservableList } from '@jupyterlab/observables';
10import {
11 IMapChange,
12 ISharedNotebook,
13 NotebookChange,
14 YNotebook
15} from '@jupyter/ydoc';
16import {
17 ITranslator,
18 nullTranslator,
19 TranslationBundle
20} from '@jupyterlab/translation';
21import { JSONExt } from '@lumino/coreutils';
22import { ISignal, Signal } from '@lumino/signaling';
23import { CellList } from './celllist';
24
25/**
26 * The definition of a model object for a notebook widget.
27 */
28export interface INotebookModel extends DocumentRegistry.IModel {
29 /**
30 * The list of cells in the notebook.
31 */
32 readonly cells: CellList;
33
34 /**
35 * The major version number of the nbformat.
36 */
37 readonly nbformat: number;
38
39 /**
40 * The minor version number of the nbformat.
41 */
42 readonly nbformatMinor: number;
43
44 /**
45 * The metadata associated with the notebook.
46 *
47 * ### Notes
48 * This is a copy of the metadata. Changing a part of it
49 * won't affect the model.
50 * As this returns a copy of all metadata, it is advised to
51 * use `getMetadata` to speed up the process of getting a single key.
52 */
53 readonly metadata: nbformat.INotebookMetadata;
54
55 /**
56 * Signal emitted when notebook metadata changes.
57 */
58 readonly metadataChanged: ISignal<INotebookModel, IMapChange>;
59
60 /**
61 * The array of deleted cells since the notebook was last run.
62 */
63 readonly deletedCells: string[];
64
65 /**
66 * Shared model
67 */
68 readonly sharedModel: ISharedNotebook;
69
70 /**
71 * Delete a metadata
72 *
73 * @param key Metadata key
74 */
75 deleteMetadata(key: string): void;
76
77 /**
78 * Get a metadata
79 *
80 * ### Notes
81 * This returns a copy of the key value.
82 *
83 * @param key Metadata key
84 */
85 getMetadata(key: string): any;
86
87 /**
88 * Set a metadata
89 *
90 * @param key Metadata key
91 * @param value Metadata value
92 */
93 setMetadata(key: string, value: any): void;
94}
95
96/**
97 * An implementation of a notebook Model.
98 */
99export class NotebookModel implements INotebookModel {
100 /**
101 * Construct a new notebook model.
102 */
103 constructor(options: NotebookModel.IOptions = {}) {
104 this.standaloneModel = typeof options.sharedModel === 'undefined';
105
106 if (options.sharedModel) {
107 this.sharedModel = options.sharedModel;
108 } else {
109 this.sharedModel = YNotebook.create({
110 disableDocumentWideUndoRedo:
111 options.disableDocumentWideUndoRedo ?? true,
112 data: {
113 nbformat: nbformat.MAJOR_VERSION,
114 nbformat_minor: nbformat.MINOR_VERSION,
115 metadata: {
116 kernelspec: { name: '', display_name: '' },
117 language_info: { name: options.languagePreference ?? '' }
118 }
119 }
120 });
121 }
122
123 this._cells = new CellList(this.sharedModel);
124 this._trans = (options.translator || nullTranslator).load('jupyterlab');
125 this._deletedCells = [];
126 this._collaborationEnabled = !!options?.collaborationEnabled;
127
128 this._cells.changed.connect(this._onCellsChanged, this);
129 this.sharedModel.changed.connect(this._onStateChanged, this);
130 this.sharedModel.metadataChanged.connect(this._onMetadataChanged, this);
131 }
132
133 /**
134 * A signal emitted when the document content changes.
135 */
136 get contentChanged(): ISignal<this, void> {
137 return this._contentChanged;
138 }
139
140 /**
141 * Signal emitted when notebook metadata changes.
142 */
143 get metadataChanged(): ISignal<INotebookModel, IMapChange<any>> {
144 return this._metadataChanged;
145 }
146
147 /**
148 * A signal emitted when the document state changes.
149 */
150 get stateChanged(): ISignal<this, IChangedArgs<any>> {
151 return this._stateChanged;
152 }
153
154 /**
155 * Get the observable list of notebook cells.
156 */
157 get cells(): CellList {
158 return this._cells;
159 }
160
161 /**
162 * The dirty state of the document.
163 */
164 get dirty(): boolean {
165 return this._dirty;
166 }
167 set dirty(newValue: boolean) {
168 const oldValue = this._dirty;
169 if (newValue === oldValue) {
170 return;
171 }
172 this._dirty = newValue;
173 this.triggerStateChange({
174 name: 'dirty',
175 oldValue,
176 newValue
177 });
178 }
179
180 /**
181 * The read only state of the document.
182 */
183 get readOnly(): boolean {
184 return this._readOnly;
185 }
186 set readOnly(newValue: boolean) {
187 if (newValue === this._readOnly) {
188 return;
189 }
190 const oldValue = this._readOnly;
191 this._readOnly = newValue;
192 this.triggerStateChange({ name: 'readOnly', oldValue, newValue });
193 }
194
195 /**
196 * The metadata associated with the notebook.
197 *
198 * ### Notes
199 * This is a copy of the metadata. Changing a part of it
200 * won't affect the model.
201 * As this returns a copy of all metadata, it is advised to
202 * use `getMetadata` to speed up the process of getting a single key.
203 */
204 get metadata(): nbformat.INotebookMetadata {
205 return this.sharedModel.metadata;
206 }
207
208 /**
209 * The major version number of the nbformat.
210 */
211 get nbformat(): number {
212 return this.sharedModel.nbformat;
213 }
214
215 /**
216 * The minor version number of the nbformat.
217 */
218 get nbformatMinor(): number {
219 return this.sharedModel.nbformat_minor;
220 }
221
222 /**
223 * The default kernel name of the document.
224 */
225 get defaultKernelName(): string {
226 const spec = this.getMetadata('kernelspec');
227 return spec?.name ?? '';
228 }
229
230 /**
231 * A list of deleted cells for the notebook..
232 */
233 get deletedCells(): string[] {
234 return this._deletedCells;
235 }
236
237 /**
238 * The default kernel language of the document.
239 */
240 get defaultKernelLanguage(): string {
241 const info = this.getMetadata('language_info');
242 return info?.name ?? '';
243 }
244
245 /**
246 * Whether the model is collaborative or not.
247 */
248 get collaborative(): boolean {
249 return this._collaborationEnabled;
250 }
251
252 /**
253 * Dispose of the resources held by the model.
254 */
255 dispose(): void {
256 // Do nothing if already disposed.
257 if (this.isDisposed) {
258 return;
259 }
260 this._isDisposed = true;
261
262 const cells = this.cells;
263 this._cells = null!;
264 cells.dispose();
265 if (this.standaloneModel) {
266 this.sharedModel.dispose();
267 }
268 Signal.clearData(this);
269 }
270
271 /**
272 * Delete a metadata
273 *
274 * @param key Metadata key
275 */
276 deleteMetadata(key: string): void {
277 return this.sharedModel.deleteMetadata(key);
278 }
279
280 /**
281 * Get a metadata
282 *
283 * ### Notes
284 * This returns a copy of the key value.
285 *
286 * @param key Metadata key
287 */
288 getMetadata(key: string): any {
289 return this.sharedModel.getMetadata(key);
290 }
291
292 /**
293 * Set a metadata
294 *
295 * @param key Metadata key
296 * @param value Metadata value
297 */
298 setMetadata(key: string, value: any): void {
299 if (typeof value === 'undefined') {
300 this.sharedModel.deleteMetadata(key);
301 } else {
302 this.sharedModel.setMetadata(key, value);
303 }
304 }
305
306 /**
307 * Serialize the model to a string.
308 */
309 toString(): string {
310 return JSON.stringify(this.toJSON());
311 }
312
313 /**
314 * Deserialize the model from a string.
315 *
316 * #### Notes
317 * Should emit a [contentChanged] signal.
318 */
319 fromString(value: string): void {
320 this.fromJSON(JSON.parse(value));
321 }
322
323 /**
324 * Serialize the model to JSON.
325 */
326 toJSON(): nbformat.INotebookContent {
327 this._ensureMetadata();
328 return this.sharedModel.toJSON();
329 }
330
331 /**
332 * Deserialize the model from JSON.
333 *
334 * #### Notes
335 * Should emit a [contentChanged] signal.
336 */
337 fromJSON(value: nbformat.INotebookContent): void {
338 const copy = JSONExt.deepCopy(value);
339 const origNbformat = value.metadata.orig_nbformat;
340
341 // Alert the user if the format changes.
342 copy.nbformat = Math.max(value.nbformat, nbformat.MAJOR_VERSION);
343 if (
344 copy.nbformat !== value.nbformat ||
345 copy.nbformat_minor < nbformat.MINOR_VERSION
346 ) {
347 copy.nbformat_minor = nbformat.MINOR_VERSION;
348 }
349 if (origNbformat !== undefined && copy.nbformat !== origNbformat) {
350 const newer = copy.nbformat > origNbformat;
351 let msg: string;
352
353 if (newer) {
354 msg = this._trans.__(
355 `This notebook has been converted from an older notebook format (v%1)
356to the current notebook format (v%2).
357The next time you save this notebook, the current notebook format (v%2) will be used.
358'Older versions of Jupyter may not be able to read the new format.' To preserve the original format version,
359close the notebook without saving it.`,
360 origNbformat,
361 copy.nbformat
362 );
363 } else {
364 msg = this._trans.__(
365 `This notebook has been converted from an newer notebook format (v%1)
366to the current notebook format (v%2).
367The next time you save this notebook, the current notebook format (v%2) will be used.
368Some features of the original notebook may not be available.' To preserve the original format version,
369close the notebook without saving it.`,
370 origNbformat,
371 copy.nbformat
372 );
373 }
374 void showDialog({
375 title: this._trans.__('Notebook converted'),
376 body: msg,
377 buttons: [Dialog.okButton({ label: this._trans.__('Ok') })]
378 });
379 }
380
381 // Ensure there is at least one cell
382 if ((copy.cells?.length ?? 0) === 0) {
383 copy['cells'] = [
384 { cell_type: 'code', source: '', metadata: { trusted: true } }
385 ];
386 }
387 this.sharedModel.fromJSON(copy);
388
389 this._ensureMetadata();
390 this.dirty = true;
391 }
392
393 /**
394 * Handle a change in the cells list.
395 */
396 private _onCellsChanged(
397 list: CellList,
398 change: IObservableList.IChangedArgs<ICellModel>
399 ): void {
400 switch (change.type) {
401 case 'add':
402 change.newValues.forEach(cell => {
403 cell.contentChanged.connect(this.triggerContentChange, this);
404 });
405 break;
406 case 'remove':
407 break;
408 case 'set':
409 change.newValues.forEach(cell => {
410 cell.contentChanged.connect(this.triggerContentChange, this);
411 });
412 break;
413 default:
414 break;
415 }
416 this.triggerContentChange();
417 }
418
419 private _onMetadataChanged(
420 sender: ISharedNotebook,
421 changes: IMapChange
422 ): void {
423 this._metadataChanged.emit(changes);
424 this.triggerContentChange();
425 }
426
427 private _onStateChanged(
428 sender: ISharedNotebook,
429 changes: NotebookChange
430 ): void {
431 if (changes.stateChange) {
432 changes.stateChange.forEach(value => {
433 if (value.name === 'dirty') {
434 // Setting `dirty` will trigger the state change.
435 // We always set `dirty` because the shared model state
436 // and the local attribute are synchronized one way shared model -> _dirty
437 this.dirty = value.newValue;
438 } else if (value.oldValue !== value.newValue) {
439 this.triggerStateChange({
440 newValue: undefined,
441 oldValue: undefined,
442 ...value
443 });
444 }
445 });
446 }
447 }
448
449 /**
450 * Make sure we have the required metadata fields.
451 */
452 private _ensureMetadata(languageName: string = ''): void {
453 if (!this.getMetadata('language_info')) {
454 this.sharedModel.setMetadata('language_info', { name: languageName });
455 }
456 if (!this.getMetadata('kernelspec')) {
457 this.sharedModel.setMetadata('kernelspec', {
458 name: '',
459 display_name: ''
460 });
461 }
462 }
463
464 /**
465 * Trigger a state change signal.
466 */
467 protected triggerStateChange(args: IChangedArgs<any>): void {
468 this._stateChanged.emit(args);
469 }
470
471 /**
472 * Trigger a content changed signal.
473 */
474 protected triggerContentChange(): void {
475 this._contentChanged.emit(void 0);
476 this.dirty = true;
477 }
478
479 /**
480 * Whether the model is disposed.
481 */
482 get isDisposed(): boolean {
483 return this._isDisposed;
484 }
485
486 /**
487 * The shared notebook model.
488 */
489 readonly sharedModel: ISharedNotebook;
490
491 /**
492 * Whether the model should disposed the shared model on disposal or not.
493 */
494 protected standaloneModel = false;
495
496 private _dirty = false;
497 private _readOnly = false;
498 private _contentChanged = new Signal<this, void>(this);
499 private _stateChanged = new Signal<this, IChangedArgs<any>>(this);
500
501 private _trans: TranslationBundle;
502 private _cells: CellList;
503 private _deletedCells: string[];
504 private _isDisposed = false;
505 private _metadataChanged = new Signal<NotebookModel, IMapChange>(this);
506 private _collaborationEnabled: boolean;
507}
508
509/**
510 * The namespace for the `NotebookModel` class statics.
511 */
512export namespace NotebookModel {
513 /**
514 * An options object for initializing a notebook model.
515 */
516 export interface IOptions
517 extends DocumentRegistry.IModelOptions<ISharedNotebook> {
518 /**
519 * Default cell type.
520 */
521 defaultCell?: 'code' | 'markdown' | 'raw';
522
523 /**
524 * Language translator.
525 */
526 translator?: ITranslator;
527
528 /**
529 * Defines if the document can be undo/redo.
530 *
531 * Default: true
532 *
533 * @experimental
534 * @alpha
535 */
536 disableDocumentWideUndoRedo?: boolean;
537 }
538}