UNPKG

16.3 kBJavaScriptView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3import { Dialog, showDialog } from '@jupyterlab/apputils';
4import { CodeCellModel, MarkdownCellModel, RawCellModel } from '@jupyterlab/cells';
5import * as nbformat from '@jupyterlab/nbformat';
6import { ModelDB } from '@jupyterlab/observables';
7import * as models from '@jupyterlab/shared-models';
8import { nullTranslator } from '@jupyterlab/translation';
9import { UUID } from '@lumino/coreutils';
10import { Signal } from '@lumino/signaling';
11import { CellList } from './celllist';
12const UNSHARED_KEYS = ['kernelspec', 'language_info'];
13/**
14 * An implementation of a notebook Model.
15 */
16export class NotebookModel {
17 /**
18 * Construct a new notebook model.
19 */
20 constructor(options = {}) {
21 /**
22 * A mutex to update the shared model.
23 */
24 this._modelDBMutex = models.createMutex();
25 this._readOnly = false;
26 this._contentChanged = new Signal(this);
27 this._stateChanged = new Signal(this);
28 this._nbformat = nbformat.MAJOR_VERSION;
29 this._nbformatMinor = nbformat.MINOR_VERSION;
30 this._isDisposed = false;
31 if (options.modelDB) {
32 this.modelDB = options.modelDB;
33 }
34 else {
35 this.modelDB = new ModelDB();
36 }
37 this.sharedModel = models.YNotebook.create(options.disableDocumentWideUndoRedo || false);
38 this._isInitialized = options.isInitialized === false ? false : true;
39 const factory = options.contentFactory || NotebookModel.defaultContentFactory;
40 this.contentFactory = factory.clone(this.modelDB.view('cells'));
41 this._cells = new CellList(this.modelDB, this.contentFactory, this.sharedModel);
42 this._trans = (options.translator || nullTranslator).load('jupyterlab');
43 this._cells.changed.connect(this._onCellsChanged, this);
44 // Handle initial metadata.
45 const metadata = this.modelDB.createMap('metadata');
46 if (!metadata.has('language_info')) {
47 const name = options.languagePreference || '';
48 metadata.set('language_info', { name });
49 }
50 this._ensureMetadata();
51 metadata.changed.connect(this._onMetadataChanged, this);
52 this._deletedCells = [];
53 this.sharedModel.dirty = false;
54 this.sharedModel.changed.connect(this._onStateChanged, this);
55 }
56 /**
57 * A signal emitted when the document content changes.
58 */
59 get contentChanged() {
60 return this._contentChanged;
61 }
62 /**
63 * A signal emitted when the document state changes.
64 */
65 get stateChanged() {
66 return this._stateChanged;
67 }
68 /**
69 * The dirty state of the document.
70 */
71 get dirty() {
72 return this.sharedModel.dirty;
73 }
74 set dirty(newValue) {
75 if (newValue === this.dirty) {
76 return;
77 }
78 this.sharedModel.dirty = newValue;
79 }
80 /**
81 * The read only state of the document.
82 */
83 get readOnly() {
84 return this._readOnly;
85 }
86 set readOnly(newValue) {
87 if (newValue === this._readOnly) {
88 return;
89 }
90 const oldValue = this._readOnly;
91 this._readOnly = newValue;
92 this.triggerStateChange({ name: 'readOnly', oldValue, newValue });
93 }
94 /**
95 * The metadata associated with the notebook.
96 */
97 get metadata() {
98 return this.modelDB.get('metadata');
99 }
100 /**
101 * Get the observable list of notebook cells.
102 */
103 get cells() {
104 return this._cells;
105 }
106 /**
107 * The major version number of the nbformat.
108 */
109 get nbformat() {
110 return this._nbformat;
111 }
112 /**
113 * The minor version number of the nbformat.
114 */
115 get nbformatMinor() {
116 return this._nbformatMinor;
117 }
118 /**
119 * The default kernel name of the document.
120 */
121 get defaultKernelName() {
122 const spec = this.metadata.get('kernelspec');
123 return spec ? spec.name : '';
124 }
125 /**
126 * A list of deleted cells for the notebook..
127 */
128 get deletedCells() {
129 return this._deletedCells;
130 }
131 /**
132 * If the model is initialized or not.
133 */
134 get isInitialized() {
135 return this._isInitialized;
136 }
137 /**
138 * The default kernel language of the document.
139 */
140 get defaultKernelLanguage() {
141 const info = this.metadata.get('language_info');
142 return info ? info.name : '';
143 }
144 /**
145 * Dispose of the resources held by the model.
146 */
147 dispose() {
148 // Do nothing if already disposed.
149 if (this.isDisposed) {
150 return;
151 }
152 const cells = this.cells;
153 this._cells = null;
154 cells.dispose();
155 this._isDisposed = true;
156 this.modelDB.dispose();
157 Signal.clearData(this);
158 }
159 /**
160 * Serialize the model to a string.
161 */
162 toString() {
163 return JSON.stringify(this.toJSON());
164 }
165 /**
166 * Deserialize the model from a string.
167 *
168 * #### Notes
169 * Should emit a [contentChanged] signal.
170 */
171 fromString(value) {
172 this.fromJSON(JSON.parse(value));
173 }
174 /**
175 * Serialize the model to JSON.
176 */
177 toJSON() {
178 var _a, _b;
179 const cells = [];
180 for (let i = 0; i < ((_b = (_a = this.cells) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0); i++) {
181 const cell = this.cells.get(i).toJSON();
182 if (this._nbformat === 4 && this._nbformatMinor <= 4) {
183 // strip cell ids if we have notebook format 4.0-4.4
184 delete cell.id;
185 }
186 cells.push(cell);
187 }
188 this._ensureMetadata();
189 const metadata = this.sharedModel.getMetadata();
190 for (const key of this.metadata.keys()) {
191 metadata[key] = JSON.parse(JSON.stringify(this.metadata.get(key)));
192 }
193 return {
194 metadata,
195 nbformat_minor: this._nbformatMinor,
196 nbformat: this._nbformat,
197 cells
198 };
199 }
200 /**
201 * Deserialize the model from JSON.
202 *
203 * #### Notes
204 * Should emit a [contentChanged] signal.
205 */
206 fromJSON(value) {
207 const cells = [];
208 const factory = this.contentFactory;
209 const useId = value.nbformat === 4 && value.nbformat_minor >= 5;
210 for (const cell of value.cells) {
211 const options = { cell };
212 if (useId) {
213 options.id = cell.id;
214 }
215 switch (cell.cell_type) {
216 case 'code':
217 cells.push(factory.createCodeCell(options));
218 break;
219 case 'markdown':
220 cells.push(factory.createMarkdownCell(options));
221 break;
222 case 'raw':
223 cells.push(factory.createRawCell(options));
224 break;
225 default:
226 continue;
227 }
228 }
229 this.cells.beginCompoundOperation();
230 this.cells.clear();
231 this.cells.pushAll(cells);
232 this.cells.endCompoundOperation();
233 this.sharedModel.nbformat_minor =
234 nbformat.MINOR_VERSION;
235 this.sharedModel.nbformat = nbformat.MAJOR_VERSION;
236 const origNbformat = value.metadata.orig_nbformat;
237 if (value.nbformat !== this._nbformat) {
238 this.sharedModel.nbformat = value.nbformat;
239 }
240 if (value.nbformat_minor > this._nbformatMinor) {
241 this.sharedModel.nbformat_minor =
242 value.nbformat_minor;
243 }
244 // Alert the user if the format changes.
245 if (origNbformat !== undefined && this._nbformat !== origNbformat) {
246 const newer = this._nbformat > origNbformat;
247 let msg;
248 if (newer) {
249 msg = this._trans.__(`This notebook has been converted from an older notebook format (v%1)
250to the current notebook format (v%2).
251The next time you save this notebook, the current notebook format (v%2) will be used.
252'Older versions of Jupyter may not be able to read the new format.' To preserve the original format version,
253close the notebook without saving it.`, origNbformat, this._nbformat);
254 }
255 else {
256 msg = this._trans.__(`This notebook has been converted from an newer notebook format (v%1)
257to the current notebook format (v%2).
258The next time you save this notebook, the current notebook format (v%2) will be used.
259Some features of the original notebook may not be available.' To preserve the original format version,
260close the notebook without saving it.`, origNbformat, this._nbformat);
261 }
262 void showDialog({
263 title: this._trans.__('Notebook converted'),
264 body: msg,
265 buttons: [Dialog.okButton({ label: this._trans.__('Ok') })]
266 });
267 }
268 // Update the metadata.
269 this.metadata.clear();
270 const metadata = value.metadata;
271 for (const key in metadata) {
272 // orig_nbformat is not intended to be stored per spec.
273 if (key === 'orig_nbformat') {
274 continue;
275 }
276 this.metadata.set(key, metadata[key]);
277 }
278 this._ensureMetadata();
279 this.dirty = true;
280 }
281 /**
282 * Initialize the model with its current state.
283 *
284 * # Notes
285 * Adds an empty code cell if the model is empty
286 * and clears undo state.
287 */
288 initialize() {
289 if (!this.cells.length) {
290 const factory = this.contentFactory;
291 this.cells.push(factory.createCodeCell({}));
292 }
293 this._isInitialized = true;
294 this.cells.clearUndo();
295 }
296 /**
297 * Handle a change in the cells list.
298 */
299 _onCellsChanged(list, change) {
300 switch (change.type) {
301 case 'add':
302 change.newValues.forEach(cell => {
303 cell.contentChanged.connect(this.triggerContentChange, this);
304 });
305 break;
306 case 'remove':
307 break;
308 case 'set':
309 change.newValues.forEach(cell => {
310 cell.contentChanged.connect(this.triggerContentChange, this);
311 });
312 break;
313 default:
314 break;
315 }
316 this.triggerContentChange();
317 }
318 _onStateChanged(sender, changes) {
319 if (changes.stateChange) {
320 changes.stateChange.forEach(value => {
321 if (value.name === 'nbformat') {
322 this._nbformat = value.newValue;
323 }
324 if (value.name === 'nbformatMinor') {
325 this._nbformatMinor = value.newValue;
326 }
327 if (value.name !== 'dirty' || value.oldValue !== value.newValue) {
328 this.triggerStateChange(value);
329 }
330 });
331 }
332 if (changes.metadataChange) {
333 const metadata = changes.metadataChange.newValue;
334 this._modelDBMutex(() => {
335 Object.entries(metadata).forEach(([key, value]) => {
336 this.metadata.set(key, value);
337 });
338 });
339 }
340 }
341 _onMetadataChanged(metadata, change) {
342 if (!UNSHARED_KEYS.includes(change.key)) {
343 this._modelDBMutex(() => {
344 this.sharedModel.updateMetadata(metadata.toJSON());
345 });
346 }
347 this.triggerContentChange();
348 }
349 /**
350 * Make sure we have the required metadata fields.
351 */
352 _ensureMetadata() {
353 const metadata = this.metadata;
354 if (!metadata.has('language_info')) {
355 metadata.set('language_info', { name: '' });
356 }
357 if (!metadata.has('kernelspec')) {
358 metadata.set('kernelspec', { name: '', display_name: '' });
359 }
360 }
361 /**
362 * Trigger a state change signal.
363 */
364 triggerStateChange(args) {
365 this._stateChanged.emit(args);
366 }
367 /**
368 * Trigger a content changed signal.
369 */
370 triggerContentChange() {
371 this._contentChanged.emit(void 0);
372 this.dirty = true;
373 }
374 /**
375 * Whether the model is disposed.
376 */
377 get isDisposed() {
378 return this._isDisposed;
379 }
380}
381/**
382 * The namespace for the `NotebookModel` class statics.
383 */
384(function (NotebookModel) {
385 /**
386 * The default implementation of an `IContentFactory`.
387 */
388 class ContentFactory {
389 /**
390 * Create a new cell model factory.
391 */
392 constructor(options) {
393 this.codeCellContentFactory =
394 options.codeCellContentFactory || CodeCellModel.defaultContentFactory;
395 this.modelDB = options.modelDB;
396 }
397 /**
398 * Create a new cell by cell type.
399 *
400 * @param type: the type of the cell to create.
401 *
402 * @param options: the cell creation options.
403 *
404 * #### Notes
405 * This method is intended to be a convenience method to programmatically
406 * call the other cell creation methods in the factory.
407 */
408 createCell(type, options) {
409 switch (type) {
410 case 'code':
411 return this.createCodeCell(options);
412 case 'markdown':
413 return this.createMarkdownCell(options);
414 case 'raw':
415 default:
416 return this.createRawCell(options);
417 }
418 }
419 /**
420 * Create a new code cell.
421 *
422 * @param source - The data to use for the original source data.
423 *
424 * @returns A new code cell. If a source cell is provided, the
425 * new cell will be initialized with the data from the source.
426 * If the contentFactory is not provided, the instance
427 * `codeCellContentFactory` will be used.
428 */
429 createCodeCell(options) {
430 if (options.contentFactory) {
431 options.contentFactory = this.codeCellContentFactory;
432 }
433 if (this.modelDB) {
434 if (!options.id) {
435 options.id = UUID.uuid4();
436 }
437 options.modelDB = this.modelDB.view(options.id);
438 }
439 return new CodeCellModel(options);
440 }
441 /**
442 * Create a new markdown cell.
443 *
444 * @param source - The data to use for the original source data.
445 *
446 * @returns A new markdown cell. If a source cell is provided, the
447 * new cell will be initialized with the data from the source.
448 */
449 createMarkdownCell(options) {
450 if (this.modelDB) {
451 if (!options.id) {
452 options.id = UUID.uuid4();
453 }
454 options.modelDB = this.modelDB.view(options.id);
455 }
456 return new MarkdownCellModel(options);
457 }
458 /**
459 * Create a new raw cell.
460 *
461 * @param source - The data to use for the original source data.
462 *
463 * @returns A new raw cell. If a source cell is provided, the
464 * new cell will be initialized with the data from the source.
465 */
466 createRawCell(options) {
467 if (this.modelDB) {
468 if (!options.id) {
469 options.id = UUID.uuid4();
470 }
471 options.modelDB = this.modelDB.view(options.id);
472 }
473 return new RawCellModel(options);
474 }
475 /**
476 * Clone the content factory with a new IModelDB.
477 */
478 clone(modelDB) {
479 return new ContentFactory({
480 modelDB: modelDB,
481 codeCellContentFactory: this.codeCellContentFactory
482 });
483 }
484 }
485 NotebookModel.ContentFactory = ContentFactory;
486 /**
487 * The default `ContentFactory` instance.
488 */
489 NotebookModel.defaultContentFactory = new ContentFactory({});
490})(NotebookModel || (NotebookModel = {}));
491//# sourceMappingURL=model.js.map
\No newline at end of file