##// END OF EJS Templates
Merge pull request #6801 from minrk/runMode...
Thomas Kluyver -
r18874:835269e6 merge
parent child Browse files
Show More
@@ -1,1 +1,1
1 Subproject commit ba94581b824a62ee630dd0b92a5aea8678248a24
1 Subproject commit 563c9e74b153e9509d94fd448353eeda13f0819c
@@ -1,2479 +1,2499
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'base/js/utils',
8 8 'base/js/dialog',
9 9 'notebook/js/textcell',
10 10 'notebook/js/codecell',
11 11 'services/sessions/session',
12 12 'notebook/js/celltoolbar',
13 13 'components/marked/lib/marked',
14 'highlight',
14 'codemirror/lib/codemirror',
15 'codemirror/addon/runmode/runmode',
15 16 'notebook/js/mathjaxutils',
16 17 'base/js/keyboard',
17 18 'notebook/js/tooltip',
18 19 'notebook/js/celltoolbarpresets/default',
19 20 'notebook/js/celltoolbarpresets/rawcell',
20 21 'notebook/js/celltoolbarpresets/slideshow',
21 22 'notebook/js/scrollmanager'
22 23 ], function (
23 24 IPython,
24 25 $,
25 26 utils,
26 27 dialog,
27 28 textcell,
28 29 codecell,
29 30 session,
30 31 celltoolbar,
31 32 marked,
32 hljs,
33 CodeMirror,
34 runMode,
33 35 mathjaxutils,
34 36 keyboard,
35 37 tooltip,
36 38 default_celltoolbar,
37 39 rawcell_celltoolbar,
38 40 slideshow_celltoolbar,
39 41 scrollmanager
40 42 ) {
41 43
42 44 var Notebook = function (selector, options) {
43 45 // Constructor
44 46 //
45 47 // A notebook contains and manages cells.
46 48 //
47 49 // Parameters:
48 50 // selector: string
49 51 // options: dictionary
50 52 // Dictionary of keyword arguments.
51 53 // events: $(Events) instance
52 54 // keyboard_manager: KeyboardManager instance
53 55 // contents: Contents instance
54 56 // save_widget: SaveWidget instance
55 57 // config: dictionary
56 58 // base_url : string
57 59 // notebook_path : string
58 60 // notebook_name : string
59 61 this.config = utils.mergeopt(Notebook, options.config);
60 62 this.base_url = options.base_url;
61 63 this.notebook_path = options.notebook_path;
62 64 this.notebook_name = options.notebook_name;
63 65 this.events = options.events;
64 66 this.keyboard_manager = options.keyboard_manager;
65 67 this.contents = options.contents;
66 68 this.save_widget = options.save_widget;
67 69 this.tooltip = new tooltip.Tooltip(this.events);
68 70 this.ws_url = options.ws_url;
69 71 this._session_starting = false;
70 72 this.default_cell_type = this.config.default_cell_type || 'code';
71 73
72 74 // Create default scroll manager.
73 75 this.scroll_manager = new scrollmanager.ScrollManager(this);
74 76
75 77 // TODO: This code smells (and the other `= this` line a couple lines down)
76 78 // We need a better way to deal with circular instance references.
77 79 this.keyboard_manager.notebook = this;
78 80 this.save_widget.notebook = this;
79 81
80 82 mathjaxutils.init();
81 83
82 84 if (marked) {
83 85 marked.setOptions({
84 86 gfm : true,
85 87 tables: true,
86 langPrefix: "language-",
87 highlight: function(code, lang) {
88 // FIXME: probably want central config for CodeMirror theme when we have js config
89 langPrefix: "cm-s-ipython language-",
90 highlight: function(code, lang, callback) {
88 91 if (!lang) {
89 92 // no language, no highlight
90 return code;
91 }
92 var highlighted;
93 try {
94 highlighted = hljs.highlight(lang, code, false);
95 } catch(err) {
96 highlighted = hljs.highlightAuto(code);
93 if (callback) {
94 callback(null, code);
95 return;
96 } else {
97 return code;
98 }
97 99 }
98 return highlighted.value;
100 utils.requireCodeMirrorMode(lang, function () {
101 var el = document.createElement("div");
102 mode = CodeMirror.getMode({}, lang);
103 if (!mode) {
104 console.log("No CodeMirror mode: " + lang);
105 callback(null, code);
106 return;
107 }
108 try {
109 CodeMirror.runMode(code, mode, el);
110 callback(null, el.innerHTML);
111 } catch (err) {
112 console.log("Failed to highlight " + lang + " code", error);
113 callback(err, code);
114 }
115 }, function (err) {
116 console.log("No CodeMirror mode: " + lang);
117 callback(err, code);
118 });
99 119 }
100 120 });
101 121 }
102 122
103 123 this.element = $(selector);
104 124 this.element.scroll();
105 125 this.element.data("notebook", this);
106 126 this.next_prompt_number = 1;
107 127 this.session = null;
108 128 this.kernel = null;
109 129 this.clipboard = null;
110 130 this.undelete_backup = null;
111 131 this.undelete_index = null;
112 132 this.undelete_below = false;
113 133 this.paste_enabled = false;
114 134 // It is important to start out in command mode to match the intial mode
115 135 // of the KeyboardManager.
116 136 this.mode = 'command';
117 137 this.set_dirty(false);
118 138 this.metadata = {};
119 139 this._checkpoint_after_save = false;
120 140 this.last_checkpoint = null;
121 141 this.checkpoints = [];
122 142 this.autosave_interval = 0;
123 143 this.autosave_timer = null;
124 144 // autosave *at most* every two minutes
125 145 this.minimum_autosave_interval = 120000;
126 146 this.notebook_name_blacklist_re = /[\/\\:]/;
127 147 this.nbformat = 4; // Increment this when changing the nbformat
128 148 this.nbformat_minor = 0; // Increment this when changing the nbformat
129 149 this.codemirror_mode = 'ipython';
130 150 this.create_elements();
131 151 this.bind_events();
132 152 this.save_notebook = function() { // don't allow save until notebook_loaded
133 153 this.save_notebook_error(null, null, "Load failed, save is disabled");
134 154 };
135 155
136 156 // Trigger cell toolbar registration.
137 157 default_celltoolbar.register(this);
138 158 rawcell_celltoolbar.register(this);
139 159 slideshow_celltoolbar.register(this);
140 160 };
141 161
142 162 Notebook.options_default = {
143 163 // can be any cell type, or the special values of
144 164 // 'above', 'below', or 'selected' to get the value from another cell.
145 165 Notebook: {
146 166 default_cell_type: 'code',
147 167 }
148 168 };
149 169
150 170
151 171 /**
152 172 * Create an HTML and CSS representation of the notebook.
153 173 *
154 174 * @method create_elements
155 175 */
156 176 Notebook.prototype.create_elements = function () {
157 177 var that = this;
158 178 this.element.attr('tabindex','-1');
159 179 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
160 180 // We add this end_space div to the end of the notebook div to:
161 181 // i) provide a margin between the last cell and the end of the notebook
162 182 // ii) to prevent the div from scrolling up when the last cell is being
163 183 // edited, but is too low on the page, which browsers will do automatically.
164 184 var end_space = $('<div/>').addClass('end_space');
165 185 end_space.dblclick(function (e) {
166 186 var ncells = that.ncells();
167 187 that.insert_cell_below('code',ncells-1);
168 188 });
169 189 this.element.append(this.container);
170 190 this.container.append(end_space);
171 191 };
172 192
173 193 /**
174 194 * Bind JavaScript events: key presses and custom IPython events.
175 195 *
176 196 * @method bind_events
177 197 */
178 198 Notebook.prototype.bind_events = function () {
179 199 var that = this;
180 200
181 201 this.events.on('set_next_input.Notebook', function (event, data) {
182 202 var index = that.find_cell_index(data.cell);
183 203 var new_cell = that.insert_cell_below('code',index);
184 204 new_cell.set_text(data.text);
185 205 that.dirty = true;
186 206 });
187 207
188 208 this.events.on('set_dirty.Notebook', function (event, data) {
189 209 that.dirty = data.value;
190 210 });
191 211
192 212 this.events.on('trust_changed.Notebook', function (event, trusted) {
193 213 that.trusted = trusted;
194 214 });
195 215
196 216 this.events.on('select.Cell', function (event, data) {
197 217 var index = that.find_cell_index(data.cell);
198 218 that.select(index);
199 219 });
200 220
201 221 this.events.on('edit_mode.Cell', function (event, data) {
202 222 that.handle_edit_mode(data.cell);
203 223 });
204 224
205 225 this.events.on('command_mode.Cell', function (event, data) {
206 226 that.handle_command_mode(data.cell);
207 227 });
208 228
209 229 this.events.on('spec_changed.Kernel', function(event, data) {
210 230 that.metadata.kernelspec =
211 231 {name: data.name, display_name: data.display_name};
212 232 });
213 233
214 234 this.events.on('kernel_ready.Kernel', function(event, data) {
215 235 var kinfo = data.kernel.info_reply;
216 236 var langinfo = kinfo.language_info || {};
217 237 if (!langinfo.name) langinfo.name = kinfo.language;
218 238
219 239 that.metadata.language_info = langinfo;
220 240 // Mode 'null' should be plain, unhighlighted text.
221 241 var cm_mode = langinfo.codemirror_mode || langinfo.language || 'null';
222 242 that.set_codemirror_mode(cm_mode);
223 243 });
224 244
225 245 var collapse_time = function (time) {
226 246 var app_height = $('#ipython-main-app').height(); // content height
227 247 var splitter_height = $('div#pager_splitter').outerHeight(true);
228 248 var new_height = app_height - splitter_height;
229 249 that.element.animate({height : new_height + 'px'}, time);
230 250 };
231 251
232 252 this.element.bind('collapse_pager', function (event, extrap) {
233 253 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
234 254 collapse_time(time);
235 255 });
236 256
237 257 var expand_time = function (time) {
238 258 var app_height = $('#ipython-main-app').height(); // content height
239 259 var splitter_height = $('div#pager_splitter').outerHeight(true);
240 260 var pager_height = $('div#pager').outerHeight(true);
241 261 var new_height = app_height - pager_height - splitter_height;
242 262 that.element.animate({height : new_height + 'px'}, time);
243 263 };
244 264
245 265 this.element.bind('expand_pager', function (event, extrap) {
246 266 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
247 267 expand_time(time);
248 268 });
249 269
250 270 // Firefox 22 broke $(window).on("beforeunload")
251 271 // I'm not sure why or how.
252 272 window.onbeforeunload = function (e) {
253 273 // TODO: Make killing the kernel configurable.
254 274 var kill_kernel = false;
255 275 if (kill_kernel) {
256 276 that.session.delete();
257 277 }
258 278 // if we are autosaving, trigger an autosave on nav-away.
259 279 // still warn, because if we don't the autosave may fail.
260 280 if (that.dirty) {
261 281 if ( that.autosave_interval ) {
262 282 // schedule autosave in a timeout
263 283 // this gives you a chance to forcefully discard changes
264 284 // by reloading the page if you *really* want to.
265 285 // the timer doesn't start until you *dismiss* the dialog.
266 286 setTimeout(function () {
267 287 if (that.dirty) {
268 288 that.save_notebook();
269 289 }
270 290 }, 1000);
271 291 return "Autosave in progress, latest changes may be lost.";
272 292 } else {
273 293 return "Unsaved changes will be lost.";
274 294 }
275 295 }
276 296 // Null is the *only* return value that will make the browser not
277 297 // pop up the "don't leave" dialog.
278 298 return null;
279 299 };
280 300 };
281 301
282 302 /**
283 303 * Set the dirty flag, and trigger the set_dirty.Notebook event
284 304 *
285 305 * @method set_dirty
286 306 */
287 307 Notebook.prototype.set_dirty = function (value) {
288 308 if (value === undefined) {
289 309 value = true;
290 310 }
291 311 if (this.dirty == value) {
292 312 return;
293 313 }
294 314 this.events.trigger('set_dirty.Notebook', {value: value});
295 315 };
296 316
297 317 /**
298 318 * Scroll the top of the page to a given cell.
299 319 *
300 320 * @method scroll_to_cell
301 321 * @param {Number} cell_number An index of the cell to view
302 322 * @param {Number} time Animation time in milliseconds
303 323 * @return {Number} Pixel offset from the top of the container
304 324 */
305 325 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
306 326 var cells = this.get_cells();
307 327 time = time || 0;
308 328 cell_number = Math.min(cells.length-1,cell_number);
309 329 cell_number = Math.max(0 ,cell_number);
310 330 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
311 331 this.element.animate({scrollTop:scroll_value}, time);
312 332 return scroll_value;
313 333 };
314 334
315 335 /**
316 336 * Scroll to the bottom of the page.
317 337 *
318 338 * @method scroll_to_bottom
319 339 */
320 340 Notebook.prototype.scroll_to_bottom = function () {
321 341 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
322 342 };
323 343
324 344 /**
325 345 * Scroll to the top of the page.
326 346 *
327 347 * @method scroll_to_top
328 348 */
329 349 Notebook.prototype.scroll_to_top = function () {
330 350 this.element.animate({scrollTop:0}, 0);
331 351 };
332 352
333 353 // Edit Notebook metadata
334 354
335 355 Notebook.prototype.edit_metadata = function () {
336 356 var that = this;
337 357 dialog.edit_metadata({
338 358 md: this.metadata,
339 359 callback: function (md) {
340 360 that.metadata = md;
341 361 },
342 362 name: 'Notebook',
343 363 notebook: this,
344 364 keyboard_manager: this.keyboard_manager});
345 365 };
346 366
347 367 // Cell indexing, retrieval, etc.
348 368
349 369 /**
350 370 * Get all cell elements in the notebook.
351 371 *
352 372 * @method get_cell_elements
353 373 * @return {jQuery} A selector of all cell elements
354 374 */
355 375 Notebook.prototype.get_cell_elements = function () {
356 376 return this.container.children("div.cell");
357 377 };
358 378
359 379 /**
360 380 * Get a particular cell element.
361 381 *
362 382 * @method get_cell_element
363 383 * @param {Number} index An index of a cell to select
364 384 * @return {jQuery} A selector of the given cell.
365 385 */
366 386 Notebook.prototype.get_cell_element = function (index) {
367 387 var result = null;
368 388 var e = this.get_cell_elements().eq(index);
369 389 if (e.length !== 0) {
370 390 result = e;
371 391 }
372 392 return result;
373 393 };
374 394
375 395 /**
376 396 * Try to get a particular cell by msg_id.
377 397 *
378 398 * @method get_msg_cell
379 399 * @param {String} msg_id A message UUID
380 400 * @return {Cell} Cell or null if no cell was found.
381 401 */
382 402 Notebook.prototype.get_msg_cell = function (msg_id) {
383 403 return codecell.CodeCell.msg_cells[msg_id] || null;
384 404 };
385 405
386 406 /**
387 407 * Count the cells in this notebook.
388 408 *
389 409 * @method ncells
390 410 * @return {Number} The number of cells in this notebook
391 411 */
392 412 Notebook.prototype.ncells = function () {
393 413 return this.get_cell_elements().length;
394 414 };
395 415
396 416 /**
397 417 * Get all Cell objects in this notebook.
398 418 *
399 419 * @method get_cells
400 420 * @return {Array} This notebook's Cell objects
401 421 */
402 422 // TODO: we are often calling cells as cells()[i], which we should optimize
403 423 // to cells(i) or a new method.
404 424 Notebook.prototype.get_cells = function () {
405 425 return this.get_cell_elements().toArray().map(function (e) {
406 426 return $(e).data("cell");
407 427 });
408 428 };
409 429
410 430 /**
411 431 * Get a Cell object from this notebook.
412 432 *
413 433 * @method get_cell
414 434 * @param {Number} index An index of a cell to retrieve
415 435 * @return {Cell} Cell or null if no cell was found.
416 436 */
417 437 Notebook.prototype.get_cell = function (index) {
418 438 var result = null;
419 439 var ce = this.get_cell_element(index);
420 440 if (ce !== null) {
421 441 result = ce.data('cell');
422 442 }
423 443 return result;
424 444 };
425 445
426 446 /**
427 447 * Get the cell below a given cell.
428 448 *
429 449 * @method get_next_cell
430 450 * @param {Cell} cell The provided cell
431 451 * @return {Cell} the next cell or null if no cell was found.
432 452 */
433 453 Notebook.prototype.get_next_cell = function (cell) {
434 454 var result = null;
435 455 var index = this.find_cell_index(cell);
436 456 if (this.is_valid_cell_index(index+1)) {
437 457 result = this.get_cell(index+1);
438 458 }
439 459 return result;
440 460 };
441 461
442 462 /**
443 463 * Get the cell above a given cell.
444 464 *
445 465 * @method get_prev_cell
446 466 * @param {Cell} cell The provided cell
447 467 * @return {Cell} The previous cell or null if no cell was found.
448 468 */
449 469 Notebook.prototype.get_prev_cell = function (cell) {
450 470 var result = null;
451 471 var index = this.find_cell_index(cell);
452 472 if (index !== null && index > 0) {
453 473 result = this.get_cell(index-1);
454 474 }
455 475 return result;
456 476 };
457 477
458 478 /**
459 479 * Get the numeric index of a given cell.
460 480 *
461 481 * @method find_cell_index
462 482 * @param {Cell} cell The provided cell
463 483 * @return {Number} The cell's numeric index or null if no cell was found.
464 484 */
465 485 Notebook.prototype.find_cell_index = function (cell) {
466 486 var result = null;
467 487 this.get_cell_elements().filter(function (index) {
468 488 if ($(this).data("cell") === cell) {
469 489 result = index;
470 490 }
471 491 });
472 492 return result;
473 493 };
474 494
475 495 /**
476 496 * Get a given index , or the selected index if none is provided.
477 497 *
478 498 * @method index_or_selected
479 499 * @param {Number} index A cell's index
480 500 * @return {Number} The given index, or selected index if none is provided.
481 501 */
482 502 Notebook.prototype.index_or_selected = function (index) {
483 503 var i;
484 504 if (index === undefined || index === null) {
485 505 i = this.get_selected_index();
486 506 if (i === null) {
487 507 i = 0;
488 508 }
489 509 } else {
490 510 i = index;
491 511 }
492 512 return i;
493 513 };
494 514
495 515 /**
496 516 * Get the currently selected cell.
497 517 * @method get_selected_cell
498 518 * @return {Cell} The selected cell
499 519 */
500 520 Notebook.prototype.get_selected_cell = function () {
501 521 var index = this.get_selected_index();
502 522 return this.get_cell(index);
503 523 };
504 524
505 525 /**
506 526 * Check whether a cell index is valid.
507 527 *
508 528 * @method is_valid_cell_index
509 529 * @param {Number} index A cell index
510 530 * @return True if the index is valid, false otherwise
511 531 */
512 532 Notebook.prototype.is_valid_cell_index = function (index) {
513 533 if (index !== null && index >= 0 && index < this.ncells()) {
514 534 return true;
515 535 } else {
516 536 return false;
517 537 }
518 538 };
519 539
520 540 /**
521 541 * Get the index of the currently selected cell.
522 542
523 543 * @method get_selected_index
524 544 * @return {Number} The selected cell's numeric index
525 545 */
526 546 Notebook.prototype.get_selected_index = function () {
527 547 var result = null;
528 548 this.get_cell_elements().filter(function (index) {
529 549 if ($(this).data("cell").selected === true) {
530 550 result = index;
531 551 }
532 552 });
533 553 return result;
534 554 };
535 555
536 556
537 557 // Cell selection.
538 558
539 559 /**
540 560 * Programmatically select a cell.
541 561 *
542 562 * @method select
543 563 * @param {Number} index A cell's index
544 564 * @return {Notebook} This notebook
545 565 */
546 566 Notebook.prototype.select = function (index) {
547 567 if (this.is_valid_cell_index(index)) {
548 568 var sindex = this.get_selected_index();
549 569 if (sindex !== null && index !== sindex) {
550 570 // If we are about to select a different cell, make sure we are
551 571 // first in command mode.
552 572 if (this.mode !== 'command') {
553 573 this.command_mode();
554 574 }
555 575 this.get_cell(sindex).unselect();
556 576 }
557 577 var cell = this.get_cell(index);
558 578 cell.select();
559 579 if (cell.cell_type === 'heading') {
560 580 this.events.trigger('selected_cell_type_changed.Notebook',
561 581 {'cell_type':cell.cell_type,level:cell.level}
562 582 );
563 583 } else {
564 584 this.events.trigger('selected_cell_type_changed.Notebook',
565 585 {'cell_type':cell.cell_type}
566 586 );
567 587 }
568 588 }
569 589 return this;
570 590 };
571 591
572 592 /**
573 593 * Programmatically select the next cell.
574 594 *
575 595 * @method select_next
576 596 * @return {Notebook} This notebook
577 597 */
578 598 Notebook.prototype.select_next = function () {
579 599 var index = this.get_selected_index();
580 600 this.select(index+1);
581 601 return this;
582 602 };
583 603
584 604 /**
585 605 * Programmatically select the previous cell.
586 606 *
587 607 * @method select_prev
588 608 * @return {Notebook} This notebook
589 609 */
590 610 Notebook.prototype.select_prev = function () {
591 611 var index = this.get_selected_index();
592 612 this.select(index-1);
593 613 return this;
594 614 };
595 615
596 616
597 617 // Edit/Command mode
598 618
599 619 /**
600 620 * Gets the index of the cell that is in edit mode.
601 621 *
602 622 * @method get_edit_index
603 623 *
604 624 * @return index {int}
605 625 **/
606 626 Notebook.prototype.get_edit_index = function () {
607 627 var result = null;
608 628 this.get_cell_elements().filter(function (index) {
609 629 if ($(this).data("cell").mode === 'edit') {
610 630 result = index;
611 631 }
612 632 });
613 633 return result;
614 634 };
615 635
616 636 /**
617 637 * Handle when a a cell blurs and the notebook should enter command mode.
618 638 *
619 639 * @method handle_command_mode
620 640 * @param [cell] {Cell} Cell to enter command mode on.
621 641 **/
622 642 Notebook.prototype.handle_command_mode = function (cell) {
623 643 if (this.mode !== 'command') {
624 644 cell.command_mode();
625 645 this.mode = 'command';
626 646 this.events.trigger('command_mode.Notebook');
627 647 this.keyboard_manager.command_mode();
628 648 }
629 649 };
630 650
631 651 /**
632 652 * Make the notebook enter command mode.
633 653 *
634 654 * @method command_mode
635 655 **/
636 656 Notebook.prototype.command_mode = function () {
637 657 var cell = this.get_cell(this.get_edit_index());
638 658 if (cell && this.mode !== 'command') {
639 659 // We don't call cell.command_mode, but rather call cell.focus_cell()
640 660 // which will blur and CM editor and trigger the call to
641 661 // handle_command_mode.
642 662 cell.focus_cell();
643 663 }
644 664 };
645 665
646 666 /**
647 667 * Handle when a cell fires it's edit_mode event.
648 668 *
649 669 * @method handle_edit_mode
650 670 * @param [cell] {Cell} Cell to enter edit mode on.
651 671 **/
652 672 Notebook.prototype.handle_edit_mode = function (cell) {
653 673 if (cell && this.mode !== 'edit') {
654 674 cell.edit_mode();
655 675 this.mode = 'edit';
656 676 this.events.trigger('edit_mode.Notebook');
657 677 this.keyboard_manager.edit_mode();
658 678 }
659 679 };
660 680
661 681 /**
662 682 * Make a cell enter edit mode.
663 683 *
664 684 * @method edit_mode
665 685 **/
666 686 Notebook.prototype.edit_mode = function () {
667 687 var cell = this.get_selected_cell();
668 688 if (cell && this.mode !== 'edit') {
669 689 cell.unrender();
670 690 cell.focus_editor();
671 691 }
672 692 };
673 693
674 694 /**
675 695 * Focus the currently selected cell.
676 696 *
677 697 * @method focus_cell
678 698 **/
679 699 Notebook.prototype.focus_cell = function () {
680 700 var cell = this.get_selected_cell();
681 701 if (cell === null) {return;} // No cell is selected
682 702 cell.focus_cell();
683 703 };
684 704
685 705 // Cell movement
686 706
687 707 /**
688 708 * Move given (or selected) cell up and select it.
689 709 *
690 710 * @method move_cell_up
691 711 * @param [index] {integer} cell index
692 712 * @return {Notebook} This notebook
693 713 **/
694 714 Notebook.prototype.move_cell_up = function (index) {
695 715 var i = this.index_or_selected(index);
696 716 if (this.is_valid_cell_index(i) && i > 0) {
697 717 var pivot = this.get_cell_element(i-1);
698 718 var tomove = this.get_cell_element(i);
699 719 if (pivot !== null && tomove !== null) {
700 720 tomove.detach();
701 721 pivot.before(tomove);
702 722 this.select(i-1);
703 723 var cell = this.get_selected_cell();
704 724 cell.focus_cell();
705 725 }
706 726 this.set_dirty(true);
707 727 }
708 728 return this;
709 729 };
710 730
711 731
712 732 /**
713 733 * Move given (or selected) cell down and select it
714 734 *
715 735 * @method move_cell_down
716 736 * @param [index] {integer} cell index
717 737 * @return {Notebook} This notebook
718 738 **/
719 739 Notebook.prototype.move_cell_down = function (index) {
720 740 var i = this.index_or_selected(index);
721 741 if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
722 742 var pivot = this.get_cell_element(i+1);
723 743 var tomove = this.get_cell_element(i);
724 744 if (pivot !== null && tomove !== null) {
725 745 tomove.detach();
726 746 pivot.after(tomove);
727 747 this.select(i+1);
728 748 var cell = this.get_selected_cell();
729 749 cell.focus_cell();
730 750 }
731 751 }
732 752 this.set_dirty();
733 753 return this;
734 754 };
735 755
736 756
737 757 // Insertion, deletion.
738 758
739 759 /**
740 760 * Delete a cell from the notebook.
741 761 *
742 762 * @method delete_cell
743 763 * @param [index] A cell's numeric index
744 764 * @return {Notebook} This notebook
745 765 */
746 766 Notebook.prototype.delete_cell = function (index) {
747 767 var i = this.index_or_selected(index);
748 768 var cell = this.get_cell(i);
749 769 if (!cell.is_deletable()) {
750 770 return this;
751 771 }
752 772
753 773 this.undelete_backup = cell.toJSON();
754 774 $('#undelete_cell').removeClass('disabled');
755 775 if (this.is_valid_cell_index(i)) {
756 776 var old_ncells = this.ncells();
757 777 var ce = this.get_cell_element(i);
758 778 ce.remove();
759 779 if (i === 0) {
760 780 // Always make sure we have at least one cell.
761 781 if (old_ncells === 1) {
762 782 this.insert_cell_below('code');
763 783 }
764 784 this.select(0);
765 785 this.undelete_index = 0;
766 786 this.undelete_below = false;
767 787 } else if (i === old_ncells-1 && i !== 0) {
768 788 this.select(i-1);
769 789 this.undelete_index = i - 1;
770 790 this.undelete_below = true;
771 791 } else {
772 792 this.select(i);
773 793 this.undelete_index = i;
774 794 this.undelete_below = false;
775 795 }
776 796 this.events.trigger('delete.Cell', {'cell': cell, 'index': i});
777 797 this.set_dirty(true);
778 798 }
779 799 return this;
780 800 };
781 801
782 802 /**
783 803 * Restore the most recently deleted cell.
784 804 *
785 805 * @method undelete
786 806 */
787 807 Notebook.prototype.undelete_cell = function() {
788 808 if (this.undelete_backup !== null && this.undelete_index !== null) {
789 809 var current_index = this.get_selected_index();
790 810 if (this.undelete_index < current_index) {
791 811 current_index = current_index + 1;
792 812 }
793 813 if (this.undelete_index >= this.ncells()) {
794 814 this.select(this.ncells() - 1);
795 815 }
796 816 else {
797 817 this.select(this.undelete_index);
798 818 }
799 819 var cell_data = this.undelete_backup;
800 820 var new_cell = null;
801 821 if (this.undelete_below) {
802 822 new_cell = this.insert_cell_below(cell_data.cell_type);
803 823 } else {
804 824 new_cell = this.insert_cell_above(cell_data.cell_type);
805 825 }
806 826 new_cell.fromJSON(cell_data);
807 827 if (this.undelete_below) {
808 828 this.select(current_index+1);
809 829 } else {
810 830 this.select(current_index);
811 831 }
812 832 this.undelete_backup = null;
813 833 this.undelete_index = null;
814 834 }
815 835 $('#undelete_cell').addClass('disabled');
816 836 };
817 837
818 838 /**
819 839 * Insert a cell so that after insertion the cell is at given index.
820 840 *
821 841 * If cell type is not provided, it will default to the type of the
822 842 * currently active cell.
823 843 *
824 844 * Similar to insert_above, but index parameter is mandatory
825 845 *
826 846 * Index will be brought back into the accessible range [0,n]
827 847 *
828 848 * @method insert_cell_at_index
829 849 * @param [type] {string} in ['code','markdown', 'raw'], defaults to 'code'
830 850 * @param [index] {int} a valid index where to insert cell
831 851 *
832 852 * @return cell {cell|null} created cell or null
833 853 **/
834 854 Notebook.prototype.insert_cell_at_index = function(type, index){
835 855
836 856 var ncells = this.ncells();
837 857 index = Math.min(index, ncells);
838 858 index = Math.max(index, 0);
839 859 var cell = null;
840 860 type = type || this.default_cell_type;
841 861 if (type === 'above') {
842 862 if (index > 0) {
843 863 type = this.get_cell(index-1).cell_type;
844 864 } else {
845 865 type = 'code';
846 866 }
847 867 } else if (type === 'below') {
848 868 if (index < ncells) {
849 869 type = this.get_cell(index).cell_type;
850 870 } else {
851 871 type = 'code';
852 872 }
853 873 } else if (type === 'selected') {
854 874 type = this.get_selected_cell().cell_type;
855 875 }
856 876
857 877 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
858 878 var cell_options = {
859 879 events: this.events,
860 880 config: this.config,
861 881 keyboard_manager: this.keyboard_manager,
862 882 notebook: this,
863 883 tooltip: this.tooltip,
864 884 };
865 885 switch(type) {
866 886 case 'code':
867 887 cell = new codecell.CodeCell(this.kernel, cell_options);
868 888 cell.set_input_prompt();
869 889 break;
870 890 case 'markdown':
871 891 cell = new textcell.MarkdownCell(cell_options);
872 892 break;
873 893 case 'raw':
874 894 cell = new textcell.RawCell(cell_options);
875 895 break;
876 896 default:
877 897 console.log("invalid cell type: ", type);
878 898 }
879 899
880 900 if(this._insert_element_at_index(cell.element,index)) {
881 901 cell.render();
882 902 this.events.trigger('create.Cell', {'cell': cell, 'index': index});
883 903 cell.refresh();
884 904 // We used to select the cell after we refresh it, but there
885 905 // are now cases were this method is called where select is
886 906 // not appropriate. The selection logic should be handled by the
887 907 // caller of the the top level insert_cell methods.
888 908 this.set_dirty(true);
889 909 }
890 910 }
891 911 return cell;
892 912
893 913 };
894 914
895 915 /**
896 916 * Insert an element at given cell index.
897 917 *
898 918 * @method _insert_element_at_index
899 919 * @param element {dom_element} a cell element
900 920 * @param [index] {int} a valid index where to inser cell
901 921 * @private
902 922 *
903 923 * return true if everything whent fine.
904 924 **/
905 925 Notebook.prototype._insert_element_at_index = function(element, index){
906 926 if (element === undefined){
907 927 return false;
908 928 }
909 929
910 930 var ncells = this.ncells();
911 931
912 932 if (ncells === 0) {
913 933 // special case append if empty
914 934 this.element.find('div.end_space').before(element);
915 935 } else if ( ncells === index ) {
916 936 // special case append it the end, but not empty
917 937 this.get_cell_element(index-1).after(element);
918 938 } else if (this.is_valid_cell_index(index)) {
919 939 // otherwise always somewhere to append to
920 940 this.get_cell_element(index).before(element);
921 941 } else {
922 942 return false;
923 943 }
924 944
925 945 if (this.undelete_index !== null && index <= this.undelete_index) {
926 946 this.undelete_index = this.undelete_index + 1;
927 947 this.set_dirty(true);
928 948 }
929 949 return true;
930 950 };
931 951
932 952 /**
933 953 * Insert a cell of given type above given index, or at top
934 954 * of notebook if index smaller than 0.
935 955 *
936 956 * default index value is the one of currently selected cell
937 957 *
938 958 * @method insert_cell_above
939 959 * @param [type] {string} cell type
940 960 * @param [index] {integer}
941 961 *
942 962 * @return handle to created cell or null
943 963 **/
944 964 Notebook.prototype.insert_cell_above = function (type, index) {
945 965 index = this.index_or_selected(index);
946 966 return this.insert_cell_at_index(type, index);
947 967 };
948 968
949 969 /**
950 970 * Insert a cell of given type below given index, or at bottom
951 971 * of notebook if index greater than number of cells
952 972 *
953 973 * default index value is the one of currently selected cell
954 974 *
955 975 * @method insert_cell_below
956 976 * @param [type] {string} cell type
957 977 * @param [index] {integer}
958 978 *
959 979 * @return handle to created cell or null
960 980 *
961 981 **/
962 982 Notebook.prototype.insert_cell_below = function (type, index) {
963 983 index = this.index_or_selected(index);
964 984 return this.insert_cell_at_index(type, index+1);
965 985 };
966 986
967 987
968 988 /**
969 989 * Insert cell at end of notebook
970 990 *
971 991 * @method insert_cell_at_bottom
972 992 * @param {String} type cell type
973 993 *
974 994 * @return the added cell; or null
975 995 **/
976 996 Notebook.prototype.insert_cell_at_bottom = function (type){
977 997 var len = this.ncells();
978 998 return this.insert_cell_below(type,len-1);
979 999 };
980 1000
981 1001 /**
982 1002 * Turn a cell into a code cell.
983 1003 *
984 1004 * @method to_code
985 1005 * @param {Number} [index] A cell's index
986 1006 */
987 1007 Notebook.prototype.to_code = function (index) {
988 1008 var i = this.index_or_selected(index);
989 1009 if (this.is_valid_cell_index(i)) {
990 1010 var source_cell = this.get_cell(i);
991 1011 if (!(source_cell instanceof codecell.CodeCell)) {
992 1012 var target_cell = this.insert_cell_below('code',i);
993 1013 var text = source_cell.get_text();
994 1014 if (text === source_cell.placeholder) {
995 1015 text = '';
996 1016 }
997 1017 //metadata
998 1018 target_cell.metadata = source_cell.metadata;
999 1019
1000 1020 target_cell.set_text(text);
1001 1021 // make this value the starting point, so that we can only undo
1002 1022 // to this state, instead of a blank cell
1003 1023 target_cell.code_mirror.clearHistory();
1004 1024 source_cell.element.remove();
1005 1025 this.select(i);
1006 1026 var cursor = source_cell.code_mirror.getCursor();
1007 1027 target_cell.code_mirror.setCursor(cursor);
1008 1028 this.set_dirty(true);
1009 1029 }
1010 1030 }
1011 1031 };
1012 1032
1013 1033 /**
1014 1034 * Turn a cell into a Markdown cell.
1015 1035 *
1016 1036 * @method to_markdown
1017 1037 * @param {Number} [index] A cell's index
1018 1038 */
1019 1039 Notebook.prototype.to_markdown = function (index) {
1020 1040 var i = this.index_or_selected(index);
1021 1041 if (this.is_valid_cell_index(i)) {
1022 1042 var source_cell = this.get_cell(i);
1023 1043
1024 1044 if (!(source_cell instanceof textcell.MarkdownCell)) {
1025 1045 var target_cell = this.insert_cell_below('markdown',i);
1026 1046 var text = source_cell.get_text();
1027 1047
1028 1048 if (text === source_cell.placeholder) {
1029 1049 text = '';
1030 1050 }
1031 1051 // metadata
1032 1052 target_cell.metadata = source_cell.metadata;
1033 1053 // We must show the editor before setting its contents
1034 1054 target_cell.unrender();
1035 1055 target_cell.set_text(text);
1036 1056 // make this value the starting point, so that we can only undo
1037 1057 // to this state, instead of a blank cell
1038 1058 target_cell.code_mirror.clearHistory();
1039 1059 source_cell.element.remove();
1040 1060 this.select(i);
1041 1061 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1042 1062 target_cell.render();
1043 1063 }
1044 1064 var cursor = source_cell.code_mirror.getCursor();
1045 1065 target_cell.code_mirror.setCursor(cursor);
1046 1066 this.set_dirty(true);
1047 1067 }
1048 1068 }
1049 1069 };
1050 1070
1051 1071 /**
1052 1072 * Turn a cell into a raw text cell.
1053 1073 *
1054 1074 * @method to_raw
1055 1075 * @param {Number} [index] A cell's index
1056 1076 */
1057 1077 Notebook.prototype.to_raw = function (index) {
1058 1078 var i = this.index_or_selected(index);
1059 1079 if (this.is_valid_cell_index(i)) {
1060 1080 var target_cell = null;
1061 1081 var source_cell = this.get_cell(i);
1062 1082
1063 1083 if (!(source_cell instanceof textcell.RawCell)) {
1064 1084 target_cell = this.insert_cell_below('raw',i);
1065 1085 var text = source_cell.get_text();
1066 1086 if (text === source_cell.placeholder) {
1067 1087 text = '';
1068 1088 }
1069 1089 //metadata
1070 1090 target_cell.metadata = source_cell.metadata;
1071 1091 // We must show the editor before setting its contents
1072 1092 target_cell.unrender();
1073 1093 target_cell.set_text(text);
1074 1094 // make this value the starting point, so that we can only undo
1075 1095 // to this state, instead of a blank cell
1076 1096 target_cell.code_mirror.clearHistory();
1077 1097 source_cell.element.remove();
1078 1098 this.select(i);
1079 1099 var cursor = source_cell.code_mirror.getCursor();
1080 1100 target_cell.code_mirror.setCursor(cursor);
1081 1101 this.set_dirty(true);
1082 1102 }
1083 1103 }
1084 1104 };
1085 1105
1086 1106 Notebook.prototype._warn_heading = function () {
1087 1107 // warn about heading cells being removed
1088 1108 dialog.modal({
1089 1109 notebook: this,
1090 1110 keyboard_manager: this.keyboard_manager,
1091 1111 title : "Use markdown headings",
1092 1112 body : $("<p/>").text(
1093 1113 'IPython no longer uses special heading cells. ' +
1094 1114 'Instead, write your headings in Markdown cells using # characters:'
1095 1115 ).append($('<pre/>').text(
1096 1116 '## This is a level 2 heading'
1097 1117 )),
1098 1118 buttons : {
1099 1119 "OK" : {},
1100 1120 }
1101 1121 });
1102 1122 };
1103 1123
1104 1124 /**
1105 1125 * Turn a cell into a markdown cell with a heading.
1106 1126 *
1107 1127 * @method to_heading
1108 1128 * @param {Number} [index] A cell's index
1109 1129 * @param {Number} [level] A heading level (e.g., 1 for h1)
1110 1130 */
1111 1131 Notebook.prototype.to_heading = function (index, level) {
1112 1132 this.to_markdown(index);
1113 1133 level = level || 1;
1114 1134 var i = this.index_or_selected(index);
1115 1135 if (this.is_valid_cell_index(i)) {
1116 1136 var cell = this.get_cell(i);
1117 1137 cell.set_heading_level(level);
1118 1138 this.set_dirty(true);
1119 1139 }
1120 1140 };
1121 1141
1122 1142
1123 1143 // Cut/Copy/Paste
1124 1144
1125 1145 /**
1126 1146 * Enable UI elements for pasting cells.
1127 1147 *
1128 1148 * @method enable_paste
1129 1149 */
1130 1150 Notebook.prototype.enable_paste = function () {
1131 1151 var that = this;
1132 1152 if (!this.paste_enabled) {
1133 1153 $('#paste_cell_replace').removeClass('disabled')
1134 1154 .on('click', function () {that.paste_cell_replace();});
1135 1155 $('#paste_cell_above').removeClass('disabled')
1136 1156 .on('click', function () {that.paste_cell_above();});
1137 1157 $('#paste_cell_below').removeClass('disabled')
1138 1158 .on('click', function () {that.paste_cell_below();});
1139 1159 this.paste_enabled = true;
1140 1160 }
1141 1161 };
1142 1162
1143 1163 /**
1144 1164 * Disable UI elements for pasting cells.
1145 1165 *
1146 1166 * @method disable_paste
1147 1167 */
1148 1168 Notebook.prototype.disable_paste = function () {
1149 1169 if (this.paste_enabled) {
1150 1170 $('#paste_cell_replace').addClass('disabled').off('click');
1151 1171 $('#paste_cell_above').addClass('disabled').off('click');
1152 1172 $('#paste_cell_below').addClass('disabled').off('click');
1153 1173 this.paste_enabled = false;
1154 1174 }
1155 1175 };
1156 1176
1157 1177 /**
1158 1178 * Cut a cell.
1159 1179 *
1160 1180 * @method cut_cell
1161 1181 */
1162 1182 Notebook.prototype.cut_cell = function () {
1163 1183 this.copy_cell();
1164 1184 this.delete_cell();
1165 1185 };
1166 1186
1167 1187 /**
1168 1188 * Copy a cell.
1169 1189 *
1170 1190 * @method copy_cell
1171 1191 */
1172 1192 Notebook.prototype.copy_cell = function () {
1173 1193 var cell = this.get_selected_cell();
1174 1194 this.clipboard = cell.toJSON();
1175 1195 // remove undeletable status from the copied cell
1176 1196 if (this.clipboard.metadata.deletable !== undefined) {
1177 1197 delete this.clipboard.metadata.deletable;
1178 1198 }
1179 1199 this.enable_paste();
1180 1200 };
1181 1201
1182 1202 /**
1183 1203 * Replace the selected cell with a cell in the clipboard.
1184 1204 *
1185 1205 * @method paste_cell_replace
1186 1206 */
1187 1207 Notebook.prototype.paste_cell_replace = function () {
1188 1208 if (this.clipboard !== null && this.paste_enabled) {
1189 1209 var cell_data = this.clipboard;
1190 1210 var new_cell = this.insert_cell_above(cell_data.cell_type);
1191 1211 new_cell.fromJSON(cell_data);
1192 1212 var old_cell = this.get_next_cell(new_cell);
1193 1213 this.delete_cell(this.find_cell_index(old_cell));
1194 1214 this.select(this.find_cell_index(new_cell));
1195 1215 }
1196 1216 };
1197 1217
1198 1218 /**
1199 1219 * Paste a cell from the clipboard above the selected cell.
1200 1220 *
1201 1221 * @method paste_cell_above
1202 1222 */
1203 1223 Notebook.prototype.paste_cell_above = function () {
1204 1224 if (this.clipboard !== null && this.paste_enabled) {
1205 1225 var cell_data = this.clipboard;
1206 1226 var new_cell = this.insert_cell_above(cell_data.cell_type);
1207 1227 new_cell.fromJSON(cell_data);
1208 1228 new_cell.focus_cell();
1209 1229 }
1210 1230 };
1211 1231
1212 1232 /**
1213 1233 * Paste a cell from the clipboard below the selected cell.
1214 1234 *
1215 1235 * @method paste_cell_below
1216 1236 */
1217 1237 Notebook.prototype.paste_cell_below = function () {
1218 1238 if (this.clipboard !== null && this.paste_enabled) {
1219 1239 var cell_data = this.clipboard;
1220 1240 var new_cell = this.insert_cell_below(cell_data.cell_type);
1221 1241 new_cell.fromJSON(cell_data);
1222 1242 new_cell.focus_cell();
1223 1243 }
1224 1244 };
1225 1245
1226 1246 // Split/merge
1227 1247
1228 1248 /**
1229 1249 * Split the selected cell into two, at the cursor.
1230 1250 *
1231 1251 * @method split_cell
1232 1252 */
1233 1253 Notebook.prototype.split_cell = function () {
1234 1254 var cell = this.get_selected_cell();
1235 1255 if (cell.is_splittable()) {
1236 1256 var texta = cell.get_pre_cursor();
1237 1257 var textb = cell.get_post_cursor();
1238 1258 cell.set_text(textb);
1239 1259 var new_cell = this.insert_cell_above(cell.cell_type);
1240 1260 // Unrender the new cell so we can call set_text.
1241 1261 new_cell.unrender();
1242 1262 new_cell.set_text(texta);
1243 1263 }
1244 1264 };
1245 1265
1246 1266 /**
1247 1267 * Combine the selected cell into the cell above it.
1248 1268 *
1249 1269 * @method merge_cell_above
1250 1270 */
1251 1271 Notebook.prototype.merge_cell_above = function () {
1252 1272 var index = this.get_selected_index();
1253 1273 var cell = this.get_cell(index);
1254 1274 var render = cell.rendered;
1255 1275 if (!cell.is_mergeable()) {
1256 1276 return;
1257 1277 }
1258 1278 if (index > 0) {
1259 1279 var upper_cell = this.get_cell(index-1);
1260 1280 if (!upper_cell.is_mergeable()) {
1261 1281 return;
1262 1282 }
1263 1283 var upper_text = upper_cell.get_text();
1264 1284 var text = cell.get_text();
1265 1285 if (cell instanceof codecell.CodeCell) {
1266 1286 cell.set_text(upper_text+'\n'+text);
1267 1287 } else {
1268 1288 cell.unrender(); // Must unrender before we set_text.
1269 1289 cell.set_text(upper_text+'\n\n'+text);
1270 1290 if (render) {
1271 1291 // The rendered state of the final cell should match
1272 1292 // that of the original selected cell;
1273 1293 cell.render();
1274 1294 }
1275 1295 }
1276 1296 this.delete_cell(index-1);
1277 1297 this.select(this.find_cell_index(cell));
1278 1298 }
1279 1299 };
1280 1300
1281 1301 /**
1282 1302 * Combine the selected cell into the cell below it.
1283 1303 *
1284 1304 * @method merge_cell_below
1285 1305 */
1286 1306 Notebook.prototype.merge_cell_below = function () {
1287 1307 var index = this.get_selected_index();
1288 1308 var cell = this.get_cell(index);
1289 1309 var render = cell.rendered;
1290 1310 if (!cell.is_mergeable()) {
1291 1311 return;
1292 1312 }
1293 1313 if (index < this.ncells()-1) {
1294 1314 var lower_cell = this.get_cell(index+1);
1295 1315 if (!lower_cell.is_mergeable()) {
1296 1316 return;
1297 1317 }
1298 1318 var lower_text = lower_cell.get_text();
1299 1319 var text = cell.get_text();
1300 1320 if (cell instanceof codecell.CodeCell) {
1301 1321 cell.set_text(text+'\n'+lower_text);
1302 1322 } else {
1303 1323 cell.unrender(); // Must unrender before we set_text.
1304 1324 cell.set_text(text+'\n\n'+lower_text);
1305 1325 if (render) {
1306 1326 // The rendered state of the final cell should match
1307 1327 // that of the original selected cell;
1308 1328 cell.render();
1309 1329 }
1310 1330 }
1311 1331 this.delete_cell(index+1);
1312 1332 this.select(this.find_cell_index(cell));
1313 1333 }
1314 1334 };
1315 1335
1316 1336
1317 1337 // Cell collapsing and output clearing
1318 1338
1319 1339 /**
1320 1340 * Hide a cell's output.
1321 1341 *
1322 1342 * @method collapse_output
1323 1343 * @param {Number} index A cell's numeric index
1324 1344 */
1325 1345 Notebook.prototype.collapse_output = function (index) {
1326 1346 var i = this.index_or_selected(index);
1327 1347 var cell = this.get_cell(i);
1328 1348 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1329 1349 cell.collapse_output();
1330 1350 this.set_dirty(true);
1331 1351 }
1332 1352 };
1333 1353
1334 1354 /**
1335 1355 * Hide each code cell's output area.
1336 1356 *
1337 1357 * @method collapse_all_output
1338 1358 */
1339 1359 Notebook.prototype.collapse_all_output = function () {
1340 1360 this.get_cells().map(function (cell, i) {
1341 1361 if (cell instanceof codecell.CodeCell) {
1342 1362 cell.collapse_output();
1343 1363 }
1344 1364 });
1345 1365 // this should not be set if the `collapse` key is removed from nbformat
1346 1366 this.set_dirty(true);
1347 1367 };
1348 1368
1349 1369 /**
1350 1370 * Show a cell's output.
1351 1371 *
1352 1372 * @method expand_output
1353 1373 * @param {Number} index A cell's numeric index
1354 1374 */
1355 1375 Notebook.prototype.expand_output = function (index) {
1356 1376 var i = this.index_or_selected(index);
1357 1377 var cell = this.get_cell(i);
1358 1378 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1359 1379 cell.expand_output();
1360 1380 this.set_dirty(true);
1361 1381 }
1362 1382 };
1363 1383
1364 1384 /**
1365 1385 * Expand each code cell's output area, and remove scrollbars.
1366 1386 *
1367 1387 * @method expand_all_output
1368 1388 */
1369 1389 Notebook.prototype.expand_all_output = function () {
1370 1390 this.get_cells().map(function (cell, i) {
1371 1391 if (cell instanceof codecell.CodeCell) {
1372 1392 cell.expand_output();
1373 1393 }
1374 1394 });
1375 1395 // this should not be set if the `collapse` key is removed from nbformat
1376 1396 this.set_dirty(true);
1377 1397 };
1378 1398
1379 1399 /**
1380 1400 * Clear the selected CodeCell's output area.
1381 1401 *
1382 1402 * @method clear_output
1383 1403 * @param {Number} index A cell's numeric index
1384 1404 */
1385 1405 Notebook.prototype.clear_output = function (index) {
1386 1406 var i = this.index_or_selected(index);
1387 1407 var cell = this.get_cell(i);
1388 1408 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1389 1409 cell.clear_output();
1390 1410 this.set_dirty(true);
1391 1411 }
1392 1412 };
1393 1413
1394 1414 /**
1395 1415 * Clear each code cell's output area.
1396 1416 *
1397 1417 * @method clear_all_output
1398 1418 */
1399 1419 Notebook.prototype.clear_all_output = function () {
1400 1420 this.get_cells().map(function (cell, i) {
1401 1421 if (cell instanceof codecell.CodeCell) {
1402 1422 cell.clear_output();
1403 1423 }
1404 1424 });
1405 1425 this.set_dirty(true);
1406 1426 };
1407 1427
1408 1428 /**
1409 1429 * Scroll the selected CodeCell's output area.
1410 1430 *
1411 1431 * @method scroll_output
1412 1432 * @param {Number} index A cell's numeric index
1413 1433 */
1414 1434 Notebook.prototype.scroll_output = function (index) {
1415 1435 var i = this.index_or_selected(index);
1416 1436 var cell = this.get_cell(i);
1417 1437 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1418 1438 cell.scroll_output();
1419 1439 this.set_dirty(true);
1420 1440 }
1421 1441 };
1422 1442
1423 1443 /**
1424 1444 * Expand each code cell's output area, and add a scrollbar for long output.
1425 1445 *
1426 1446 * @method scroll_all_output
1427 1447 */
1428 1448 Notebook.prototype.scroll_all_output = function () {
1429 1449 this.get_cells().map(function (cell, i) {
1430 1450 if (cell instanceof codecell.CodeCell) {
1431 1451 cell.scroll_output();
1432 1452 }
1433 1453 });
1434 1454 // this should not be set if the `collapse` key is removed from nbformat
1435 1455 this.set_dirty(true);
1436 1456 };
1437 1457
1438 1458 /** Toggle whether a cell's output is collapsed or expanded.
1439 1459 *
1440 1460 * @method toggle_output
1441 1461 * @param {Number} index A cell's numeric index
1442 1462 */
1443 1463 Notebook.prototype.toggle_output = function (index) {
1444 1464 var i = this.index_or_selected(index);
1445 1465 var cell = this.get_cell(i);
1446 1466 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1447 1467 cell.toggle_output();
1448 1468 this.set_dirty(true);
1449 1469 }
1450 1470 };
1451 1471
1452 1472 /**
1453 1473 * Hide/show the output of all cells.
1454 1474 *
1455 1475 * @method toggle_all_output
1456 1476 */
1457 1477 Notebook.prototype.toggle_all_output = function () {
1458 1478 this.get_cells().map(function (cell, i) {
1459 1479 if (cell instanceof codecell.CodeCell) {
1460 1480 cell.toggle_output();
1461 1481 }
1462 1482 });
1463 1483 // this should not be set if the `collapse` key is removed from nbformat
1464 1484 this.set_dirty(true);
1465 1485 };
1466 1486
1467 1487 /**
1468 1488 * Toggle a scrollbar for long cell outputs.
1469 1489 *
1470 1490 * @method toggle_output_scroll
1471 1491 * @param {Number} index A cell's numeric index
1472 1492 */
1473 1493 Notebook.prototype.toggle_output_scroll = function (index) {
1474 1494 var i = this.index_or_selected(index);
1475 1495 var cell = this.get_cell(i);
1476 1496 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1477 1497 cell.toggle_output_scroll();
1478 1498 this.set_dirty(true);
1479 1499 }
1480 1500 };
1481 1501
1482 1502 /**
1483 1503 * Toggle the scrolling of long output on all cells.
1484 1504 *
1485 1505 * @method toggle_all_output_scrolling
1486 1506 */
1487 1507 Notebook.prototype.toggle_all_output_scroll = function () {
1488 1508 this.get_cells().map(function (cell, i) {
1489 1509 if (cell instanceof codecell.CodeCell) {
1490 1510 cell.toggle_output_scroll();
1491 1511 }
1492 1512 });
1493 1513 // this should not be set if the `collapse` key is removed from nbformat
1494 1514 this.set_dirty(true);
1495 1515 };
1496 1516
1497 1517 // Other cell functions: line numbers, ...
1498 1518
1499 1519 /**
1500 1520 * Toggle line numbers in the selected cell's input area.
1501 1521 *
1502 1522 * @method cell_toggle_line_numbers
1503 1523 */
1504 1524 Notebook.prototype.cell_toggle_line_numbers = function() {
1505 1525 this.get_selected_cell().toggle_line_numbers();
1506 1526 };
1507 1527
1508 1528 /**
1509 1529 * Set the codemirror mode for all code cells, including the default for
1510 1530 * new code cells.
1511 1531 *
1512 1532 * @method set_codemirror_mode
1513 1533 */
1514 1534 Notebook.prototype.set_codemirror_mode = function(newmode){
1515 1535 if (newmode === this.codemirror_mode) {
1516 1536 return;
1517 1537 }
1518 1538 this.codemirror_mode = newmode;
1519 1539 codecell.CodeCell.options_default.cm_config.mode = newmode;
1520 1540 var modename = newmode.mode || newmode.name || newmode;
1521 1541
1522 1542 var that = this;
1523 1543 utils.requireCodeMirrorMode(modename, function () {
1524 1544 that.get_cells().map(function(cell, i) {
1525 1545 if (cell.cell_type === 'code'){
1526 1546 cell.code_mirror.setOption('mode', newmode);
1527 1547 // This is currently redundant, because cm_config ends up as
1528 1548 // codemirror's own .options object, but I don't want to
1529 1549 // rely on that.
1530 1550 cell.cm_config.mode = newmode;
1531 1551 }
1532 1552 });
1533 1553 });
1534 1554 };
1535 1555
1536 1556 // Session related things
1537 1557
1538 1558 /**
1539 1559 * Start a new session and set it on each code cell.
1540 1560 *
1541 1561 * @method start_session
1542 1562 */
1543 1563 Notebook.prototype.start_session = function (kernel_name) {
1544 1564 if (this._session_starting) {
1545 1565 throw new session.SessionAlreadyStarting();
1546 1566 }
1547 1567 this._session_starting = true;
1548 1568
1549 1569 var options = {
1550 1570 base_url: this.base_url,
1551 1571 ws_url: this.ws_url,
1552 1572 notebook_path: this.notebook_path,
1553 1573 notebook_name: this.notebook_name,
1554 1574 kernel_name: kernel_name,
1555 1575 notebook: this
1556 1576 };
1557 1577
1558 1578 var success = $.proxy(this._session_started, this);
1559 1579 var failure = $.proxy(this._session_start_failed, this);
1560 1580
1561 1581 if (this.session !== null) {
1562 1582 this.session.restart(options, success, failure);
1563 1583 } else {
1564 1584 this.session = new session.Session(options);
1565 1585 this.session.start(success, failure);
1566 1586 }
1567 1587 };
1568 1588
1569 1589
1570 1590 /**
1571 1591 * Once a session is started, link the code cells to the kernel and pass the
1572 1592 * comm manager to the widget manager
1573 1593 *
1574 1594 */
1575 1595 Notebook.prototype._session_started = function (){
1576 1596 this._session_starting = false;
1577 1597 this.kernel = this.session.kernel;
1578 1598 var ncells = this.ncells();
1579 1599 for (var i=0; i<ncells; i++) {
1580 1600 var cell = this.get_cell(i);
1581 1601 if (cell instanceof codecell.CodeCell) {
1582 1602 cell.set_kernel(this.session.kernel);
1583 1603 }
1584 1604 }
1585 1605 };
1586 1606 Notebook.prototype._session_start_failed = function (jqxhr, status, error){
1587 1607 this._session_starting = false;
1588 1608 utils.log_ajax_error(jqxhr, status, error);
1589 1609 };
1590 1610
1591 1611 /**
1592 1612 * Prompt the user to restart the IPython kernel.
1593 1613 *
1594 1614 * @method restart_kernel
1595 1615 */
1596 1616 Notebook.prototype.restart_kernel = function () {
1597 1617 var that = this;
1598 1618 dialog.modal({
1599 1619 notebook: this,
1600 1620 keyboard_manager: this.keyboard_manager,
1601 1621 title : "Restart kernel or continue running?",
1602 1622 body : $("<p/>").text(
1603 1623 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1604 1624 ),
1605 1625 buttons : {
1606 1626 "Continue running" : {},
1607 1627 "Restart" : {
1608 1628 "class" : "btn-danger",
1609 1629 "click" : function() {
1610 1630 that.kernel.restart();
1611 1631 }
1612 1632 }
1613 1633 }
1614 1634 });
1615 1635 };
1616 1636
1617 1637 /**
1618 1638 * Execute or render cell outputs and go into command mode.
1619 1639 *
1620 1640 * @method execute_cell
1621 1641 */
1622 1642 Notebook.prototype.execute_cell = function () {
1623 1643 // mode = shift, ctrl, alt
1624 1644 var cell = this.get_selected_cell();
1625 1645
1626 1646 cell.execute();
1627 1647 this.command_mode();
1628 1648 this.set_dirty(true);
1629 1649 };
1630 1650
1631 1651 /**
1632 1652 * Execute or render cell outputs and insert a new cell below.
1633 1653 *
1634 1654 * @method execute_cell_and_insert_below
1635 1655 */
1636 1656 Notebook.prototype.execute_cell_and_insert_below = function () {
1637 1657 var cell = this.get_selected_cell();
1638 1658 var cell_index = this.find_cell_index(cell);
1639 1659
1640 1660 cell.execute();
1641 1661
1642 1662 // If we are at the end always insert a new cell and return
1643 1663 if (cell_index === (this.ncells()-1)) {
1644 1664 this.command_mode();
1645 1665 this.insert_cell_below();
1646 1666 this.select(cell_index+1);
1647 1667 this.edit_mode();
1648 1668 this.scroll_to_bottom();
1649 1669 this.set_dirty(true);
1650 1670 return;
1651 1671 }
1652 1672
1653 1673 this.command_mode();
1654 1674 this.insert_cell_below();
1655 1675 this.select(cell_index+1);
1656 1676 this.edit_mode();
1657 1677 this.set_dirty(true);
1658 1678 };
1659 1679
1660 1680 /**
1661 1681 * Execute or render cell outputs and select the next cell.
1662 1682 *
1663 1683 * @method execute_cell_and_select_below
1664 1684 */
1665 1685 Notebook.prototype.execute_cell_and_select_below = function () {
1666 1686
1667 1687 var cell = this.get_selected_cell();
1668 1688 var cell_index = this.find_cell_index(cell);
1669 1689
1670 1690 cell.execute();
1671 1691
1672 1692 // If we are at the end always insert a new cell and return
1673 1693 if (cell_index === (this.ncells()-1)) {
1674 1694 this.command_mode();
1675 1695 this.insert_cell_below();
1676 1696 this.select(cell_index+1);
1677 1697 this.edit_mode();
1678 1698 this.scroll_to_bottom();
1679 1699 this.set_dirty(true);
1680 1700 return;
1681 1701 }
1682 1702
1683 1703 this.command_mode();
1684 1704 this.select(cell_index+1);
1685 1705 this.focus_cell();
1686 1706 this.set_dirty(true);
1687 1707 };
1688 1708
1689 1709 /**
1690 1710 * Execute all cells below the selected cell.
1691 1711 *
1692 1712 * @method execute_cells_below
1693 1713 */
1694 1714 Notebook.prototype.execute_cells_below = function () {
1695 1715 this.execute_cell_range(this.get_selected_index(), this.ncells());
1696 1716 this.scroll_to_bottom();
1697 1717 };
1698 1718
1699 1719 /**
1700 1720 * Execute all cells above the selected cell.
1701 1721 *
1702 1722 * @method execute_cells_above
1703 1723 */
1704 1724 Notebook.prototype.execute_cells_above = function () {
1705 1725 this.execute_cell_range(0, this.get_selected_index());
1706 1726 };
1707 1727
1708 1728 /**
1709 1729 * Execute all cells.
1710 1730 *
1711 1731 * @method execute_all_cells
1712 1732 */
1713 1733 Notebook.prototype.execute_all_cells = function () {
1714 1734 this.execute_cell_range(0, this.ncells());
1715 1735 this.scroll_to_bottom();
1716 1736 };
1717 1737
1718 1738 /**
1719 1739 * Execute a contiguous range of cells.
1720 1740 *
1721 1741 * @method execute_cell_range
1722 1742 * @param {Number} start Index of the first cell to execute (inclusive)
1723 1743 * @param {Number} end Index of the last cell to execute (exclusive)
1724 1744 */
1725 1745 Notebook.prototype.execute_cell_range = function (start, end) {
1726 1746 this.command_mode();
1727 1747 for (var i=start; i<end; i++) {
1728 1748 this.select(i);
1729 1749 this.execute_cell();
1730 1750 }
1731 1751 };
1732 1752
1733 1753 // Persistance and loading
1734 1754
1735 1755 /**
1736 1756 * Getter method for this notebook's name.
1737 1757 *
1738 1758 * @method get_notebook_name
1739 1759 * @return {String} This notebook's name (excluding file extension)
1740 1760 */
1741 1761 Notebook.prototype.get_notebook_name = function () {
1742 1762 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1743 1763 return nbname;
1744 1764 };
1745 1765
1746 1766 /**
1747 1767 * Setter method for this notebook's name.
1748 1768 *
1749 1769 * @method set_notebook_name
1750 1770 * @param {String} name A new name for this notebook
1751 1771 */
1752 1772 Notebook.prototype.set_notebook_name = function (name) {
1753 1773 var parent = utils.url_path_split(this.notebook_path)[0];
1754 1774 this.notebook_name = name;
1755 1775 this.notebook_path = utils.url_path_join(parent, name);
1756 1776 };
1757 1777
1758 1778 /**
1759 1779 * Check that a notebook's name is valid.
1760 1780 *
1761 1781 * @method test_notebook_name
1762 1782 * @param {String} nbname A name for this notebook
1763 1783 * @return {Boolean} True if the name is valid, false if invalid
1764 1784 */
1765 1785 Notebook.prototype.test_notebook_name = function (nbname) {
1766 1786 nbname = nbname || '';
1767 1787 if (nbname.length>0 && !this.notebook_name_blacklist_re.test(nbname)) {
1768 1788 return true;
1769 1789 } else {
1770 1790 return false;
1771 1791 }
1772 1792 };
1773 1793
1774 1794 /**
1775 1795 * Load a notebook from JSON (.ipynb).
1776 1796 *
1777 1797 * @method fromJSON
1778 1798 * @param {Object} data JSON representation of a notebook
1779 1799 */
1780 1800 Notebook.prototype.fromJSON = function (data) {
1781 1801
1782 1802 var content = data.content;
1783 1803 var ncells = this.ncells();
1784 1804 var i;
1785 1805 for (i=0; i<ncells; i++) {
1786 1806 // Always delete cell 0 as they get renumbered as they are deleted.
1787 1807 this.delete_cell(0);
1788 1808 }
1789 1809 // Save the metadata and name.
1790 1810 this.metadata = content.metadata;
1791 1811 this.notebook_name = data.name;
1792 1812 this.notebook_path = data.path;
1793 1813 var trusted = true;
1794 1814
1795 1815 // Trigger an event changing the kernel spec - this will set the default
1796 1816 // codemirror mode
1797 1817 if (this.metadata.kernelspec !== undefined) {
1798 1818 this.events.trigger('spec_changed.Kernel', this.metadata.kernelspec);
1799 1819 }
1800 1820
1801 1821 // Set the codemirror mode from language_info metadata
1802 1822 if (this.metadata.language_info !== undefined) {
1803 1823 var langinfo = this.metadata.language_info;
1804 1824 // Mode 'null' should be plain, unhighlighted text.
1805 1825 var cm_mode = langinfo.codemirror_mode || langinfo.language || 'null';
1806 1826 this.set_codemirror_mode(cm_mode);
1807 1827 }
1808 1828
1809 1829 var new_cells = content.cells;
1810 1830 ncells = new_cells.length;
1811 1831 var cell_data = null;
1812 1832 var new_cell = null;
1813 1833 for (i=0; i<ncells; i++) {
1814 1834 cell_data = new_cells[i];
1815 1835 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1816 1836 new_cell.fromJSON(cell_data);
1817 1837 if (new_cell.cell_type == 'code' && !new_cell.output_area.trusted) {
1818 1838 trusted = false;
1819 1839 }
1820 1840 }
1821 1841 if (trusted !== this.trusted) {
1822 1842 this.trusted = trusted;
1823 1843 this.events.trigger("trust_changed.Notebook", trusted);
1824 1844 }
1825 1845 };
1826 1846
1827 1847 /**
1828 1848 * Dump this notebook into a JSON-friendly object.
1829 1849 *
1830 1850 * @method toJSON
1831 1851 * @return {Object} A JSON-friendly representation of this notebook.
1832 1852 */
1833 1853 Notebook.prototype.toJSON = function () {
1834 1854 // remove the conversion indicator, which only belongs in-memory
1835 1855 delete this.metadata.orig_nbformat;
1836 1856 delete this.metadata.orig_nbformat_minor;
1837 1857
1838 1858 var cells = this.get_cells();
1839 1859 var ncells = cells.length;
1840 1860 var cell_array = new Array(ncells);
1841 1861 var trusted = true;
1842 1862 for (var i=0; i<ncells; i++) {
1843 1863 var cell = cells[i];
1844 1864 if (cell.cell_type == 'code' && !cell.output_area.trusted) {
1845 1865 trusted = false;
1846 1866 }
1847 1867 cell_array[i] = cell.toJSON();
1848 1868 }
1849 1869 var data = {
1850 1870 cells: cell_array,
1851 1871 metadata: this.metadata,
1852 1872 nbformat: this.nbformat,
1853 1873 nbformat_minor: this.nbformat_minor
1854 1874 };
1855 1875 if (trusted != this.trusted) {
1856 1876 this.trusted = trusted;
1857 1877 this.events.trigger("trust_changed.Notebook", trusted);
1858 1878 }
1859 1879 return data;
1860 1880 };
1861 1881
1862 1882 /**
1863 1883 * Start an autosave timer, for periodically saving the notebook.
1864 1884 *
1865 1885 * @method set_autosave_interval
1866 1886 * @param {Integer} interval the autosave interval in milliseconds
1867 1887 */
1868 1888 Notebook.prototype.set_autosave_interval = function (interval) {
1869 1889 var that = this;
1870 1890 // clear previous interval, so we don't get simultaneous timers
1871 1891 if (this.autosave_timer) {
1872 1892 clearInterval(this.autosave_timer);
1873 1893 }
1874 1894
1875 1895 this.autosave_interval = this.minimum_autosave_interval = interval;
1876 1896 if (interval) {
1877 1897 this.autosave_timer = setInterval(function() {
1878 1898 if (that.dirty) {
1879 1899 that.save_notebook();
1880 1900 }
1881 1901 }, interval);
1882 1902 this.events.trigger("autosave_enabled.Notebook", interval);
1883 1903 } else {
1884 1904 this.autosave_timer = null;
1885 1905 this.events.trigger("autosave_disabled.Notebook");
1886 1906 }
1887 1907 };
1888 1908
1889 1909 /**
1890 1910 * Save this notebook on the server. This becomes a notebook instance's
1891 1911 * .save_notebook method *after* the entire notebook has been loaded.
1892 1912 *
1893 1913 * @method save_notebook
1894 1914 */
1895 1915 Notebook.prototype.save_notebook = function () {
1896 1916 // Create a JSON model to be sent to the server.
1897 1917 var model = {
1898 1918 type : "notebook",
1899 1919 content : this.toJSON()
1900 1920 };
1901 1921 // time the ajax call for autosave tuning purposes.
1902 1922 var start = new Date().getTime();
1903 1923
1904 1924 var that = this;
1905 1925 this.contents.save(this.notebook_path, model).then(
1906 1926 $.proxy(this.save_notebook_success, this, start),
1907 1927 function (error) {
1908 1928 that.events.trigger('notebook_save_failed.Notebook', error);
1909 1929 }
1910 1930 );
1911 1931 };
1912 1932
1913 1933 /**
1914 1934 * Success callback for saving a notebook.
1915 1935 *
1916 1936 * @method save_notebook_success
1917 1937 * @param {Integer} start Time when the save request start
1918 1938 * @param {Object} data JSON representation of a notebook
1919 1939 */
1920 1940 Notebook.prototype.save_notebook_success = function (start, data) {
1921 1941 this.set_dirty(false);
1922 1942 if (data.message) {
1923 1943 // save succeeded, but validation failed.
1924 1944 var body = $("<div>");
1925 1945 var title = "Notebook validation failed";
1926 1946
1927 1947 body.append($("<p>").text(
1928 1948 "The save operation succeeded," +
1929 1949 " but the notebook does not appear to be valid." +
1930 1950 " The validation error was:"
1931 1951 )).append($("<div>").addClass("validation-error").append(
1932 1952 $("<pre>").text(data.message)
1933 1953 ));
1934 1954 dialog.modal({
1935 1955 notebook: this,
1936 1956 keyboard_manager: this.keyboard_manager,
1937 1957 title: title,
1938 1958 body: body,
1939 1959 buttons : {
1940 1960 OK : {
1941 1961 "class" : "btn-primary"
1942 1962 }
1943 1963 }
1944 1964 });
1945 1965 }
1946 1966 this.events.trigger('notebook_saved.Notebook');
1947 1967 this._update_autosave_interval(start);
1948 1968 if (this._checkpoint_after_save) {
1949 1969 this.create_checkpoint();
1950 1970 this._checkpoint_after_save = false;
1951 1971 }
1952 1972 };
1953 1973
1954 1974 /**
1955 1975 * update the autosave interval based on how long the last save took
1956 1976 *
1957 1977 * @method _update_autosave_interval
1958 1978 * @param {Integer} timestamp when the save request started
1959 1979 */
1960 1980 Notebook.prototype._update_autosave_interval = function (start) {
1961 1981 var duration = (new Date().getTime() - start);
1962 1982 if (this.autosave_interval) {
1963 1983 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
1964 1984 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
1965 1985 // round to 10 seconds, otherwise we will be setting a new interval too often
1966 1986 interval = 10000 * Math.round(interval / 10000);
1967 1987 // set new interval, if it's changed
1968 1988 if (interval != this.autosave_interval) {
1969 1989 this.set_autosave_interval(interval);
1970 1990 }
1971 1991 }
1972 1992 };
1973 1993
1974 1994 /**
1975 1995 * Explicitly trust the output of this notebook.
1976 1996 *
1977 1997 * @method trust_notebook
1978 1998 */
1979 1999 Notebook.prototype.trust_notebook = function () {
1980 2000 var body = $("<div>").append($("<p>")
1981 2001 .text("A trusted IPython notebook may execute hidden malicious code ")
1982 2002 .append($("<strong>")
1983 2003 .append(
1984 2004 $("<em>").text("when you open it")
1985 2005 )
1986 2006 ).append(".").append(
1987 2007 " Selecting trust will immediately reload this notebook in a trusted state."
1988 2008 ).append(
1989 2009 " For more information, see the "
1990 2010 ).append($("<a>").attr("href", "http://ipython.org/ipython-doc/2/notebook/security.html")
1991 2011 .text("IPython security documentation")
1992 2012 ).append(".")
1993 2013 );
1994 2014
1995 2015 var nb = this;
1996 2016 dialog.modal({
1997 2017 notebook: this,
1998 2018 keyboard_manager: this.keyboard_manager,
1999 2019 title: "Trust this notebook?",
2000 2020 body: body,
2001 2021
2002 2022 buttons: {
2003 2023 Cancel : {},
2004 2024 Trust : {
2005 2025 class : "btn-danger",
2006 2026 click : function () {
2007 2027 var cells = nb.get_cells();
2008 2028 for (var i = 0; i < cells.length; i++) {
2009 2029 var cell = cells[i];
2010 2030 if (cell.cell_type == 'code') {
2011 2031 cell.output_area.trusted = true;
2012 2032 }
2013 2033 }
2014 2034 nb.events.on('notebook_saved.Notebook', function () {
2015 2035 window.location.reload();
2016 2036 });
2017 2037 nb.save_notebook();
2018 2038 }
2019 2039 }
2020 2040 }
2021 2041 });
2022 2042 };
2023 2043
2024 2044 Notebook.prototype.copy_notebook = function(){
2025 2045 var base_url = this.base_url;
2026 2046 var w = window.open();
2027 2047 var parent = utils.url_path_split(this.notebook_path)[0];
2028 2048 this.contents.copy(this.notebook_path, parent).then(
2029 2049 function (data) {
2030 2050 w.location = utils.url_join_encode(
2031 2051 base_url, 'notebooks', data.path
2032 2052 );
2033 2053 },
2034 2054 function(error) {
2035 2055 w.close();
2036 2056 console.log(error);
2037 2057 }
2038 2058 );
2039 2059 };
2040 2060
2041 2061 Notebook.prototype.rename = function (new_name) {
2042 2062 if (!new_name.match(/\.ipynb$/)) {
2043 2063 new_name = new_name + ".ipynb";
2044 2064 }
2045 2065
2046 2066 var that = this;
2047 2067 var parent = utils.url_path_split(this.notebook_path)[0];
2048 2068 var new_path = utils.url_path_join(parent, new_name);
2049 2069 this.contents.rename(this.notebook_path, new_path).then(
2050 2070 function (json) {
2051 2071 that.notebook_name = json.name;
2052 2072 that.notebook_path = json.path;
2053 2073 that.session.rename_notebook(json.path);
2054 2074 that.events.trigger('notebook_renamed.Notebook', json);
2055 2075 },
2056 2076 $.proxy(this.rename_error, this)
2057 2077 );
2058 2078 };
2059 2079
2060 2080 Notebook.prototype.delete = function () {
2061 2081 this.contents.delete(this.notebook_path);
2062 2082 };
2063 2083
2064 2084 Notebook.prototype.rename_error = function (error) {
2065 2085 var that = this;
2066 2086 var dialog_body = $('<div/>').append(
2067 2087 $("<p/>").text('This notebook name already exists.')
2068 2088 );
2069 2089 this.events.trigger('notebook_rename_failed.Notebook', error);
2070 2090 dialog.modal({
2071 2091 notebook: this,
2072 2092 keyboard_manager: this.keyboard_manager,
2073 2093 title: "Notebook Rename Error!",
2074 2094 body: dialog_body,
2075 2095 buttons : {
2076 2096 "Cancel": {},
2077 2097 "OK": {
2078 2098 class: "btn-primary",
2079 2099 click: function () {
2080 2100 that.save_widget.rename_notebook({notebook:that});
2081 2101 }}
2082 2102 },
2083 2103 open : function (event, ui) {
2084 2104 var that = $(this);
2085 2105 // Upon ENTER, click the OK button.
2086 2106 that.find('input[type="text"]').keydown(function (event, ui) {
2087 2107 if (event.which === this.keyboard.keycodes.enter) {
2088 2108 that.find('.btn-primary').first().click();
2089 2109 }
2090 2110 });
2091 2111 that.find('input[type="text"]').focus();
2092 2112 }
2093 2113 });
2094 2114 };
2095 2115
2096 2116 /**
2097 2117 * Request a notebook's data from the server.
2098 2118 *
2099 2119 * @method load_notebook
2100 2120 * @param {String} notebook_path A notebook to load
2101 2121 */
2102 2122 Notebook.prototype.load_notebook = function (notebook_path) {
2103 2123 this.notebook_path = notebook_path;
2104 2124 this.notebook_name = utils.url_path_split(this.notebook_path)[1];
2105 2125 this.events.trigger('notebook_loading.Notebook');
2106 2126 this.contents.get(notebook_path, {type: 'notebook'}).then(
2107 2127 $.proxy(this.load_notebook_success, this),
2108 2128 $.proxy(this.load_notebook_error, this)
2109 2129 );
2110 2130 };
2111 2131
2112 2132 /**
2113 2133 * Success callback for loading a notebook from the server.
2114 2134 *
2115 2135 * Load notebook data from the JSON response.
2116 2136 *
2117 2137 * @method load_notebook_success
2118 2138 * @param {Object} data JSON representation of a notebook
2119 2139 */
2120 2140 Notebook.prototype.load_notebook_success = function (data) {
2121 2141 var failed, msg;
2122 2142 try {
2123 2143 this.fromJSON(data);
2124 2144 } catch (e) {
2125 2145 failed = e;
2126 2146 console.log("Notebook failed to load from JSON:", e);
2127 2147 }
2128 2148 if (failed || data.message) {
2129 2149 // *either* fromJSON failed or validation failed
2130 2150 var body = $("<div>");
2131 2151 var title;
2132 2152 if (failed) {
2133 2153 title = "Notebook failed to load";
2134 2154 body.append($("<p>").text(
2135 2155 "The error was: "
2136 2156 )).append($("<div>").addClass("js-error").text(
2137 2157 failed.toString()
2138 2158 )).append($("<p>").text(
2139 2159 "See the error console for details."
2140 2160 ));
2141 2161 } else {
2142 2162 title = "Notebook validation failed";
2143 2163 }
2144 2164
2145 2165 if (data.message) {
2146 2166 if (failed) {
2147 2167 msg = "The notebook also failed validation:";
2148 2168 } else {
2149 2169 msg = "An invalid notebook may not function properly." +
2150 2170 " The validation error was:";
2151 2171 }
2152 2172 body.append($("<p>").text(
2153 2173 msg
2154 2174 )).append($("<div>").addClass("validation-error").append(
2155 2175 $("<pre>").text(data.message)
2156 2176 ));
2157 2177 }
2158 2178
2159 2179 dialog.modal({
2160 2180 notebook: this,
2161 2181 keyboard_manager: this.keyboard_manager,
2162 2182 title: title,
2163 2183 body: body,
2164 2184 buttons : {
2165 2185 OK : {
2166 2186 "class" : "btn-primary"
2167 2187 }
2168 2188 }
2169 2189 });
2170 2190 }
2171 2191 if (this.ncells() === 0) {
2172 2192 this.insert_cell_below('code');
2173 2193 this.edit_mode(0);
2174 2194 } else {
2175 2195 this.select(0);
2176 2196 this.handle_command_mode(this.get_cell(0));
2177 2197 }
2178 2198 this.set_dirty(false);
2179 2199 this.scroll_to_top();
2180 2200 var nbmodel = data.content;
2181 2201 var orig_nbformat = nbmodel.metadata.orig_nbformat;
2182 2202 var orig_nbformat_minor = nbmodel.metadata.orig_nbformat_minor;
2183 2203 if (orig_nbformat !== undefined && nbmodel.nbformat !== orig_nbformat) {
2184 2204 var src;
2185 2205 if (nbmodel.nbformat > orig_nbformat) {
2186 2206 src = " an older notebook format ";
2187 2207 } else {
2188 2208 src = " a newer notebook format ";
2189 2209 }
2190 2210
2191 2211 msg = "This notebook has been converted from" + src +
2192 2212 "(v"+orig_nbformat+") to the current notebook " +
2193 2213 "format (v"+nbmodel.nbformat+"). The next time you save this notebook, the " +
2194 2214 "current notebook format will be used.";
2195 2215
2196 2216 if (nbmodel.nbformat > orig_nbformat) {
2197 2217 msg += " Older versions of IPython may not be able to read the new format.";
2198 2218 } else {
2199 2219 msg += " Some features of the original notebook may not be available.";
2200 2220 }
2201 2221 msg += " To preserve the original version, close the " +
2202 2222 "notebook without saving it.";
2203 2223 dialog.modal({
2204 2224 notebook: this,
2205 2225 keyboard_manager: this.keyboard_manager,
2206 2226 title : "Notebook converted",
2207 2227 body : msg,
2208 2228 buttons : {
2209 2229 OK : {
2210 2230 class : "btn-primary"
2211 2231 }
2212 2232 }
2213 2233 });
2214 2234 } else if (orig_nbformat_minor !== undefined && nbmodel.nbformat_minor < orig_nbformat_minor) {
2215 2235 var that = this;
2216 2236 var orig_vs = 'v' + nbmodel.nbformat + '.' + orig_nbformat_minor;
2217 2237 var this_vs = 'v' + nbmodel.nbformat + '.' + this.nbformat_minor;
2218 2238 msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
2219 2239 this_vs + ". You can still work with this notebook, but some features " +
2220 2240 "introduced in later notebook versions may not be available.";
2221 2241
2222 2242 dialog.modal({
2223 2243 notebook: this,
2224 2244 keyboard_manager: this.keyboard_manager,
2225 2245 title : "Newer Notebook",
2226 2246 body : msg,
2227 2247 buttons : {
2228 2248 OK : {
2229 2249 class : "btn-danger"
2230 2250 }
2231 2251 }
2232 2252 });
2233 2253
2234 2254 }
2235 2255
2236 2256 // Create the session after the notebook is completely loaded to prevent
2237 2257 // code execution upon loading, which is a security risk.
2238 2258 if (this.session === null) {
2239 2259 var kernelspec = this.metadata.kernelspec || {};
2240 2260 var kernel_name = kernelspec.name;
2241 2261
2242 2262 this.start_session(kernel_name);
2243 2263 }
2244 2264 // load our checkpoint list
2245 2265 this.list_checkpoints();
2246 2266
2247 2267 // load toolbar state
2248 2268 if (this.metadata.celltoolbar) {
2249 2269 celltoolbar.CellToolbar.global_show();
2250 2270 celltoolbar.CellToolbar.activate_preset(this.metadata.celltoolbar);
2251 2271 } else {
2252 2272 celltoolbar.CellToolbar.global_hide();
2253 2273 }
2254 2274
2255 2275 // now that we're fully loaded, it is safe to restore save functionality
2256 2276 delete(this.save_notebook);
2257 2277 this.events.trigger('notebook_loaded.Notebook');
2258 2278 };
2259 2279
2260 2280 /**
2261 2281 * Failure callback for loading a notebook from the server.
2262 2282 *
2263 2283 * @method load_notebook_error
2264 2284 * @param {Error} error
2265 2285 */
2266 2286 Notebook.prototype.load_notebook_error = function (error) {
2267 2287 this.events.trigger('notebook_load_failed.Notebook', error);
2268 2288 var msg;
2269 2289 if (error.name === utils.XHR_ERROR && error.xhr.status === 500) {
2270 2290 utils.log_ajax_error(error.xhr, error.xhr_status, error.xhr_error);
2271 2291 msg = "An unknown error occurred while loading this notebook. " +
2272 2292 "This version can load notebook formats " +
2273 2293 "v" + this.nbformat + " or earlier. See the server log for details.";
2274 2294 } else {
2275 2295 msg = error.message;
2276 2296 }
2277 2297 dialog.modal({
2278 2298 notebook: this,
2279 2299 keyboard_manager: this.keyboard_manager,
2280 2300 title: "Error loading notebook",
2281 2301 body : msg,
2282 2302 buttons : {
2283 2303 "OK": {}
2284 2304 }
2285 2305 });
2286 2306 };
2287 2307
2288 2308 /********************* checkpoint-related *********************/
2289 2309
2290 2310 /**
2291 2311 * Save the notebook then immediately create a checkpoint.
2292 2312 *
2293 2313 * @method save_checkpoint
2294 2314 */
2295 2315 Notebook.prototype.save_checkpoint = function () {
2296 2316 this._checkpoint_after_save = true;
2297 2317 this.save_notebook();
2298 2318 };
2299 2319
2300 2320 /**
2301 2321 * Add a checkpoint for this notebook.
2302 2322 * for use as a callback from checkpoint creation.
2303 2323 *
2304 2324 * @method add_checkpoint
2305 2325 */
2306 2326 Notebook.prototype.add_checkpoint = function (checkpoint) {
2307 2327 var found = false;
2308 2328 for (var i = 0; i < this.checkpoints.length; i++) {
2309 2329 var existing = this.checkpoints[i];
2310 2330 if (existing.id == checkpoint.id) {
2311 2331 found = true;
2312 2332 this.checkpoints[i] = checkpoint;
2313 2333 break;
2314 2334 }
2315 2335 }
2316 2336 if (!found) {
2317 2337 this.checkpoints.push(checkpoint);
2318 2338 }
2319 2339 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
2320 2340 };
2321 2341
2322 2342 /**
2323 2343 * List checkpoints for this notebook.
2324 2344 *
2325 2345 * @method list_checkpoints
2326 2346 */
2327 2347 Notebook.prototype.list_checkpoints = function () {
2328 2348 var that = this;
2329 2349 this.contents.list_checkpoints(this.notebook_path).then(
2330 2350 $.proxy(this.list_checkpoints_success, this),
2331 2351 function(error) {
2332 2352 that.events.trigger('list_checkpoints_failed.Notebook', error);
2333 2353 }
2334 2354 );
2335 2355 };
2336 2356
2337 2357 /**
2338 2358 * Success callback for listing checkpoints.
2339 2359 *
2340 2360 * @method list_checkpoint_success
2341 2361 * @param {Object} data JSON representation of a checkpoint
2342 2362 */
2343 2363 Notebook.prototype.list_checkpoints_success = function (data) {
2344 2364 this.checkpoints = data;
2345 2365 if (data.length) {
2346 2366 this.last_checkpoint = data[data.length - 1];
2347 2367 } else {
2348 2368 this.last_checkpoint = null;
2349 2369 }
2350 2370 this.events.trigger('checkpoints_listed.Notebook', [data]);
2351 2371 };
2352 2372
2353 2373 /**
2354 2374 * Create a checkpoint of this notebook on the server from the most recent save.
2355 2375 *
2356 2376 * @method create_checkpoint
2357 2377 */
2358 2378 Notebook.prototype.create_checkpoint = function () {
2359 2379 var that = this;
2360 2380 this.contents.create_checkpoint(this.notebook_path).then(
2361 2381 $.proxy(this.create_checkpoint_success, this),
2362 2382 function (error) {
2363 2383 that.events.trigger('checkpoint_failed.Notebook', error);
2364 2384 }
2365 2385 );
2366 2386 };
2367 2387
2368 2388 /**
2369 2389 * Success callback for creating a checkpoint.
2370 2390 *
2371 2391 * @method create_checkpoint_success
2372 2392 * @param {Object} data JSON representation of a checkpoint
2373 2393 */
2374 2394 Notebook.prototype.create_checkpoint_success = function (data) {
2375 2395 this.add_checkpoint(data);
2376 2396 this.events.trigger('checkpoint_created.Notebook', data);
2377 2397 };
2378 2398
2379 2399 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2380 2400 var that = this;
2381 2401 checkpoint = checkpoint || this.last_checkpoint;
2382 2402 if ( ! checkpoint ) {
2383 2403 console.log("restore dialog, but no checkpoint to restore to!");
2384 2404 return;
2385 2405 }
2386 2406 var body = $('<div/>').append(
2387 2407 $('<p/>').addClass("p-space").text(
2388 2408 "Are you sure you want to revert the notebook to " +
2389 2409 "the latest checkpoint?"
2390 2410 ).append(
2391 2411 $("<strong/>").text(
2392 2412 " This cannot be undone."
2393 2413 )
2394 2414 )
2395 2415 ).append(
2396 2416 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2397 2417 ).append(
2398 2418 $('<p/>').addClass("p-space").text(
2399 2419 Date(checkpoint.last_modified)
2400 2420 ).css("text-align", "center")
2401 2421 );
2402 2422
2403 2423 dialog.modal({
2404 2424 notebook: this,
2405 2425 keyboard_manager: this.keyboard_manager,
2406 2426 title : "Revert notebook to checkpoint",
2407 2427 body : body,
2408 2428 buttons : {
2409 2429 Revert : {
2410 2430 class : "btn-danger",
2411 2431 click : function () {
2412 2432 that.restore_checkpoint(checkpoint.id);
2413 2433 }
2414 2434 },
2415 2435 Cancel : {}
2416 2436 }
2417 2437 });
2418 2438 };
2419 2439
2420 2440 /**
2421 2441 * Restore the notebook to a checkpoint state.
2422 2442 *
2423 2443 * @method restore_checkpoint
2424 2444 * @param {String} checkpoint ID
2425 2445 */
2426 2446 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2427 2447 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2428 2448 var that = this;
2429 2449 this.contents.restore_checkpoint(this.notebook_path, checkpoint).then(
2430 2450 $.proxy(this.restore_checkpoint_success, this),
2431 2451 function (error) {
2432 2452 that.events.trigger('checkpoint_restore_failed.Notebook', error);
2433 2453 }
2434 2454 );
2435 2455 };
2436 2456
2437 2457 /**
2438 2458 * Success callback for restoring a notebook to a checkpoint.
2439 2459 *
2440 2460 * @method restore_checkpoint_success
2441 2461 */
2442 2462 Notebook.prototype.restore_checkpoint_success = function () {
2443 2463 this.events.trigger('checkpoint_restored.Notebook');
2444 2464 this.load_notebook(this.notebook_path);
2445 2465 };
2446 2466
2447 2467 /**
2448 2468 * Delete a notebook checkpoint.
2449 2469 *
2450 2470 * @method delete_checkpoint
2451 2471 * @param {String} checkpoint ID
2452 2472 */
2453 2473 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2454 2474 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2455 2475 var that = this;
2456 2476 this.contents.delete_checkpoint(this.notebook_path, checkpoint).then(
2457 2477 $.proxy(this.delete_checkpoint_success, this),
2458 2478 function (error) {
2459 2479 that.events.trigger('checkpoint_delete_failed.Notebook', error);
2460 2480 }
2461 2481 );
2462 2482 };
2463 2483
2464 2484 /**
2465 2485 * Success callback for deleting a notebook checkpoint
2466 2486 *
2467 2487 * @method delete_checkpoint_success
2468 2488 */
2469 2489 Notebook.prototype.delete_checkpoint_success = function () {
2470 2490 this.events.trigger('checkpoint_deleted.Notebook');
2471 2491 this.load_notebook(this.notebook_path);
2472 2492 };
2473 2493
2474 2494
2475 2495 // For backwards compatability.
2476 2496 IPython.Notebook = Notebook;
2477 2497
2478 2498 return {'Notebook': Notebook};
2479 2499 });
@@ -1,931 +1,932
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jqueryui',
7 7 'base/js/utils',
8 8 'base/js/security',
9 9 'base/js/keyboard',
10 10 'notebook/js/mathjaxutils',
11 11 'components/marked/lib/marked',
12 12 ], function(IPython, $, utils, security, keyboard, mathjaxutils, marked) {
13 13 "use strict";
14 14
15 15 /**
16 16 * @class OutputArea
17 17 *
18 18 * @constructor
19 19 */
20 20
21 21 var OutputArea = function (options) {
22 22 this.selector = options.selector;
23 23 this.events = options.events;
24 24 this.keyboard_manager = options.keyboard_manager;
25 25 this.wrapper = $(options.selector);
26 26 this.outputs = [];
27 27 this.collapsed = false;
28 28 this.scrolled = false;
29 29 this.trusted = true;
30 30 this.clear_queued = null;
31 31 if (options.prompt_area === undefined) {
32 32 this.prompt_area = true;
33 33 } else {
34 34 this.prompt_area = options.prompt_area;
35 35 }
36 36 this.create_elements();
37 37 this.style();
38 38 this.bind_events();
39 39 };
40 40
41 41
42 42 /**
43 43 * Class prototypes
44 44 **/
45 45
46 46 OutputArea.prototype.create_elements = function () {
47 47 this.element = $("<div/>");
48 48 this.collapse_button = $("<div/>");
49 49 this.prompt_overlay = $("<div/>");
50 50 this.wrapper.append(this.prompt_overlay);
51 51 this.wrapper.append(this.element);
52 52 this.wrapper.append(this.collapse_button);
53 53 };
54 54
55 55
56 56 OutputArea.prototype.style = function () {
57 57 this.collapse_button.hide();
58 58 this.prompt_overlay.hide();
59 59
60 60 this.wrapper.addClass('output_wrapper');
61 61 this.element.addClass('output');
62 62
63 63 this.collapse_button.addClass("btn btn-default output_collapsed");
64 64 this.collapse_button.attr('title', 'click to expand output');
65 65 this.collapse_button.text('. . .');
66 66
67 67 this.prompt_overlay.addClass('out_prompt_overlay prompt');
68 68 this.prompt_overlay.attr('title', 'click to expand output; double click to hide output');
69 69
70 70 this.collapse();
71 71 };
72 72
73 73 /**
74 74 * Should the OutputArea scroll?
75 75 * Returns whether the height (in lines) exceeds a threshold.
76 76 *
77 77 * @private
78 78 * @method _should_scroll
79 79 * @param [lines=100]{Integer}
80 80 * @return {Bool}
81 81 *
82 82 */
83 83 OutputArea.prototype._should_scroll = function (lines) {
84 84 if (lines <=0 ){ return; }
85 85 if (!lines) {
86 86 lines = 100;
87 87 }
88 88 // line-height from http://stackoverflow.com/questions/1185151
89 89 var fontSize = this.element.css('font-size');
90 90 var lineHeight = Math.floor(parseInt(fontSize.replace('px','')) * 1.5);
91 91
92 92 return (this.element.height() > lines * lineHeight);
93 93 };
94 94
95 95
96 96 OutputArea.prototype.bind_events = function () {
97 97 var that = this;
98 98 this.prompt_overlay.dblclick(function () { that.toggle_output(); });
99 99 this.prompt_overlay.click(function () { that.toggle_scroll(); });
100 100
101 101 this.element.resize(function () {
102 102 // FIXME: Firefox on Linux misbehaves, so automatic scrolling is disabled
103 103 if ( utils.browser[0] === "Firefox" ) {
104 104 return;
105 105 }
106 106 // maybe scroll output,
107 107 // if it's grown large enough and hasn't already been scrolled.
108 108 if ( !that.scrolled && that._should_scroll(OutputArea.auto_scroll_threshold)) {
109 109 that.scroll_area();
110 110 }
111 111 });
112 112 this.collapse_button.click(function () {
113 113 that.expand();
114 114 });
115 115 };
116 116
117 117
118 118 OutputArea.prototype.collapse = function () {
119 119 if (!this.collapsed) {
120 120 this.element.hide();
121 121 this.prompt_overlay.hide();
122 122 if (this.element.html()){
123 123 this.collapse_button.show();
124 124 }
125 125 this.collapsed = true;
126 126 }
127 127 };
128 128
129 129
130 130 OutputArea.prototype.expand = function () {
131 131 if (this.collapsed) {
132 132 this.collapse_button.hide();
133 133 this.element.show();
134 134 this.prompt_overlay.show();
135 135 this.collapsed = false;
136 136 }
137 137 };
138 138
139 139
140 140 OutputArea.prototype.toggle_output = function () {
141 141 if (this.collapsed) {
142 142 this.expand();
143 143 } else {
144 144 this.collapse();
145 145 }
146 146 };
147 147
148 148
149 149 OutputArea.prototype.scroll_area = function () {
150 150 this.element.addClass('output_scroll');
151 151 this.prompt_overlay.attr('title', 'click to unscroll output; double click to hide');
152 152 this.scrolled = true;
153 153 };
154 154
155 155
156 156 OutputArea.prototype.unscroll_area = function () {
157 157 this.element.removeClass('output_scroll');
158 158 this.prompt_overlay.attr('title', 'click to scroll output; double click to hide');
159 159 this.scrolled = false;
160 160 };
161 161
162 162 /**
163 163 *
164 164 * Scroll OutputArea if height supperior than a threshold (in lines).
165 165 *
166 166 * Threshold is a maximum number of lines. If unspecified, defaults to
167 167 * OutputArea.minimum_scroll_threshold.
168 168 *
169 169 * Negative threshold will prevent the OutputArea from ever scrolling.
170 170 *
171 171 * @method scroll_if_long
172 172 *
173 173 * @param [lines=20]{Number} Default to 20 if not set,
174 174 * behavior undefined for value of `0`.
175 175 *
176 176 **/
177 177 OutputArea.prototype.scroll_if_long = function (lines) {
178 178 var n = lines | OutputArea.minimum_scroll_threshold;
179 179 if(n <= 0){
180 180 return;
181 181 }
182 182
183 183 if (this._should_scroll(n)) {
184 184 // only allow scrolling long-enough output
185 185 this.scroll_area();
186 186 }
187 187 };
188 188
189 189
190 190 OutputArea.prototype.toggle_scroll = function () {
191 191 if (this.scrolled) {
192 192 this.unscroll_area();
193 193 } else {
194 194 // only allow scrolling long-enough output
195 195 this.scroll_if_long();
196 196 }
197 197 };
198 198
199 199
200 200 // typeset with MathJax if MathJax is available
201 201 OutputArea.prototype.typeset = function () {
202 202 if (window.MathJax){
203 203 MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
204 204 }
205 205 };
206 206
207 207
208 208 OutputArea.prototype.handle_output = function (msg) {
209 209 var json = {};
210 210 var msg_type = json.output_type = msg.header.msg_type;
211 211 var content = msg.content;
212 212 if (msg_type === "stream") {
213 213 json.text = content.text;
214 214 json.name = content.name;
215 215 } else if (msg_type === "display_data") {
216 216 json.data = content.data;
217 217 json.output_type = msg_type;
218 218 json.metadata = content.metadata;
219 219 } else if (msg_type === "execute_result") {
220 220 json.data = content.data;
221 221 json.output_type = msg_type;
222 222 json.metadata = content.metadata;
223 223 json.execution_count = content.execution_count;
224 224 } else if (msg_type === "error") {
225 225 json.ename = content.ename;
226 226 json.evalue = content.evalue;
227 227 json.traceback = content.traceback;
228 228 } else {
229 229 console.log("unhandled output message", msg);
230 230 return;
231 231 }
232 232 this.append_output(json);
233 233 };
234 234
235 235
236 236 OutputArea.output_types = [
237 237 'application/javascript',
238 238 'text/html',
239 239 'text/markdown',
240 240 'text/latex',
241 241 'image/svg+xml',
242 242 'image/png',
243 243 'image/jpeg',
244 244 'application/pdf',
245 245 'text/plain'
246 246 ];
247 247
248 248 OutputArea.prototype.validate_output = function (json) {
249 249 // scrub invalid outputs
250 250 var data = json.data;
251 251 $.map(OutputArea.output_types, function(key){
252 252 if (key !== 'application/json' &&
253 253 data[key] !== undefined &&
254 254 typeof data[key] !== 'string'
255 255 ) {
256 256 console.log("Invalid type for " + key, data[key]);
257 257 delete data[key];
258 258 }
259 259 });
260 260 return json;
261 261 };
262 262
263 263 OutputArea.prototype.append_output = function (json) {
264 264 this.expand();
265 265
266 266 // validate output data types
267 267 if (json.data) {
268 268 json = this.validate_output(json);
269 269 }
270 270
271 271 // Clear the output if clear is queued.
272 272 var needs_height_reset = false;
273 273 if (this.clear_queued) {
274 274 this.clear_output(false);
275 275 needs_height_reset = true;
276 276 }
277 277
278 278 var record_output = true;
279 279
280 280 if (json.output_type === 'execute_result') {
281 281 this.append_execute_result(json);
282 282 } else if (json.output_type === 'error') {
283 283 this.append_error(json);
284 284 } else if (json.output_type === 'stream') {
285 285 // append_stream might have merged the output with earlier stream output
286 286 record_output = this.append_stream(json);
287 287 }
288 288
289 289 // We must release the animation fixed height in a callback since Gecko
290 290 // (FireFox) doesn't render the image immediately as the data is
291 291 // available.
292 292 var that = this;
293 293 var handle_appended = function ($el) {
294 294 // Only reset the height to automatic if the height is currently
295 295 // fixed (done by wait=True flag on clear_output).
296 296 if (needs_height_reset) {
297 297 that.element.height('');
298 298 }
299 299 that.element.trigger('resize');
300 300 };
301 301 if (json.output_type === 'display_data') {
302 302 this.append_display_data(json, handle_appended);
303 303 } else {
304 304 handle_appended();
305 305 }
306 306
307 307 if (record_output) {
308 308 this.outputs.push(json);
309 309 }
310 310 };
311 311
312 312
313 313 OutputArea.prototype.create_output_area = function () {
314 314 var oa = $("<div/>").addClass("output_area");
315 315 if (this.prompt_area) {
316 316 oa.append($('<div/>').addClass('prompt'));
317 317 }
318 318 return oa;
319 319 };
320 320
321 321
322 322 function _get_metadata_key(metadata, key, mime) {
323 323 var mime_md = metadata[mime];
324 324 // mime-specific higher priority
325 325 if (mime_md && mime_md[key] !== undefined) {
326 326 return mime_md[key];
327 327 }
328 328 // fallback on global
329 329 return metadata[key];
330 330 }
331 331
332 332 OutputArea.prototype.create_output_subarea = function(md, classes, mime) {
333 333 var subarea = $('<div/>').addClass('output_subarea').addClass(classes);
334 334 if (_get_metadata_key(md, 'isolated', mime)) {
335 335 // Create an iframe to isolate the subarea from the rest of the
336 336 // document
337 337 var iframe = $('<iframe/>').addClass('box-flex1');
338 338 iframe.css({'height':1, 'width':'100%', 'display':'block'});
339 339 iframe.attr('frameborder', 0);
340 340 iframe.attr('scrolling', 'auto');
341 341
342 342 // Once the iframe is loaded, the subarea is dynamically inserted
343 343 iframe.on('load', function() {
344 344 // Workaround needed by Firefox, to properly render svg inside
345 345 // iframes, see http://stackoverflow.com/questions/10177190/
346 346 // svg-dynamically-added-to-iframe-does-not-render-correctly
347 347 this.contentDocument.open();
348 348
349 349 // Insert the subarea into the iframe
350 350 // We must directly write the html. When using Jquery's append
351 351 // method, javascript is evaluated in the parent document and
352 352 // not in the iframe document. At this point, subarea doesn't
353 353 // contain any user content.
354 354 this.contentDocument.write(subarea.html());
355 355
356 356 this.contentDocument.close();
357 357
358 358 var body = this.contentDocument.body;
359 359 // Adjust the iframe height automatically
360 360 iframe.height(body.scrollHeight + 'px');
361 361 });
362 362
363 363 // Elements should be appended to the inner subarea and not to the
364 364 // iframe
365 365 iframe.append = function(that) {
366 366 subarea.append(that);
367 367 };
368 368
369 369 return iframe;
370 370 } else {
371 371 return subarea;
372 372 }
373 373 };
374 374
375 375
376 376 OutputArea.prototype._append_javascript_error = function (err, element) {
377 377 // display a message when a javascript error occurs in display output
378 378 var msg = "Javascript error adding output!";
379 379 if ( element === undefined ) return;
380 380 element
381 381 .append($('<div/>').text(msg).addClass('js-error'))
382 382 .append($('<div/>').text(err.toString()).addClass('js-error'))
383 383 .append($('<div/>').text('See your browser Javascript console for more details.').addClass('js-error'));
384 384 };
385 385
386 386 OutputArea.prototype._safe_append = function (toinsert) {
387 387 // safely append an item to the document
388 388 // this is an object created by user code,
389 389 // and may have errors, which should not be raised
390 390 // under any circumstances.
391 391 try {
392 392 this.element.append(toinsert);
393 393 } catch(err) {
394 394 console.log(err);
395 395 // Create an actual output_area and output_subarea, which creates
396 396 // the prompt area and the proper indentation.
397 397 var toinsert = this.create_output_area();
398 398 var subarea = $('<div/>').addClass('output_subarea');
399 399 toinsert.append(subarea);
400 400 this._append_javascript_error(err, subarea);
401 401 this.element.append(toinsert);
402 402 }
403 403 };
404 404
405 405
406 406 OutputArea.prototype.append_execute_result = function (json) {
407 407 var n = json.execution_count || ' ';
408 408 var toinsert = this.create_output_area();
409 409 if (this.prompt_area) {
410 410 toinsert.find('div.prompt').addClass('output_prompt').text('Out[' + n + ']:');
411 411 }
412 412 var inserted = this.append_mime_type(json, toinsert);
413 413 if (inserted) {
414 414 inserted.addClass('output_result');
415 415 }
416 416 this._safe_append(toinsert);
417 417 // If we just output latex, typeset it.
418 418 if ((json['text/latex'] !== undefined) ||
419 419 (json['text/html'] !== undefined) ||
420 420 (json['text/markdown'] !== undefined)) {
421 421 this.typeset();
422 422 }
423 423 };
424 424
425 425
426 426 OutputArea.prototype.append_error = function (json) {
427 427 var tb = json.traceback;
428 428 if (tb !== undefined && tb.length > 0) {
429 429 var s = '';
430 430 var len = tb.length;
431 431 for (var i=0; i<len; i++) {
432 432 s = s + tb[i] + '\n';
433 433 }
434 434 s = s + '\n';
435 435 var toinsert = this.create_output_area();
436 436 var append_text = OutputArea.append_map['text/plain'];
437 437 if (append_text) {
438 438 append_text.apply(this, [s, {}, toinsert]).addClass('output_error');
439 439 }
440 440 this._safe_append(toinsert);
441 441 }
442 442 };
443 443
444 444
445 445 OutputArea.prototype.append_stream = function (json) {
446 446 var text = json.text;
447 447 var subclass = "output_"+json.name;
448 448 if (this.outputs.length > 0){
449 449 // have at least one output to consider
450 450 var last = this.outputs[this.outputs.length-1];
451 451 if (last.output_type == 'stream' && json.name == last.name){
452 452 // latest output was in the same stream,
453 453 // so append directly into its pre tag
454 454 // escape ANSI & HTML specials:
455 455 last.text = utils.fixCarriageReturn(last.text + json.text);
456 456 var pre = this.element.find('div.'+subclass).last().find('pre');
457 457 var html = utils.fixConsole(last.text);
458 458 // The only user content injected with this HTML call is
459 459 // escaped by the fixConsole() method.
460 460 pre.html(html);
461 461 // return false signals that we merged this output with the previous one,
462 462 // and the new output shouldn't be recorded.
463 463 return false;
464 464 }
465 465 }
466 466
467 467 if (!text.replace("\r", "")) {
468 468 // text is nothing (empty string, \r, etc.)
469 469 // so don't append any elements, which might add undesirable space
470 470 // return true to indicate the output should be recorded.
471 471 return true;
472 472 }
473 473
474 474 // If we got here, attach a new div
475 475 var toinsert = this.create_output_area();
476 476 var append_text = OutputArea.append_map['text/plain'];
477 477 if (append_text) {
478 478 append_text.apply(this, [text, {}, toinsert]).addClass("output_stream " + subclass);
479 479 }
480 480 this._safe_append(toinsert);
481 481 return true;
482 482 };
483 483
484 484
485 485 OutputArea.prototype.append_display_data = function (json, handle_inserted) {
486 486 var toinsert = this.create_output_area();
487 487 if (this.append_mime_type(json, toinsert, handle_inserted)) {
488 488 this._safe_append(toinsert);
489 489 // If we just output latex, typeset it.
490 490 if ((json['text/latex'] !== undefined) ||
491 491 (json['text/html'] !== undefined) ||
492 492 (json['text/markdown'] !== undefined)) {
493 493 this.typeset();
494 494 }
495 495 }
496 496 };
497 497
498 498
499 499 OutputArea.safe_outputs = {
500 500 'text/plain' : true,
501 501 'text/latex' : true,
502 502 'image/png' : true,
503 503 'image/jpeg' : true
504 504 };
505 505
506 506 OutputArea.prototype.append_mime_type = function (json, element, handle_inserted) {
507 507 for (var i=0; i < OutputArea.display_order.length; i++) {
508 508 var type = OutputArea.display_order[i];
509 509 var append = OutputArea.append_map[type];
510 510 if ((json.data[type] !== undefined) && append) {
511 511 var value = json.data[type];
512 512 if (!this.trusted && !OutputArea.safe_outputs[type]) {
513 513 // not trusted, sanitize HTML
514 514 if (type==='text/html' || type==='text/svg') {
515 515 value = security.sanitize_html(value);
516 516 } else {
517 517 // don't display if we don't know how to sanitize it
518 518 console.log("Ignoring untrusted " + type + " output.");
519 519 continue;
520 520 }
521 521 }
522 522 var md = json.metadata || {};
523 523 var toinsert = append.apply(this, [value, md, element, handle_inserted]);
524 524 // Since only the png and jpeg mime types call the inserted
525 525 // callback, if the mime type is something other we must call the
526 526 // inserted callback only when the element is actually inserted
527 527 // into the DOM. Use a timeout of 0 to do this.
528 528 if (['image/png', 'image/jpeg'].indexOf(type) < 0 && handle_inserted !== undefined) {
529 529 setTimeout(handle_inserted, 0);
530 530 }
531 531 this.events.trigger('output_appended.OutputArea', [type, value, md, toinsert]);
532 532 return toinsert;
533 533 }
534 534 }
535 535 return null;
536 536 };
537 537
538 538
539 539 var append_html = function (html, md, element) {
540 540 var type = 'text/html';
541 541 var toinsert = this.create_output_subarea(md, "output_html rendered_html", type);
542 542 this.keyboard_manager.register_events(toinsert);
543 543 toinsert.append(html);
544 544 element.append(toinsert);
545 545 return toinsert;
546 546 };
547 547
548 548
549 549 var append_markdown = function(markdown, md, element) {
550 550 var type = 'text/markdown';
551 551 var toinsert = this.create_output_subarea(md, "output_markdown", type);
552 552 var text_and_math = mathjaxutils.remove_math(markdown);
553 553 var text = text_and_math[0];
554 554 var math = text_and_math[1];
555 var html = marked.parser(marked.lexer(text));
556 html = mathjaxutils.replace_math(html, math);
557 toinsert.append(html);
555 marked(text, function (err, html) {
556 html = mathjaxutils.replace_math(html, math);
557 toinsert.append(html);
558 });
558 559 element.append(toinsert);
559 560 return toinsert;
560 561 };
561 562
562 563
563 564 var append_javascript = function (js, md, element) {
564 565 // We just eval the JS code, element appears in the local scope.
565 566 var type = 'application/javascript';
566 567 var toinsert = this.create_output_subarea(md, "output_javascript", type);
567 568 this.keyboard_manager.register_events(toinsert);
568 569 element.append(toinsert);
569 570
570 571 // Fix for ipython/issues/5293, make sure `element` is the area which
571 572 // output can be inserted into at the time of JS execution.
572 573 element = toinsert;
573 574 try {
574 575 eval(js);
575 576 } catch(err) {
576 577 console.log(err);
577 578 this._append_javascript_error(err, toinsert);
578 579 }
579 580 return toinsert;
580 581 };
581 582
582 583
583 584 var append_text = function (data, md, element) {
584 585 var type = 'text/plain';
585 586 var toinsert = this.create_output_subarea(md, "output_text", type);
586 587 // escape ANSI & HTML specials in plaintext:
587 588 data = utils.fixConsole(data);
588 589 data = utils.fixCarriageReturn(data);
589 590 data = utils.autoLinkUrls(data);
590 591 // The only user content injected with this HTML call is
591 592 // escaped by the fixConsole() method.
592 593 toinsert.append($("<pre/>").html(data));
593 594 element.append(toinsert);
594 595 return toinsert;
595 596 };
596 597
597 598
598 599 var append_svg = function (svg_html, md, element) {
599 600 var type = 'image/svg+xml';
600 601 var toinsert = this.create_output_subarea(md, "output_svg", type);
601 602
602 603 // Get the svg element from within the HTML.
603 604 var svg = $('<div />').html(svg_html).find('svg');
604 605 var svg_area = $('<div />');
605 606 var width = svg.attr('width');
606 607 var height = svg.attr('height');
607 608 svg
608 609 .width('100%')
609 610 .height('100%');
610 611 svg_area
611 612 .width(width)
612 613 .height(height);
613 614
614 615 // The jQuery resize handlers don't seem to work on the svg element.
615 616 // When the svg renders completely, measure it's size and set the parent
616 617 // div to that size. Then set the svg to 100% the size of the parent
617 618 // div and make the parent div resizable.
618 619 this._dblclick_to_reset_size(svg_area, true, false);
619 620
620 621 svg_area.append(svg);
621 622 toinsert.append(svg_area);
622 623 element.append(toinsert);
623 624
624 625 return toinsert;
625 626 };
626 627
627 628 OutputArea.prototype._dblclick_to_reset_size = function (img, immediately, resize_parent) {
628 629 // Add a resize handler to an element
629 630 //
630 631 // img: jQuery element
631 632 // immediately: bool=False
632 633 // Wait for the element to load before creating the handle.
633 634 // resize_parent: bool=True
634 635 // Should the parent of the element be resized when the element is
635 636 // reset (by double click).
636 637 var callback = function (){
637 638 var h0 = img.height();
638 639 var w0 = img.width();
639 640 if (!(h0 && w0)) {
640 641 // zero size, don't make it resizable
641 642 return;
642 643 }
643 644 img.resizable({
644 645 aspectRatio: true,
645 646 autoHide: true
646 647 });
647 648 img.dblclick(function () {
648 649 // resize wrapper & image together for some reason:
649 650 img.height(h0);
650 651 img.width(w0);
651 652 if (resize_parent === undefined || resize_parent) {
652 653 img.parent().height(h0);
653 654 img.parent().width(w0);
654 655 }
655 656 });
656 657 };
657 658
658 659 if (immediately) {
659 660 callback();
660 661 } else {
661 662 img.on("load", callback);
662 663 }
663 664 };
664 665
665 666 var set_width_height = function (img, md, mime) {
666 667 // set width and height of an img element from metadata
667 668 var height = _get_metadata_key(md, 'height', mime);
668 669 if (height !== undefined) img.attr('height', height);
669 670 var width = _get_metadata_key(md, 'width', mime);
670 671 if (width !== undefined) img.attr('width', width);
671 672 };
672 673
673 674 var append_png = function (png, md, element, handle_inserted) {
674 675 var type = 'image/png';
675 676 var toinsert = this.create_output_subarea(md, "output_png", type);
676 677 var img = $("<img/>");
677 678 if (handle_inserted !== undefined) {
678 679 img.on('load', function(){
679 680 handle_inserted(img);
680 681 });
681 682 }
682 683 img[0].src = 'data:image/png;base64,'+ png;
683 684 set_width_height(img, md, 'image/png');
684 685 this._dblclick_to_reset_size(img);
685 686 toinsert.append(img);
686 687 element.append(toinsert);
687 688 return toinsert;
688 689 };
689 690
690 691
691 692 var append_jpeg = function (jpeg, md, element, handle_inserted) {
692 693 var type = 'image/jpeg';
693 694 var toinsert = this.create_output_subarea(md, "output_jpeg", type);
694 695 var img = $("<img/>");
695 696 if (handle_inserted !== undefined) {
696 697 img.on('load', function(){
697 698 handle_inserted(img);
698 699 });
699 700 }
700 701 img[0].src = 'data:image/jpeg;base64,'+ jpeg;
701 702 set_width_height(img, md, 'image/jpeg');
702 703 this._dblclick_to_reset_size(img);
703 704 toinsert.append(img);
704 705 element.append(toinsert);
705 706 return toinsert;
706 707 };
707 708
708 709
709 710 var append_pdf = function (pdf, md, element) {
710 711 var type = 'application/pdf';
711 712 var toinsert = this.create_output_subarea(md, "output_pdf", type);
712 713 var a = $('<a/>').attr('href', 'data:application/pdf;base64,'+pdf);
713 714 a.attr('target', '_blank');
714 715 a.text('View PDF');
715 716 toinsert.append(a);
716 717 element.append(toinsert);
717 718 return toinsert;
718 719 };
719 720
720 721 var append_latex = function (latex, md, element) {
721 722 // This method cannot do the typesetting because the latex first has to
722 723 // be on the page.
723 724 var type = 'text/latex';
724 725 var toinsert = this.create_output_subarea(md, "output_latex", type);
725 726 toinsert.append(latex);
726 727 element.append(toinsert);
727 728 return toinsert;
728 729 };
729 730
730 731
731 732 OutputArea.prototype.append_raw_input = function (msg) {
732 733 var that = this;
733 734 this.expand();
734 735 var content = msg.content;
735 736 var area = this.create_output_area();
736 737
737 738 // disable any other raw_inputs, if they are left around
738 739 $("div.output_subarea.raw_input_container").remove();
739 740
740 741 var input_type = content.password ? 'password' : 'text';
741 742
742 743 area.append(
743 744 $("<div/>")
744 745 .addClass("box-flex1 output_subarea raw_input_container")
745 746 .append(
746 747 $("<span/>")
747 748 .addClass("raw_input_prompt")
748 749 .text(content.prompt)
749 750 )
750 751 .append(
751 752 $("<input/>")
752 753 .addClass("raw_input")
753 754 .attr('type', input_type)
754 755 .attr("size", 47)
755 756 .keydown(function (event, ui) {
756 757 // make sure we submit on enter,
757 758 // and don't re-execute the *cell* on shift-enter
758 759 if (event.which === keyboard.keycodes.enter) {
759 760 that._submit_raw_input();
760 761 return false;
761 762 }
762 763 })
763 764 )
764 765 );
765 766
766 767 this.element.append(area);
767 768 var raw_input = area.find('input.raw_input');
768 769 // Register events that enable/disable the keyboard manager while raw
769 770 // input is focused.
770 771 this.keyboard_manager.register_events(raw_input);
771 772 // Note, the following line used to read raw_input.focus().focus().
772 773 // This seemed to be needed otherwise only the cell would be focused.
773 774 // But with the modal UI, this seems to work fine with one call to focus().
774 775 raw_input.focus();
775 776 };
776 777
777 778 OutputArea.prototype._submit_raw_input = function (evt) {
778 779 var container = this.element.find("div.raw_input_container");
779 780 var theprompt = container.find("span.raw_input_prompt");
780 781 var theinput = container.find("input.raw_input");
781 782 var value = theinput.val();
782 783 var echo = value;
783 784 // don't echo if it's a password
784 785 if (theinput.attr('type') == 'password') {
785 786 echo = '········';
786 787 }
787 788 var content = {
788 789 output_type : 'stream',
789 790 stream : 'stdout',
790 791 text : theprompt.text() + echo + '\n'
791 792 };
792 793 // remove form container
793 794 container.parent().remove();
794 795 // replace with plaintext version in stdout
795 796 this.append_output(content, false);
796 797 this.events.trigger('send_input_reply.Kernel', value);
797 798 };
798 799
799 800
800 801 OutputArea.prototype.handle_clear_output = function (msg) {
801 802 // msg spec v4 had stdout, stderr, display keys
802 803 // v4.1 replaced these with just wait
803 804 // The default behavior is the same (stdout=stderr=display=True, wait=False),
804 805 // so v4 messages will still be properly handled,
805 806 // except for the rarely used clearing less than all output.
806 807 this.clear_output(msg.content.wait || false);
807 808 };
808 809
809 810
810 811 OutputArea.prototype.clear_output = function(wait) {
811 812 if (wait) {
812 813
813 814 // If a clear is queued, clear before adding another to the queue.
814 815 if (this.clear_queued) {
815 816 this.clear_output(false);
816 817 }
817 818
818 819 this.clear_queued = true;
819 820 } else {
820 821
821 822 // Fix the output div's height if the clear_output is waiting for
822 823 // new output (it is being used in an animation).
823 824 if (this.clear_queued) {
824 825 var height = this.element.height();
825 826 this.element.height(height);
826 827 this.clear_queued = false;
827 828 }
828 829
829 830 // Clear all
830 831 // Remove load event handlers from img tags because we don't want
831 832 // them to fire if the image is never added to the page.
832 833 this.element.find('img').off('load');
833 834 this.element.html("");
834 835 this.outputs = [];
835 836 this.trusted = true;
836 837 this.unscroll_area();
837 838 return;
838 839 }
839 840 };
840 841
841 842
842 843 // JSON serialization
843 844
844 845 OutputArea.prototype.fromJSON = function (outputs, metadata) {
845 846 var len = outputs.length;
846 847 metadata = metadata || {};
847 848
848 849 for (var i=0; i<len; i++) {
849 850 this.append_output(outputs[i]);
850 851 }
851 852
852 853 if (metadata.collapsed !== undefined) {
853 854 this.collapsed = metadata.collapsed;
854 855 if (metadata.collapsed) {
855 856 this.collapse_output();
856 857 }
857 858 }
858 859 if (metadata.autoscroll !== undefined) {
859 860 this.collapsed = metadata.collapsed;
860 861 if (metadata.collapsed) {
861 862 this.collapse_output();
862 863 } else {
863 864 this.expand_output();
864 865 }
865 866 }
866 867 };
867 868
868 869
869 870 OutputArea.prototype.toJSON = function () {
870 871 return this.outputs;
871 872 };
872 873
873 874 /**
874 875 * Class properties
875 876 **/
876 877
877 878 /**
878 879 * Threshold to trigger autoscroll when the OutputArea is resized,
879 880 * typically when new outputs are added.
880 881 *
881 882 * Behavior is undefined if autoscroll is lower than minimum_scroll_threshold,
882 883 * unless it is < 0, in which case autoscroll will never be triggered
883 884 *
884 885 * @property auto_scroll_threshold
885 886 * @type Number
886 887 * @default 100
887 888 *
888 889 **/
889 890 OutputArea.auto_scroll_threshold = 100;
890 891
891 892 /**
892 893 * Lower limit (in lines) for OutputArea to be made scrollable. OutputAreas
893 894 * shorter than this are never scrolled.
894 895 *
895 896 * @property minimum_scroll_threshold
896 897 * @type Number
897 898 * @default 20
898 899 *
899 900 **/
900 901 OutputArea.minimum_scroll_threshold = 20;
901 902
902 903
903 904 OutputArea.display_order = [
904 905 'application/javascript',
905 906 'text/html',
906 907 'text/markdown',
907 908 'text/latex',
908 909 'image/svg+xml',
909 910 'image/png',
910 911 'image/jpeg',
911 912 'application/pdf',
912 913 'text/plain'
913 914 ];
914 915
915 916 OutputArea.append_map = {
916 917 "text/plain" : append_text,
917 918 "text/html" : append_html,
918 919 "text/markdown": append_markdown,
919 920 "image/svg+xml" : append_svg,
920 921 "image/png" : append_png,
921 922 "image/jpeg" : append_jpeg,
922 923 "text/latex" : append_latex,
923 924 "application/javascript" : append_javascript,
924 925 "application/pdf" : append_pdf
925 926 };
926 927
927 928 // For backwards compatability.
928 929 IPython.OutputArea = OutputArea;
929 930
930 931 return {'OutputArea': OutputArea};
931 932 });
@@ -1,345 +1,346
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'base/js/utils',
7 7 'jquery',
8 8 'notebook/js/cell',
9 9 'base/js/security',
10 10 'notebook/js/mathjaxutils',
11 11 'notebook/js/celltoolbar',
12 12 'components/marked/lib/marked',
13 13 'codemirror/lib/codemirror',
14 14 'codemirror/mode/gfm/gfm',
15 15 'notebook/js/codemirror-ipythongfm'
16 16 ], function(IPython,utils , $, cell, security, mathjaxutils, celltoolbar, marked, CodeMirror, gfm, ipgfm) {
17 17 "use strict";
18 18 var Cell = cell.Cell;
19 19
20 20 var TextCell = function (options) {
21 21 // Constructor
22 22 //
23 23 // Construct a new TextCell, codemirror mode is by default 'htmlmixed',
24 24 // and cell type is 'text' cell start as not redered.
25 25 //
26 26 // Parameters:
27 27 // options: dictionary
28 28 // Dictionary of keyword arguments.
29 29 // events: $(Events) instance
30 30 // config: dictionary
31 31 // keyboard_manager: KeyboardManager instance
32 32 // notebook: Notebook instance
33 33 options = options || {};
34 34
35 35 // in all TextCell/Cell subclasses
36 36 // do not assign most of members here, just pass it down
37 37 // in the options dict potentially overwriting what you wish.
38 38 // they will be assigned in the base class.
39 39 this.notebook = options.notebook;
40 40 this.events = options.events;
41 41 this.config = options.config;
42 42
43 43 // we cannot put this as a class key as it has handle to "this".
44 44 var cm_overwrite_options = {
45 45 onKeyEvent: $.proxy(this.handle_keyevent,this)
46 46 };
47 47 var config = utils.mergeopt(TextCell, this.config, {cm_config:cm_overwrite_options});
48 48 Cell.apply(this, [{
49 49 config: config,
50 50 keyboard_manager: options.keyboard_manager,
51 51 events: this.events}]);
52 52
53 53 this.cell_type = this.cell_type || 'text';
54 54 mathjaxutils = mathjaxutils;
55 55 this.rendered = false;
56 56 };
57 57
58 58 TextCell.prototype = Object.create(Cell.prototype);
59 59
60 60 TextCell.options_default = {
61 61 cm_config : {
62 62 extraKeys: {"Tab": "indentMore","Shift-Tab" : "indentLess"},
63 63 mode: 'htmlmixed',
64 64 lineWrapping : true,
65 65 }
66 66 };
67 67
68 68
69 69 /**
70 70 * Create the DOM element of the TextCell
71 71 * @method create_element
72 72 * @private
73 73 */
74 74 TextCell.prototype.create_element = function () {
75 75 Cell.prototype.create_element.apply(this, arguments);
76 76
77 77 var cell = $("<div>").addClass('cell text_cell');
78 78 cell.attr('tabindex','2');
79 79
80 80 var prompt = $('<div/>').addClass('prompt input_prompt');
81 81 cell.append(prompt);
82 82 var inner_cell = $('<div/>').addClass('inner_cell');
83 83 this.celltoolbar = new celltoolbar.CellToolbar({
84 84 cell: this,
85 85 notebook: this.notebook});
86 86 inner_cell.append(this.celltoolbar.element);
87 87 var input_area = $('<div/>').addClass('input_area');
88 88 this.code_mirror = new CodeMirror(input_area.get(0), this.cm_config);
89 89 // The tabindex=-1 makes this div focusable.
90 90 var render_area = $('<div/>').addClass('text_cell_render rendered_html')
91 91 .attr('tabindex','-1');
92 92 inner_cell.append(input_area).append(render_area);
93 93 cell.append(inner_cell);
94 94 this.element = cell;
95 95 };
96 96
97 97
98 98 // Cell level actions
99 99
100 100 TextCell.prototype.select = function () {
101 101 var cont = Cell.prototype.select.apply(this);
102 102 if (cont) {
103 103 if (this.mode === 'edit') {
104 104 this.code_mirror.refresh();
105 105 }
106 106 }
107 107 return cont;
108 108 };
109 109
110 110 TextCell.prototype.unrender = function () {
111 111 if (this.read_only) return;
112 112 var cont = Cell.prototype.unrender.apply(this);
113 113 if (cont) {
114 114 var text_cell = this.element;
115 115 var output = text_cell.find("div.text_cell_render");
116 116 if (this.get_text() === this.placeholder) {
117 117 this.set_text('');
118 118 }
119 119 this.refresh();
120 120 }
121 121 return cont;
122 122 };
123 123
124 124 TextCell.prototype.execute = function () {
125 125 this.render();
126 126 };
127 127
128 128 /**
129 129 * setter: {{#crossLink "TextCell/set_text"}}{{/crossLink}}
130 130 * @method get_text
131 131 * @retrun {string} CodeMirror current text value
132 132 */
133 133 TextCell.prototype.get_text = function() {
134 134 return this.code_mirror.getValue();
135 135 };
136 136
137 137 /**
138 138 * @param {string} text - Codemiror text value
139 139 * @see TextCell#get_text
140 140 * @method set_text
141 141 * */
142 142 TextCell.prototype.set_text = function(text) {
143 143 this.code_mirror.setValue(text);
144 144 this.unrender();
145 145 this.code_mirror.refresh();
146 146 };
147 147
148 148 /**
149 149 * setter :{{#crossLink "TextCell/set_rendered"}}{{/crossLink}}
150 150 * @method get_rendered
151 151 * */
152 152 TextCell.prototype.get_rendered = function() {
153 153 return this.element.find('div.text_cell_render').html();
154 154 };
155 155
156 156 /**
157 157 * @method set_rendered
158 158 */
159 159 TextCell.prototype.set_rendered = function(text) {
160 160 this.element.find('div.text_cell_render').html(text);
161 161 };
162 162
163 163
164 164 /**
165 165 * Create Text cell from JSON
166 166 * @param {json} data - JSON serialized text-cell
167 167 * @method fromJSON
168 168 */
169 169 TextCell.prototype.fromJSON = function (data) {
170 170 Cell.prototype.fromJSON.apply(this, arguments);
171 171 if (data.cell_type === this.cell_type) {
172 172 if (data.source !== undefined) {
173 173 this.set_text(data.source);
174 174 // make this value the starting point, so that we can only undo
175 175 // to this state, instead of a blank cell
176 176 this.code_mirror.clearHistory();
177 177 // TODO: This HTML needs to be treated as potentially dangerous
178 178 // user input and should be handled before set_rendered.
179 179 this.set_rendered(data.rendered || '');
180 180 this.rendered = false;
181 181 this.render();
182 182 }
183 183 }
184 184 };
185 185
186 186 /** Generate JSON from cell
187 187 * @return {object} cell data serialised to json
188 188 */
189 189 TextCell.prototype.toJSON = function () {
190 190 var data = Cell.prototype.toJSON.apply(this);
191 191 data.source = this.get_text();
192 192 if (data.source == this.placeholder) {
193 193 data.source = "";
194 194 }
195 195 return data;
196 196 };
197 197
198 198
199 199 var MarkdownCell = function (options) {
200 200 // Constructor
201 201 //
202 202 // Parameters:
203 203 // options: dictionary
204 204 // Dictionary of keyword arguments.
205 205 // events: $(Events) instance
206 206 // config: dictionary
207 207 // keyboard_manager: KeyboardManager instance
208 208 // notebook: Notebook instance
209 209 options = options || {};
210 210 var config = utils.mergeopt(MarkdownCell, options.config);
211 211 TextCell.apply(this, [$.extend({}, options, {config: config})]);
212 212
213 213 this.cell_type = 'markdown';
214 214 };
215 215
216 216 MarkdownCell.options_default = {
217 217 cm_config: {
218 218 mode: 'ipythongfm'
219 219 },
220 220 placeholder: "Type *Markdown* and LaTeX: $\\alpha^2$"
221 221 };
222 222
223 223 MarkdownCell.prototype = Object.create(TextCell.prototype);
224 224
225 225 MarkdownCell.prototype.set_heading_level = function (level) {
226 226 // make a markdown cell a heading
227 227 level = level || 1;
228 228 var source = this.get_text();
229 229 source = source.replace(/^(#*)\s?/,
230 230 new Array(level + 1).join('#') + ' ');
231 231 this.set_text(source);
232 232 this.refresh();
233 233 if (this.rendered) {
234 234 this.render();
235 235 }
236 236 };
237 237
238 238 /**
239 239 * @method render
240 240 */
241 241 MarkdownCell.prototype.render = function () {
242 242 var cont = TextCell.prototype.render.apply(this);
243 243 if (cont) {
244 var that = this;
244 245 var text = this.get_text();
245 246 var math = null;
246 247 if (text === "") { text = this.placeholder; }
247 248 var text_and_math = mathjaxutils.remove_math(text);
248 249 text = text_and_math[0];
249 250 math = text_and_math[1];
250 var html = marked.parser(marked.lexer(text));
251 html = mathjaxutils.replace_math(html, math);
252 html = security.sanitize_html(html);
253 html = $($.parseHTML(html));
254 // add anchors to headings
255 // console.log(html);
256 html.find(":header").addBack(":header").each(function (i, h) {
257 h = $(h);
258 var hash = h.text().replace(/ /g, '-');
259 h.attr('id', hash);
260 h.append(
261 $('<a/>')
262 .addClass('anchor-link')
263 .attr('href', '#' + hash)
264 .text('¶')
265 );
266 })
267 // links in markdown cells should open in new tabs
268 html.find("a[href]").not('[href^="#"]').attr("target", "_blank");
269 this.set_rendered(html);
270 this.typeset();
271 this.events.trigger("rendered.MarkdownCell", {cell: this})
251 marked(text, function (err, html) {
252 html = mathjaxutils.replace_math(html, math);
253 html = security.sanitize_html(html);
254 html = $($.parseHTML(html));
255 // add anchors to headings
256 html.find(":header").addBack(":header").each(function (i, h) {
257 h = $(h);
258 var hash = h.text().replace(/ /g, '-');
259 h.attr('id', hash);
260 h.append(
261 $('<a/>')
262 .addClass('anchor-link')
263 .attr('href', '#' + hash)
264 .text('¶')
265 );
266 });
267 // links in markdown cells should open in new tabs
268 html.find("a[href]").not('[href^="#"]').attr("target", "_blank");
269 that.set_rendered(html);
270 that.typeset();
271 that.events.trigger("rendered.MarkdownCell", {cell: that});
272 });
272 273 }
273 274 return cont;
274 275 };
275 276
276 277
277 278 var RawCell = function (options) {
278 279 // Constructor
279 280 //
280 281 // Parameters:
281 282 // options: dictionary
282 283 // Dictionary of keyword arguments.
283 284 // events: $(Events) instance
284 285 // config: dictionary
285 286 // keyboard_manager: KeyboardManager instance
286 287 // notebook: Notebook instance
287 288 options = options || {};
288 289 var config = utils.mergeopt(RawCell, options.config);
289 290 TextCell.apply(this, [$.extend({}, options, {config: config})]);
290 291
291 292 this.cell_type = 'raw';
292 293 };
293 294
294 295 RawCell.options_default = {
295 296 placeholder : "Write raw LaTeX or other formats here, for use with nbconvert. " +
296 297 "It will not be rendered in the notebook. " +
297 298 "When passing through nbconvert, a Raw Cell's content is added to the output unmodified."
298 299 };
299 300
300 301 RawCell.prototype = Object.create(TextCell.prototype);
301 302
302 303 /** @method bind_events **/
303 304 RawCell.prototype.bind_events = function () {
304 305 TextCell.prototype.bind_events.apply(this);
305 306 var that = this;
306 307 this.element.focusout(function() {
307 308 that.auto_highlight();
308 309 that.render();
309 310 });
310 311
311 312 this.code_mirror.on('focus', function() { that.unrender(); });
312 313 };
313 314
314 315 /**
315 316 * Trigger autodetection of highlight scheme for current cell
316 317 * @method auto_highlight
317 318 */
318 319 RawCell.prototype.auto_highlight = function () {
319 320 this._auto_highlight(this.config.raw_cell_highlight);
320 321 };
321 322
322 323 /** @method render **/
323 324 RawCell.prototype.render = function () {
324 325 var cont = TextCell.prototype.render.apply(this);
325 326 if (cont){
326 327 var text = this.get_text();
327 328 if (text === "") { text = this.placeholder; }
328 329 this.set_text(text);
329 330 this.element.removeClass('rendered');
330 331 }
331 332 return cont;
332 333 };
333 334
334 335 // Backwards compatability.
335 336 IPython.TextCell = TextCell;
336 337 IPython.MarkdownCell = MarkdownCell;
337 338 IPython.RawCell = RawCell;
338 339
339 340 var textcell = {
340 341 TextCell: TextCell,
341 342 MarkdownCell: MarkdownCell,
342 343 RawCell: RawCell,
343 344 };
344 345 return textcell;
345 346 });
@@ -1,108 +1,104
1 1 <!DOCTYPE HTML>
2 2 <html>
3 3
4 4 <head>
5 5 <meta charset="utf-8">
6 6
7 7 <title>{% block title %}IPython Notebook{% endblock %}</title>
8 8 <link rel="shortcut icon" type="image/x-icon" href="{{static_url("base/images/favicon.ico") }}">
9 9 <meta http-equiv="X-UA-Compatible" content="chrome=1">
10 10 <link rel="stylesheet" href="{{static_url("components/jquery-ui/themes/smoothness/jquery-ui.min.css") }}" type="text/css" />
11 11 <meta name="viewport" content="width=device-width, initial-scale=1.0">
12 12
13 13 {% block stylesheet %}
14 14 <link rel="stylesheet" href="{{ static_url("style/style.min.css") }}" type="text/css"/>
15 15 {% endblock %}
16 16 <link rel="stylesheet" href="{{ static_url("custom/custom.css") }}" type="text/css" />
17 17 <script src="{{static_url("components/es6-promise/promise.min.js")}}" type="text/javascript" charset="utf-8"></script>
18 18 <script src="{{static_url("components/requirejs/require.js") }}" type="text/javascript" charset="utf-8"></script>
19 19 <script>
20 20 require.config({
21 21 baseUrl: '{{static_url("", include_version=False)}}',
22 22 paths: {
23 23 nbextensions : '{{ base_url }}nbextensions',
24 24 underscore : 'components/underscore/underscore-min',
25 25 backbone : 'components/backbone/backbone-min',
26 26 jquery: 'components/jquery/jquery.min',
27 27 bootstrap: 'components/bootstrap/js/bootstrap.min',
28 28 bootstraptour: 'components/bootstrap-tour/build/js/bootstrap-tour.min',
29 29 jqueryui: 'components/jquery-ui/ui/minified/jquery-ui.min',
30 highlight: 'components/highlight.js/build/highlight.pack',
31 30 moment: "components/moment/moment",
32 31 codemirror: 'components/codemirror',
33 32 termjs: "components/term.js/src/term",
34 33 contents: '{{ contents_js_source }}',
35 34 },
36 35 shim: {
37 36 underscore: {
38 37 exports: '_'
39 38 },
40 39 backbone: {
41 40 deps: ["underscore", "jquery"],
42 41 exports: "Backbone"
43 42 },
44 43 bootstrap: {
45 44 deps: ["jquery"],
46 45 exports: "bootstrap"
47 46 },
48 47 bootstraptour: {
49 48 deps: ["bootstrap"],
50 49 exports: "Tour"
51 50 },
52 51 jqueryui: {
53 52 deps: ["jquery"],
54 53 exports: "$"
55 },
56 highlight: {
57 exports: "hljs"
58 },
54 }
59 55 }
60 56 });
61 57 </script>
62 58
63 59 {% block meta %}
64 60 {% endblock %}
65 61
66 62 </head>
67 63
68 64 <body {% block params %}{% endblock %}>
69 65
70 66 <noscript>
71 67 <div id='noscript'>
72 68 IPython Notebook requires JavaScript.<br>
73 69 Please enable it to proceed.
74 70 </div>
75 71 </noscript>
76 72
77 73 <div id="header" class="navbar navbar-static-top">
78 74 <div class="container">
79 75 <div id="ipython_notebook" class="nav navbar-brand pull-left"><a href="{{base_url}}tree" alt='dashboard'><img src='{{static_url("base/images/ipynblogo.png") }}' alt='IPython Notebook'/></a></div>
80 76
81 77 {% block login_widget %}
82 78
83 79 <span id="login_widget">
84 80 {% if logged_in %}
85 81 <button id="logout">Logout</button>
86 82 {% elif login_available and not logged_in %}
87 83 <button id="login">Login</button>
88 84 {% endif %}
89 85 </span>
90 86
91 87 {% endblock %}
92 88
93 89 {% block header %}
94 90 {% endblock %}
95 91 </div>
96 92 </div>
97 93
98 94 <div id="site">
99 95 {% block site %}
100 96 {% endblock %}
101 97 </div>
102 98
103 99 {% block script %}
104 100 {% endblock %}
105 101
106 102 </body>
107 103
108 104 </html>
@@ -1,755 +1,754
1 1 # encoding: utf-8
2 2 """
3 3 This module defines the things that are used in setup.py for building IPython
4 4
5 5 This includes:
6 6
7 7 * The basic arguments to setup
8 8 * Functions for finding things like packages, package data, etc.
9 9 * A function for checking dependencies.
10 10 """
11 11
12 12 # Copyright (c) IPython Development Team.
13 13 # Distributed under the terms of the Modified BSD License.
14 14
15 15 from __future__ import print_function
16 16
17 17 import errno
18 18 import os
19 19 import sys
20 20
21 21 from distutils import log
22 22 from distutils.command.build_py import build_py
23 23 from distutils.command.build_scripts import build_scripts
24 24 from distutils.command.install import install
25 25 from distutils.command.install_scripts import install_scripts
26 26 from distutils.cmd import Command
27 27 from fnmatch import fnmatch
28 28 from glob import glob
29 29 from subprocess import check_call
30 30
31 31 from setupext import install_data_ext
32 32
33 33 #-------------------------------------------------------------------------------
34 34 # Useful globals and utility functions
35 35 #-------------------------------------------------------------------------------
36 36
37 37 # A few handy globals
38 38 isfile = os.path.isfile
39 39 pjoin = os.path.join
40 40 repo_root = os.path.dirname(os.path.abspath(__file__))
41 41
42 42 def oscmd(s):
43 43 print(">", s)
44 44 os.system(s)
45 45
46 46 # Py3 compatibility hacks, without assuming IPython itself is installed with
47 47 # the full py3compat machinery.
48 48
49 49 try:
50 50 execfile
51 51 except NameError:
52 52 def execfile(fname, globs, locs=None):
53 53 locs = locs or globs
54 54 exec(compile(open(fname).read(), fname, "exec"), globs, locs)
55 55
56 56 # A little utility we'll need below, since glob() does NOT allow you to do
57 57 # exclusion on multiple endings!
58 58 def file_doesnt_endwith(test,endings):
59 59 """Return true if test is a file and its name does NOT end with any
60 60 of the strings listed in endings."""
61 61 if not isfile(test):
62 62 return False
63 63 for e in endings:
64 64 if test.endswith(e):
65 65 return False
66 66 return True
67 67
68 68 #---------------------------------------------------------------------------
69 69 # Basic project information
70 70 #---------------------------------------------------------------------------
71 71
72 72 # release.py contains version, authors, license, url, keywords, etc.
73 73 execfile(pjoin(repo_root, 'IPython','core','release.py'), globals())
74 74
75 75 # Create a dict with the basic information
76 76 # This dict is eventually passed to setup after additional keys are added.
77 77 setup_args = dict(
78 78 name = name,
79 79 version = version,
80 80 description = description,
81 81 long_description = long_description,
82 82 author = author,
83 83 author_email = author_email,
84 84 url = url,
85 85 download_url = download_url,
86 86 license = license,
87 87 platforms = platforms,
88 88 keywords = keywords,
89 89 classifiers = classifiers,
90 90 cmdclass = {'install_data': install_data_ext},
91 91 )
92 92
93 93
94 94 #---------------------------------------------------------------------------
95 95 # Find packages
96 96 #---------------------------------------------------------------------------
97 97
98 98 def find_packages():
99 99 """
100 100 Find all of IPython's packages.
101 101 """
102 102 excludes = ['deathrow', 'quarantine']
103 103 packages = []
104 104 for dir,subdirs,files in os.walk('IPython'):
105 105 package = dir.replace(os.path.sep, '.')
106 106 if any(package.startswith('IPython.'+exc) for exc in excludes):
107 107 # package is to be excluded (e.g. deathrow)
108 108 continue
109 109 if '__init__.py' not in files:
110 110 # not a package
111 111 continue
112 112 packages.append(package)
113 113 return packages
114 114
115 115 #---------------------------------------------------------------------------
116 116 # Find package data
117 117 #---------------------------------------------------------------------------
118 118
119 119 def find_package_data():
120 120 """
121 121 Find IPython's package_data.
122 122 """
123 123 # This is not enough for these things to appear in an sdist.
124 124 # We need to muck with the MANIFEST to get this to work
125 125
126 126 # exclude components and less from the walk;
127 127 # we will build the components separately
128 128 excludes = [
129 129 pjoin('static', 'components'),
130 130 pjoin('static', '*', 'less'),
131 131 ]
132 132
133 133 # walk notebook resources:
134 134 cwd = os.getcwd()
135 135 os.chdir(os.path.join('IPython', 'html'))
136 136 static_data = []
137 137 for parent, dirs, files in os.walk('static'):
138 138 if any(fnmatch(parent, pat) for pat in excludes):
139 139 # prevent descending into subdirs
140 140 dirs[:] = []
141 141 continue
142 142 for f in files:
143 143 static_data.append(pjoin(parent, f))
144 144
145 145 components = pjoin("static", "components")
146 146 # select the components we actually need to install
147 147 # (there are lots of resources we bundle for sdist-reasons that we don't actually use)
148 148 static_data.extend([
149 149 pjoin(components, "backbone", "backbone-min.js"),
150 150 pjoin(components, "bootstrap", "js", "bootstrap.min.js"),
151 151 pjoin(components, "bootstrap-tour", "build", "css", "bootstrap-tour.min.css"),
152 152 pjoin(components, "bootstrap-tour", "build", "js", "bootstrap-tour.min.js"),
153 153 pjoin(components, "es6-promise", "*.js"),
154 154 pjoin(components, "font-awesome", "fonts", "*.*"),
155 155 pjoin(components, "google-caja", "html-css-sanitizer-minified.js"),
156 pjoin(components, "highlight.js", "build", "highlight.pack.js"),
157 156 pjoin(components, "jquery", "jquery.min.js"),
158 157 pjoin(components, "jquery-ui", "ui", "minified", "jquery-ui.min.js"),
159 158 pjoin(components, "jquery-ui", "themes", "smoothness", "jquery-ui.min.css"),
160 159 pjoin(components, "jquery-ui", "themes", "smoothness", "images", "*"),
161 160 pjoin(components, "marked", "lib", "marked.js"),
162 161 pjoin(components, "requirejs", "require.js"),
163 162 pjoin(components, "underscore", "underscore-min.js"),
164 163 pjoin(components, "moment", "moment.js"),
165 164 pjoin(components, "moment", "min", "moment.min.js"),
166 165 pjoin(components, "term.js", "src", "term.js"),
167 166 pjoin(components, "text-encoding", "lib", "encoding.js"),
168 167 ])
169 168
170 169 # Ship all of Codemirror's CSS and JS
171 170 for parent, dirs, files in os.walk(pjoin(components, 'codemirror')):
172 171 for f in files:
173 172 if f.endswith(('.js', '.css')):
174 173 static_data.append(pjoin(parent, f))
175 174
176 175 os.chdir(os.path.join('tests',))
177 176 js_tests = glob('*.js') + glob('*/*.js')
178 177
179 178 os.chdir(os.path.join(cwd, 'IPython', 'nbconvert'))
180 179 nbconvert_templates = [os.path.join(dirpath, '*.*')
181 180 for dirpath, _, _ in os.walk('templates')]
182 181
183 182 os.chdir(cwd)
184 183
185 184 package_data = {
186 185 'IPython.config.profile' : ['README*', '*/*.py'],
187 186 'IPython.core.tests' : ['*.png', '*.jpg'],
188 187 'IPython.lib.tests' : ['*.wav'],
189 188 'IPython.testing.plugin' : ['*.txt'],
190 189 'IPython.html' : ['templates/*'] + static_data,
191 190 'IPython.html.tests' : js_tests,
192 191 'IPython.qt.console' : ['resources/icon/*.svg'],
193 192 'IPython.nbconvert' : nbconvert_templates +
194 193 [
195 194 'tests/files/*.*',
196 195 'exporters/tests/files/*.*',
197 196 'preprocessors/tests/files/*.*',
198 197 ],
199 198 'IPython.nbconvert.filters' : ['marked.js'],
200 199 'IPython.nbformat' : [
201 200 'tests/*.ipynb',
202 201 'v3/nbformat.v3.schema.json',
203 202 'v4/nbformat.v4.schema.json',
204 203 ]
205 204 }
206 205
207 206 return package_data
208 207
209 208
210 209 def check_package_data(package_data):
211 210 """verify that package_data globs make sense"""
212 211 print("checking package data")
213 212 for pkg, data in package_data.items():
214 213 pkg_root = pjoin(*pkg.split('.'))
215 214 for d in data:
216 215 path = pjoin(pkg_root, d)
217 216 if '*' in path:
218 217 assert len(glob(path)) > 0, "No files match pattern %s" % path
219 218 else:
220 219 assert os.path.exists(path), "Missing package data: %s" % path
221 220
222 221
223 222 def check_package_data_first(command):
224 223 """decorator for checking package_data before running a given command
225 224
226 225 Probably only needs to wrap build_py
227 226 """
228 227 class DecoratedCommand(command):
229 228 def run(self):
230 229 check_package_data(self.package_data)
231 230 command.run(self)
232 231 return DecoratedCommand
233 232
234 233
235 234 #---------------------------------------------------------------------------
236 235 # Find data files
237 236 #---------------------------------------------------------------------------
238 237
239 238 def make_dir_struct(tag,base,out_base):
240 239 """Make the directory structure of all files below a starting dir.
241 240
242 241 This is just a convenience routine to help build a nested directory
243 242 hierarchy because distutils is too stupid to do this by itself.
244 243
245 244 XXX - this needs a proper docstring!
246 245 """
247 246
248 247 # we'll use these a lot below
249 248 lbase = len(base)
250 249 pathsep = os.path.sep
251 250 lpathsep = len(pathsep)
252 251
253 252 out = []
254 253 for (dirpath,dirnames,filenames) in os.walk(base):
255 254 # we need to strip out the dirpath from the base to map it to the
256 255 # output (installation) path. This requires possibly stripping the
257 256 # path separator, because otherwise pjoin will not work correctly
258 257 # (pjoin('foo/','/bar') returns '/bar').
259 258
260 259 dp_eff = dirpath[lbase:]
261 260 if dp_eff.startswith(pathsep):
262 261 dp_eff = dp_eff[lpathsep:]
263 262 # The output path must be anchored at the out_base marker
264 263 out_path = pjoin(out_base,dp_eff)
265 264 # Now we can generate the final filenames. Since os.walk only produces
266 265 # filenames, we must join back with the dirpath to get full valid file
267 266 # paths:
268 267 pfiles = [pjoin(dirpath,f) for f in filenames]
269 268 # Finally, generate the entry we need, which is a pari of (output
270 269 # path, files) for use as a data_files parameter in install_data.
271 270 out.append((out_path, pfiles))
272 271
273 272 return out
274 273
275 274
276 275 def find_data_files():
277 276 """
278 277 Find IPython's data_files.
279 278
280 279 Just man pages at this point.
281 280 """
282 281
283 282 manpagebase = pjoin('share', 'man', 'man1')
284 283
285 284 # Simple file lists can be made by hand
286 285 manpages = [f for f in glob(pjoin('docs','man','*.1.gz')) if isfile(f)]
287 286 if not manpages:
288 287 # When running from a source tree, the manpages aren't gzipped
289 288 manpages = [f for f in glob(pjoin('docs','man','*.1')) if isfile(f)]
290 289
291 290 # And assemble the entire output list
292 291 data_files = [ (manpagebase, manpages) ]
293 292
294 293 return data_files
295 294
296 295
297 296 def make_man_update_target(manpage):
298 297 """Return a target_update-compliant tuple for the given manpage.
299 298
300 299 Parameters
301 300 ----------
302 301 manpage : string
303 302 Name of the manpage, must include the section number (trailing number).
304 303
305 304 Example
306 305 -------
307 306
308 307 >>> make_man_update_target('ipython.1') #doctest: +NORMALIZE_WHITESPACE
309 308 ('docs/man/ipython.1.gz',
310 309 ['docs/man/ipython.1'],
311 310 'cd docs/man && gzip -9c ipython.1 > ipython.1.gz')
312 311 """
313 312 man_dir = pjoin('docs', 'man')
314 313 manpage_gz = manpage + '.gz'
315 314 manpath = pjoin(man_dir, manpage)
316 315 manpath_gz = pjoin(man_dir, manpage_gz)
317 316 gz_cmd = ( "cd %(man_dir)s && gzip -9c %(manpage)s > %(manpage_gz)s" %
318 317 locals() )
319 318 return (manpath_gz, [manpath], gz_cmd)
320 319
321 320 # The two functions below are copied from IPython.utils.path, so we don't need
322 321 # to import IPython during setup, which fails on Python 3.
323 322
324 323 def target_outdated(target,deps):
325 324 """Determine whether a target is out of date.
326 325
327 326 target_outdated(target,deps) -> 1/0
328 327
329 328 deps: list of filenames which MUST exist.
330 329 target: single filename which may or may not exist.
331 330
332 331 If target doesn't exist or is older than any file listed in deps, return
333 332 true, otherwise return false.
334 333 """
335 334 try:
336 335 target_time = os.path.getmtime(target)
337 336 except os.error:
338 337 return 1
339 338 for dep in deps:
340 339 dep_time = os.path.getmtime(dep)
341 340 if dep_time > target_time:
342 341 #print "For target",target,"Dep failed:",dep # dbg
343 342 #print "times (dep,tar):",dep_time,target_time # dbg
344 343 return 1
345 344 return 0
346 345
347 346
348 347 def target_update(target,deps,cmd):
349 348 """Update a target with a given command given a list of dependencies.
350 349
351 350 target_update(target,deps,cmd) -> runs cmd if target is outdated.
352 351
353 352 This is just a wrapper around target_outdated() which calls the given
354 353 command if target is outdated."""
355 354
356 355 if target_outdated(target,deps):
357 356 os.system(cmd)
358 357
359 358 #---------------------------------------------------------------------------
360 359 # Find scripts
361 360 #---------------------------------------------------------------------------
362 361
363 362 def find_entry_points():
364 363 """Defines the command line entry points for IPython
365 364
366 365 This always uses setuptools-style entry points. When setuptools is not in
367 366 use, our own build_scripts_entrypt class below parses these and builds
368 367 command line scripts.
369 368
370 369 Each of our entry points gets both a plain name, e.g. ipython, and one
371 370 suffixed with the Python major version number, e.g. ipython3.
372 371 """
373 372 ep = [
374 373 'ipython%s = IPython:start_ipython',
375 374 'ipcontroller%s = IPython.parallel.apps.ipcontrollerapp:launch_new_instance',
376 375 'ipengine%s = IPython.parallel.apps.ipengineapp:launch_new_instance',
377 376 'ipcluster%s = IPython.parallel.apps.ipclusterapp:launch_new_instance',
378 377 'iptest%s = IPython.testing.iptestcontroller:main',
379 378 ]
380 379 suffix = str(sys.version_info[0])
381 380 return [e % '' for e in ep] + [e % suffix for e in ep]
382 381
383 382 script_src = """#!{executable}
384 383 # This script was automatically generated by setup.py
385 384 if __name__ == '__main__':
386 385 from {mod} import {func}
387 386 {func}()
388 387 """
389 388
390 389 class build_scripts_entrypt(build_scripts):
391 390 """Build the command line scripts
392 391
393 392 Parse setuptools style entry points and write simple scripts to run the
394 393 target functions.
395 394
396 395 On Windows, this also creates .cmd wrappers for the scripts so that you can
397 396 easily launch them from a command line.
398 397 """
399 398 def run(self):
400 399 self.mkpath(self.build_dir)
401 400 outfiles = []
402 401 for script in find_entry_points():
403 402 name, entrypt = script.split('=')
404 403 name = name.strip()
405 404 entrypt = entrypt.strip()
406 405 outfile = os.path.join(self.build_dir, name)
407 406 outfiles.append(outfile)
408 407 print('Writing script to', outfile)
409 408
410 409 mod, func = entrypt.split(':')
411 410 with open(outfile, 'w') as f:
412 411 f.write(script_src.format(executable=sys.executable,
413 412 mod=mod, func=func))
414 413
415 414 if sys.platform == 'win32':
416 415 # Write .cmd wrappers for Windows so 'ipython' etc. work at the
417 416 # command line
418 417 cmd_file = os.path.join(self.build_dir, name + '.cmd')
419 418 cmd = '@"{python}" "%~dp0\{script}" %*\r\n'.format(
420 419 python=sys.executable, script=name)
421 420 log.info("Writing %s wrapper script" % cmd_file)
422 421 with open(cmd_file, 'w') as f:
423 422 f.write(cmd)
424 423
425 424 return outfiles, outfiles
426 425
427 426 class install_lib_symlink(Command):
428 427 user_options = [
429 428 ('install-dir=', 'd', "directory to install to"),
430 429 ]
431 430
432 431 def initialize_options(self):
433 432 self.install_dir = None
434 433
435 434 def finalize_options(self):
436 435 self.set_undefined_options('symlink',
437 436 ('install_lib', 'install_dir'),
438 437 )
439 438
440 439 def run(self):
441 440 if sys.platform == 'win32':
442 441 raise Exception("This doesn't work on Windows.")
443 442 pkg = os.path.join(os.getcwd(), 'IPython')
444 443 dest = os.path.join(self.install_dir, 'IPython')
445 444 if os.path.islink(dest):
446 445 print('removing existing symlink at %s' % dest)
447 446 os.unlink(dest)
448 447 print('symlinking %s -> %s' % (pkg, dest))
449 448 os.symlink(pkg, dest)
450 449
451 450 class unsymlink(install):
452 451 def run(self):
453 452 dest = os.path.join(self.install_lib, 'IPython')
454 453 if os.path.islink(dest):
455 454 print('removing symlink at %s' % dest)
456 455 os.unlink(dest)
457 456 else:
458 457 print('No symlink exists at %s' % dest)
459 458
460 459 class install_symlinked(install):
461 460 def run(self):
462 461 if sys.platform == 'win32':
463 462 raise Exception("This doesn't work on Windows.")
464 463
465 464 # Run all sub-commands (at least those that need to be run)
466 465 for cmd_name in self.get_sub_commands():
467 466 self.run_command(cmd_name)
468 467
469 468 # 'sub_commands': a list of commands this command might have to run to
470 469 # get its work done. See cmd.py for more info.
471 470 sub_commands = [('install_lib_symlink', lambda self:True),
472 471 ('install_scripts_sym', lambda self:True),
473 472 ]
474 473
475 474 class install_scripts_for_symlink(install_scripts):
476 475 """Redefined to get options from 'symlink' instead of 'install'.
477 476
478 477 I love distutils almost as much as I love setuptools.
479 478 """
480 479 def finalize_options(self):
481 480 self.set_undefined_options('build', ('build_scripts', 'build_dir'))
482 481 self.set_undefined_options('symlink',
483 482 ('install_scripts', 'install_dir'),
484 483 ('force', 'force'),
485 484 ('skip_build', 'skip_build'),
486 485 )
487 486
488 487 #---------------------------------------------------------------------------
489 488 # Verify all dependencies
490 489 #---------------------------------------------------------------------------
491 490
492 491 def check_for_dependencies():
493 492 """Check for IPython's dependencies.
494 493
495 494 This function should NOT be called if running under setuptools!
496 495 """
497 496 from setupext.setupext import (
498 497 print_line, print_raw, print_status,
499 498 check_for_sphinx, check_for_pygments,
500 499 check_for_nose, check_for_pexpect,
501 500 check_for_pyzmq, check_for_readline,
502 501 check_for_jinja2, check_for_tornado
503 502 )
504 503 print_line()
505 504 print_raw("BUILDING IPYTHON")
506 505 print_status('python', sys.version)
507 506 print_status('platform', sys.platform)
508 507 if sys.platform == 'win32':
509 508 print_status('Windows version', sys.getwindowsversion())
510 509
511 510 print_raw("")
512 511 print_raw("OPTIONAL DEPENDENCIES")
513 512
514 513 check_for_sphinx()
515 514 check_for_pygments()
516 515 check_for_nose()
517 516 if os.name == 'posix':
518 517 check_for_pexpect()
519 518 check_for_pyzmq()
520 519 check_for_tornado()
521 520 check_for_readline()
522 521 check_for_jinja2()
523 522
524 523 #---------------------------------------------------------------------------
525 524 # VCS related
526 525 #---------------------------------------------------------------------------
527 526
528 527 # utils.submodule has checks for submodule status
529 528 execfile(pjoin('IPython','utils','submodule.py'), globals())
530 529
531 530 class UpdateSubmodules(Command):
532 531 """Update git submodules
533 532
534 533 IPython's external javascript dependencies live in a separate repo.
535 534 """
536 535 description = "Update git submodules"
537 536 user_options = []
538 537
539 538 def initialize_options(self):
540 539 pass
541 540
542 541 def finalize_options(self):
543 542 pass
544 543
545 544 def run(self):
546 545 failure = False
547 546 try:
548 547 self.spawn('git submodule init'.split())
549 548 self.spawn('git submodule update --recursive'.split())
550 549 except Exception as e:
551 550 failure = e
552 551 print(e)
553 552
554 553 if not check_submodule_status(repo_root) == 'clean':
555 554 print("submodules could not be checked out")
556 555 sys.exit(1)
557 556
558 557
559 558 def git_prebuild(pkg_dir, build_cmd=build_py):
560 559 """Return extended build or sdist command class for recording commit
561 560
562 561 records git commit in IPython.utils._sysinfo.commit
563 562
564 563 for use in IPython.utils.sysinfo.sys_info() calls after installation.
565 564
566 565 Also ensures that submodules exist prior to running
567 566 """
568 567
569 568 class MyBuildPy(build_cmd):
570 569 ''' Subclass to write commit data into installation tree '''
571 570 def run(self):
572 571 build_cmd.run(self)
573 572 # this one will only fire for build commands
574 573 if hasattr(self, 'build_lib'):
575 574 self._record_commit(self.build_lib)
576 575
577 576 def make_release_tree(self, base_dir, files):
578 577 # this one will fire for sdist
579 578 build_cmd.make_release_tree(self, base_dir, files)
580 579 self._record_commit(base_dir)
581 580
582 581 def _record_commit(self, base_dir):
583 582 import subprocess
584 583 proc = subprocess.Popen('git rev-parse --short HEAD',
585 584 stdout=subprocess.PIPE,
586 585 stderr=subprocess.PIPE,
587 586 shell=True)
588 587 repo_commit, _ = proc.communicate()
589 588 repo_commit = repo_commit.strip().decode("ascii")
590 589
591 590 out_pth = pjoin(base_dir, pkg_dir, 'utils', '_sysinfo.py')
592 591 if os.path.isfile(out_pth) and not repo_commit:
593 592 # nothing to write, don't clobber
594 593 return
595 594
596 595 print("writing git commit '%s' to %s" % (repo_commit, out_pth))
597 596
598 597 # remove to avoid overwriting original via hard link
599 598 try:
600 599 os.remove(out_pth)
601 600 except (IOError, OSError):
602 601 pass
603 602 with open(out_pth, 'w') as out_file:
604 603 out_file.writelines([
605 604 '# GENERATED BY setup.py\n',
606 605 'commit = u"%s"\n' % repo_commit,
607 606 ])
608 607 return require_submodules(MyBuildPy)
609 608
610 609
611 610 def require_submodules(command):
612 611 """decorator for instructing a command to check for submodules before running"""
613 612 class DecoratedCommand(command):
614 613 def run(self):
615 614 if not check_submodule_status(repo_root) == 'clean':
616 615 print("submodules missing! Run `setup.py submodule` and try again")
617 616 sys.exit(1)
618 617 command.run(self)
619 618 return DecoratedCommand
620 619
621 620 #---------------------------------------------------------------------------
622 621 # bdist related
623 622 #---------------------------------------------------------------------------
624 623
625 624 def get_bdist_wheel():
626 625 """Construct bdist_wheel command for building wheels
627 626
628 627 Constructs py2-none-any tag, instead of py2.7-none-any
629 628 """
630 629 class RequiresWheel(Command):
631 630 description = "Dummy command for missing bdist_wheel"
632 631 user_options = []
633 632
634 633 def initialize_options(self):
635 634 pass
636 635
637 636 def finalize_options(self):
638 637 pass
639 638
640 639 def run(self):
641 640 print("bdist_wheel requires the wheel package")
642 641 sys.exit(1)
643 642
644 643 if 'setuptools' not in sys.modules:
645 644 return RequiresWheel
646 645 else:
647 646 try:
648 647 from wheel.bdist_wheel import bdist_wheel, read_pkg_info, write_pkg_info
649 648 except ImportError:
650 649 return RequiresWheel
651 650
652 651 class bdist_wheel_tag(bdist_wheel):
653 652
654 653 def add_requirements(self, metadata_path):
655 654 """transform platform-dependent requirements"""
656 655 pkg_info = read_pkg_info(metadata_path)
657 656 # pkg_info is an email.Message object (?!)
658 657 # we have to remove the unconditional 'readline' and/or 'pyreadline' entries
659 658 # and transform them to conditionals
660 659 requires = pkg_info.get_all('Requires-Dist')
661 660 del pkg_info['Requires-Dist']
662 661 def _remove_startswith(lis, prefix):
663 662 """like list.remove, but with startswith instead of =="""
664 663 found = False
665 664 for idx, item in enumerate(lis):
666 665 if item.startswith(prefix):
667 666 found = True
668 667 break
669 668 if found:
670 669 lis.pop(idx)
671 670
672 671 for pkg in ("gnureadline", "pyreadline", "mock"):
673 672 _remove_startswith(requires, pkg)
674 673 requires.append("gnureadline; sys.platform == 'darwin' and platform.python_implementation == 'CPython'")
675 674 requires.append("pyreadline (>=2.0); extra == 'terminal' and sys.platform == 'win32' and platform.python_implementation == 'CPython'")
676 675 requires.append("pyreadline (>=2.0); extra == 'all' and sys.platform == 'win32' and platform.python_implementation == 'CPython'")
677 676 requires.append("mock; extra == 'test' and python_version < '3.3'")
678 677 for r in requires:
679 678 pkg_info['Requires-Dist'] = r
680 679 write_pkg_info(metadata_path, pkg_info)
681 680
682 681 return bdist_wheel_tag
683 682
684 683 #---------------------------------------------------------------------------
685 684 # Notebook related
686 685 #---------------------------------------------------------------------------
687 686
688 687 class CompileCSS(Command):
689 688 """Recompile Notebook CSS
690 689
691 690 Regenerate the compiled CSS from LESS sources.
692 691
693 692 Requires various dev dependencies, such as invoke and lessc.
694 693 """
695 694 description = "Recompile Notebook CSS"
696 695 user_options = [
697 696 ('minify', 'x', "minify CSS"),
698 697 ('force', 'f', "force recompilation of CSS"),
699 698 ]
700 699
701 700 def initialize_options(self):
702 701 self.minify = False
703 702 self.force = False
704 703
705 704 def finalize_options(self):
706 705 self.minify = bool(self.minify)
707 706 self.force = bool(self.force)
708 707
709 708 def run(self):
710 709 cmd = ['invoke', 'css']
711 710 if self.minify:
712 711 cmd.append('--minify')
713 712 if self.force:
714 713 cmd.append('--force')
715 714 check_call(cmd, cwd=pjoin(repo_root, "IPython", "html"))
716 715
717 716
718 717 class JavascriptVersion(Command):
719 718 """write the javascript version to notebook javascript"""
720 719 description = "Write IPython version to javascript"
721 720 user_options = []
722 721
723 722 def initialize_options(self):
724 723 pass
725 724
726 725 def finalize_options(self):
727 726 pass
728 727
729 728 def run(self):
730 729 nsfile = pjoin(repo_root, "IPython", "html", "static", "base", "js", "namespace.js")
731 730 with open(nsfile) as f:
732 731 lines = f.readlines()
733 732 with open(nsfile, 'w') as f:
734 733 for line in lines:
735 734 if line.startswith("IPython.version"):
736 735 line = 'IPython.version = "{0}";\n'.format(version)
737 736 f.write(line)
738 737
739 738
740 739 def css_js_prerelease(command, strict=True):
741 740 """decorator for building js/minified css prior to a release"""
742 741 class DecoratedCommand(command):
743 742 def run(self):
744 743 self.distribution.run_command('jsversion')
745 744 css = self.distribution.get_command_obj('css')
746 745 css.minify = True
747 746 try:
748 747 self.distribution.run_command('css')
749 748 except Exception as e:
750 749 if strict:
751 750 raise
752 751 else:
753 752 log.warn("Failed to build css sourcemaps: %s" % e)
754 753 command.run(self)
755 754 return DecoratedCommand
General Comments 0
You need to be logged in to leave comments. Login now