model.ts 16 KB

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