model.ts 15 KB

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