##// END OF EJS Templates
Merge pull request #6303 from minrk/nbformat-error...
Thomas Kluyver -
r17644:6b260753 merge
parent child Browse files
Show More
@@ -1,557 +1,563
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 ], function(IPython, $){
8 8 "use strict";
9 9
10 10 IPython.load_extensions = function () {
11 11 // load one or more IPython notebook extensions with requirejs
12 12
13 13 var extensions = [];
14 14 var extension_names = arguments;
15 15 for (var i = 0; i < extension_names.length; i++) {
16 16 extensions.push("nbextensions/" + arguments[i]);
17 17 }
18 18
19 19 require(extensions,
20 20 function () {
21 21 for (var i = 0; i < arguments.length; i++) {
22 22 var ext = arguments[i];
23 23 var ext_name = extension_names[i];
24 24 // success callback
25 25 console.log("Loaded extension: " + ext_name);
26 26 if (ext && ext.load_ipython_extension !== undefined) {
27 27 ext.load_ipython_extension();
28 28 }
29 29 }
30 30 },
31 31 function (err) {
32 32 // failure callback
33 33 console.log("Failed to load extension(s):", err.requireModules, err);
34 34 }
35 35 );
36 36 };
37 37
38 38 //============================================================================
39 39 // Cross-browser RegEx Split
40 40 //============================================================================
41 41
42 42 // This code has been MODIFIED from the code licensed below to not replace the
43 43 // default browser split. The license is reproduced here.
44 44
45 45 // see http://blog.stevenlevithan.com/archives/cross-browser-split for more info:
46 46 /*!
47 47 * Cross-Browser Split 1.1.1
48 48 * Copyright 2007-2012 Steven Levithan <stevenlevithan.com>
49 49 * Available under the MIT License
50 50 * ECMAScript compliant, uniform cross-browser split method
51 51 */
52 52
53 53 /**
54 54 * Splits a string into an array of strings using a regex or string
55 55 * separator. Matches of the separator are not included in the result array.
56 56 * However, if `separator` is a regex that contains capturing groups,
57 57 * backreferences are spliced into the result each time `separator` is
58 58 * matched. Fixes browser bugs compared to the native
59 59 * `String.prototype.split` and can be used reliably cross-browser.
60 60 * @param {String} str String to split.
61 61 * @param {RegExp|String} separator Regex or string to use for separating
62 62 * the string.
63 63 * @param {Number} [limit] Maximum number of items to include in the result
64 64 * array.
65 65 * @returns {Array} Array of substrings.
66 66 * @example
67 67 *
68 68 * // Basic use
69 69 * regex_split('a b c d', ' ');
70 70 * // -> ['a', 'b', 'c', 'd']
71 71 *
72 72 * // With limit
73 73 * regex_split('a b c d', ' ', 2);
74 74 * // -> ['a', 'b']
75 75 *
76 76 * // Backreferences in result array
77 77 * regex_split('..word1 word2..', /([a-z]+)(\d+)/i);
78 78 * // -> ['..', 'word', '1', ' ', 'word', '2', '..']
79 79 */
80 80 var regex_split = function (str, separator, limit) {
81 81 // If `separator` is not a regex, use `split`
82 82 if (Object.prototype.toString.call(separator) !== "[object RegExp]") {
83 83 return split.call(str, separator, limit);
84 84 }
85 85 var output = [],
86 86 flags = (separator.ignoreCase ? "i" : "") +
87 87 (separator.multiline ? "m" : "") +
88 88 (separator.extended ? "x" : "") + // Proposed for ES6
89 89 (separator.sticky ? "y" : ""), // Firefox 3+
90 90 lastLastIndex = 0,
91 91 // Make `global` and avoid `lastIndex` issues by working with a copy
92 92 separator = new RegExp(separator.source, flags + "g"),
93 93 separator2, match, lastIndex, lastLength;
94 94 str += ""; // Type-convert
95 95
96 96 var compliantExecNpcg = typeof(/()??/.exec("")[1]) === "undefined";
97 97 if (!compliantExecNpcg) {
98 98 // Doesn't need flags gy, but they don't hurt
99 99 separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags);
100 100 }
101 101 /* Values for `limit`, per the spec:
102 102 * If undefined: 4294967295 // Math.pow(2, 32) - 1
103 103 * If 0, Infinity, or NaN: 0
104 104 * If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296;
105 105 * If negative number: 4294967296 - Math.floor(Math.abs(limit))
106 106 * If other: Type-convert, then use the above rules
107 107 */
108 108 limit = typeof(limit) === "undefined" ?
109 109 -1 >>> 0 : // Math.pow(2, 32) - 1
110 110 limit >>> 0; // ToUint32(limit)
111 111 while (match = separator.exec(str)) {
112 112 // `separator.lastIndex` is not reliable cross-browser
113 113 lastIndex = match.index + match[0].length;
114 114 if (lastIndex > lastLastIndex) {
115 115 output.push(str.slice(lastLastIndex, match.index));
116 116 // Fix browsers whose `exec` methods don't consistently return `undefined` for
117 117 // nonparticipating capturing groups
118 118 if (!compliantExecNpcg && match.length > 1) {
119 119 match[0].replace(separator2, function () {
120 120 for (var i = 1; i < arguments.length - 2; i++) {
121 121 if (typeof(arguments[i]) === "undefined") {
122 122 match[i] = undefined;
123 123 }
124 124 }
125 125 });
126 126 }
127 127 if (match.length > 1 && match.index < str.length) {
128 128 Array.prototype.push.apply(output, match.slice(1));
129 129 }
130 130 lastLength = match[0].length;
131 131 lastLastIndex = lastIndex;
132 132 if (output.length >= limit) {
133 133 break;
134 134 }
135 135 }
136 136 if (separator.lastIndex === match.index) {
137 137 separator.lastIndex++; // Avoid an infinite loop
138 138 }
139 139 }
140 140 if (lastLastIndex === str.length) {
141 141 if (lastLength || !separator.test("")) {
142 142 output.push("");
143 143 }
144 144 } else {
145 145 output.push(str.slice(lastLastIndex));
146 146 }
147 147 return output.length > limit ? output.slice(0, limit) : output;
148 148 };
149 149
150 150 //============================================================================
151 151 // End contributed Cross-browser RegEx Split
152 152 //============================================================================
153 153
154 154
155 155 var uuid = function () {
156 156 // http://www.ietf.org/rfc/rfc4122.txt
157 157 var s = [];
158 158 var hexDigits = "0123456789ABCDEF";
159 159 for (var i = 0; i < 32; i++) {
160 160 s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
161 161 }
162 162 s[12] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
163 163 s[16] = hexDigits.substr((s[16] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
164 164
165 165 var uuid = s.join("");
166 166 return uuid;
167 167 };
168 168
169 169
170 170 //Fix raw text to parse correctly in crazy XML
171 171 function xmlencode(string) {
172 172 return string.replace(/\&/g,'&'+'amp;')
173 173 .replace(/</g,'&'+'lt;')
174 174 .replace(/>/g,'&'+'gt;')
175 175 .replace(/\'/g,'&'+'apos;')
176 176 .replace(/\"/g,'&'+'quot;')
177 177 .replace(/`/g,'&'+'#96;');
178 178 }
179 179
180 180
181 181 //Map from terminal commands to CSS classes
182 182 var ansi_colormap = {
183 183 "01":"ansibold",
184 184
185 185 "30":"ansiblack",
186 186 "31":"ansired",
187 187 "32":"ansigreen",
188 188 "33":"ansiyellow",
189 189 "34":"ansiblue",
190 190 "35":"ansipurple",
191 191 "36":"ansicyan",
192 192 "37":"ansigray",
193 193
194 194 "40":"ansibgblack",
195 195 "41":"ansibgred",
196 196 "42":"ansibggreen",
197 197 "43":"ansibgyellow",
198 198 "44":"ansibgblue",
199 199 "45":"ansibgpurple",
200 200 "46":"ansibgcyan",
201 201 "47":"ansibggray"
202 202 };
203 203
204 204 function _process_numbers(attrs, numbers) {
205 205 // process ansi escapes
206 206 var n = numbers.shift();
207 207 if (ansi_colormap[n]) {
208 208 if ( ! attrs["class"] ) {
209 209 attrs["class"] = ansi_colormap[n];
210 210 } else {
211 211 attrs["class"] += " " + ansi_colormap[n];
212 212 }
213 213 } else if (n == "38" || n == "48") {
214 214 // VT100 256 color or 24 bit RGB
215 215 if (numbers.length < 2) {
216 216 console.log("Not enough fields for VT100 color", numbers);
217 217 return;
218 218 }
219 219
220 220 var index_or_rgb = numbers.shift();
221 221 var r,g,b;
222 222 if (index_or_rgb == "5") {
223 223 // 256 color
224 224 var idx = parseInt(numbers.shift());
225 225 if (idx < 16) {
226 226 // indexed ANSI
227 227 // ignore bright / non-bright distinction
228 228 idx = idx % 8;
229 229 var ansiclass = ansi_colormap[n[0] + (idx % 8).toString()];
230 230 if ( ! attrs["class"] ) {
231 231 attrs["class"] = ansiclass;
232 232 } else {
233 233 attrs["class"] += " " + ansiclass;
234 234 }
235 235 return;
236 236 } else if (idx < 232) {
237 237 // 216 color 6x6x6 RGB
238 238 idx = idx - 16;
239 239 b = idx % 6;
240 240 g = Math.floor(idx / 6) % 6;
241 241 r = Math.floor(idx / 36) % 6;
242 242 // convert to rgb
243 243 r = (r * 51);
244 244 g = (g * 51);
245 245 b = (b * 51);
246 246 } else {
247 247 // grayscale
248 248 idx = idx - 231;
249 249 // it's 1-24 and should *not* include black or white,
250 250 // so a 26 point scale
251 251 r = g = b = Math.floor(idx * 256 / 26);
252 252 }
253 253 } else if (index_or_rgb == "2") {
254 254 // Simple 24 bit RGB
255 255 if (numbers.length > 3) {
256 256 console.log("Not enough fields for RGB", numbers);
257 257 return;
258 258 }
259 259 r = numbers.shift();
260 260 g = numbers.shift();
261 261 b = numbers.shift();
262 262 } else {
263 263 console.log("unrecognized control", numbers);
264 264 return;
265 265 }
266 266 if (r !== undefined) {
267 267 // apply the rgb color
268 268 var line;
269 269 if (n == "38") {
270 270 line = "color: ";
271 271 } else {
272 272 line = "background-color: ";
273 273 }
274 274 line = line + "rgb(" + r + "," + g + "," + b + ");"
275 275 if ( !attrs["style"] ) {
276 276 attrs["style"] = line;
277 277 } else {
278 278 attrs["style"] += " " + line;
279 279 }
280 280 }
281 281 }
282 282 }
283 283
284 284 function ansispan(str) {
285 285 // ansispan function adapted from github.com/mmalecki/ansispan (MIT License)
286 286 // regular ansi escapes (using the table above)
287 287 return str.replace(/\033\[(0?[01]|22|39)?([;\d]+)?m/g, function(match, prefix, pattern) {
288 288 if (!pattern) {
289 289 // [(01|22|39|)m close spans
290 290 return "</span>";
291 291 }
292 292 // consume sequence of color escapes
293 293 var numbers = pattern.match(/\d+/g);
294 294 var attrs = {};
295 295 while (numbers.length > 0) {
296 296 _process_numbers(attrs, numbers);
297 297 }
298 298
299 299 var span = "<span ";
300 300 for (var attr in attrs) {
301 301 var value = attrs[attr];
302 302 span = span + " " + attr + '="' + attrs[attr] + '"';
303 303 }
304 304 return span + ">";
305 305 });
306 306 };
307 307
308 308 // Transform ANSI color escape codes into HTML <span> tags with css
309 309 // classes listed in the above ansi_colormap object. The actual color used
310 310 // are set in the css file.
311 311 function fixConsole(txt) {
312 312 txt = xmlencode(txt);
313 313 var re = /\033\[([\dA-Fa-f;]*?)m/;
314 314 var opened = false;
315 315 var cmds = [];
316 316 var opener = "";
317 317 var closer = "";
318 318
319 319 // Strip all ANSI codes that are not color related. Matches
320 320 // all ANSI codes that do not end with "m".
321 321 var ignored_re = /(?=(\033\[[\d;=]*[a-ln-zA-Z]{1}))\1(?!m)/g;
322 322 txt = txt.replace(ignored_re, "");
323 323
324 324 // color ansi codes
325 325 txt = ansispan(txt);
326 326 return txt;
327 327 }
328 328
329 329 // Remove chunks that should be overridden by the effect of
330 330 // carriage return characters
331 331 function fixCarriageReturn(txt) {
332 332 var tmp = txt;
333 333 do {
334 334 txt = tmp;
335 335 tmp = txt.replace(/\r+\n/gm, '\n'); // \r followed by \n --> newline
336 336 tmp = tmp.replace(/^.*\r+/gm, ''); // Other \r --> clear line
337 337 } while (tmp.length < txt.length);
338 338 return txt;
339 339 }
340 340
341 341 // Locate any URLs and convert them to a anchor tag
342 342 function autoLinkUrls(txt) {
343 343 return txt.replace(/(^|\s)(https?|ftp)(:[^'">\s]+)/gi,
344 344 "$1<a target=\"_blank\" href=\"$2$3\">$2$3</a>");
345 345 }
346 346
347 347 var points_to_pixels = function (points) {
348 348 // A reasonably good way of converting between points and pixels.
349 349 var test = $('<div style="display: none; width: 10000pt; padding:0; border:0;"></div>');
350 350 $(body).append(test);
351 351 var pixel_per_point = test.width()/10000;
352 352 test.remove();
353 353 return Math.floor(points*pixel_per_point);
354 354 };
355 355
356 356 var always_new = function (constructor) {
357 357 // wrapper around contructor to avoid requiring `var a = new constructor()`
358 358 // useful for passing constructors as callbacks,
359 359 // not for programmer laziness.
360 360 // from http://programmers.stackexchange.com/questions/118798
361 361 return function () {
362 362 var obj = Object.create(constructor.prototype);
363 363 constructor.apply(obj, arguments);
364 364 return obj;
365 365 };
366 366 };
367 367
368 368 var url_path_join = function () {
369 369 // join a sequence of url components with '/'
370 370 var url = '';
371 371 for (var i = 0; i < arguments.length; i++) {
372 372 if (arguments[i] === '') {
373 373 continue;
374 374 }
375 375 if (url.length > 0 && url[url.length-1] != '/') {
376 376 url = url + '/' + arguments[i];
377 377 } else {
378 378 url = url + arguments[i];
379 379 }
380 380 }
381 381 url = url.replace(/\/\/+/, '/');
382 382 return url;
383 383 };
384 384
385 385 var parse_url = function (url) {
386 386 // an `a` element with an href allows attr-access to the parsed segments of a URL
387 387 // a = parse_url("http://localhost:8888/path/name#hash")
388 388 // a.protocol = "http:"
389 389 // a.host = "localhost:8888"
390 390 // a.hostname = "localhost"
391 391 // a.port = 8888
392 392 // a.pathname = "/path/name"
393 393 // a.hash = "#hash"
394 394 var a = document.createElement("a");
395 395 a.href = url;
396 396 return a;
397 397 };
398 398
399 399 var encode_uri_components = function (uri) {
400 400 // encode just the components of a multi-segment uri,
401 401 // leaving '/' separators
402 402 return uri.split('/').map(encodeURIComponent).join('/');
403 403 };
404 404
405 405 var url_join_encode = function () {
406 406 // join a sequence of url components with '/',
407 407 // encoding each component with encodeURIComponent
408 408 return encode_uri_components(url_path_join.apply(null, arguments));
409 409 };
410 410
411 411
412 412 var splitext = function (filename) {
413 413 // mimic Python os.path.splitext
414 414 // Returns ['base', '.ext']
415 415 var idx = filename.lastIndexOf('.');
416 416 if (idx > 0) {
417 417 return [filename.slice(0, idx), filename.slice(idx)];
418 418 } else {
419 419 return [filename, ''];
420 420 }
421 421 };
422 422
423 423
424 424 var escape_html = function (text) {
425 425 // escape text to HTML
426 426 return $("<div/>").text(text).html();
427 427 };
428 428
429 429
430 430 var get_body_data = function(key) {
431 431 // get a url-encoded item from body.data and decode it
432 432 // we should never have any encoded URLs anywhere else in code
433 433 // until we are building an actual request
434 434 return decodeURIComponent($('body').data(key));
435 435 };
436 436
437 437 var to_absolute_cursor_pos = function (cm, cursor) {
438 438 // get the absolute cursor position from CodeMirror's col, ch
439 439 if (!cursor) {
440 440 cursor = cm.getCursor();
441 441 }
442 442 var cursor_pos = cursor.ch;
443 443 for (var i = 0; i < cursor.line; i++) {
444 444 cursor_pos += cm.getLine(i).length + 1;
445 445 }
446 446 return cursor_pos;
447 447 };
448 448
449 449 var from_absolute_cursor_pos = function (cm, cursor_pos) {
450 450 // turn absolute cursor postion into CodeMirror col, ch cursor
451 451 var i, line;
452 452 var offset = 0;
453 453 for (i = 0, line=cm.getLine(i); line !== undefined; i++, line=cm.getLine(i)) {
454 454 if (offset + line.length < cursor_pos) {
455 455 offset += line.length + 1;
456 456 } else {
457 457 return {
458 458 line : i,
459 459 ch : cursor_pos - offset,
460 460 };
461 461 }
462 462 }
463 463 // reached end, return endpoint
464 464 return {
465 465 ch : line.length - 1,
466 466 line : i - 1,
467 467 };
468 468 };
469 469
470 470 // http://stackoverflow.com/questions/2400935/browser-detection-in-javascript
471 471 var browser = (function() {
472 472 if (typeof navigator === 'undefined') {
473 473 // navigator undefined in node
474 474 return 'None';
475 475 }
476 476 var N= navigator.appName, ua= navigator.userAgent, tem;
477 477 var M= ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i);
478 478 if (M && (tem= ua.match(/version\/([\.\d]+)/i)) !== null) M[2]= tem[1];
479 479 M= M? [M[1], M[2]]: [N, navigator.appVersion,'-?'];
480 480 return M;
481 481 })();
482 482
483 483 // http://stackoverflow.com/questions/11219582/how-to-detect-my-browser-version-and-operating-system-using-javascript
484 484 var platform = (function () {
485 485 if (typeof navigator === 'undefined') {
486 486 // navigator undefined in node
487 487 return 'None';
488 488 }
489 489 var OSName="None";
490 490 if (navigator.appVersion.indexOf("Win")!=-1) OSName="Windows";
491 491 if (navigator.appVersion.indexOf("Mac")!=-1) OSName="MacOS";
492 492 if (navigator.appVersion.indexOf("X11")!=-1) OSName="UNIX";
493 493 if (navigator.appVersion.indexOf("Linux")!=-1) OSName="Linux";
494 494 return OSName;
495 495 })();
496 496
497 497 var is_or_has = function (a, b) {
498 498 // Is b a child of a or a itself?
499 499 return a.has(b).length !==0 || a.is(b);
500 500 };
501 501
502 502 var is_focused = function (e) {
503 503 // Is element e, or one of its children focused?
504 504 e = $(e);
505 505 var target = $(document.activeElement);
506 506 if (target.length > 0) {
507 507 if (is_or_has(e, target)) {
508 508 return true;
509 509 } else {
510 510 return false;
511 511 }
512 512 } else {
513 513 return false;
514 514 }
515 515 };
516 516
517 var ajax_error_msg = function (jqXHR) {
518 // Return a JSON error message if there is one,
519 // otherwise the basic HTTP status text.
520 if (jqXHR.responseJSON && jqXHR.responseJSON.message) {
521 return jqXHR.responseJSON.message;
522 } else {
523 return jqXHR.statusText;
524 }
525 }
517 526 var log_ajax_error = function (jqXHR, status, error) {
518 527 // log ajax failures with informative messages
519 528 var msg = "API request failed (" + jqXHR.status + "): ";
520 529 console.log(jqXHR);
521 if (jqXHR.responseJSON && jqXHR.responseJSON.message) {
522 msg += jqXHR.responseJSON.message;
523 } else {
524 msg += jqXHR.statusText;
525 }
530 msg += ajax_error_msg(jqXHR);
526 531 console.log(msg);
527 532 };
528 533
529 534 var utils = {
530 535 regex_split : regex_split,
531 536 uuid : uuid,
532 537 fixConsole : fixConsole,
533 538 fixCarriageReturn : fixCarriageReturn,
534 539 autoLinkUrls : autoLinkUrls,
535 540 points_to_pixels : points_to_pixels,
536 541 get_body_data : get_body_data,
537 542 parse_url : parse_url,
538 543 url_path_join : url_path_join,
539 544 url_join_encode : url_join_encode,
540 545 encode_uri_components : encode_uri_components,
541 546 splitext : splitext,
542 547 escape_html : escape_html,
543 548 always_new : always_new,
544 549 to_absolute_cursor_pos : to_absolute_cursor_pos,
545 550 from_absolute_cursor_pos : from_absolute_cursor_pos,
546 551 browser : browser,
547 552 platform: platform,
548 553 is_or_has : is_or_has,
549 554 is_focused : is_focused,
555 ajax_error_msg : ajax_error_msg,
550 556 log_ajax_error : log_ajax_error,
551 557 };
552 558
553 559 // Backwards compatability.
554 560 IPython.utils = utils;
555 561
556 562 return utils;
557 563 });
@@ -1,2581 +1,2582
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/js/session',
12 12 'notebook/js/celltoolbar',
13 13 'components/marked/lib/marked',
14 14 'highlight',
15 15 'notebook/js/mathjaxutils',
16 16 'base/js/keyboard',
17 17 'notebook/js/tooltip',
18 18 'notebook/js/celltoolbarpresets/default',
19 19 'notebook/js/celltoolbarpresets/rawcell',
20 20 'notebook/js/celltoolbarpresets/slideshow',
21 21 ], function (
22 22 IPython,
23 23 $,
24 24 utils,
25 25 dialog,
26 26 textcell,
27 27 codecell,
28 28 session,
29 29 celltoolbar,
30 30 marked,
31 31 hljs,
32 32 mathjaxutils,
33 33 keyboard,
34 34 tooltip,
35 35 default_celltoolbar,
36 36 rawcell_celltoolbar,
37 37 slideshow_celltoolbar
38 38 ) {
39 39
40 40 var Notebook = function (selector, options) {
41 41 // Constructor
42 42 //
43 43 // A notebook contains and manages cells.
44 44 //
45 45 // Parameters:
46 46 // selector: string
47 47 // options: dictionary
48 48 // Dictionary of keyword arguments.
49 49 // events: $(Events) instance
50 50 // keyboard_manager: KeyboardManager instance
51 51 // save_widget: SaveWidget instance
52 52 // config: dictionary
53 53 // base_url : string
54 54 // notebook_path : string
55 55 // notebook_name : string
56 56 this.config = options.config || {};
57 57 this.base_url = options.base_url;
58 58 this.notebook_path = options.notebook_path;
59 59 this.notebook_name = options.notebook_name;
60 60 this.events = options.events;
61 61 this.keyboard_manager = options.keyboard_manager;
62 62 this.save_widget = options.save_widget;
63 63 this.tooltip = new tooltip.Tooltip(this.events);
64 64 this.ws_url = options.ws_url;
65 65 // default_kernel_name is a temporary measure while we implement proper
66 66 // kernel selection and delayed start. Do not rely on it.
67 67 this.default_kernel_name = 'python';
68 68 // TODO: This code smells (and the other `= this` line a couple lines down)
69 69 // We need a better way to deal with circular instance references.
70 70 this.keyboard_manager.notebook = this;
71 71 this.save_widget.notebook = this;
72 72
73 73 mathjaxutils.init();
74 74
75 75 if (marked) {
76 76 marked.setOptions({
77 77 gfm : true,
78 78 tables: true,
79 79 langPrefix: "language-",
80 80 highlight: function(code, lang) {
81 81 if (!lang) {
82 82 // no language, no highlight
83 83 return code;
84 84 }
85 85 var highlighted;
86 86 try {
87 87 highlighted = hljs.highlight(lang, code, false);
88 88 } catch(err) {
89 89 highlighted = hljs.highlightAuto(code);
90 90 }
91 91 return highlighted.value;
92 92 }
93 93 });
94 94 }
95 95
96 96 this.element = $(selector);
97 97 this.element.scroll();
98 98 this.element.data("notebook", this);
99 99 this.next_prompt_number = 1;
100 100 this.session = null;
101 101 this.kernel = null;
102 102 this.clipboard = null;
103 103 this.undelete_backup = null;
104 104 this.undelete_index = null;
105 105 this.undelete_below = false;
106 106 this.paste_enabled = false;
107 107 // It is important to start out in command mode to match the intial mode
108 108 // of the KeyboardManager.
109 109 this.mode = 'command';
110 110 this.set_dirty(false);
111 111 this.metadata = {};
112 112 this._checkpoint_after_save = false;
113 113 this.last_checkpoint = null;
114 114 this.checkpoints = [];
115 115 this.autosave_interval = 0;
116 116 this.autosave_timer = null;
117 117 // autosave *at most* every two minutes
118 118 this.minimum_autosave_interval = 120000;
119 119 // single worksheet for now
120 120 this.worksheet_metadata = {};
121 121 this.notebook_name_blacklist_re = /[\/\\:]/;
122 122 this.nbformat = 3; // Increment this when changing the nbformat
123 123 this.nbformat_minor = 0; // Increment this when changing the nbformat
124 124 this.codemirror_mode = 'ipython';
125 125 this.create_elements();
126 126 this.bind_events();
127 127 this.save_notebook = function() { // don't allow save until notebook_loaded
128 128 this.save_notebook_error(null, null, "Load failed, save is disabled");
129 129 };
130 130
131 131 // Trigger cell toolbar registration.
132 132 default_celltoolbar.register(this);
133 133 rawcell_celltoolbar.register(this);
134 134 slideshow_celltoolbar.register(this);
135 135 };
136 136
137 137
138 138 /**
139 139 * Create an HTML and CSS representation of the notebook.
140 140 *
141 141 * @method create_elements
142 142 */
143 143 Notebook.prototype.create_elements = function () {
144 144 var that = this;
145 145 this.element.attr('tabindex','-1');
146 146 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
147 147 // We add this end_space div to the end of the notebook div to:
148 148 // i) provide a margin between the last cell and the end of the notebook
149 149 // ii) to prevent the div from scrolling up when the last cell is being
150 150 // edited, but is too low on the page, which browsers will do automatically.
151 151 var end_space = $('<div/>').addClass('end_space');
152 152 end_space.dblclick(function (e) {
153 153 var ncells = that.ncells();
154 154 that.insert_cell_below('code',ncells-1);
155 155 });
156 156 this.element.append(this.container);
157 157 this.container.append(end_space);
158 158 };
159 159
160 160 /**
161 161 * Bind JavaScript events: key presses and custom IPython events.
162 162 *
163 163 * @method bind_events
164 164 */
165 165 Notebook.prototype.bind_events = function () {
166 166 var that = this;
167 167
168 168 this.events.on('set_next_input.Notebook', function (event, data) {
169 169 var index = that.find_cell_index(data.cell);
170 170 var new_cell = that.insert_cell_below('code',index);
171 171 new_cell.set_text(data.text);
172 172 that.dirty = true;
173 173 });
174 174
175 175 this.events.on('set_dirty.Notebook', function (event, data) {
176 176 that.dirty = data.value;
177 177 });
178 178
179 179 this.events.on('trust_changed.Notebook', function (event, data) {
180 180 that.trusted = data.value;
181 181 });
182 182
183 183 this.events.on('select.Cell', function (event, data) {
184 184 var index = that.find_cell_index(data.cell);
185 185 that.select(index);
186 186 });
187 187
188 188 this.events.on('edit_mode.Cell', function (event, data) {
189 189 that.handle_edit_mode(data.cell);
190 190 });
191 191
192 192 this.events.on('command_mode.Cell', function (event, data) {
193 193 that.handle_command_mode(data.cell);
194 194 });
195 195
196 196 this.events.on('status_autorestarting.Kernel', function () {
197 197 dialog.modal({
198 198 notebook: that,
199 199 keyboard_manager: that.keyboard_manager,
200 200 title: "Kernel Restarting",
201 201 body: "The kernel appears to have died. It will restart automatically.",
202 202 buttons: {
203 203 OK : {
204 204 class : "btn-primary"
205 205 }
206 206 }
207 207 });
208 208 });
209 209
210 210 this.events.on('spec_changed.Kernel', function(event, data) {
211 211 that.set_kernelspec_metadata(data);
212 212 if (data.codemirror_mode) {
213 213 that.set_codemirror_mode(data.codemirror_mode);
214 214 }
215 215 });
216 216
217 217 var collapse_time = function (time) {
218 218 var app_height = $('#ipython-main-app').height(); // content height
219 219 var splitter_height = $('div#pager_splitter').outerHeight(true);
220 220 var new_height = app_height - splitter_height;
221 221 that.element.animate({height : new_height + 'px'}, time);
222 222 };
223 223
224 224 this.element.bind('collapse_pager', function (event, extrap) {
225 225 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
226 226 collapse_time(time);
227 227 });
228 228
229 229 var expand_time = function (time) {
230 230 var app_height = $('#ipython-main-app').height(); // content height
231 231 var splitter_height = $('div#pager_splitter').outerHeight(true);
232 232 var pager_height = $('div#pager').outerHeight(true);
233 233 var new_height = app_height - pager_height - splitter_height;
234 234 that.element.animate({height : new_height + 'px'}, time);
235 235 };
236 236
237 237 this.element.bind('expand_pager', function (event, extrap) {
238 238 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
239 239 expand_time(time);
240 240 });
241 241
242 242 // Firefox 22 broke $(window).on("beforeunload")
243 243 // I'm not sure why or how.
244 244 window.onbeforeunload = function (e) {
245 245 // TODO: Make killing the kernel configurable.
246 246 var kill_kernel = false;
247 247 if (kill_kernel) {
248 248 that.session.kill_kernel();
249 249 }
250 250 // if we are autosaving, trigger an autosave on nav-away.
251 251 // still warn, because if we don't the autosave may fail.
252 252 if (that.dirty) {
253 253 if ( that.autosave_interval ) {
254 254 // schedule autosave in a timeout
255 255 // this gives you a chance to forcefully discard changes
256 256 // by reloading the page if you *really* want to.
257 257 // the timer doesn't start until you *dismiss* the dialog.
258 258 setTimeout(function () {
259 259 if (that.dirty) {
260 260 that.save_notebook();
261 261 }
262 262 }, 1000);
263 263 return "Autosave in progress, latest changes may be lost.";
264 264 } else {
265 265 return "Unsaved changes will be lost.";
266 266 }
267 267 }
268 268 // Null is the *only* return value that will make the browser not
269 269 // pop up the "don't leave" dialog.
270 270 return null;
271 271 };
272 272 };
273 273
274 274 /**
275 275 * Set the dirty flag, and trigger the set_dirty.Notebook event
276 276 *
277 277 * @method set_dirty
278 278 */
279 279 Notebook.prototype.set_dirty = function (value) {
280 280 if (value === undefined) {
281 281 value = true;
282 282 }
283 283 if (this.dirty == value) {
284 284 return;
285 285 }
286 286 this.events.trigger('set_dirty.Notebook', {value: value});
287 287 };
288 288
289 289 /**
290 290 * Scroll the top of the page to a given cell.
291 291 *
292 292 * @method scroll_to_cell
293 293 * @param {Number} cell_number An index of the cell to view
294 294 * @param {Number} time Animation time in milliseconds
295 295 * @return {Number} Pixel offset from the top of the container
296 296 */
297 297 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
298 298 var cells = this.get_cells();
299 299 time = time || 0;
300 300 cell_number = Math.min(cells.length-1,cell_number);
301 301 cell_number = Math.max(0 ,cell_number);
302 302 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
303 303 this.element.animate({scrollTop:scroll_value}, time);
304 304 return scroll_value;
305 305 };
306 306
307 307 /**
308 308 * Scroll to the bottom of the page.
309 309 *
310 310 * @method scroll_to_bottom
311 311 */
312 312 Notebook.prototype.scroll_to_bottom = function () {
313 313 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
314 314 };
315 315
316 316 /**
317 317 * Scroll to the top of the page.
318 318 *
319 319 * @method scroll_to_top
320 320 */
321 321 Notebook.prototype.scroll_to_top = function () {
322 322 this.element.animate({scrollTop:0}, 0);
323 323 };
324 324
325 325 // Edit Notebook metadata
326 326
327 327 Notebook.prototype.edit_metadata = function () {
328 328 var that = this;
329 329 dialog.edit_metadata({
330 330 md: this.metadata,
331 331 callback: function (md) {
332 332 that.metadata = md;
333 333 },
334 334 name: 'Notebook',
335 335 notebook: this,
336 336 keyboard_manager: this.keyboard_manager});
337 337 };
338 338
339 339 Notebook.prototype.set_kernelspec_metadata = function(ks) {
340 340 var tostore = {};
341 341 $.map(ks, function(value, field) {
342 342 if (field !== 'argv' && field !== 'env') {
343 343 tostore[field] = value;
344 344 }
345 345 });
346 346 this.metadata.kernelspec = tostore;
347 347 }
348 348
349 349 // Cell indexing, retrieval, etc.
350 350
351 351 /**
352 352 * Get all cell elements in the notebook.
353 353 *
354 354 * @method get_cell_elements
355 355 * @return {jQuery} A selector of all cell elements
356 356 */
357 357 Notebook.prototype.get_cell_elements = function () {
358 358 return this.container.children("div.cell");
359 359 };
360 360
361 361 /**
362 362 * Get a particular cell element.
363 363 *
364 364 * @method get_cell_element
365 365 * @param {Number} index An index of a cell to select
366 366 * @return {jQuery} A selector of the given cell.
367 367 */
368 368 Notebook.prototype.get_cell_element = function (index) {
369 369 var result = null;
370 370 var e = this.get_cell_elements().eq(index);
371 371 if (e.length !== 0) {
372 372 result = e;
373 373 }
374 374 return result;
375 375 };
376 376
377 377 /**
378 378 * Try to get a particular cell by msg_id.
379 379 *
380 380 * @method get_msg_cell
381 381 * @param {String} msg_id A message UUID
382 382 * @return {Cell} Cell or null if no cell was found.
383 383 */
384 384 Notebook.prototype.get_msg_cell = function (msg_id) {
385 385 return codecell.CodeCell.msg_cells[msg_id] || null;
386 386 };
387 387
388 388 /**
389 389 * Count the cells in this notebook.
390 390 *
391 391 * @method ncells
392 392 * @return {Number} The number of cells in this notebook
393 393 */
394 394 Notebook.prototype.ncells = function () {
395 395 return this.get_cell_elements().length;
396 396 };
397 397
398 398 /**
399 399 * Get all Cell objects in this notebook.
400 400 *
401 401 * @method get_cells
402 402 * @return {Array} This notebook's Cell objects
403 403 */
404 404 // TODO: we are often calling cells as cells()[i], which we should optimize
405 405 // to cells(i) or a new method.
406 406 Notebook.prototype.get_cells = function () {
407 407 return this.get_cell_elements().toArray().map(function (e) {
408 408 return $(e).data("cell");
409 409 });
410 410 };
411 411
412 412 /**
413 413 * Get a Cell object from this notebook.
414 414 *
415 415 * @method get_cell
416 416 * @param {Number} index An index of a cell to retrieve
417 417 * @return {Cell} A particular cell
418 418 */
419 419 Notebook.prototype.get_cell = function (index) {
420 420 var result = null;
421 421 var ce = this.get_cell_element(index);
422 422 if (ce !== null) {
423 423 result = ce.data('cell');
424 424 }
425 425 return result;
426 426 };
427 427
428 428 /**
429 429 * Get the cell below a given cell.
430 430 *
431 431 * @method get_next_cell
432 432 * @param {Cell} cell The provided cell
433 433 * @return {Cell} The next cell
434 434 */
435 435 Notebook.prototype.get_next_cell = function (cell) {
436 436 var result = null;
437 437 var index = this.find_cell_index(cell);
438 438 if (this.is_valid_cell_index(index+1)) {
439 439 result = this.get_cell(index+1);
440 440 }
441 441 return result;
442 442 };
443 443
444 444 /**
445 445 * Get the cell above a given cell.
446 446 *
447 447 * @method get_prev_cell
448 448 * @param {Cell} cell The provided cell
449 449 * @return {Cell} The previous cell
450 450 */
451 451 Notebook.prototype.get_prev_cell = function (cell) {
452 452 // TODO: off-by-one
453 453 // nb.get_prev_cell(nb.get_cell(1)) is null
454 454 var result = null;
455 455 var index = this.find_cell_index(cell);
456 456 if (index !== null && index > 1) {
457 457 result = this.get_cell(index-1);
458 458 }
459 459 return result;
460 460 };
461 461
462 462 /**
463 463 * Get the numeric index of a given cell.
464 464 *
465 465 * @method find_cell_index
466 466 * @param {Cell} cell The provided cell
467 467 * @return {Number} The cell's numeric index
468 468 */
469 469 Notebook.prototype.find_cell_index = function (cell) {
470 470 var result = null;
471 471 this.get_cell_elements().filter(function (index) {
472 472 if ($(this).data("cell") === cell) {
473 473 result = index;
474 474 }
475 475 });
476 476 return result;
477 477 };
478 478
479 479 /**
480 480 * Get a given index , or the selected index if none is provided.
481 481 *
482 482 * @method index_or_selected
483 483 * @param {Number} index A cell's index
484 484 * @return {Number} The given index, or selected index if none is provided.
485 485 */
486 486 Notebook.prototype.index_or_selected = function (index) {
487 487 var i;
488 488 if (index === undefined || index === null) {
489 489 i = this.get_selected_index();
490 490 if (i === null) {
491 491 i = 0;
492 492 }
493 493 } else {
494 494 i = index;
495 495 }
496 496 return i;
497 497 };
498 498
499 499 /**
500 500 * Get the currently selected cell.
501 501 * @method get_selected_cell
502 502 * @return {Cell} The selected cell
503 503 */
504 504 Notebook.prototype.get_selected_cell = function () {
505 505 var index = this.get_selected_index();
506 506 return this.get_cell(index);
507 507 };
508 508
509 509 /**
510 510 * Check whether a cell index is valid.
511 511 *
512 512 * @method is_valid_cell_index
513 513 * @param {Number} index A cell index
514 514 * @return True if the index is valid, false otherwise
515 515 */
516 516 Notebook.prototype.is_valid_cell_index = function (index) {
517 517 if (index !== null && index >= 0 && index < this.ncells()) {
518 518 return true;
519 519 } else {
520 520 return false;
521 521 }
522 522 };
523 523
524 524 /**
525 525 * Get the index of the currently selected cell.
526 526
527 527 * @method get_selected_index
528 528 * @return {Number} The selected cell's numeric index
529 529 */
530 530 Notebook.prototype.get_selected_index = function () {
531 531 var result = null;
532 532 this.get_cell_elements().filter(function (index) {
533 533 if ($(this).data("cell").selected === true) {
534 534 result = index;
535 535 }
536 536 });
537 537 return result;
538 538 };
539 539
540 540
541 541 // Cell selection.
542 542
543 543 /**
544 544 * Programmatically select a cell.
545 545 *
546 546 * @method select
547 547 * @param {Number} index A cell's index
548 548 * @return {Notebook} This notebook
549 549 */
550 550 Notebook.prototype.select = function (index) {
551 551 if (this.is_valid_cell_index(index)) {
552 552 var sindex = this.get_selected_index();
553 553 if (sindex !== null && index !== sindex) {
554 554 // If we are about to select a different cell, make sure we are
555 555 // first in command mode.
556 556 if (this.mode !== 'command') {
557 557 this.command_mode();
558 558 }
559 559 this.get_cell(sindex).unselect();
560 560 }
561 561 var cell = this.get_cell(index);
562 562 cell.select();
563 563 if (cell.cell_type === 'heading') {
564 564 this.events.trigger('selected_cell_type_changed.Notebook',
565 565 {'cell_type':cell.cell_type,level:cell.level}
566 566 );
567 567 } else {
568 568 this.events.trigger('selected_cell_type_changed.Notebook',
569 569 {'cell_type':cell.cell_type}
570 570 );
571 571 }
572 572 }
573 573 return this;
574 574 };
575 575
576 576 /**
577 577 * Programmatically select the next cell.
578 578 *
579 579 * @method select_next
580 580 * @return {Notebook} This notebook
581 581 */
582 582 Notebook.prototype.select_next = function () {
583 583 var index = this.get_selected_index();
584 584 this.select(index+1);
585 585 return this;
586 586 };
587 587
588 588 /**
589 589 * Programmatically select the previous cell.
590 590 *
591 591 * @method select_prev
592 592 * @return {Notebook} This notebook
593 593 */
594 594 Notebook.prototype.select_prev = function () {
595 595 var index = this.get_selected_index();
596 596 this.select(index-1);
597 597 return this;
598 598 };
599 599
600 600
601 601 // Edit/Command mode
602 602
603 603 /**
604 604 * Gets the index of the cell that is in edit mode.
605 605 *
606 606 * @method get_edit_index
607 607 *
608 608 * @return index {int}
609 609 **/
610 610 Notebook.prototype.get_edit_index = function () {
611 611 var result = null;
612 612 this.get_cell_elements().filter(function (index) {
613 613 if ($(this).data("cell").mode === 'edit') {
614 614 result = index;
615 615 }
616 616 });
617 617 return result;
618 618 };
619 619
620 620 /**
621 621 * Handle when a a cell blurs and the notebook should enter command mode.
622 622 *
623 623 * @method handle_command_mode
624 624 * @param [cell] {Cell} Cell to enter command mode on.
625 625 **/
626 626 Notebook.prototype.handle_command_mode = function (cell) {
627 627 if (this.mode !== 'command') {
628 628 cell.command_mode();
629 629 this.mode = 'command';
630 630 this.events.trigger('command_mode.Notebook');
631 631 this.keyboard_manager.command_mode();
632 632 }
633 633 };
634 634
635 635 /**
636 636 * Make the notebook enter command mode.
637 637 *
638 638 * @method command_mode
639 639 **/
640 640 Notebook.prototype.command_mode = function () {
641 641 var cell = this.get_cell(this.get_edit_index());
642 642 if (cell && this.mode !== 'command') {
643 643 // We don't call cell.command_mode, but rather call cell.focus_cell()
644 644 // which will blur and CM editor and trigger the call to
645 645 // handle_command_mode.
646 646 cell.focus_cell();
647 647 }
648 648 };
649 649
650 650 /**
651 651 * Handle when a cell fires it's edit_mode event.
652 652 *
653 653 * @method handle_edit_mode
654 654 * @param [cell] {Cell} Cell to enter edit mode on.
655 655 **/
656 656 Notebook.prototype.handle_edit_mode = function (cell) {
657 657 if (cell && this.mode !== 'edit') {
658 658 cell.edit_mode();
659 659 this.mode = 'edit';
660 660 this.events.trigger('edit_mode.Notebook');
661 661 this.keyboard_manager.edit_mode();
662 662 }
663 663 };
664 664
665 665 /**
666 666 * Make a cell enter edit mode.
667 667 *
668 668 * @method edit_mode
669 669 **/
670 670 Notebook.prototype.edit_mode = function () {
671 671 var cell = this.get_selected_cell();
672 672 if (cell && this.mode !== 'edit') {
673 673 cell.unrender();
674 674 cell.focus_editor();
675 675 }
676 676 };
677 677
678 678 /**
679 679 * Focus the currently selected cell.
680 680 *
681 681 * @method focus_cell
682 682 **/
683 683 Notebook.prototype.focus_cell = function () {
684 684 var cell = this.get_selected_cell();
685 685 if (cell === null) {return;} // No cell is selected
686 686 cell.focus_cell();
687 687 };
688 688
689 689 // Cell movement
690 690
691 691 /**
692 692 * Move given (or selected) cell up and select it.
693 693 *
694 694 * @method move_cell_up
695 695 * @param [index] {integer} cell index
696 696 * @return {Notebook} This notebook
697 697 **/
698 698 Notebook.prototype.move_cell_up = function (index) {
699 699 var i = this.index_or_selected(index);
700 700 if (this.is_valid_cell_index(i) && i > 0) {
701 701 var pivot = this.get_cell_element(i-1);
702 702 var tomove = this.get_cell_element(i);
703 703 if (pivot !== null && tomove !== null) {
704 704 tomove.detach();
705 705 pivot.before(tomove);
706 706 this.select(i-1);
707 707 var cell = this.get_selected_cell();
708 708 cell.focus_cell();
709 709 }
710 710 this.set_dirty(true);
711 711 }
712 712 return this;
713 713 };
714 714
715 715
716 716 /**
717 717 * Move given (or selected) cell down and select it
718 718 *
719 719 * @method move_cell_down
720 720 * @param [index] {integer} cell index
721 721 * @return {Notebook} This notebook
722 722 **/
723 723 Notebook.prototype.move_cell_down = function (index) {
724 724 var i = this.index_or_selected(index);
725 725 if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
726 726 var pivot = this.get_cell_element(i+1);
727 727 var tomove = this.get_cell_element(i);
728 728 if (pivot !== null && tomove !== null) {
729 729 tomove.detach();
730 730 pivot.after(tomove);
731 731 this.select(i+1);
732 732 var cell = this.get_selected_cell();
733 733 cell.focus_cell();
734 734 }
735 735 }
736 736 this.set_dirty();
737 737 return this;
738 738 };
739 739
740 740
741 741 // Insertion, deletion.
742 742
743 743 /**
744 744 * Delete a cell from the notebook.
745 745 *
746 746 * @method delete_cell
747 747 * @param [index] A cell's numeric index
748 748 * @return {Notebook} This notebook
749 749 */
750 750 Notebook.prototype.delete_cell = function (index) {
751 751 var i = this.index_or_selected(index);
752 752 var cell = this.get_selected_cell();
753 753 this.undelete_backup = cell.toJSON();
754 754 $('#undelete_cell').removeClass('disabled');
755 755 if (this.is_valid_cell_index(i)) {
756 756 var old_ncells = this.ncells();
757 757 var ce = this.get_cell_element(i);
758 758 ce.remove();
759 759 if (i === 0) {
760 760 // Always make sure we have at least one cell.
761 761 if (old_ncells === 1) {
762 762 this.insert_cell_below('code');
763 763 }
764 764 this.select(0);
765 765 this.undelete_index = 0;
766 766 this.undelete_below = false;
767 767 } else if (i === old_ncells-1 && i !== 0) {
768 768 this.select(i-1);
769 769 this.undelete_index = i - 1;
770 770 this.undelete_below = true;
771 771 } else {
772 772 this.select(i);
773 773 this.undelete_index = i;
774 774 this.undelete_below = false;
775 775 }
776 776 this.events.trigger('delete.Cell', {'cell': cell, 'index': i});
777 777 this.set_dirty(true);
778 778 }
779 779 return this;
780 780 };
781 781
782 782 /**
783 783 * Restore the most recently deleted cell.
784 784 *
785 785 * @method undelete
786 786 */
787 787 Notebook.prototype.undelete_cell = function() {
788 788 if (this.undelete_backup !== null && this.undelete_index !== null) {
789 789 var current_index = this.get_selected_index();
790 790 if (this.undelete_index < current_index) {
791 791 current_index = current_index + 1;
792 792 }
793 793 if (this.undelete_index >= this.ncells()) {
794 794 this.select(this.ncells() - 1);
795 795 }
796 796 else {
797 797 this.select(this.undelete_index);
798 798 }
799 799 var cell_data = this.undelete_backup;
800 800 var new_cell = null;
801 801 if (this.undelete_below) {
802 802 new_cell = this.insert_cell_below(cell_data.cell_type);
803 803 } else {
804 804 new_cell = this.insert_cell_above(cell_data.cell_type);
805 805 }
806 806 new_cell.fromJSON(cell_data);
807 807 if (this.undelete_below) {
808 808 this.select(current_index+1);
809 809 } else {
810 810 this.select(current_index);
811 811 }
812 812 this.undelete_backup = null;
813 813 this.undelete_index = null;
814 814 }
815 815 $('#undelete_cell').addClass('disabled');
816 816 };
817 817
818 818 /**
819 819 * Insert a cell so that after insertion the cell is at given index.
820 820 *
821 821 * If cell type is not provided, it will default to the type of the
822 822 * currently active cell.
823 823 *
824 824 * Similar to insert_above, but index parameter is mandatory
825 825 *
826 826 * Index will be brought back into the accessible range [0,n]
827 827 *
828 828 * @method insert_cell_at_index
829 829 * @param [type] {string} in ['code','markdown','heading'], defaults to 'code'
830 830 * @param [index] {int} a valid index where to insert cell
831 831 *
832 832 * @return cell {cell|null} created cell or null
833 833 **/
834 834 Notebook.prototype.insert_cell_at_index = function(type, index){
835 835
836 836 var ncells = this.ncells();
837 837 index = Math.min(index,ncells);
838 838 index = Math.max(index,0);
839 839 var cell = null;
840 840 type = type || this.get_selected_cell().cell_type;
841 841
842 842 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
843 843 var cell_options = {
844 844 events: this.events,
845 845 config: this.config,
846 846 keyboard_manager: this.keyboard_manager,
847 847 notebook: this,
848 848 tooltip: this.tooltip,
849 849 };
850 850 if (type === 'code') {
851 851 cell = new codecell.CodeCell(this.kernel, cell_options);
852 852 cell.set_input_prompt();
853 853 } else if (type === 'markdown') {
854 854 cell = new textcell.MarkdownCell(cell_options);
855 855 } else if (type === 'raw') {
856 856 cell = new textcell.RawCell(cell_options);
857 857 } else if (type === 'heading') {
858 858 cell = new textcell.HeadingCell(cell_options);
859 859 }
860 860
861 861 if(this._insert_element_at_index(cell.element,index)) {
862 862 cell.render();
863 863 this.events.trigger('create.Cell', {'cell': cell, 'index': index});
864 864 cell.refresh();
865 865 // We used to select the cell after we refresh it, but there
866 866 // are now cases were this method is called where select is
867 867 // not appropriate. The selection logic should be handled by the
868 868 // caller of the the top level insert_cell methods.
869 869 this.set_dirty(true);
870 870 }
871 871 }
872 872 return cell;
873 873
874 874 };
875 875
876 876 /**
877 877 * Insert an element at given cell index.
878 878 *
879 879 * @method _insert_element_at_index
880 880 * @param element {dom element} a cell element
881 881 * @param [index] {int} a valid index where to inser cell
882 882 * @private
883 883 *
884 884 * return true if everything whent fine.
885 885 **/
886 886 Notebook.prototype._insert_element_at_index = function(element, index){
887 887 if (element === undefined){
888 888 return false;
889 889 }
890 890
891 891 var ncells = this.ncells();
892 892
893 893 if (ncells === 0) {
894 894 // special case append if empty
895 895 this.element.find('div.end_space').before(element);
896 896 } else if ( ncells === index ) {
897 897 // special case append it the end, but not empty
898 898 this.get_cell_element(index-1).after(element);
899 899 } else if (this.is_valid_cell_index(index)) {
900 900 // otherwise always somewhere to append to
901 901 this.get_cell_element(index).before(element);
902 902 } else {
903 903 return false;
904 904 }
905 905
906 906 if (this.undelete_index !== null && index <= this.undelete_index) {
907 907 this.undelete_index = this.undelete_index + 1;
908 908 this.set_dirty(true);
909 909 }
910 910 return true;
911 911 };
912 912
913 913 /**
914 914 * Insert a cell of given type above given index, or at top
915 915 * of notebook if index smaller than 0.
916 916 *
917 917 * default index value is the one of currently selected cell
918 918 *
919 919 * @method insert_cell_above
920 920 * @param [type] {string} cell type
921 921 * @param [index] {integer}
922 922 *
923 923 * @return handle to created cell or null
924 924 **/
925 925 Notebook.prototype.insert_cell_above = function (type, index) {
926 926 index = this.index_or_selected(index);
927 927 return this.insert_cell_at_index(type, index);
928 928 };
929 929
930 930 /**
931 931 * Insert a cell of given type below given index, or at bottom
932 932 * of notebook if index greater than number of cells
933 933 *
934 934 * default index value is the one of currently selected cell
935 935 *
936 936 * @method insert_cell_below
937 937 * @param [type] {string} cell type
938 938 * @param [index] {integer}
939 939 *
940 940 * @return handle to created cell or null
941 941 *
942 942 **/
943 943 Notebook.prototype.insert_cell_below = function (type, index) {
944 944 index = this.index_or_selected(index);
945 945 return this.insert_cell_at_index(type, index+1);
946 946 };
947 947
948 948
949 949 /**
950 950 * Insert cell at end of notebook
951 951 *
952 952 * @method insert_cell_at_bottom
953 953 * @param {String} type cell type
954 954 *
955 955 * @return the added cell; or null
956 956 **/
957 957 Notebook.prototype.insert_cell_at_bottom = function (type){
958 958 var len = this.ncells();
959 959 return this.insert_cell_below(type,len-1);
960 960 };
961 961
962 962 /**
963 963 * Turn a cell into a code cell.
964 964 *
965 965 * @method to_code
966 966 * @param {Number} [index] A cell's index
967 967 */
968 968 Notebook.prototype.to_code = function (index) {
969 969 var i = this.index_or_selected(index);
970 970 if (this.is_valid_cell_index(i)) {
971 971 var source_element = this.get_cell_element(i);
972 972 var source_cell = source_element.data("cell");
973 973 if (!(source_cell instanceof codecell.CodeCell)) {
974 974 var target_cell = this.insert_cell_below('code',i);
975 975 var text = source_cell.get_text();
976 976 if (text === source_cell.placeholder) {
977 977 text = '';
978 978 }
979 979 target_cell.set_text(text);
980 980 // make this value the starting point, so that we can only undo
981 981 // to this state, instead of a blank cell
982 982 target_cell.code_mirror.clearHistory();
983 983 source_element.remove();
984 984 this.select(i);
985 985 var cursor = source_cell.code_mirror.getCursor();
986 986 target_cell.code_mirror.setCursor(cursor);
987 987 this.set_dirty(true);
988 988 }
989 989 }
990 990 };
991 991
992 992 /**
993 993 * Turn a cell into a Markdown cell.
994 994 *
995 995 * @method to_markdown
996 996 * @param {Number} [index] A cell's index
997 997 */
998 998 Notebook.prototype.to_markdown = function (index) {
999 999 var i = this.index_or_selected(index);
1000 1000 if (this.is_valid_cell_index(i)) {
1001 1001 var source_element = this.get_cell_element(i);
1002 1002 var source_cell = source_element.data("cell");
1003 1003 if (!(source_cell instanceof textcell.MarkdownCell)) {
1004 1004 var target_cell = this.insert_cell_below('markdown',i);
1005 1005 var text = source_cell.get_text();
1006 1006 if (text === source_cell.placeholder) {
1007 1007 text = '';
1008 1008 }
1009 1009 // We must show the editor before setting its contents
1010 1010 target_cell.unrender();
1011 1011 target_cell.set_text(text);
1012 1012 // make this value the starting point, so that we can only undo
1013 1013 // to this state, instead of a blank cell
1014 1014 target_cell.code_mirror.clearHistory();
1015 1015 source_element.remove();
1016 1016 this.select(i);
1017 1017 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1018 1018 target_cell.render();
1019 1019 }
1020 1020 var cursor = source_cell.code_mirror.getCursor();
1021 1021 target_cell.code_mirror.setCursor(cursor);
1022 1022 this.set_dirty(true);
1023 1023 }
1024 1024 }
1025 1025 };
1026 1026
1027 1027 /**
1028 1028 * Turn a cell into a raw text cell.
1029 1029 *
1030 1030 * @method to_raw
1031 1031 * @param {Number} [index] A cell's index
1032 1032 */
1033 1033 Notebook.prototype.to_raw = function (index) {
1034 1034 var i = this.index_or_selected(index);
1035 1035 if (this.is_valid_cell_index(i)) {
1036 1036 var source_element = this.get_cell_element(i);
1037 1037 var source_cell = source_element.data("cell");
1038 1038 var target_cell = null;
1039 1039 if (!(source_cell instanceof textcell.RawCell)) {
1040 1040 target_cell = this.insert_cell_below('raw',i);
1041 1041 var text = source_cell.get_text();
1042 1042 if (text === source_cell.placeholder) {
1043 1043 text = '';
1044 1044 }
1045 1045 // We must show the editor before setting its contents
1046 1046 target_cell.unrender();
1047 1047 target_cell.set_text(text);
1048 1048 // make this value the starting point, so that we can only undo
1049 1049 // to this state, instead of a blank cell
1050 1050 target_cell.code_mirror.clearHistory();
1051 1051 source_element.remove();
1052 1052 this.select(i);
1053 1053 var cursor = source_cell.code_mirror.getCursor();
1054 1054 target_cell.code_mirror.setCursor(cursor);
1055 1055 this.set_dirty(true);
1056 1056 }
1057 1057 }
1058 1058 };
1059 1059
1060 1060 /**
1061 1061 * Turn a cell into a heading cell.
1062 1062 *
1063 1063 * @method to_heading
1064 1064 * @param {Number} [index] A cell's index
1065 1065 * @param {Number} [level] A heading level (e.g., 1 becomes &lt;h1&gt;)
1066 1066 */
1067 1067 Notebook.prototype.to_heading = function (index, level) {
1068 1068 level = level || 1;
1069 1069 var i = this.index_or_selected(index);
1070 1070 if (this.is_valid_cell_index(i)) {
1071 1071 var source_element = this.get_cell_element(i);
1072 1072 var source_cell = source_element.data("cell");
1073 1073 var target_cell = null;
1074 1074 if (source_cell instanceof textcell.HeadingCell) {
1075 1075 source_cell.set_level(level);
1076 1076 } else {
1077 1077 target_cell = this.insert_cell_below('heading',i);
1078 1078 var text = source_cell.get_text();
1079 1079 if (text === source_cell.placeholder) {
1080 1080 text = '';
1081 1081 }
1082 1082 // We must show the editor before setting its contents
1083 1083 target_cell.set_level(level);
1084 1084 target_cell.unrender();
1085 1085 target_cell.set_text(text);
1086 1086 // make this value the starting point, so that we can only undo
1087 1087 // to this state, instead of a blank cell
1088 1088 target_cell.code_mirror.clearHistory();
1089 1089 source_element.remove();
1090 1090 this.select(i);
1091 1091 var cursor = source_cell.code_mirror.getCursor();
1092 1092 target_cell.code_mirror.setCursor(cursor);
1093 1093 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1094 1094 target_cell.render();
1095 1095 }
1096 1096 }
1097 1097 this.set_dirty(true);
1098 1098 this.events.trigger('selected_cell_type_changed.Notebook',
1099 1099 {'cell_type':'heading',level:level}
1100 1100 );
1101 1101 }
1102 1102 };
1103 1103
1104 1104
1105 1105 // Cut/Copy/Paste
1106 1106
1107 1107 /**
1108 1108 * Enable UI elements for pasting cells.
1109 1109 *
1110 1110 * @method enable_paste
1111 1111 */
1112 1112 Notebook.prototype.enable_paste = function () {
1113 1113 var that = this;
1114 1114 if (!this.paste_enabled) {
1115 1115 $('#paste_cell_replace').removeClass('disabled')
1116 1116 .on('click', function () {that.paste_cell_replace();});
1117 1117 $('#paste_cell_above').removeClass('disabled')
1118 1118 .on('click', function () {that.paste_cell_above();});
1119 1119 $('#paste_cell_below').removeClass('disabled')
1120 1120 .on('click', function () {that.paste_cell_below();});
1121 1121 this.paste_enabled = true;
1122 1122 }
1123 1123 };
1124 1124
1125 1125 /**
1126 1126 * Disable UI elements for pasting cells.
1127 1127 *
1128 1128 * @method disable_paste
1129 1129 */
1130 1130 Notebook.prototype.disable_paste = function () {
1131 1131 if (this.paste_enabled) {
1132 1132 $('#paste_cell_replace').addClass('disabled').off('click');
1133 1133 $('#paste_cell_above').addClass('disabled').off('click');
1134 1134 $('#paste_cell_below').addClass('disabled').off('click');
1135 1135 this.paste_enabled = false;
1136 1136 }
1137 1137 };
1138 1138
1139 1139 /**
1140 1140 * Cut a cell.
1141 1141 *
1142 1142 * @method cut_cell
1143 1143 */
1144 1144 Notebook.prototype.cut_cell = function () {
1145 1145 this.copy_cell();
1146 1146 this.delete_cell();
1147 1147 };
1148 1148
1149 1149 /**
1150 1150 * Copy a cell.
1151 1151 *
1152 1152 * @method copy_cell
1153 1153 */
1154 1154 Notebook.prototype.copy_cell = function () {
1155 1155 var cell = this.get_selected_cell();
1156 1156 this.clipboard = cell.toJSON();
1157 1157 this.enable_paste();
1158 1158 };
1159 1159
1160 1160 /**
1161 1161 * Replace the selected cell with a cell in the clipboard.
1162 1162 *
1163 1163 * @method paste_cell_replace
1164 1164 */
1165 1165 Notebook.prototype.paste_cell_replace = function () {
1166 1166 if (this.clipboard !== null && this.paste_enabled) {
1167 1167 var cell_data = this.clipboard;
1168 1168 var new_cell = this.insert_cell_above(cell_data.cell_type);
1169 1169 new_cell.fromJSON(cell_data);
1170 1170 var old_cell = this.get_next_cell(new_cell);
1171 1171 this.delete_cell(this.find_cell_index(old_cell));
1172 1172 this.select(this.find_cell_index(new_cell));
1173 1173 }
1174 1174 };
1175 1175
1176 1176 /**
1177 1177 * Paste a cell from the clipboard above the selected cell.
1178 1178 *
1179 1179 * @method paste_cell_above
1180 1180 */
1181 1181 Notebook.prototype.paste_cell_above = function () {
1182 1182 if (this.clipboard !== null && this.paste_enabled) {
1183 1183 var cell_data = this.clipboard;
1184 1184 var new_cell = this.insert_cell_above(cell_data.cell_type);
1185 1185 new_cell.fromJSON(cell_data);
1186 1186 new_cell.focus_cell();
1187 1187 }
1188 1188 };
1189 1189
1190 1190 /**
1191 1191 * Paste a cell from the clipboard below the selected cell.
1192 1192 *
1193 1193 * @method paste_cell_below
1194 1194 */
1195 1195 Notebook.prototype.paste_cell_below = function () {
1196 1196 if (this.clipboard !== null && this.paste_enabled) {
1197 1197 var cell_data = this.clipboard;
1198 1198 var new_cell = this.insert_cell_below(cell_data.cell_type);
1199 1199 new_cell.fromJSON(cell_data);
1200 1200 new_cell.focus_cell();
1201 1201 }
1202 1202 };
1203 1203
1204 1204 // Split/merge
1205 1205
1206 1206 /**
1207 1207 * Split the selected cell into two, at the cursor.
1208 1208 *
1209 1209 * @method split_cell
1210 1210 */
1211 1211 Notebook.prototype.split_cell = function () {
1212 1212 var mdc = textcell.MarkdownCell;
1213 1213 var rc = textcell.RawCell;
1214 1214 var cell = this.get_selected_cell();
1215 1215 if (cell.is_splittable()) {
1216 1216 var texta = cell.get_pre_cursor();
1217 1217 var textb = cell.get_post_cursor();
1218 1218 cell.set_text(textb);
1219 1219 var new_cell = this.insert_cell_above(cell.cell_type);
1220 1220 // Unrender the new cell so we can call set_text.
1221 1221 new_cell.unrender();
1222 1222 new_cell.set_text(texta);
1223 1223 }
1224 1224 };
1225 1225
1226 1226 /**
1227 1227 * Combine the selected cell into the cell above it.
1228 1228 *
1229 1229 * @method merge_cell_above
1230 1230 */
1231 1231 Notebook.prototype.merge_cell_above = function () {
1232 1232 var mdc = textcell.MarkdownCell;
1233 1233 var rc = textcell.RawCell;
1234 1234 var index = this.get_selected_index();
1235 1235 var cell = this.get_cell(index);
1236 1236 var render = cell.rendered;
1237 1237 if (!cell.is_mergeable()) {
1238 1238 return;
1239 1239 }
1240 1240 if (index > 0) {
1241 1241 var upper_cell = this.get_cell(index-1);
1242 1242 if (!upper_cell.is_mergeable()) {
1243 1243 return;
1244 1244 }
1245 1245 var upper_text = upper_cell.get_text();
1246 1246 var text = cell.get_text();
1247 1247 if (cell instanceof codecell.CodeCell) {
1248 1248 cell.set_text(upper_text+'\n'+text);
1249 1249 } else {
1250 1250 cell.unrender(); // Must unrender before we set_text.
1251 1251 cell.set_text(upper_text+'\n\n'+text);
1252 1252 if (render) {
1253 1253 // The rendered state of the final cell should match
1254 1254 // that of the original selected cell;
1255 1255 cell.render();
1256 1256 }
1257 1257 }
1258 1258 this.delete_cell(index-1);
1259 1259 this.select(this.find_cell_index(cell));
1260 1260 }
1261 1261 };
1262 1262
1263 1263 /**
1264 1264 * Combine the selected cell into the cell below it.
1265 1265 *
1266 1266 * @method merge_cell_below
1267 1267 */
1268 1268 Notebook.prototype.merge_cell_below = function () {
1269 1269 var mdc = textcell.MarkdownCell;
1270 1270 var rc = textcell.RawCell;
1271 1271 var index = this.get_selected_index();
1272 1272 var cell = this.get_cell(index);
1273 1273 var render = cell.rendered;
1274 1274 if (!cell.is_mergeable()) {
1275 1275 return;
1276 1276 }
1277 1277 if (index < this.ncells()-1) {
1278 1278 var lower_cell = this.get_cell(index+1);
1279 1279 if (!lower_cell.is_mergeable()) {
1280 1280 return;
1281 1281 }
1282 1282 var lower_text = lower_cell.get_text();
1283 1283 var text = cell.get_text();
1284 1284 if (cell instanceof codecell.CodeCell) {
1285 1285 cell.set_text(text+'\n'+lower_text);
1286 1286 } else {
1287 1287 cell.unrender(); // Must unrender before we set_text.
1288 1288 cell.set_text(text+'\n\n'+lower_text);
1289 1289 if (render) {
1290 1290 // The rendered state of the final cell should match
1291 1291 // that of the original selected cell;
1292 1292 cell.render();
1293 1293 }
1294 1294 }
1295 1295 this.delete_cell(index+1);
1296 1296 this.select(this.find_cell_index(cell));
1297 1297 }
1298 1298 };
1299 1299
1300 1300
1301 1301 // Cell collapsing and output clearing
1302 1302
1303 1303 /**
1304 1304 * Hide a cell's output.
1305 1305 *
1306 1306 * @method collapse_output
1307 1307 * @param {Number} index A cell's numeric index
1308 1308 */
1309 1309 Notebook.prototype.collapse_output = function (index) {
1310 1310 var i = this.index_or_selected(index);
1311 1311 var cell = this.get_cell(i);
1312 1312 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1313 1313 cell.collapse_output();
1314 1314 this.set_dirty(true);
1315 1315 }
1316 1316 };
1317 1317
1318 1318 /**
1319 1319 * Hide each code cell's output area.
1320 1320 *
1321 1321 * @method collapse_all_output
1322 1322 */
1323 1323 Notebook.prototype.collapse_all_output = function () {
1324 1324 $.map(this.get_cells(), function (cell, i) {
1325 1325 if (cell instanceof codecell.CodeCell) {
1326 1326 cell.collapse_output();
1327 1327 }
1328 1328 });
1329 1329 // this should not be set if the `collapse` key is removed from nbformat
1330 1330 this.set_dirty(true);
1331 1331 };
1332 1332
1333 1333 /**
1334 1334 * Show a cell's output.
1335 1335 *
1336 1336 * @method expand_output
1337 1337 * @param {Number} index A cell's numeric index
1338 1338 */
1339 1339 Notebook.prototype.expand_output = function (index) {
1340 1340 var i = this.index_or_selected(index);
1341 1341 var cell = this.get_cell(i);
1342 1342 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1343 1343 cell.expand_output();
1344 1344 this.set_dirty(true);
1345 1345 }
1346 1346 };
1347 1347
1348 1348 /**
1349 1349 * Expand each code cell's output area, and remove scrollbars.
1350 1350 *
1351 1351 * @method expand_all_output
1352 1352 */
1353 1353 Notebook.prototype.expand_all_output = function () {
1354 1354 $.map(this.get_cells(), function (cell, i) {
1355 1355 if (cell instanceof codecell.CodeCell) {
1356 1356 cell.expand_output();
1357 1357 }
1358 1358 });
1359 1359 // this should not be set if the `collapse` key is removed from nbformat
1360 1360 this.set_dirty(true);
1361 1361 };
1362 1362
1363 1363 /**
1364 1364 * Clear the selected CodeCell's output area.
1365 1365 *
1366 1366 * @method clear_output
1367 1367 * @param {Number} index A cell's numeric index
1368 1368 */
1369 1369 Notebook.prototype.clear_output = function (index) {
1370 1370 var i = this.index_or_selected(index);
1371 1371 var cell = this.get_cell(i);
1372 1372 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1373 1373 cell.clear_output();
1374 1374 this.set_dirty(true);
1375 1375 }
1376 1376 };
1377 1377
1378 1378 /**
1379 1379 * Clear each code cell's output area.
1380 1380 *
1381 1381 * @method clear_all_output
1382 1382 */
1383 1383 Notebook.prototype.clear_all_output = function () {
1384 1384 $.map(this.get_cells(), function (cell, i) {
1385 1385 if (cell instanceof codecell.CodeCell) {
1386 1386 cell.clear_output();
1387 1387 }
1388 1388 });
1389 1389 this.set_dirty(true);
1390 1390 };
1391 1391
1392 1392 /**
1393 1393 * Scroll the selected CodeCell's output area.
1394 1394 *
1395 1395 * @method scroll_output
1396 1396 * @param {Number} index A cell's numeric index
1397 1397 */
1398 1398 Notebook.prototype.scroll_output = function (index) {
1399 1399 var i = this.index_or_selected(index);
1400 1400 var cell = this.get_cell(i);
1401 1401 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1402 1402 cell.scroll_output();
1403 1403 this.set_dirty(true);
1404 1404 }
1405 1405 };
1406 1406
1407 1407 /**
1408 1408 * Expand each code cell's output area, and add a scrollbar for long output.
1409 1409 *
1410 1410 * @method scroll_all_output
1411 1411 */
1412 1412 Notebook.prototype.scroll_all_output = function () {
1413 1413 $.map(this.get_cells(), function (cell, i) {
1414 1414 if (cell instanceof codecell.CodeCell) {
1415 1415 cell.scroll_output();
1416 1416 }
1417 1417 });
1418 1418 // this should not be set if the `collapse` key is removed from nbformat
1419 1419 this.set_dirty(true);
1420 1420 };
1421 1421
1422 1422 /** Toggle whether a cell's output is collapsed or expanded.
1423 1423 *
1424 1424 * @method toggle_output
1425 1425 * @param {Number} index A cell's numeric index
1426 1426 */
1427 1427 Notebook.prototype.toggle_output = function (index) {
1428 1428 var i = this.index_or_selected(index);
1429 1429 var cell = this.get_cell(i);
1430 1430 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1431 1431 cell.toggle_output();
1432 1432 this.set_dirty(true);
1433 1433 }
1434 1434 };
1435 1435
1436 1436 /**
1437 1437 * Hide/show the output of all cells.
1438 1438 *
1439 1439 * @method toggle_all_output
1440 1440 */
1441 1441 Notebook.prototype.toggle_all_output = function () {
1442 1442 $.map(this.get_cells(), function (cell, i) {
1443 1443 if (cell instanceof codecell.CodeCell) {
1444 1444 cell.toggle_output();
1445 1445 }
1446 1446 });
1447 1447 // this should not be set if the `collapse` key is removed from nbformat
1448 1448 this.set_dirty(true);
1449 1449 };
1450 1450
1451 1451 /**
1452 1452 * Toggle a scrollbar for long cell outputs.
1453 1453 *
1454 1454 * @method toggle_output_scroll
1455 1455 * @param {Number} index A cell's numeric index
1456 1456 */
1457 1457 Notebook.prototype.toggle_output_scroll = function (index) {
1458 1458 var i = this.index_or_selected(index);
1459 1459 var cell = this.get_cell(i);
1460 1460 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1461 1461 cell.toggle_output_scroll();
1462 1462 this.set_dirty(true);
1463 1463 }
1464 1464 };
1465 1465
1466 1466 /**
1467 1467 * Toggle the scrolling of long output on all cells.
1468 1468 *
1469 1469 * @method toggle_all_output_scrolling
1470 1470 */
1471 1471 Notebook.prototype.toggle_all_output_scroll = function () {
1472 1472 $.map(this.get_cells(), function (cell, i) {
1473 1473 if (cell instanceof codecell.CodeCell) {
1474 1474 cell.toggle_output_scroll();
1475 1475 }
1476 1476 });
1477 1477 // this should not be set if the `collapse` key is removed from nbformat
1478 1478 this.set_dirty(true);
1479 1479 };
1480 1480
1481 1481 // Other cell functions: line numbers, ...
1482 1482
1483 1483 /**
1484 1484 * Toggle line numbers in the selected cell's input area.
1485 1485 *
1486 1486 * @method cell_toggle_line_numbers
1487 1487 */
1488 1488 Notebook.prototype.cell_toggle_line_numbers = function() {
1489 1489 this.get_selected_cell().toggle_line_numbers();
1490 1490 };
1491 1491
1492 1492 /**
1493 1493 * Set the codemirror mode for all code cells, including the default for
1494 1494 * new code cells.
1495 1495 *
1496 1496 * @method set_codemirror_mode
1497 1497 */
1498 1498 Notebook.prototype.set_codemirror_mode = function(newmode){
1499 1499 if (newmode === this.codemirror_mode) {
1500 1500 return;
1501 1501 }
1502 1502 this.codemirror_mode = newmode;
1503 1503 codecell.CodeCell.options_default.cm_config.mode = newmode;
1504 1504 modename = newmode.name || newmode
1505 1505
1506 1506 that = this;
1507 1507 CodeMirror.requireMode(modename, function(){
1508 1508 $.map(that.get_cells(), function(cell, i) {
1509 1509 if (cell.cell_type === 'code'){
1510 1510 cell.code_mirror.setOption('mode', newmode);
1511 1511 // This is currently redundant, because cm_config ends up as
1512 1512 // codemirror's own .options object, but I don't want to
1513 1513 // rely on that.
1514 1514 cell.cm_config.mode = newmode;
1515 1515 }
1516 1516 });
1517 1517 })
1518 1518 };
1519 1519
1520 1520 // Session related things
1521 1521
1522 1522 /**
1523 1523 * Start a new session and set it on each code cell.
1524 1524 *
1525 1525 * @method start_session
1526 1526 */
1527 1527 Notebook.prototype.start_session = function (kernel_name) {
1528 1528 if (kernel_name === undefined) {
1529 1529 kernel_name = this.default_kernel_name;
1530 1530 }
1531 1531 this.session = new session.Session({
1532 1532 base_url: this.base_url,
1533 1533 ws_url: this.ws_url,
1534 1534 notebook_path: this.notebook_path,
1535 1535 notebook_name: this.notebook_name,
1536 1536 // For now, create all sessions with the 'python' kernel, which is the
1537 1537 // default. Later, the user will be able to select kernels. This is
1538 1538 // overridden if KernelManager.kernel_cmd is specified for the server.
1539 1539 kernel_name: kernel_name,
1540 1540 notebook: this});
1541 1541
1542 1542 this.session.start($.proxy(this._session_started, this));
1543 1543 };
1544 1544
1545 1545
1546 1546 /**
1547 1547 * Once a session is started, link the code cells to the kernel and pass the
1548 1548 * comm manager to the widget manager
1549 1549 *
1550 1550 */
1551 1551 Notebook.prototype._session_started = function(){
1552 1552 this.kernel = this.session.kernel;
1553 1553 var ncells = this.ncells();
1554 1554 for (var i=0; i<ncells; i++) {
1555 1555 var cell = this.get_cell(i);
1556 1556 if (cell instanceof codecell.CodeCell) {
1557 1557 cell.set_kernel(this.session.kernel);
1558 1558 }
1559 1559 }
1560 1560 };
1561 1561
1562 1562 /**
1563 1563 * Prompt the user to restart the IPython kernel.
1564 1564 *
1565 1565 * @method restart_kernel
1566 1566 */
1567 1567 Notebook.prototype.restart_kernel = function () {
1568 1568 var that = this;
1569 1569 dialog.modal({
1570 1570 notebook: this,
1571 1571 keyboard_manager: this.keyboard_manager,
1572 1572 title : "Restart kernel or continue running?",
1573 1573 body : $("<p/>").text(
1574 1574 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1575 1575 ),
1576 1576 buttons : {
1577 1577 "Continue running" : {},
1578 1578 "Restart" : {
1579 1579 "class" : "btn-danger",
1580 1580 "click" : function() {
1581 1581 that.session.restart_kernel();
1582 1582 }
1583 1583 }
1584 1584 }
1585 1585 });
1586 1586 };
1587 1587
1588 1588 /**
1589 1589 * Execute or render cell outputs and go into command mode.
1590 1590 *
1591 1591 * @method execute_cell
1592 1592 */
1593 1593 Notebook.prototype.execute_cell = function () {
1594 1594 // mode = shift, ctrl, alt
1595 1595 var cell = this.get_selected_cell();
1596 1596 var cell_index = this.find_cell_index(cell);
1597 1597
1598 1598 cell.execute();
1599 1599 this.command_mode();
1600 1600 this.set_dirty(true);
1601 1601 };
1602 1602
1603 1603 /**
1604 1604 * Execute or render cell outputs and insert a new cell below.
1605 1605 *
1606 1606 * @method execute_cell_and_insert_below
1607 1607 */
1608 1608 Notebook.prototype.execute_cell_and_insert_below = function () {
1609 1609 var cell = this.get_selected_cell();
1610 1610 var cell_index = this.find_cell_index(cell);
1611 1611
1612 1612 cell.execute();
1613 1613
1614 1614 // If we are at the end always insert a new cell and return
1615 1615 if (cell_index === (this.ncells()-1)) {
1616 1616 this.command_mode();
1617 1617 this.insert_cell_below();
1618 1618 this.select(cell_index+1);
1619 1619 this.edit_mode();
1620 1620 this.scroll_to_bottom();
1621 1621 this.set_dirty(true);
1622 1622 return;
1623 1623 }
1624 1624
1625 1625 this.command_mode();
1626 1626 this.insert_cell_below();
1627 1627 this.select(cell_index+1);
1628 1628 this.edit_mode();
1629 1629 this.set_dirty(true);
1630 1630 };
1631 1631
1632 1632 /**
1633 1633 * Execute or render cell outputs and select the next cell.
1634 1634 *
1635 1635 * @method execute_cell_and_select_below
1636 1636 */
1637 1637 Notebook.prototype.execute_cell_and_select_below = function () {
1638 1638
1639 1639 var cell = this.get_selected_cell();
1640 1640 var cell_index = this.find_cell_index(cell);
1641 1641
1642 1642 cell.execute();
1643 1643
1644 1644 // If we are at the end always insert a new cell and return
1645 1645 if (cell_index === (this.ncells()-1)) {
1646 1646 this.command_mode();
1647 1647 this.insert_cell_below();
1648 1648 this.select(cell_index+1);
1649 1649 this.edit_mode();
1650 1650 this.scroll_to_bottom();
1651 1651 this.set_dirty(true);
1652 1652 return;
1653 1653 }
1654 1654
1655 1655 this.command_mode();
1656 1656 this.select(cell_index+1);
1657 1657 this.focus_cell();
1658 1658 this.set_dirty(true);
1659 1659 };
1660 1660
1661 1661 /**
1662 1662 * Execute all cells below the selected cell.
1663 1663 *
1664 1664 * @method execute_cells_below
1665 1665 */
1666 1666 Notebook.prototype.execute_cells_below = function () {
1667 1667 this.execute_cell_range(this.get_selected_index(), this.ncells());
1668 1668 this.scroll_to_bottom();
1669 1669 };
1670 1670
1671 1671 /**
1672 1672 * Execute all cells above the selected cell.
1673 1673 *
1674 1674 * @method execute_cells_above
1675 1675 */
1676 1676 Notebook.prototype.execute_cells_above = function () {
1677 1677 this.execute_cell_range(0, this.get_selected_index());
1678 1678 };
1679 1679
1680 1680 /**
1681 1681 * Execute all cells.
1682 1682 *
1683 1683 * @method execute_all_cells
1684 1684 */
1685 1685 Notebook.prototype.execute_all_cells = function () {
1686 1686 this.execute_cell_range(0, this.ncells());
1687 1687 this.scroll_to_bottom();
1688 1688 };
1689 1689
1690 1690 /**
1691 1691 * Execute a contiguous range of cells.
1692 1692 *
1693 1693 * @method execute_cell_range
1694 1694 * @param {Number} start Index of the first cell to execute (inclusive)
1695 1695 * @param {Number} end Index of the last cell to execute (exclusive)
1696 1696 */
1697 1697 Notebook.prototype.execute_cell_range = function (start, end) {
1698 1698 this.command_mode();
1699 1699 for (var i=start; i<end; i++) {
1700 1700 this.select(i);
1701 1701 this.execute_cell();
1702 1702 }
1703 1703 };
1704 1704
1705 1705 // Persistance and loading
1706 1706
1707 1707 /**
1708 1708 * Getter method for this notebook's name.
1709 1709 *
1710 1710 * @method get_notebook_name
1711 1711 * @return {String} This notebook's name (excluding file extension)
1712 1712 */
1713 1713 Notebook.prototype.get_notebook_name = function () {
1714 1714 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1715 1715 return nbname;
1716 1716 };
1717 1717
1718 1718 /**
1719 1719 * Setter method for this notebook's name.
1720 1720 *
1721 1721 * @method set_notebook_name
1722 1722 * @param {String} name A new name for this notebook
1723 1723 */
1724 1724 Notebook.prototype.set_notebook_name = function (name) {
1725 1725 this.notebook_name = name;
1726 1726 };
1727 1727
1728 1728 /**
1729 1729 * Check that a notebook's name is valid.
1730 1730 *
1731 1731 * @method test_notebook_name
1732 1732 * @param {String} nbname A name for this notebook
1733 1733 * @return {Boolean} True if the name is valid, false if invalid
1734 1734 */
1735 1735 Notebook.prototype.test_notebook_name = function (nbname) {
1736 1736 nbname = nbname || '';
1737 1737 if (nbname.length>0 && !this.notebook_name_blacklist_re.test(nbname)) {
1738 1738 return true;
1739 1739 } else {
1740 1740 return false;
1741 1741 }
1742 1742 };
1743 1743
1744 1744 /**
1745 1745 * Load a notebook from JSON (.ipynb).
1746 1746 *
1747 1747 * This currently handles one worksheet: others are deleted.
1748 1748 *
1749 1749 * @method fromJSON
1750 1750 * @param {Object} data JSON representation of a notebook
1751 1751 */
1752 1752 Notebook.prototype.fromJSON = function (data) {
1753 1753 var content = data.content;
1754 1754 var ncells = this.ncells();
1755 1755 var i;
1756 1756 for (i=0; i<ncells; i++) {
1757 1757 // Always delete cell 0 as they get renumbered as they are deleted.
1758 1758 this.delete_cell(0);
1759 1759 }
1760 1760 // Save the metadata and name.
1761 1761 this.metadata = content.metadata;
1762 1762 this.notebook_name = data.name;
1763 1763 var trusted = true;
1764 1764
1765 1765 // Trigger an event changing the kernel spec - this will set the default
1766 1766 // codemirror mode
1767 1767 if (this.metadata.kernelspec !== undefined) {
1768 1768 this.events.trigger('spec_changed.Kernel', this.metadata.kernelspec);
1769 1769 }
1770 1770
1771 1771 // Only handle 1 worksheet for now.
1772 1772 var worksheet = content.worksheets[0];
1773 1773 if (worksheet !== undefined) {
1774 1774 if (worksheet.metadata) {
1775 1775 this.worksheet_metadata = worksheet.metadata;
1776 1776 }
1777 1777 var new_cells = worksheet.cells;
1778 1778 ncells = new_cells.length;
1779 1779 var cell_data = null;
1780 1780 var new_cell = null;
1781 1781 for (i=0; i<ncells; i++) {
1782 1782 cell_data = new_cells[i];
1783 1783 // VERSIONHACK: plaintext -> raw
1784 1784 // handle never-released plaintext name for raw cells
1785 1785 if (cell_data.cell_type === 'plaintext'){
1786 1786 cell_data.cell_type = 'raw';
1787 1787 }
1788 1788
1789 1789 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1790 1790 new_cell.fromJSON(cell_data);
1791 1791 if (new_cell.cell_type == 'code' && !new_cell.output_area.trusted) {
1792 1792 trusted = false;
1793 1793 }
1794 1794 }
1795 1795 }
1796 1796 if (trusted != this.trusted) {
1797 1797 this.trusted = trusted;
1798 1798 this.events.trigger("trust_changed.Notebook", trusted);
1799 1799 }
1800 1800 if (content.worksheets.length > 1) {
1801 1801 dialog.modal({
1802 1802 notebook: this,
1803 1803 keyboard_manager: this.keyboard_manager,
1804 1804 title : "Multiple worksheets",
1805 1805 body : "This notebook has " + data.worksheets.length + " worksheets, " +
1806 1806 "but this version of IPython can only handle the first. " +
1807 1807 "If you save this notebook, worksheets after the first will be lost.",
1808 1808 buttons : {
1809 1809 OK : {
1810 1810 class : "btn-danger"
1811 1811 }
1812 1812 }
1813 1813 });
1814 1814 }
1815 1815 };
1816 1816
1817 1817 /**
1818 1818 * Dump this notebook into a JSON-friendly object.
1819 1819 *
1820 1820 * @method toJSON
1821 1821 * @return {Object} A JSON-friendly representation of this notebook.
1822 1822 */
1823 1823 Notebook.prototype.toJSON = function () {
1824 1824 var cells = this.get_cells();
1825 1825 var ncells = cells.length;
1826 1826 var cell_array = new Array(ncells);
1827 1827 var trusted = true;
1828 1828 for (var i=0; i<ncells; i++) {
1829 1829 var cell = cells[i];
1830 1830 if (cell.cell_type == 'code' && !cell.output_area.trusted) {
1831 1831 trusted = false;
1832 1832 }
1833 1833 cell_array[i] = cell.toJSON();
1834 1834 }
1835 1835 var data = {
1836 1836 // Only handle 1 worksheet for now.
1837 1837 worksheets : [{
1838 1838 cells: cell_array,
1839 1839 metadata: this.worksheet_metadata
1840 1840 }],
1841 1841 metadata : this.metadata
1842 1842 };
1843 1843 if (trusted != this.trusted) {
1844 1844 this.trusted = trusted;
1845 1845 this.events.trigger("trust_changed.Notebook", trusted);
1846 1846 }
1847 1847 return data;
1848 1848 };
1849 1849
1850 1850 /**
1851 1851 * Start an autosave timer, for periodically saving the notebook.
1852 1852 *
1853 1853 * @method set_autosave_interval
1854 1854 * @param {Integer} interval the autosave interval in milliseconds
1855 1855 */
1856 1856 Notebook.prototype.set_autosave_interval = function (interval) {
1857 1857 var that = this;
1858 1858 // clear previous interval, so we don't get simultaneous timers
1859 1859 if (this.autosave_timer) {
1860 1860 clearInterval(this.autosave_timer);
1861 1861 }
1862 1862
1863 1863 this.autosave_interval = this.minimum_autosave_interval = interval;
1864 1864 if (interval) {
1865 1865 this.autosave_timer = setInterval(function() {
1866 1866 if (that.dirty) {
1867 1867 that.save_notebook();
1868 1868 }
1869 1869 }, interval);
1870 1870 this.events.trigger("autosave_enabled.Notebook", interval);
1871 1871 } else {
1872 1872 this.autosave_timer = null;
1873 1873 this.events.trigger("autosave_disabled.Notebook");
1874 1874 }
1875 1875 };
1876 1876
1877 1877 /**
1878 1878 * Save this notebook on the server. This becomes a notebook instance's
1879 1879 * .save_notebook method *after* the entire notebook has been loaded.
1880 1880 *
1881 1881 * @method save_notebook
1882 1882 */
1883 1883 Notebook.prototype.save_notebook = function (extra_settings) {
1884 1884 // Create a JSON model to be sent to the server.
1885 1885 var model = {};
1886 1886 model.name = this.notebook_name;
1887 1887 model.path = this.notebook_path;
1888 1888 model.type = 'notebook';
1889 1889 model.format = 'json';
1890 1890 model.content = this.toJSON();
1891 1891 model.content.nbformat = this.nbformat;
1892 1892 model.content.nbformat_minor = this.nbformat_minor;
1893 1893 // time the ajax call for autosave tuning purposes.
1894 1894 var start = new Date().getTime();
1895 1895 // We do the call with settings so we can set cache to false.
1896 1896 var settings = {
1897 1897 processData : false,
1898 1898 cache : false,
1899 1899 type : "PUT",
1900 1900 data : JSON.stringify(model),
1901 1901 headers : {'Content-Type': 'application/json'},
1902 1902 success : $.proxy(this.save_notebook_success, this, start),
1903 1903 error : $.proxy(this.save_notebook_error, this)
1904 1904 };
1905 1905 if (extra_settings) {
1906 1906 for (var key in extra_settings) {
1907 1907 settings[key] = extra_settings[key];
1908 1908 }
1909 1909 }
1910 1910 this.events.trigger('notebook_saving.Notebook');
1911 1911 var url = utils.url_join_encode(
1912 1912 this.base_url,
1913 1913 'api/contents',
1914 1914 this.notebook_path,
1915 1915 this.notebook_name
1916 1916 );
1917 1917 $.ajax(url, settings);
1918 1918 };
1919 1919
1920 1920 /**
1921 1921 * Success callback for saving a notebook.
1922 1922 *
1923 1923 * @method save_notebook_success
1924 1924 * @param {Integer} start the time when the save request started
1925 1925 * @param {Object} data JSON representation of a notebook
1926 1926 * @param {String} status Description of response status
1927 1927 * @param {jqXHR} xhr jQuery Ajax object
1928 1928 */
1929 1929 Notebook.prototype.save_notebook_success = function (start, data, status, xhr) {
1930 1930 this.set_dirty(false);
1931 1931 this.events.trigger('notebook_saved.Notebook');
1932 1932 this._update_autosave_interval(start);
1933 1933 if (this._checkpoint_after_save) {
1934 1934 this.create_checkpoint();
1935 1935 this._checkpoint_after_save = false;
1936 1936 }
1937 1937 };
1938 1938
1939 1939 /**
1940 1940 * update the autosave interval based on how long the last save took
1941 1941 *
1942 1942 * @method _update_autosave_interval
1943 1943 * @param {Integer} timestamp when the save request started
1944 1944 */
1945 1945 Notebook.prototype._update_autosave_interval = function (start) {
1946 1946 var duration = (new Date().getTime() - start);
1947 1947 if (this.autosave_interval) {
1948 1948 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
1949 1949 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
1950 1950 // round to 10 seconds, otherwise we will be setting a new interval too often
1951 1951 interval = 10000 * Math.round(interval / 10000);
1952 1952 // set new interval, if it's changed
1953 1953 if (interval != this.autosave_interval) {
1954 1954 this.set_autosave_interval(interval);
1955 1955 }
1956 1956 }
1957 1957 };
1958 1958
1959 1959 /**
1960 1960 * Failure callback for saving a notebook.
1961 1961 *
1962 1962 * @method save_notebook_error
1963 1963 * @param {jqXHR} xhr jQuery Ajax object
1964 1964 * @param {String} status Description of response status
1965 1965 * @param {String} error HTTP error message
1966 1966 */
1967 1967 Notebook.prototype.save_notebook_error = function (xhr, status, error) {
1968 1968 this.events.trigger('notebook_save_failed.Notebook', [xhr, status, error]);
1969 1969 };
1970 1970
1971 1971 /**
1972 1972 * Explicitly trust the output of this notebook.
1973 1973 *
1974 1974 * @method trust_notebook
1975 1975 */
1976 1976 Notebook.prototype.trust_notebook = function (extra_settings) {
1977 1977 var body = $("<div>").append($("<p>")
1978 1978 .text("A trusted IPython notebook may execute hidden malicious code ")
1979 1979 .append($("<strong>")
1980 1980 .append(
1981 1981 $("<em>").text("when you open it")
1982 1982 )
1983 1983 ).append(".").append(
1984 1984 " Selecting trust will immediately reload this notebook in a trusted state."
1985 1985 ).append(
1986 1986 " For more information, see the "
1987 1987 ).append($("<a>").attr("href", "http://ipython.org/ipython-doc/2/notebook/security.html")
1988 1988 .text("IPython security documentation")
1989 1989 ).append(".")
1990 1990 );
1991 1991
1992 1992 var nb = this;
1993 1993 dialog.modal({
1994 1994 notebook: this,
1995 1995 keyboard_manager: this.keyboard_manager,
1996 1996 title: "Trust this notebook?",
1997 1997 body: body,
1998 1998
1999 1999 buttons: {
2000 2000 Cancel : {},
2001 2001 Trust : {
2002 2002 class : "btn-danger",
2003 2003 click : function () {
2004 2004 var cells = nb.get_cells();
2005 2005 for (var i = 0; i < cells.length; i++) {
2006 2006 var cell = cells[i];
2007 2007 if (cell.cell_type == 'code') {
2008 2008 cell.output_area.trusted = true;
2009 2009 }
2010 2010 }
2011 2011 this.events.on('notebook_saved.Notebook', function () {
2012 2012 window.location.reload();
2013 2013 });
2014 2014 nb.save_notebook();
2015 2015 }
2016 2016 }
2017 2017 }
2018 2018 });
2019 2019 };
2020 2020
2021 2021 Notebook.prototype.new_notebook = function(){
2022 2022 var path = this.notebook_path;
2023 2023 var base_url = this.base_url;
2024 2024 var settings = {
2025 2025 processData : false,
2026 2026 cache : false,
2027 2027 type : "POST",
2028 2028 dataType : "json",
2029 2029 async : false,
2030 2030 success : function (data, status, xhr){
2031 2031 var notebook_name = data.name;
2032 2032 window.open(
2033 2033 utils.url_join_encode(
2034 2034 base_url,
2035 2035 'notebooks',
2036 2036 path,
2037 2037 notebook_name
2038 2038 ),
2039 2039 '_blank'
2040 2040 );
2041 2041 },
2042 2042 error : utils.log_ajax_error,
2043 2043 };
2044 2044 var url = utils.url_join_encode(
2045 2045 base_url,
2046 2046 'api/contents',
2047 2047 path
2048 2048 );
2049 2049 $.ajax(url,settings);
2050 2050 };
2051 2051
2052 2052
2053 2053 Notebook.prototype.copy_notebook = function(){
2054 2054 var path = this.notebook_path;
2055 2055 var base_url = this.base_url;
2056 2056 var settings = {
2057 2057 processData : false,
2058 2058 cache : false,
2059 2059 type : "POST",
2060 2060 dataType : "json",
2061 2061 data : JSON.stringify({copy_from : this.notebook_name}),
2062 2062 async : false,
2063 2063 success : function (data, status, xhr) {
2064 2064 window.open(utils.url_join_encode(
2065 2065 base_url,
2066 2066 'notebooks',
2067 2067 data.path,
2068 2068 data.name
2069 2069 ), '_blank');
2070 2070 },
2071 2071 error : utils.log_ajax_error,
2072 2072 };
2073 2073 var url = utils.url_join_encode(
2074 2074 base_url,
2075 2075 'api/contents',
2076 2076 path
2077 2077 );
2078 2078 $.ajax(url,settings);
2079 2079 };
2080 2080
2081 2081 Notebook.prototype.rename = function (nbname) {
2082 2082 var that = this;
2083 2083 if (!nbname.match(/\.ipynb$/)) {
2084 2084 nbname = nbname + ".ipynb";
2085 2085 }
2086 2086 var data = {name: nbname};
2087 2087 var settings = {
2088 2088 processData : false,
2089 2089 cache : false,
2090 2090 type : "PATCH",
2091 2091 data : JSON.stringify(data),
2092 2092 dataType: "json",
2093 2093 headers : {'Content-Type': 'application/json'},
2094 2094 success : $.proxy(that.rename_success, this),
2095 2095 error : $.proxy(that.rename_error, this)
2096 2096 };
2097 2097 this.events.trigger('rename_notebook.Notebook', data);
2098 2098 var url = utils.url_join_encode(
2099 2099 this.base_url,
2100 2100 'api/contents',
2101 2101 this.notebook_path,
2102 2102 this.notebook_name
2103 2103 );
2104 2104 $.ajax(url, settings);
2105 2105 };
2106 2106
2107 2107 Notebook.prototype.delete = function () {
2108 2108 var that = this;
2109 2109 var settings = {
2110 2110 processData : false,
2111 2111 cache : false,
2112 2112 type : "DELETE",
2113 2113 dataType: "json",
2114 2114 error : utils.log_ajax_error,
2115 2115 };
2116 2116 var url = utils.url_join_encode(
2117 2117 this.base_url,
2118 2118 'api/contents',
2119 2119 this.notebook_path,
2120 2120 this.notebook_name
2121 2121 );
2122 2122 $.ajax(url, settings);
2123 2123 };
2124 2124
2125 2125
2126 2126 Notebook.prototype.rename_success = function (json, status, xhr) {
2127 2127 var name = this.notebook_name = json.name;
2128 2128 var path = json.path;
2129 2129 this.session.rename_notebook(name, path);
2130 2130 this.events.trigger('notebook_renamed.Notebook', json);
2131 2131 };
2132 2132
2133 2133 Notebook.prototype.rename_error = function (xhr, status, error) {
2134 2134 var that = this;
2135 2135 var dialog_body = $('<div/>').append(
2136 2136 $("<p/>").text('This notebook name already exists.')
2137 2137 );
2138 2138 this.events.trigger('notebook_rename_failed.Notebook', [xhr, status, error]);
2139 2139 dialog.modal({
2140 2140 notebook: this,
2141 2141 keyboard_manager: this.keyboard_manager,
2142 2142 title: "Notebook Rename Error!",
2143 2143 body: dialog_body,
2144 2144 buttons : {
2145 2145 "Cancel": {},
2146 2146 "OK": {
2147 2147 class: "btn-primary",
2148 2148 click: function () {
2149 2149 this.save_widget.rename_notebook({notebook:that});
2150 2150 }}
2151 2151 },
2152 2152 open : function (event, ui) {
2153 2153 var that = $(this);
2154 2154 // Upon ENTER, click the OK button.
2155 2155 that.find('input[type="text"]').keydown(function (event, ui) {
2156 2156 if (event.which === this.keyboard.keycodes.enter) {
2157 2157 that.find('.btn-primary').first().click();
2158 2158 }
2159 2159 });
2160 2160 that.find('input[type="text"]').focus();
2161 2161 }
2162 2162 });
2163 2163 };
2164 2164
2165 2165 /**
2166 2166 * Request a notebook's data from the server.
2167 2167 *
2168 2168 * @method load_notebook
2169 2169 * @param {String} notebook_name and path A notebook to load
2170 2170 */
2171 2171 Notebook.prototype.load_notebook = function (notebook_name, notebook_path) {
2172 2172 var that = this;
2173 2173 this.notebook_name = notebook_name;
2174 2174 this.notebook_path = notebook_path;
2175 2175 // We do the call with settings so we can set cache to false.
2176 2176 var settings = {
2177 2177 processData : false,
2178 2178 cache : false,
2179 2179 type : "GET",
2180 2180 dataType : "json",
2181 2181 success : $.proxy(this.load_notebook_success,this),
2182 2182 error : $.proxy(this.load_notebook_error,this),
2183 2183 };
2184 2184 this.events.trigger('notebook_loading.Notebook');
2185 2185 var url = utils.url_join_encode(
2186 2186 this.base_url,
2187 2187 'api/contents',
2188 2188 this.notebook_path,
2189 2189 this.notebook_name
2190 2190 );
2191 2191 $.ajax(url, settings);
2192 2192 };
2193 2193
2194 2194 /**
2195 2195 * Success callback for loading a notebook from the server.
2196 2196 *
2197 2197 * Load notebook data from the JSON response.
2198 2198 *
2199 2199 * @method load_notebook_success
2200 2200 * @param {Object} data JSON representation of a notebook
2201 2201 * @param {String} status Description of response status
2202 2202 * @param {jqXHR} xhr jQuery Ajax object
2203 2203 */
2204 2204 Notebook.prototype.load_notebook_success = function (data, status, xhr) {
2205 2205 this.fromJSON(data);
2206 2206 if (this.ncells() === 0) {
2207 2207 this.insert_cell_below('code');
2208 2208 this.edit_mode(0);
2209 2209 } else {
2210 2210 this.select(0);
2211 2211 this.handle_command_mode(this.get_cell(0));
2212 2212 }
2213 2213 this.set_dirty(false);
2214 2214 this.scroll_to_top();
2215 2215 if (data.orig_nbformat !== undefined && data.nbformat !== data.orig_nbformat) {
2216 2216 var msg = "This notebook has been converted from an older " +
2217 2217 "notebook format (v"+data.orig_nbformat+") to the current notebook " +
2218 2218 "format (v"+data.nbformat+"). The next time you save this notebook, the " +
2219 2219 "newer notebook format will be used and older versions of IPython " +
2220 2220 "may not be able to read it. To keep the older version, close the " +
2221 2221 "notebook without saving it.";
2222 2222 dialog.modal({
2223 2223 notebook: this,
2224 2224 keyboard_manager: this.keyboard_manager,
2225 2225 title : "Notebook converted",
2226 2226 body : msg,
2227 2227 buttons : {
2228 2228 OK : {
2229 2229 class : "btn-primary"
2230 2230 }
2231 2231 }
2232 2232 });
2233 2233 } else if (data.orig_nbformat_minor !== undefined && data.nbformat_minor !== data.orig_nbformat_minor) {
2234 2234 var that = this;
2235 2235 var orig_vs = 'v' + data.nbformat + '.' + data.orig_nbformat_minor;
2236 2236 var this_vs = 'v' + data.nbformat + '.' + this.nbformat_minor;
2237 2237 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
2238 2238 this_vs + ". You can still work with this notebook, but some features " +
2239 2239 "introduced in later notebook versions may not be available.";
2240 2240
2241 2241 dialog.modal({
2242 2242 notebook: this,
2243 2243 keyboard_manager: this.keyboard_manager,
2244 2244 title : "Newer Notebook",
2245 2245 body : msg,
2246 2246 buttons : {
2247 2247 OK : {
2248 2248 class : "btn-danger"
2249 2249 }
2250 2250 }
2251 2251 });
2252 2252
2253 2253 }
2254 2254
2255 2255 // Create the session after the notebook is completely loaded to prevent
2256 2256 // code execution upon loading, which is a security risk.
2257 2257 if (this.session === null) {
2258 2258 var kernelspec = this.metadata.kernelspec || {};
2259 2259 var kernel_name = kernelspec.name || this.default_kernel_name;
2260 2260
2261 2261 this.start_session(kernel_name);
2262 2262 }
2263 2263 // load our checkpoint list
2264 2264 this.list_checkpoints();
2265 2265
2266 2266 // load toolbar state
2267 2267 if (this.metadata.celltoolbar) {
2268 2268 celltoolbar.CellToolbar.global_show();
2269 2269 celltoolbar.CellToolbar.activate_preset(this.metadata.celltoolbar);
2270 2270 } else {
2271 2271 celltoolbar.CellToolbar.global_hide();
2272 2272 }
2273 2273
2274 2274 // now that we're fully loaded, it is safe to restore save functionality
2275 2275 delete(this.save_notebook);
2276 2276 this.events.trigger('notebook_loaded.Notebook');
2277 2277 };
2278 2278
2279 2279 /**
2280 2280 * Failure callback for loading a notebook from the server.
2281 2281 *
2282 2282 * @method load_notebook_error
2283 2283 * @param {jqXHR} xhr jQuery Ajax object
2284 2284 * @param {String} status Description of response status
2285 2285 * @param {String} error HTTP error message
2286 2286 */
2287 2287 Notebook.prototype.load_notebook_error = function (xhr, status, error) {
2288 2288 this.events.trigger('notebook_load_failed.Notebook', [xhr, status, error]);
2289 utils.log_ajax_error(xhr, status, error);
2289 2290 var msg;
2290 2291 if (xhr.status === 400) {
2291 msg = error;
2292 msg = escape(utils.ajax_error_msg(xhr));
2292 2293 } else if (xhr.status === 500) {
2293 2294 msg = "An unknown error occurred while loading this notebook. " +
2294 2295 "This version can load notebook formats " +
2295 "v" + this.nbformat + " or earlier.";
2296 "v" + this.nbformat + " or earlier. See the server log for details.";
2296 2297 }
2297 2298 dialog.modal({
2298 2299 notebook: this,
2299 2300 keyboard_manager: this.keyboard_manager,
2300 2301 title: "Error loading notebook",
2301 2302 body : msg,
2302 2303 buttons : {
2303 2304 "OK": {}
2304 2305 }
2305 2306 });
2306 2307 };
2307 2308
2308 2309 /********************* checkpoint-related *********************/
2309 2310
2310 2311 /**
2311 2312 * Save the notebook then immediately create a checkpoint.
2312 2313 *
2313 2314 * @method save_checkpoint
2314 2315 */
2315 2316 Notebook.prototype.save_checkpoint = function () {
2316 2317 this._checkpoint_after_save = true;
2317 2318 this.save_notebook();
2318 2319 };
2319 2320
2320 2321 /**
2321 2322 * Add a checkpoint for this notebook.
2322 2323 * for use as a callback from checkpoint creation.
2323 2324 *
2324 2325 * @method add_checkpoint
2325 2326 */
2326 2327 Notebook.prototype.add_checkpoint = function (checkpoint) {
2327 2328 var found = false;
2328 2329 for (var i = 0; i < this.checkpoints.length; i++) {
2329 2330 var existing = this.checkpoints[i];
2330 2331 if (existing.id == checkpoint.id) {
2331 2332 found = true;
2332 2333 this.checkpoints[i] = checkpoint;
2333 2334 break;
2334 2335 }
2335 2336 }
2336 2337 if (!found) {
2337 2338 this.checkpoints.push(checkpoint);
2338 2339 }
2339 2340 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
2340 2341 };
2341 2342
2342 2343 /**
2343 2344 * List checkpoints for this notebook.
2344 2345 *
2345 2346 * @method list_checkpoints
2346 2347 */
2347 2348 Notebook.prototype.list_checkpoints = function () {
2348 2349 var url = utils.url_join_encode(
2349 2350 this.base_url,
2350 2351 'api/contents',
2351 2352 this.notebook_path,
2352 2353 this.notebook_name,
2353 2354 'checkpoints'
2354 2355 );
2355 2356 $.get(url).done(
2356 2357 $.proxy(this.list_checkpoints_success, this)
2357 2358 ).fail(
2358 2359 $.proxy(this.list_checkpoints_error, this)
2359 2360 );
2360 2361 };
2361 2362
2362 2363 /**
2363 2364 * Success callback for listing checkpoints.
2364 2365 *
2365 2366 * @method list_checkpoint_success
2366 2367 * @param {Object} data JSON representation of a checkpoint
2367 2368 * @param {String} status Description of response status
2368 2369 * @param {jqXHR} xhr jQuery Ajax object
2369 2370 */
2370 2371 Notebook.prototype.list_checkpoints_success = function (data, status, xhr) {
2371 2372 data = $.parseJSON(data);
2372 2373 this.checkpoints = data;
2373 2374 if (data.length) {
2374 2375 this.last_checkpoint = data[data.length - 1];
2375 2376 } else {
2376 2377 this.last_checkpoint = null;
2377 2378 }
2378 2379 this.events.trigger('checkpoints_listed.Notebook', [data]);
2379 2380 };
2380 2381
2381 2382 /**
2382 2383 * Failure callback for listing a checkpoint.
2383 2384 *
2384 2385 * @method list_checkpoint_error
2385 2386 * @param {jqXHR} xhr jQuery Ajax object
2386 2387 * @param {String} status Description of response status
2387 2388 * @param {String} error_msg HTTP error message
2388 2389 */
2389 2390 Notebook.prototype.list_checkpoints_error = function (xhr, status, error_msg) {
2390 2391 this.events.trigger('list_checkpoints_failed.Notebook');
2391 2392 };
2392 2393
2393 2394 /**
2394 2395 * Create a checkpoint of this notebook on the server from the most recent save.
2395 2396 *
2396 2397 * @method create_checkpoint
2397 2398 */
2398 2399 Notebook.prototype.create_checkpoint = function () {
2399 2400 var url = utils.url_join_encode(
2400 2401 this.base_url,
2401 2402 'api/contents',
2402 2403 this.notebook_path,
2403 2404 this.notebook_name,
2404 2405 'checkpoints'
2405 2406 );
2406 2407 $.post(url).done(
2407 2408 $.proxy(this.create_checkpoint_success, this)
2408 2409 ).fail(
2409 2410 $.proxy(this.create_checkpoint_error, this)
2410 2411 );
2411 2412 };
2412 2413
2413 2414 /**
2414 2415 * Success callback for creating a checkpoint.
2415 2416 *
2416 2417 * @method create_checkpoint_success
2417 2418 * @param {Object} data JSON representation of a checkpoint
2418 2419 * @param {String} status Description of response status
2419 2420 * @param {jqXHR} xhr jQuery Ajax object
2420 2421 */
2421 2422 Notebook.prototype.create_checkpoint_success = function (data, status, xhr) {
2422 2423 data = $.parseJSON(data);
2423 2424 this.add_checkpoint(data);
2424 2425 this.events.trigger('checkpoint_created.Notebook', data);
2425 2426 };
2426 2427
2427 2428 /**
2428 2429 * Failure callback for creating a checkpoint.
2429 2430 *
2430 2431 * @method create_checkpoint_error
2431 2432 * @param {jqXHR} xhr jQuery Ajax object
2432 2433 * @param {String} status Description of response status
2433 2434 * @param {String} error_msg HTTP error message
2434 2435 */
2435 2436 Notebook.prototype.create_checkpoint_error = function (xhr, status, error_msg) {
2436 2437 this.events.trigger('checkpoint_failed.Notebook');
2437 2438 };
2438 2439
2439 2440 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2440 2441 var that = this;
2441 2442 checkpoint = checkpoint || this.last_checkpoint;
2442 2443 if ( ! checkpoint ) {
2443 2444 console.log("restore dialog, but no checkpoint to restore to!");
2444 2445 return;
2445 2446 }
2446 2447 var body = $('<div/>').append(
2447 2448 $('<p/>').addClass("p-space").text(
2448 2449 "Are you sure you want to revert the notebook to " +
2449 2450 "the latest checkpoint?"
2450 2451 ).append(
2451 2452 $("<strong/>").text(
2452 2453 " This cannot be undone."
2453 2454 )
2454 2455 )
2455 2456 ).append(
2456 2457 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2457 2458 ).append(
2458 2459 $('<p/>').addClass("p-space").text(
2459 2460 Date(checkpoint.last_modified)
2460 2461 ).css("text-align", "center")
2461 2462 );
2462 2463
2463 2464 dialog.modal({
2464 2465 notebook: this,
2465 2466 keyboard_manager: this.keyboard_manager,
2466 2467 title : "Revert notebook to checkpoint",
2467 2468 body : body,
2468 2469 buttons : {
2469 2470 Revert : {
2470 2471 class : "btn-danger",
2471 2472 click : function () {
2472 2473 that.restore_checkpoint(checkpoint.id);
2473 2474 }
2474 2475 },
2475 2476 Cancel : {}
2476 2477 }
2477 2478 });
2478 2479 };
2479 2480
2480 2481 /**
2481 2482 * Restore the notebook to a checkpoint state.
2482 2483 *
2483 2484 * @method restore_checkpoint
2484 2485 * @param {String} checkpoint ID
2485 2486 */
2486 2487 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2487 2488 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2488 2489 var url = utils.url_join_encode(
2489 2490 this.base_url,
2490 2491 'api/contents',
2491 2492 this.notebook_path,
2492 2493 this.notebook_name,
2493 2494 'checkpoints',
2494 2495 checkpoint
2495 2496 );
2496 2497 $.post(url).done(
2497 2498 $.proxy(this.restore_checkpoint_success, this)
2498 2499 ).fail(
2499 2500 $.proxy(this.restore_checkpoint_error, this)
2500 2501 );
2501 2502 };
2502 2503
2503 2504 /**
2504 2505 * Success callback for restoring a notebook to a checkpoint.
2505 2506 *
2506 2507 * @method restore_checkpoint_success
2507 2508 * @param {Object} data (ignored, should be empty)
2508 2509 * @param {String} status Description of response status
2509 2510 * @param {jqXHR} xhr jQuery Ajax object
2510 2511 */
2511 2512 Notebook.prototype.restore_checkpoint_success = function (data, status, xhr) {
2512 2513 this.events.trigger('checkpoint_restored.Notebook');
2513 2514 this.load_notebook(this.notebook_name, this.notebook_path);
2514 2515 };
2515 2516
2516 2517 /**
2517 2518 * Failure callback for restoring a notebook to a checkpoint.
2518 2519 *
2519 2520 * @method restore_checkpoint_error
2520 2521 * @param {jqXHR} xhr jQuery Ajax object
2521 2522 * @param {String} status Description of response status
2522 2523 * @param {String} error_msg HTTP error message
2523 2524 */
2524 2525 Notebook.prototype.restore_checkpoint_error = function (xhr, status, error_msg) {
2525 2526 this.events.trigger('checkpoint_restore_failed.Notebook');
2526 2527 };
2527 2528
2528 2529 /**
2529 2530 * Delete a notebook checkpoint.
2530 2531 *
2531 2532 * @method delete_checkpoint
2532 2533 * @param {String} checkpoint ID
2533 2534 */
2534 2535 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2535 2536 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2536 2537 var url = utils.url_join_encode(
2537 2538 this.base_url,
2538 2539 'api/contents',
2539 2540 this.notebook_path,
2540 2541 this.notebook_name,
2541 2542 'checkpoints',
2542 2543 checkpoint
2543 2544 );
2544 2545 $.ajax(url, {
2545 2546 type: 'DELETE',
2546 2547 success: $.proxy(this.delete_checkpoint_success, this),
2547 2548 error: $.proxy(this.delete_checkpoint_error, this)
2548 2549 });
2549 2550 };
2550 2551
2551 2552 /**
2552 2553 * Success callback for deleting a notebook checkpoint
2553 2554 *
2554 2555 * @method delete_checkpoint_success
2555 2556 * @param {Object} data (ignored, should be empty)
2556 2557 * @param {String} status Description of response status
2557 2558 * @param {jqXHR} xhr jQuery Ajax object
2558 2559 */
2559 2560 Notebook.prototype.delete_checkpoint_success = function (data, status, xhr) {
2560 2561 this.events.trigger('checkpoint_deleted.Notebook', data);
2561 2562 this.load_notebook(this.notebook_name, this.notebook_path);
2562 2563 };
2563 2564
2564 2565 /**
2565 2566 * Failure callback for deleting a notebook checkpoint.
2566 2567 *
2567 2568 * @method delete_checkpoint_error
2568 2569 * @param {jqXHR} xhr jQuery Ajax object
2569 2570 * @param {String} status Description of response status
2570 * @param {String} error_msg HTTP error message
2571 * @param {String} error HTTP error message
2571 2572 */
2572 Notebook.prototype.delete_checkpoint_error = function (xhr, status, error_msg) {
2573 this.events.trigger('checkpoint_delete_failed.Notebook');
2573 Notebook.prototype.delete_checkpoint_error = function (xhr, status, error) {
2574 this.events.trigger('checkpoint_delete_failed.Notebook', [xhr, status, error]);
2574 2575 };
2575 2576
2576 2577
2577 2578 // For backwards compatability.
2578 2579 IPython.Notebook = Notebook;
2579 2580
2580 2581 return {'Notebook': Notebook};
2581 2582 });
@@ -1,107 +1,109
1 1 """API for reading notebooks.
2 2
3 3 Authors:
4 4
5 5 * Jonathan Frederic
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2013 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 import json
20 20
21 21 from . import v1
22 22 from . import v2
23 23 from . import v3
24 24
25 25 versions = {
26 26 1: v1,
27 27 2: v2,
28 28 3: v3,
29 29 }
30 30
31 31 #-----------------------------------------------------------------------------
32 32 # Code
33 33 #-----------------------------------------------------------------------------
34 34
35 35 class NotJSONError(ValueError):
36 36 pass
37 37
38 38 def parse_json(s, **kwargs):
39 39 """Parse a JSON string into a dict."""
40 40 try:
41 41 nb_dict = json.loads(s, **kwargs)
42 42 except ValueError:
43 43 # Limit the error message to 80 characters. Display whatever JSON will fit.
44 44 raise NotJSONError(("Notebook does not appear to be JSON: %r" % s)[:77] + "...")
45 45 return nb_dict
46 46
47 47 # High level API
48 48
49 49 def get_version(nb):
50 50 """Get the version of a notebook.
51 51
52 52 Parameters
53 53 ----------
54 54 nb : dict
55 55 NotebookNode or dict containing notebook data.
56 56
57 57 Returns
58 58 -------
59 59 Tuple containing major (int) and minor (int) version numbers
60 60 """
61 61 major = nb.get('nbformat', 1)
62 62 minor = nb.get('nbformat_minor', 0)
63 63 return (major, minor)
64 64
65 65
66 66 def reads(s, **kwargs):
67 67 """Read a notebook from a json string and return the
68 68 NotebookNode object.
69 69
70 70 This function properly reads notebooks of any version. No version
71 71 conversion is performed.
72 72
73 73 Parameters
74 74 ----------
75 75 s : unicode
76 76 The raw unicode string to read the notebook from.
77 77
78 78 Returns
79 79 -------
80 80 nb : NotebookNode
81 81 The notebook that was read.
82 82 """
83 from .current import NBFormatError
84
83 85 nb_dict = parse_json(s, **kwargs)
84 86 (major, minor) = get_version(nb_dict)
85 87 if major in versions:
86 88 return versions[major].to_notebook_json(nb_dict, minor=minor)
87 89 else:
88 90 raise NBFormatError('Unsupported nbformat version %s' % major)
89 91
90 92
91 93 def read(fp, **kwargs):
92 94 """Read a notebook from a file and return the NotebookNode object.
93 95
94 96 This function properly reads notebooks of any version. No version
95 97 conversion is performed.
96 98
97 99 Parameters
98 100 ----------
99 101 fp : file
100 102 Any file-like object with a read method.
101 103
102 104 Returns
103 105 -------
104 106 nb : NotebookNode
105 107 The notebook that was read.
106 108 """
107 109 return reads(fp.read(), **kwargs)
General Comments 0
You need to be logged in to leave comments. Login now