##// END OF EJS Templates
Merge remote-tracking branch 'upstream/master'
Doug Blank -
r15247:13bba28f merge
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,701 +1,772 b''
1 1 # -*- coding: utf-8 -*-
2 2 """Top-level display functions for displaying object in different formats.
3 3
4 4 Authors:
5 5
6 6 * Brian Granger
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2013 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 from __future__ import print_function
21 21
22 22 import os
23 23 import struct
24 24
25 25 from IPython.utils.py3compat import (string_types, cast_bytes_py2, cast_unicode,
26 26 unicode_type)
27
27 from IPython.testing.skipdoctest import skip_doctest
28 28 from .displaypub import publish_display_data
29 29
30 30 #-----------------------------------------------------------------------------
31 31 # utility functions
32 32 #-----------------------------------------------------------------------------
33 33
34 34 def _safe_exists(path):
35 35 """Check path, but don't let exceptions raise"""
36 36 try:
37 37 return os.path.exists(path)
38 38 except Exception:
39 39 return False
40 40
41 41 def _merge(d1, d2):
42 42 """Like update, but merges sub-dicts instead of clobbering at the top level.
43 43
44 44 Updates d1 in-place
45 45 """
46 46
47 47 if not isinstance(d2, dict) or not isinstance(d1, dict):
48 48 return d2
49 49 for key, value in d2.items():
50 50 d1[key] = _merge(d1.get(key), value)
51 51 return d1
52 52
53 53 def _display_mimetype(mimetype, objs, raw=False, metadata=None):
54 54 """internal implementation of all display_foo methods
55 55
56 56 Parameters
57 57 ----------
58 58 mimetype : str
59 59 The mimetype to be published (e.g. 'image/png')
60 60 objs : tuple of objects
61 61 The Python objects to display, or if raw=True raw text data to
62 62 display.
63 63 raw : bool
64 64 Are the data objects raw data or Python objects that need to be
65 65 formatted before display? [default: False]
66 66 metadata : dict (optional)
67 67 Metadata to be associated with the specific mimetype output.
68 68 """
69 69 if metadata:
70 70 metadata = {mimetype: metadata}
71 71 if raw:
72 72 # turn list of pngdata into list of { 'image/png': pngdata }
73 73 objs = [ {mimetype: obj} for obj in objs ]
74 74 display(*objs, raw=raw, metadata=metadata, include=[mimetype])
75 75
76 76 #-----------------------------------------------------------------------------
77 77 # Main functions
78 78 #-----------------------------------------------------------------------------
79 79
80 80 def display(*objs, **kwargs):
81 81 """Display a Python object in all frontends.
82 82
83 83 By default all representations will be computed and sent to the frontends.
84 84 Frontends can decide which representation is used and how.
85 85
86 86 Parameters
87 87 ----------
88 88 objs : tuple of objects
89 89 The Python objects to display.
90 90 raw : bool, optional
91 91 Are the objects to be displayed already mimetype-keyed dicts of raw display data,
92 92 or Python objects that need to be formatted before display? [default: False]
93 93 include : list or tuple, optional
94 94 A list of format type strings (MIME types) to include in the
95 95 format data dict. If this is set *only* the format types included
96 96 in this list will be computed.
97 97 exclude : list or tuple, optional
98 98 A list of format type strings (MIME types) to exclude in the format
99 99 data dict. If this is set all format types will be computed,
100 100 except for those included in this argument.
101 101 metadata : dict, optional
102 102 A dictionary of metadata to associate with the output.
103 103 mime-type keys in this dictionary will be associated with the individual
104 104 representation formats, if they exist.
105 105 """
106 106 raw = kwargs.get('raw', False)
107 107 include = kwargs.get('include')
108 108 exclude = kwargs.get('exclude')
109 109 metadata = kwargs.get('metadata')
110 110
111 111 from IPython.core.interactiveshell import InteractiveShell
112 112
113 113 if not raw:
114 114 format = InteractiveShell.instance().display_formatter.format
115 115
116 116 for obj in objs:
117 117
118 118 # If _ipython_display_ is defined, use that to display this object.
119 119 display_method = getattr(obj, '_ipython_display_', None)
120 120 if display_method is not None:
121 121 try:
122 122 display_method(**kwargs)
123 123 except NotImplementedError:
124 124 pass
125 125 else:
126 126 continue
127 127 if raw:
128 128 publish_display_data('display', obj, metadata)
129 129 else:
130 130 format_dict, md_dict = format(obj, include=include, exclude=exclude)
131 131 if metadata:
132 132 # kwarg-specified metadata gets precedence
133 133 _merge(md_dict, metadata)
134 134 publish_display_data('display', format_dict, md_dict)
135 135
136 136
137 137 def display_pretty(*objs, **kwargs):
138 138 """Display the pretty (default) representation of an object.
139 139
140 140 Parameters
141 141 ----------
142 142 objs : tuple of objects
143 143 The Python objects to display, or if raw=True raw text data to
144 144 display.
145 145 raw : bool
146 146 Are the data objects raw data or Python objects that need to be
147 147 formatted before display? [default: False]
148 148 metadata : dict (optional)
149 149 Metadata to be associated with the specific mimetype output.
150 150 """
151 151 _display_mimetype('text/plain', objs, **kwargs)
152 152
153 153
154 154 def display_html(*objs, **kwargs):
155 155 """Display the HTML representation of an object.
156 156
157 157 Parameters
158 158 ----------
159 159 objs : tuple of objects
160 160 The Python objects to display, or if raw=True raw HTML data to
161 161 display.
162 162 raw : bool
163 163 Are the data objects raw data or Python objects that need to be
164 164 formatted before display? [default: False]
165 165 metadata : dict (optional)
166 166 Metadata to be associated with the specific mimetype output.
167 167 """
168 168 _display_mimetype('text/html', objs, **kwargs)
169 169
170 170
171 171 def display_svg(*objs, **kwargs):
172 172 """Display the SVG representation of an object.
173 173
174 174 Parameters
175 175 ----------
176 176 objs : tuple of objects
177 177 The Python objects to display, or if raw=True raw svg data to
178 178 display.
179 179 raw : bool
180 180 Are the data objects raw data or Python objects that need to be
181 181 formatted before display? [default: False]
182 182 metadata : dict (optional)
183 183 Metadata to be associated with the specific mimetype output.
184 184 """
185 185 _display_mimetype('image/svg+xml', objs, **kwargs)
186 186
187 187
188 188 def display_png(*objs, **kwargs):
189 189 """Display the PNG representation of an object.
190 190
191 191 Parameters
192 192 ----------
193 193 objs : tuple of objects
194 194 The Python objects to display, or if raw=True raw png data to
195 195 display.
196 196 raw : bool
197 197 Are the data objects raw data or Python objects that need to be
198 198 formatted before display? [default: False]
199 199 metadata : dict (optional)
200 200 Metadata to be associated with the specific mimetype output.
201 201 """
202 202 _display_mimetype('image/png', objs, **kwargs)
203 203
204 204
205 205 def display_jpeg(*objs, **kwargs):
206 206 """Display the JPEG representation of an object.
207 207
208 208 Parameters
209 209 ----------
210 210 objs : tuple of objects
211 211 The Python objects to display, or if raw=True raw JPEG data to
212 212 display.
213 213 raw : bool
214 214 Are the data objects raw data or Python objects that need to be
215 215 formatted before display? [default: False]
216 216 metadata : dict (optional)
217 217 Metadata to be associated with the specific mimetype output.
218 218 """
219 219 _display_mimetype('image/jpeg', objs, **kwargs)
220 220
221 221
222 222 def display_latex(*objs, **kwargs):
223 223 """Display the LaTeX representation of an object.
224 224
225 225 Parameters
226 226 ----------
227 227 objs : tuple of objects
228 228 The Python objects to display, or if raw=True raw latex data to
229 229 display.
230 230 raw : bool
231 231 Are the data objects raw data or Python objects that need to be
232 232 formatted before display? [default: False]
233 233 metadata : dict (optional)
234 234 Metadata to be associated with the specific mimetype output.
235 235 """
236 236 _display_mimetype('text/latex', objs, **kwargs)
237 237
238 238
239 239 def display_json(*objs, **kwargs):
240 240 """Display the JSON representation of an object.
241 241
242 242 Note that not many frontends support displaying JSON.
243 243
244 244 Parameters
245 245 ----------
246 246 objs : tuple of objects
247 247 The Python objects to display, or if raw=True raw json data to
248 248 display.
249 249 raw : bool
250 250 Are the data objects raw data or Python objects that need to be
251 251 formatted before display? [default: False]
252 252 metadata : dict (optional)
253 253 Metadata to be associated with the specific mimetype output.
254 254 """
255 255 _display_mimetype('application/json', objs, **kwargs)
256 256
257 257
258 258 def display_javascript(*objs, **kwargs):
259 259 """Display the Javascript representation of an object.
260 260
261 261 Parameters
262 262 ----------
263 263 objs : tuple of objects
264 264 The Python objects to display, or if raw=True raw javascript data to
265 265 display.
266 266 raw : bool
267 267 Are the data objects raw data or Python objects that need to be
268 268 formatted before display? [default: False]
269 269 metadata : dict (optional)
270 270 Metadata to be associated with the specific mimetype output.
271 271 """
272 272 _display_mimetype('application/javascript', objs, **kwargs)
273 273
274
275 def display_pdf(*objs, **kwargs):
276 """Display the PDF representation of an object.
277
278 Parameters
279 ----------
280 objs : tuple of objects
281 The Python objects to display, or if raw=True raw javascript data to
282 display.
283 raw : bool
284 Are the data objects raw data or Python objects that need to be
285 formatted before display? [default: False]
286 metadata : dict (optional)
287 Metadata to be associated with the specific mimetype output.
288 """
289 _display_mimetype('application/pdf', objs, **kwargs)
290
291
274 292 #-----------------------------------------------------------------------------
275 293 # Smart classes
276 294 #-----------------------------------------------------------------------------
277 295
278 296
279 297 class DisplayObject(object):
280 298 """An object that wraps data to be displayed."""
281 299
282 300 _read_flags = 'r'
283 301
284 302 def __init__(self, data=None, url=None, filename=None):
285 303 """Create a display object given raw data.
286 304
287 305 When this object is returned by an expression or passed to the
288 306 display function, it will result in the data being displayed
289 307 in the frontend. The MIME type of the data should match the
290 308 subclasses used, so the Png subclass should be used for 'image/png'
291 309 data. If the data is a URL, the data will first be downloaded
292 310 and then displayed. If
293 311
294 312 Parameters
295 313 ----------
296 314 data : unicode, str or bytes
297 315 The raw data or a URL or file to load the data from
298 316 url : unicode
299 317 A URL to download the data from.
300 318 filename : unicode
301 319 Path to a local file to load the data from.
302 320 """
303 321 if data is not None and isinstance(data, string_types):
304 322 if data.startswith('http') and url is None:
305 323 url = data
306 324 filename = None
307 325 data = None
308 326 elif _safe_exists(data) and filename is None:
309 327 url = None
310 328 filename = data
311 329 data = None
312 330
313 331 self.data = data
314 332 self.url = url
315 333 self.filename = None if filename is None else unicode_type(filename)
316 334
317 335 self.reload()
318 336 self._check_data()
319 337
320 338 def _check_data(self):
321 339 """Override in subclasses if there's something to check."""
322 340 pass
323 341
324 342 def reload(self):
325 343 """Reload the raw data from file or URL."""
326 344 if self.filename is not None:
327 345 with open(self.filename, self._read_flags) as f:
328 346 self.data = f.read()
329 347 elif self.url is not None:
330 348 try:
331 349 try:
332 350 from urllib.request import urlopen # Py3
333 351 except ImportError:
334 352 from urllib2 import urlopen
335 353 response = urlopen(self.url)
336 354 self.data = response.read()
337 355 # extract encoding from header, if there is one:
338 356 encoding = None
339 357 for sub in response.headers['content-type'].split(';'):
340 358 sub = sub.strip()
341 359 if sub.startswith('charset'):
342 360 encoding = sub.split('=')[-1].strip()
343 361 break
344 362 # decode data, if an encoding was specified
345 363 if encoding:
346 364 self.data = self.data.decode(encoding, 'replace')
347 365 except:
348 366 self.data = None
349 367
350 368 class TextDisplayObject(DisplayObject):
351 369 """Validate that display data is text"""
352 370 def _check_data(self):
353 371 if self.data is not None and not isinstance(self.data, string_types):
354 372 raise TypeError("%s expects text, not %r" % (self.__class__.__name__, self.data))
355 373
356 374 class Pretty(TextDisplayObject):
357 375
358 376 def _repr_pretty_(self):
359 377 return self.data
360 378
361 379
362 380 class HTML(TextDisplayObject):
363 381
364 382 def _repr_html_(self):
365 383 return self.data
366 384
367 385 def __html__(self):
368 386 """
369 387 This method exists to inform other HTML-using modules (e.g. Markupsafe,
370 388 htmltag, etc) that this object is HTML and does not need things like
371 389 special characters (<>&) escaped.
372 390 """
373 391 return self._repr_html_()
374 392
375 393
376 394 class Math(TextDisplayObject):
377 395
378 396 def _repr_latex_(self):
379 397 s = self.data.strip('$')
380 398 return "$$%s$$" % s
381 399
382 400
383 401 class Latex(TextDisplayObject):
384 402
385 403 def _repr_latex_(self):
386 404 return self.data
387 405
388 406
389 407 class SVG(DisplayObject):
390 408
391 409 # wrap data in a property, which extracts the <svg> tag, discarding
392 410 # document headers
393 411 _data = None
394 412
395 413 @property
396 414 def data(self):
397 415 return self._data
398 416
399 417 @data.setter
400 418 def data(self, svg):
401 419 if svg is None:
402 420 self._data = None
403 421 return
404 422 # parse into dom object
405 423 from xml.dom import minidom
406 424 svg = cast_bytes_py2(svg)
407 425 x = minidom.parseString(svg)
408 426 # get svg tag (should be 1)
409 427 found_svg = x.getElementsByTagName('svg')
410 428 if found_svg:
411 429 svg = found_svg[0].toxml()
412 430 else:
413 431 # fallback on the input, trust the user
414 432 # but this is probably an error.
415 433 pass
416 434 svg = cast_unicode(svg)
417 435 self._data = svg
418 436
419 437 def _repr_svg_(self):
420 438 return self.data
421 439
422 440
423 441 class JSON(TextDisplayObject):
424 442
425 443 def _repr_json_(self):
426 444 return self.data
427 445
428 446 css_t = """$("head").append($("<link/>").attr({
429 447 rel: "stylesheet",
430 448 type: "text/css",
431 449 href: "%s"
432 450 }));
433 451 """
434 452
435 453 lib_t1 = """$.getScript("%s", function () {
436 454 """
437 455 lib_t2 = """});
438 456 """
439 457
440 458 class Javascript(TextDisplayObject):
441 459
442 460 def __init__(self, data=None, url=None, filename=None, lib=None, css=None):
443 461 """Create a Javascript display object given raw data.
444 462
445 463 When this object is returned by an expression or passed to the
446 464 display function, it will result in the data being displayed
447 465 in the frontend. If the data is a URL, the data will first be
448 466 downloaded and then displayed.
449 467
450 468 In the Notebook, the containing element will be available as `element`,
451 469 and jQuery will be available. The output area starts hidden, so if
452 470 the js appends content to `element` that should be visible, then
453 471 it must call `container.show()` to unhide the area.
454 472
455 473 Parameters
456 474 ----------
457 475 data : unicode, str or bytes
458 476 The Javascript source code or a URL to download it from.
459 477 url : unicode
460 478 A URL to download the data from.
461 479 filename : unicode
462 480 Path to a local file to load the data from.
463 481 lib : list or str
464 482 A sequence of Javascript library URLs to load asynchronously before
465 483 running the source code. The full URLs of the libraries should
466 484 be given. A single Javascript library URL can also be given as a
467 485 string.
468 486 css: : list or str
469 487 A sequence of css files to load before running the source code.
470 488 The full URLs of the css files should be given. A single css URL
471 489 can also be given as a string.
472 490 """
473 491 if isinstance(lib, string_types):
474 492 lib = [lib]
475 493 elif lib is None:
476 494 lib = []
477 495 if isinstance(css, string_types):
478 496 css = [css]
479 497 elif css is None:
480 498 css = []
481 499 if not isinstance(lib, (list,tuple)):
482 500 raise TypeError('expected sequence, got: %r' % lib)
483 501 if not isinstance(css, (list,tuple)):
484 502 raise TypeError('expected sequence, got: %r' % css)
485 503 self.lib = lib
486 504 self.css = css
487 505 super(Javascript, self).__init__(data=data, url=url, filename=filename)
488 506
489 507 def _repr_javascript_(self):
490 508 r = ''
491 509 for c in self.css:
492 510 r += css_t % c
493 511 for l in self.lib:
494 512 r += lib_t1 % l
495 513 r += self.data
496 514 r += lib_t2*len(self.lib)
497 515 return r
498 516
499 517 # constants for identifying png/jpeg data
500 518 _PNG = b'\x89PNG\r\n\x1a\n'
501 519 _JPEG = b'\xff\xd8'
502 520
503 521 def _pngxy(data):
504 522 """read the (width, height) from a PNG header"""
505 523 ihdr = data.index(b'IHDR')
506 524 # next 8 bytes are width/height
507 525 w4h4 = data[ihdr+4:ihdr+12]
508 526 return struct.unpack('>ii', w4h4)
509 527
510 528 def _jpegxy(data):
511 529 """read the (width, height) from a JPEG header"""
512 530 # adapted from http://www.64lines.com/jpeg-width-height
513 531
514 532 idx = 4
515 533 while True:
516 534 block_size = struct.unpack('>H', data[idx:idx+2])[0]
517 535 idx = idx + block_size
518 536 if data[idx:idx+2] == b'\xFF\xC0':
519 537 # found Start of Frame
520 538 iSOF = idx
521 539 break
522 540 else:
523 541 # read another block
524 542 idx += 2
525 543
526 544 h, w = struct.unpack('>HH', data[iSOF+5:iSOF+9])
527 545 return w, h
528 546
529 547 class Image(DisplayObject):
530 548
531 549 _read_flags = 'rb'
532 550 _FMT_JPEG = u'jpeg'
533 551 _FMT_PNG = u'png'
534 552 _ACCEPTABLE_EMBEDDINGS = [_FMT_JPEG, _FMT_PNG]
535 553
536 554 def __init__(self, data=None, url=None, filename=None, format=u'png', embed=None, width=None, height=None, retina=False):
537 555 """Create a PNG/JPEG image object given raw data.
538 556
539 557 When this object is returned by an input cell or passed to the
540 558 display function, it will result in the image being displayed
541 559 in the frontend.
542 560
543 561 Parameters
544 562 ----------
545 563 data : unicode, str or bytes
546 564 The raw image data or a URL or filename to load the data from.
547 565 This always results in embedded image data.
548 566 url : unicode
549 567 A URL to download the data from. If you specify `url=`,
550 568 the image data will not be embedded unless you also specify `embed=True`.
551 569 filename : unicode
552 570 Path to a local file to load the data from.
553 571 Images from a file are always embedded.
554 572 format : unicode
555 573 The format of the image data (png/jpeg/jpg). If a filename or URL is given
556 574 for format will be inferred from the filename extension.
557 575 embed : bool
558 576 Should the image data be embedded using a data URI (True) or be
559 577 loaded using an <img> tag. Set this to True if you want the image
560 578 to be viewable later with no internet connection in the notebook.
561 579
562 580 Default is `True`, unless the keyword argument `url` is set, then
563 581 default value is `False`.
564 582
565 583 Note that QtConsole is not able to display images if `embed` is set to `False`
566 584 width : int
567 585 Width to which to constrain the image in html
568 586 height : int
569 587 Height to which to constrain the image in html
570 588 retina : bool
571 589 Automatically set the width and height to half of the measured
572 590 width and height.
573 591 This only works for embedded images because it reads the width/height
574 592 from image data.
575 593 For non-embedded images, you can just set the desired display width
576 594 and height directly.
577 595
578 596 Examples
579 597 --------
580 598 # embedded image data, works in qtconsole and notebook
581 599 # when passed positionally, the first arg can be any of raw image data,
582 600 # a URL, or a filename from which to load image data.
583 601 # The result is always embedding image data for inline images.
584 602 Image('http://www.google.fr/images/srpr/logo3w.png')
585 603 Image('/path/to/image.jpg')
586 604 Image(b'RAW_PNG_DATA...')
587 605
588 606 # Specifying Image(url=...) does not embed the image data,
589 607 # it only generates `<img>` tag with a link to the source.
590 608 # This will not work in the qtconsole or offline.
591 609 Image(url='http://www.google.fr/images/srpr/logo3w.png')
592 610
593 611 """
594 612 if filename is not None:
595 613 ext = self._find_ext(filename)
596 614 elif url is not None:
597 615 ext = self._find_ext(url)
598 616 elif data is None:
599 617 raise ValueError("No image data found. Expecting filename, url, or data.")
600 618 elif isinstance(data, string_types) and (
601 619 data.startswith('http') or _safe_exists(data)
602 620 ):
603 621 ext = self._find_ext(data)
604 622 else:
605 623 ext = None
606 624
607 625 if ext is not None:
608 626 format = ext.lower()
609 627 if ext == u'jpg' or ext == u'jpeg':
610 628 format = self._FMT_JPEG
611 629 if ext == u'png':
612 630 format = self._FMT_PNG
613 631 elif isinstance(data, bytes) and format == 'png':
614 632 # infer image type from image data header,
615 633 # only if format might not have been specified.
616 634 if data[:2] == _JPEG:
617 635 format = 'jpeg'
618 636
619 637 self.format = unicode_type(format).lower()
620 638 self.embed = embed if embed is not None else (url is None)
621 639
622 640 if self.embed and self.format not in self._ACCEPTABLE_EMBEDDINGS:
623 641 raise ValueError("Cannot embed the '%s' image format" % (self.format))
624 642 self.width = width
625 643 self.height = height
626 644 self.retina = retina
627 645 super(Image, self).__init__(data=data, url=url, filename=filename)
628 646
629 647 if retina:
630 648 self._retina_shape()
631 649
632 650 def _retina_shape(self):
633 651 """load pixel-doubled width and height from image data"""
634 652 if not self.embed:
635 653 return
636 654 if self.format == 'png':
637 655 w, h = _pngxy(self.data)
638 656 elif self.format == 'jpeg':
639 657 w, h = _jpegxy(self.data)
640 658 else:
641 659 # retina only supports png
642 660 return
643 661 self.width = w // 2
644 662 self.height = h // 2
645 663
646 664 def reload(self):
647 665 """Reload the raw data from file or URL."""
648 666 if self.embed:
649 667 super(Image,self).reload()
650 668 if self.retina:
651 669 self._retina_shape()
652 670
653 671 def _repr_html_(self):
654 672 if not self.embed:
655 673 width = height = ''
656 674 if self.width:
657 675 width = ' width="%d"' % self.width
658 676 if self.height:
659 677 height = ' height="%d"' % self.height
660 678 return u'<img src="%s"%s%s/>' % (self.url, width, height)
661 679
662 680 def _data_and_metadata(self):
663 681 """shortcut for returning metadata with shape information, if defined"""
664 682 md = {}
665 683 if self.width:
666 684 md['width'] = self.width
667 685 if self.height:
668 686 md['height'] = self.height
669 687 if md:
670 688 return self.data, md
671 689 else:
672 690 return self.data
673 691
674 692 def _repr_png_(self):
675 693 if self.embed and self.format == u'png':
676 694 return self._data_and_metadata()
677 695
678 696 def _repr_jpeg_(self):
679 697 if self.embed and (self.format == u'jpeg' or self.format == u'jpg'):
680 698 return self._data_and_metadata()
681 699
682 700 def _find_ext(self, s):
683 701 return unicode_type(s.split('.')[-1].lower())
684 702
685 703
686 704 def clear_output(wait=False):
687 705 """Clear the output of the current cell receiving output.
688 706
689 707 Parameters
690 708 ----------
691 709 wait : bool [default: false]
692 710 Wait to clear the output until new output is available to replace it."""
693 711 from IPython.core.interactiveshell import InteractiveShell
694 712 if InteractiveShell.initialized():
695 713 InteractiveShell.instance().display_pub.clear_output(wait)
696 714 else:
697 715 from IPython.utils import io
698 716 print('\033[2K\r', file=io.stdout, end='')
699 717 io.stdout.flush()
700 718 print('\033[2K\r', file=io.stderr, end='')
701 719 io.stderr.flush()
720
721
722 @skip_doctest
723 def set_matplotlib_formats(*formats, **kwargs):
724 """Select figure formats for the inline backend. Optionally pass quality for JPEG.
725
726 For example, this enables PNG and JPEG output with a JPEG quality of 90%::
727
728 In [1]: set_matplotlib_formats('png', 'jpeg', quality=90)
729
730 To set this in your config files use the following::
731
732 c.InlineBackend.figure_formats = {'pdf', 'png', 'svg'}
733 c.InlineBackend.quality = 90
734
735 Parameters
736 ----------
737 *formats : list, tuple
738 One or a set of figure formats to enable: 'png', 'retina', 'jpeg', 'svg', 'pdf'.
739 quality : int
740 A percentage for the quality of JPEG figures. Defaults to 90.
741 """
742 from IPython.core.interactiveshell import InteractiveShell
743 from IPython.core.pylabtools import select_figure_formats
744 shell = InteractiveShell.instance()
745 select_figure_formats(shell, formats, quality=90)
746
747 @skip_doctest
748 def set_matplotlib_close(close):
749 """Set whether the inline backend closes all figures automatically or not.
750
751 By default, the inline backend used in the IPython Notebook will close all
752 matplotlib figures automatically after each cell is run. This means that
753 plots in different cells won't interfere. Sometimes, you may want to make
754 a plot in one cell and then refine it in later cells. This can be accomplished
755 by::
756
757 In [1]: set_matplotlib_close(False)
758
759 To set this in your config files use the following::
760
761 c.InlineBackend.close_figures = False
762
763 Parameters
764 ----------
765 close : bool
766 Should all matplotlib figures be automatically closed after each cell is
767 run?
768 """
769 from IPython.kernel.zmq.pylab.backend_inline import InlineBackend
770 ilbe = InlineBackend.instance()
771 ilbe.close_figures = close
772
@@ -1,825 +1,846 b''
1 1 # -*- coding: utf-8 -*-
2 2 """Display formatters.
3 3
4 4 Inheritance diagram:
5 5
6 6 .. inheritance-diagram:: IPython.core.formatters
7 7 :parts: 3
8 8
9 9 Authors:
10 10
11 11 * Robert Kern
12 12 * Brian Granger
13 13 """
14 14 #-----------------------------------------------------------------------------
15 15 # Copyright (C) 2010-2011, IPython Development Team.
16 16 #
17 17 # Distributed under the terms of the Modified BSD License.
18 18 #
19 19 # The full license is in the file COPYING.txt, distributed with this software.
20 20 #-----------------------------------------------------------------------------
21 21
22 22 #-----------------------------------------------------------------------------
23 23 # Imports
24 24 #-----------------------------------------------------------------------------
25 25
26 26 # Stdlib imports
27 27 import abc
28 28 import sys
29 29 import warnings
30 30
31 31 from IPython.external.decorator import decorator
32 32
33 33 # Our own imports
34 34 from IPython.config.configurable import Configurable
35 35 from IPython.lib import pretty
36 36 from IPython.utils import io
37 37 from IPython.utils.traitlets import (
38 38 Bool, Dict, Integer, Unicode, CUnicode, ObjectName, List,
39 39 )
40 40 from IPython.utils.warn import warn
41 41 from IPython.utils.py3compat import (
42 42 unicode_to_str, with_metaclass, PY3, string_types, unicode_type,
43 43 )
44 44
45 45 if PY3:
46 46 from io import StringIO
47 47 else:
48 48 from StringIO import StringIO
49 49
50 50
51 51 #-----------------------------------------------------------------------------
52 52 # The main DisplayFormatter class
53 53 #-----------------------------------------------------------------------------
54 54
55 55 class DisplayFormatter(Configurable):
56 56
57 57 # When set to true only the default plain text formatter will be used.
58 58 plain_text_only = Bool(False, config=True)
59 59 def _plain_text_only_changed(self, name, old, new):
60 60 warnings.warn("""DisplayFormatter.plain_text_only is deprecated.
61 61
62 62 Use DisplayFormatter.active_types = ['text/plain']
63 63 for the same effect.
64 64 """, DeprecationWarning)
65 65 if new:
66 66 self.active_types = ['text/plain']
67 67 else:
68 68 self.active_types = self.format_types
69 69
70 70 active_types = List(Unicode, config=True,
71 71 help="""List of currently active mime-types to display.
72 72 You can use this to set a white-list for formats to display.
73 73
74 74 Most users will not need to change this value.
75 75 """)
76 76 def _active_types_default(self):
77 77 return self.format_types
78 78
79 79 def _active_types_changed(self, name, old, new):
80 80 for key, formatter in self.formatters.items():
81 81 if key in new:
82 82 formatter.enabled = True
83 83 else:
84 84 formatter.enabled = False
85 85
86 86 # A dict of formatter whose keys are format types (MIME types) and whose
87 87 # values are subclasses of BaseFormatter.
88 88 formatters = Dict()
89 89 def _formatters_default(self):
90 90 """Activate the default formatters."""
91 91 formatter_classes = [
92 92 PlainTextFormatter,
93 93 HTMLFormatter,
94 94 SVGFormatter,
95 95 PNGFormatter,
96 PDFFormatter,
96 97 JPEGFormatter,
97 98 LatexFormatter,
98 99 JSONFormatter,
99 100 JavascriptFormatter
100 101 ]
101 102 d = {}
102 103 for cls in formatter_classes:
103 104 f = cls(parent=self)
104 105 d[f.format_type] = f
105 106 return d
106 107
107 108 def format(self, obj, include=None, exclude=None):
108 109 """Return a format data dict for an object.
109 110
110 111 By default all format types will be computed.
111 112
112 113 The following MIME types are currently implemented:
113 114
114 115 * text/plain
115 116 * text/html
116 117 * text/latex
117 118 * application/json
118 119 * application/javascript
120 * application/pdf
119 121 * image/png
120 122 * image/jpeg
121 123 * image/svg+xml
122 124
123 125 Parameters
124 126 ----------
125 127 obj : object
126 128 The Python object whose format data will be computed.
127 129 include : list or tuple, optional
128 130 A list of format type strings (MIME types) to include in the
129 131 format data dict. If this is set *only* the format types included
130 132 in this list will be computed.
131 133 exclude : list or tuple, optional
132 134 A list of format type string (MIME types) to exclude in the format
133 135 data dict. If this is set all format types will be computed,
134 136 except for those included in this argument.
135 137
136 138 Returns
137 139 -------
138 140 (format_dict, metadata_dict) : tuple of two dicts
139 141
140 142 format_dict is a dictionary of key/value pairs, one of each format that was
141 143 generated for the object. The keys are the format types, which
142 144 will usually be MIME type strings and the values and JSON'able
143 145 data structure containing the raw data for the representation in
144 146 that format.
145 147
146 148 metadata_dict is a dictionary of metadata about each mime-type output.
147 149 Its keys will be a strict subset of the keys in format_dict.
148 150 """
149 151 format_dict = {}
150 152 md_dict = {}
151 153
152 154 for format_type, formatter in self.formatters.items():
153 155 if include and format_type not in include:
154 156 continue
155 157 if exclude and format_type in exclude:
156 158 continue
157 159
158 160 md = None
159 161 try:
160 162 data = formatter(obj)
161 163 except:
162 164 # FIXME: log the exception
163 165 raise
164 166
165 167 # formatters can return raw data or (data, metadata)
166 168 if isinstance(data, tuple) and len(data) == 2:
167 169 data, md = data
168 170
169 171 if data is not None:
170 172 format_dict[format_type] = data
171 173 if md is not None:
172 174 md_dict[format_type] = md
173 175
174 176 return format_dict, md_dict
175 177
176 178 @property
177 179 def format_types(self):
178 180 """Return the format types (MIME types) of the active formatters."""
179 181 return list(self.formatters.keys())
180 182
181 183
182 184 #-----------------------------------------------------------------------------
183 185 # Formatters for specific format types (text, html, svg, etc.)
184 186 #-----------------------------------------------------------------------------
185 187
186 188 class FormatterWarning(UserWarning):
187 189 """Warning class for errors in formatters"""
188 190
189 191 @decorator
190 192 def warn_format_error(method, self, *args, **kwargs):
191 193 """decorator for warning on failed format call"""
192 194 try:
193 195 r = method(self, *args, **kwargs)
194 196 except NotImplementedError as e:
195 197 # don't warn on NotImplementedErrors
196 198 return None
197 199 except Exception as e:
198 200 warnings.warn("Exception in %s formatter: %s" % (self.format_type, e),
199 201 FormatterWarning,
200 202 )
201 203 return None
202 204 if r is None or isinstance(r, self._return_type) or \
203 205 (isinstance(r, tuple) and r and isinstance(r[0], self._return_type)):
204 206 return r
205 207 else:
206 208 warnings.warn(
207 209 "%s formatter returned invalid type %s (expected %s) for object: %s" % \
208 210 (self.format_type, type(r), self._return_type, pretty._safe_repr(args[0])),
209 211 FormatterWarning
210 212 )
211 213
212 214
213 215 class FormatterABC(with_metaclass(abc.ABCMeta, object)):
214 216 """ Abstract base class for Formatters.
215 217
216 218 A formatter is a callable class that is responsible for computing the
217 219 raw format data for a particular format type (MIME type). For example,
218 220 an HTML formatter would have a format type of `text/html` and would return
219 221 the HTML representation of the object when called.
220 222 """
221 223
222 224 # The format type of the data returned, usually a MIME type.
223 225 format_type = 'text/plain'
224 226
225 227 # Is the formatter enabled...
226 228 enabled = True
227 229
228 230 @abc.abstractmethod
229 231 @warn_format_error
230 232 def __call__(self, obj):
231 233 """Return a JSON'able representation of the object.
232 234
233 235 If the object cannot be formatted by this formatter,
234 236 warn and return None.
235 237 """
236 238 return repr(obj)
237 239
238 240
239 241 def _mod_name_key(typ):
240 242 """Return a (__module__, __name__) tuple for a type.
241 243
242 244 Used as key in Formatter.deferred_printers.
243 245 """
244 246 module = getattr(typ, '__module__', None)
245 247 name = getattr(typ, '__name__', None)
246 248 return (module, name)
247 249
248 250
249 251 def _get_type(obj):
250 252 """Return the type of an instance (old and new-style)"""
251 253 return getattr(obj, '__class__', None) or type(obj)
252 254
253 255 _raise_key_error = object()
254 256
255 257
256 258 class BaseFormatter(Configurable):
257 259 """A base formatter class that is configurable.
258 260
259 261 This formatter should usually be used as the base class of all formatters.
260 262 It is a traited :class:`Configurable` class and includes an extensible
261 263 API for users to determine how their objects are formatted. The following
262 264 logic is used to find a function to format an given object.
263 265
264 266 1. The object is introspected to see if it has a method with the name
265 267 :attr:`print_method`. If is does, that object is passed to that method
266 268 for formatting.
267 269 2. If no print method is found, three internal dictionaries are consulted
268 270 to find print method: :attr:`singleton_printers`, :attr:`type_printers`
269 271 and :attr:`deferred_printers`.
270 272
271 273 Users should use these dictionaries to register functions that will be
272 274 used to compute the format data for their objects (if those objects don't
273 275 have the special print methods). The easiest way of using these
274 276 dictionaries is through the :meth:`for_type` and :meth:`for_type_by_name`
275 277 methods.
276 278
277 279 If no function/callable is found to compute the format data, ``None`` is
278 280 returned and this format type is not used.
279 281 """
280 282
281 283 format_type = Unicode('text/plain')
282 284 _return_type = string_types
283 285
284 286 enabled = Bool(True, config=True)
285 287
286 288 print_method = ObjectName('__repr__')
287 289
288 290 # The singleton printers.
289 291 # Maps the IDs of the builtin singleton objects to the format functions.
290 292 singleton_printers = Dict(config=True)
291 293
292 294 # The type-specific printers.
293 295 # Map type objects to the format functions.
294 296 type_printers = Dict(config=True)
295 297
296 298 # The deferred-import type-specific printers.
297 299 # Map (modulename, classname) pairs to the format functions.
298 300 deferred_printers = Dict(config=True)
299 301
300 302 @warn_format_error
301 303 def __call__(self, obj):
302 304 """Compute the format for an object."""
303 305 if self.enabled:
304 306 # lookup registered printer
305 307 try:
306 308 printer = self.lookup(obj)
307 309 except KeyError:
308 310 pass
309 311 else:
310 312 return printer(obj)
311 313 # Finally look for special method names
312 314 method = pretty._safe_getattr(obj, self.print_method, None)
313 315 if method is not None:
314 316 return method()
315 317 return None
316 318 else:
317 319 return None
318 320
319 321 def __contains__(self, typ):
320 322 """map in to lookup_by_type"""
321 323 try:
322 324 self.lookup_by_type(typ)
323 325 except KeyError:
324 326 return False
325 327 else:
326 328 return True
327 329
328 330 def lookup(self, obj):
329 331 """Look up the formatter for a given instance.
330 332
331 333 Parameters
332 334 ----------
333 335 obj : object instance
334 336
335 337 Returns
336 338 -------
337 339 f : callable
338 340 The registered formatting callable for the type.
339 341
340 342 Raises
341 343 ------
342 344 KeyError if the type has not been registered.
343 345 """
344 346 # look for singleton first
345 347 obj_id = id(obj)
346 348 if obj_id in self.singleton_printers:
347 349 return self.singleton_printers[obj_id]
348 350 # then lookup by type
349 351 return self.lookup_by_type(_get_type(obj))
350 352
351 353 def lookup_by_type(self, typ):
352 354 """Look up the registered formatter for a type.
353 355
354 356 Parameters
355 357 ----------
356 358 typ : type or '__module__.__name__' string for a type
357 359
358 360 Returns
359 361 -------
360 362 f : callable
361 363 The registered formatting callable for the type.
362 364
363 365 Raises
364 366 ------
365 367 KeyError if the type has not been registered.
366 368 """
367 369 if isinstance(typ, string_types):
368 370 typ_key = tuple(typ.rsplit('.',1))
369 371 if typ_key not in self.deferred_printers:
370 372 # We may have it cached in the type map. We will have to
371 373 # iterate over all of the types to check.
372 374 for cls in self.type_printers:
373 375 if _mod_name_key(cls) == typ_key:
374 376 return self.type_printers[cls]
375 377 else:
376 378 return self.deferred_printers[typ_key]
377 379 else:
378 380 for cls in pretty._get_mro(typ):
379 381 if cls in self.type_printers or self._in_deferred_types(cls):
380 382 return self.type_printers[cls]
381 383
382 384 # If we have reached here, the lookup failed.
383 385 raise KeyError("No registered printer for {0!r}".format(typ))
384 386
385 387 def for_type(self, typ, func=None):
386 388 """Add a format function for a given type.
387 389
388 390 Parameters
389 391 -----------
390 392 typ : type or '__module__.__name__' string for a type
391 393 The class of the object that will be formatted using `func`.
392 394 func : callable
393 395 A callable for computing the format data.
394 396 `func` will be called with the object to be formatted,
395 397 and will return the raw data in this formatter's format.
396 398 Subclasses may use a different call signature for the
397 399 `func` argument.
398 400
399 401 If `func` is None or not specified, there will be no change,
400 402 only returning the current value.
401 403
402 404 Returns
403 405 -------
404 406 oldfunc : callable
405 407 The currently registered callable.
406 408 If you are registering a new formatter,
407 409 this will be the previous value (to enable restoring later).
408 410 """
409 411 # if string given, interpret as 'pkg.module.class_name'
410 412 if isinstance(typ, string_types):
411 413 type_module, type_name = typ.rsplit('.', 1)
412 414 return self.for_type_by_name(type_module, type_name, func)
413 415
414 416 try:
415 417 oldfunc = self.lookup_by_type(typ)
416 418 except KeyError:
417 419 oldfunc = None
418 420
419 421 if func is not None:
420 422 self.type_printers[typ] = func
421 423
422 424 return oldfunc
423 425
424 426 def for_type_by_name(self, type_module, type_name, func=None):
425 427 """Add a format function for a type specified by the full dotted
426 428 module and name of the type, rather than the type of the object.
427 429
428 430 Parameters
429 431 ----------
430 432 type_module : str
431 433 The full dotted name of the module the type is defined in, like
432 434 ``numpy``.
433 435 type_name : str
434 436 The name of the type (the class name), like ``dtype``
435 437 func : callable
436 438 A callable for computing the format data.
437 439 `func` will be called with the object to be formatted,
438 440 and will return the raw data in this formatter's format.
439 441 Subclasses may use a different call signature for the
440 442 `func` argument.
441 443
442 444 If `func` is None or unspecified, there will be no change,
443 445 only returning the current value.
444 446
445 447 Returns
446 448 -------
447 449 oldfunc : callable
448 450 The currently registered callable.
449 451 If you are registering a new formatter,
450 452 this will be the previous value (to enable restoring later).
451 453 """
452 454 key = (type_module, type_name)
453 455
454 456 try:
455 457 oldfunc = self.lookup_by_type("%s.%s" % key)
456 458 except KeyError:
457 459 oldfunc = None
458 460
459 461 if func is not None:
460 462 self.deferred_printers[key] = func
461 463 return oldfunc
462 464
463 465 def pop(self, typ, default=_raise_key_error):
464 466 """Pop a formatter for the given type.
465 467
466 468 Parameters
467 469 ----------
468 470 typ : type or '__module__.__name__' string for a type
469 471 default : object
470 472 value to be returned if no formatter is registered for typ.
471 473
472 474 Returns
473 475 -------
474 476 obj : object
475 477 The last registered object for the type.
476 478
477 479 Raises
478 480 ------
479 481 KeyError if the type is not registered and default is not specified.
480 482 """
481 483
482 484 if isinstance(typ, string_types):
483 485 typ_key = tuple(typ.rsplit('.',1))
484 486 if typ_key not in self.deferred_printers:
485 487 # We may have it cached in the type map. We will have to
486 488 # iterate over all of the types to check.
487 489 for cls in self.type_printers:
488 490 if _mod_name_key(cls) == typ_key:
489 491 old = self.type_printers.pop(cls)
490 492 break
491 493 else:
492 494 old = default
493 495 else:
494 496 old = self.deferred_printers.pop(typ_key)
495 497 else:
496 498 if typ in self.type_printers:
497 499 old = self.type_printers.pop(typ)
498 500 else:
499 501 old = self.deferred_printers.pop(_mod_name_key(typ), default)
500 502 if old is _raise_key_error:
501 503 raise KeyError("No registered value for {0!r}".format(typ))
502 504 return old
503 505
504 506 def _in_deferred_types(self, cls):
505 507 """
506 508 Check if the given class is specified in the deferred type registry.
507 509
508 510 Successful matches will be moved to the regular type registry for future use.
509 511 """
510 512 mod = getattr(cls, '__module__', None)
511 513 name = getattr(cls, '__name__', None)
512 514 key = (mod, name)
513 515 if key in self.deferred_printers:
514 516 # Move the printer over to the regular registry.
515 517 printer = self.deferred_printers.pop(key)
516 518 self.type_printers[cls] = printer
517 519 return True
518 520 return False
519 521
520 522
521 523 class PlainTextFormatter(BaseFormatter):
522 524 """The default pretty-printer.
523 525
524 526 This uses :mod:`IPython.lib.pretty` to compute the format data of
525 527 the object. If the object cannot be pretty printed, :func:`repr` is used.
526 528 See the documentation of :mod:`IPython.lib.pretty` for details on
527 529 how to write pretty printers. Here is a simple example::
528 530
529 531 def dtype_pprinter(obj, p, cycle):
530 532 if cycle:
531 533 return p.text('dtype(...)')
532 534 if hasattr(obj, 'fields'):
533 535 if obj.fields is None:
534 536 p.text(repr(obj))
535 537 else:
536 538 p.begin_group(7, 'dtype([')
537 539 for i, field in enumerate(obj.descr):
538 540 if i > 0:
539 541 p.text(',')
540 542 p.breakable()
541 543 p.pretty(field)
542 544 p.end_group(7, '])')
543 545 """
544 546
545 547 # The format type of data returned.
546 548 format_type = Unicode('text/plain')
547 549
548 550 # This subclass ignores this attribute as it always need to return
549 551 # something.
550 552 enabled = Bool(True, config=False)
551 553
552 554 # Look for a _repr_pretty_ methods to use for pretty printing.
553 555 print_method = ObjectName('_repr_pretty_')
554 556
555 557 # Whether to pretty-print or not.
556 558 pprint = Bool(True, config=True)
557 559
558 560 # Whether to be verbose or not.
559 561 verbose = Bool(False, config=True)
560 562
561 563 # The maximum width.
562 564 max_width = Integer(79, config=True)
563 565
564 566 # The newline character.
565 567 newline = Unicode('\n', config=True)
566 568
567 569 # format-string for pprinting floats
568 570 float_format = Unicode('%r')
569 571 # setter for float precision, either int or direct format-string
570 572 float_precision = CUnicode('', config=True)
571 573
572 574 def _float_precision_changed(self, name, old, new):
573 575 """float_precision changed, set float_format accordingly.
574 576
575 577 float_precision can be set by int or str.
576 578 This will set float_format, after interpreting input.
577 579 If numpy has been imported, numpy print precision will also be set.
578 580
579 581 integer `n` sets format to '%.nf', otherwise, format set directly.
580 582
581 583 An empty string returns to defaults (repr for float, 8 for numpy).
582 584
583 585 This parameter can be set via the '%precision' magic.
584 586 """
585 587
586 588 if '%' in new:
587 589 # got explicit format string
588 590 fmt = new
589 591 try:
590 592 fmt%3.14159
591 593 except Exception:
592 594 raise ValueError("Precision must be int or format string, not %r"%new)
593 595 elif new:
594 596 # otherwise, should be an int
595 597 try:
596 598 i = int(new)
597 599 assert i >= 0
598 600 except ValueError:
599 601 raise ValueError("Precision must be int or format string, not %r"%new)
600 602 except AssertionError:
601 603 raise ValueError("int precision must be non-negative, not %r"%i)
602 604
603 605 fmt = '%%.%if'%i
604 606 if 'numpy' in sys.modules:
605 607 # set numpy precision if it has been imported
606 608 import numpy
607 609 numpy.set_printoptions(precision=i)
608 610 else:
609 611 # default back to repr
610 612 fmt = '%r'
611 613 if 'numpy' in sys.modules:
612 614 import numpy
613 615 # numpy default is 8
614 616 numpy.set_printoptions(precision=8)
615 617 self.float_format = fmt
616 618
617 619 # Use the default pretty printers from IPython.lib.pretty.
618 620 def _singleton_printers_default(self):
619 621 return pretty._singleton_pprinters.copy()
620 622
621 623 def _type_printers_default(self):
622 624 d = pretty._type_pprinters.copy()
623 625 d[float] = lambda obj,p,cycle: p.text(self.float_format%obj)
624 626 return d
625 627
626 628 def _deferred_printers_default(self):
627 629 return pretty._deferred_type_pprinters.copy()
628 630
629 631 #### FormatterABC interface ####
630 632
631 633 @warn_format_error
632 634 def __call__(self, obj):
633 635 """Compute the pretty representation of the object."""
634 636 if not self.pprint:
635 637 return pretty._safe_repr(obj)
636 638 else:
637 639 # This uses use StringIO, as cStringIO doesn't handle unicode.
638 640 stream = StringIO()
639 641 # self.newline.encode() is a quick fix for issue gh-597. We need to
640 642 # ensure that stream does not get a mix of unicode and bytestrings,
641 643 # or it will cause trouble.
642 644 printer = pretty.RepresentationPrinter(stream, self.verbose,
643 645 self.max_width, unicode_to_str(self.newline),
644 646 singleton_pprinters=self.singleton_printers,
645 647 type_pprinters=self.type_printers,
646 648 deferred_pprinters=self.deferred_printers)
647 649 printer.pretty(obj)
648 650 printer.flush()
649 651 return stream.getvalue()
650 652
651 653
652 654 class HTMLFormatter(BaseFormatter):
653 655 """An HTML formatter.
654 656
655 657 To define the callables that compute the HTML representation of your
656 658 objects, define a :meth:`_repr_html_` method or use the :meth:`for_type`
657 659 or :meth:`for_type_by_name` methods to register functions that handle
658 660 this.
659 661
660 662 The return value of this formatter should be a valid HTML snippet that
661 663 could be injected into an existing DOM. It should *not* include the
662 664 ```<html>`` or ```<body>`` tags.
663 665 """
664 666 format_type = Unicode('text/html')
665 667
666 668 print_method = ObjectName('_repr_html_')
667 669
668 670
669 671 class SVGFormatter(BaseFormatter):
670 672 """An SVG formatter.
671 673
672 674 To define the callables that compute the SVG representation of your
673 675 objects, define a :meth:`_repr_svg_` method or use the :meth:`for_type`
674 676 or :meth:`for_type_by_name` methods to register functions that handle
675 677 this.
676 678
677 679 The return value of this formatter should be valid SVG enclosed in
678 680 ```<svg>``` tags, that could be injected into an existing DOM. It should
679 681 *not* include the ```<html>`` or ```<body>`` tags.
680 682 """
681 683 format_type = Unicode('image/svg+xml')
682 684
683 685 print_method = ObjectName('_repr_svg_')
684 686
685 687
686 688 class PNGFormatter(BaseFormatter):
687 689 """A PNG formatter.
688 690
689 691 To define the callables that compute the PNG representation of your
690 692 objects, define a :meth:`_repr_png_` method or use the :meth:`for_type`
691 693 or :meth:`for_type_by_name` methods to register functions that handle
692 694 this.
693 695
694 696 The return value of this formatter should be raw PNG data, *not*
695 697 base64 encoded.
696 698 """
697 699 format_type = Unicode('image/png')
698 700
699 701 print_method = ObjectName('_repr_png_')
700 702
701 703 _return_type = (bytes, unicode_type)
702 704
703 705
704 706 class JPEGFormatter(BaseFormatter):
705 707 """A JPEG formatter.
706 708
707 709 To define the callables that compute the JPEG representation of your
708 710 objects, define a :meth:`_repr_jpeg_` method or use the :meth:`for_type`
709 711 or :meth:`for_type_by_name` methods to register functions that handle
710 712 this.
711 713
712 714 The return value of this formatter should be raw JPEG data, *not*
713 715 base64 encoded.
714 716 """
715 717 format_type = Unicode('image/jpeg')
716 718
717 719 print_method = ObjectName('_repr_jpeg_')
718 720
719 721 _return_type = (bytes, unicode_type)
720 722
721 723
722 724 class LatexFormatter(BaseFormatter):
723 725 """A LaTeX formatter.
724 726
725 727 To define the callables that compute the LaTeX representation of your
726 728 objects, define a :meth:`_repr_latex_` method or use the :meth:`for_type`
727 729 or :meth:`for_type_by_name` methods to register functions that handle
728 730 this.
729 731
730 732 The return value of this formatter should be a valid LaTeX equation,
731 733 enclosed in either ```$```, ```$$``` or another LaTeX equation
732 734 environment.
733 735 """
734 736 format_type = Unicode('text/latex')
735 737
736 738 print_method = ObjectName('_repr_latex_')
737 739
738 740
739 741 class JSONFormatter(BaseFormatter):
740 742 """A JSON string formatter.
741 743
742 744 To define the callables that compute the JSON string representation of
743 745 your objects, define a :meth:`_repr_json_` method or use the :meth:`for_type`
744 746 or :meth:`for_type_by_name` methods to register functions that handle
745 747 this.
746 748
747 749 The return value of this formatter should be a valid JSON string.
748 750 """
749 751 format_type = Unicode('application/json')
750 752
751 753 print_method = ObjectName('_repr_json_')
752 754
753 755
754 756 class JavascriptFormatter(BaseFormatter):
755 757 """A Javascript formatter.
756 758
757 759 To define the callables that compute the Javascript representation of
758 760 your objects, define a :meth:`_repr_javascript_` method or use the
759 761 :meth:`for_type` or :meth:`for_type_by_name` methods to register functions
760 762 that handle this.
761 763
762 764 The return value of this formatter should be valid Javascript code and
763 765 should *not* be enclosed in ```<script>``` tags.
764 766 """
765 767 format_type = Unicode('application/javascript')
766 768
767 769 print_method = ObjectName('_repr_javascript_')
768 770
771
772 class PDFFormatter(BaseFormatter):
773 """A PDF formatter.
774
775 To defined the callables that compute to PDF representation of your
776 objects, define a :meth:`_repr_pdf_` method or use the :meth:`for_type`
777 or :meth:`for_type_by_name` methods to register functions that handle
778 this.
779
780 The return value of this formatter should be raw PDF data, *not*
781 base64 encoded.
782 """
783 format_type = Unicode('application/pdf')
784
785 print_method = ObjectName('_repr_pdf_')
786
787
769 788 FormatterABC.register(BaseFormatter)
770 789 FormatterABC.register(PlainTextFormatter)
771 790 FormatterABC.register(HTMLFormatter)
772 791 FormatterABC.register(SVGFormatter)
773 792 FormatterABC.register(PNGFormatter)
793 FormatterABC.register(PDFFormatter)
774 794 FormatterABC.register(JPEGFormatter)
775 795 FormatterABC.register(LatexFormatter)
776 796 FormatterABC.register(JSONFormatter)
777 797 FormatterABC.register(JavascriptFormatter)
778 798
779 799
780 800 def format_display_data(obj, include=None, exclude=None):
781 801 """Return a format data dict for an object.
782 802
783 803 By default all format types will be computed.
784 804
785 805 The following MIME types are currently implemented:
786 806
787 807 * text/plain
788 808 * text/html
789 809 * text/latex
790 810 * application/json
791 811 * application/javascript
812 * application/pdf
792 813 * image/png
793 814 * image/jpeg
794 815 * image/svg+xml
795 816
796 817 Parameters
797 818 ----------
798 819 obj : object
799 820 The Python object whose format data will be computed.
800 821
801 822 Returns
802 823 -------
803 824 format_dict : dict
804 825 A dictionary of key/value pairs, one or each format that was
805 826 generated for the object. The keys are the format types, which
806 827 will usually be MIME type strings and the values and JSON'able
807 828 data structure containing the raw data for the representation in
808 829 that format.
809 830 include : list or tuple, optional
810 831 A list of format type strings (MIME types) to include in the
811 832 format data dict. If this is set *only* the format types included
812 833 in this list will be computed.
813 834 exclude : list or tuple, optional
814 835 A list of format type string (MIME types) to exclue in the format
815 836 data dict. If this is set all format types will be computed,
816 837 except for those included in this argument.
817 838 """
818 839 from IPython.core.interactiveshell import InteractiveShell
819 840
820 841 InteractiveShell.instance().display_formatter.format(
821 842 obj,
822 843 include,
823 844 exclude
824 845 )
825 846
@@ -1,143 +1,147 b''
1 1 """Implementation of magic functions for matplotlib/pylab support.
2 2 """
3 3 from __future__ import print_function
4 4 #-----------------------------------------------------------------------------
5 5 # Copyright (c) 2012 The IPython Development Team.
6 6 #
7 7 # Distributed under the terms of the Modified BSD License.
8 8 #
9 9 # The full license is in the file COPYING.txt, distributed with this software.
10 10 #-----------------------------------------------------------------------------
11 11
12 12 #-----------------------------------------------------------------------------
13 13 # Imports
14 14 #-----------------------------------------------------------------------------
15 15
16 16 # Our own packages
17 17 from IPython.config.application import Application
18 18 from IPython.core import magic_arguments
19 19 from IPython.core.magic import Magics, magics_class, line_magic
20 20 from IPython.testing.skipdoctest import skip_doctest
21 21 from IPython.utils.warn import warn
22 22 from IPython.core.pylabtools import backends
23 23
24 24 #-----------------------------------------------------------------------------
25 25 # Magic implementation classes
26 26 #-----------------------------------------------------------------------------
27 27
28 28 magic_gui_arg = magic_arguments.argument(
29 29 'gui', nargs='?',
30 30 help="""Name of the matplotlib backend to use %s.
31 31 If given, the corresponding matplotlib backend is used,
32 32 otherwise it will be matplotlib's default
33 33 (which you can set in your matplotlib config file).
34 34 """ % str(tuple(sorted(backends.keys())))
35 35 )
36 36
37 37
38 38 @magics_class
39 39 class PylabMagics(Magics):
40 40 """Magics related to matplotlib's pylab support"""
41 41
42 42 @skip_doctest
43 43 @line_magic
44 44 @magic_arguments.magic_arguments()
45 45 @magic_gui_arg
46 46 def matplotlib(self, line=''):
47 47 """Set up matplotlib to work interactively.
48 48
49 49 This function lets you activate matplotlib interactive support
50 at any point during an IPython session.
51 It does not import anything into the interactive namespace.
50 at any point during an IPython session. It does not import anything
51 into the interactive namespace.
52 52
53 If you are using the inline matplotlib backend for embedded figures,
54 you can adjust its behavior via the %config magic::
55
56 # enable SVG figures, necessary for SVG+XHTML export in the qtconsole
57 In [1]: %config InlineBackend.figure_format = 'svg'
53 If you are using the inline matplotlib backend in the IPython Notebook
54 you can set which figure formats are enabled using the following::
55
56 In [1]: from IPython.display import set_matplotlib_formats
57
58 In [2]: set_matplotlib_formats('pdf', 'svg')
58 59
59 # change the behavior of closing all figures at the end of each
60 # execution (cell), or allowing reuse of active figures across
61 # cells:
62 In [2]: %config InlineBackend.close_figures = False
60 See the docstring of `IPython.display.set_matplotlib_formats` and
61 `IPython.display.set_matplotlib_close` for more information on
62 changing the behavior of the inline backend.
63 63
64 64 Examples
65 65 --------
66 In this case, where the MPL default is TkAgg::
66 To enable the inline backend for usage with the IPython Notebook::
67
68 In [1]: %matplotlib inline
69
70 In this case, where the matplotlib default is TkAgg::
67 71
68 72 In [2]: %matplotlib
69 73 Using matplotlib backend: TkAgg
70 74
71 But you can explicitly request a different backend::
75 But you can explicitly request a different GUI backend::
72 76
73 77 In [3]: %matplotlib qt
74 78 """
75 79 args = magic_arguments.parse_argstring(self.matplotlib, line)
76 80 gui, backend = self.shell.enable_matplotlib(args.gui)
77 81 self._show_matplotlib_backend(args.gui, backend)
78 82
79 83 @skip_doctest
80 84 @line_magic
81 85 @magic_arguments.magic_arguments()
82 86 @magic_arguments.argument(
83 87 '--no-import-all', action='store_true', default=None,
84 88 help="""Prevent IPython from performing ``import *`` into the interactive namespace.
85 89
86 90 You can govern the default behavior of this flag with the
87 91 InteractiveShellApp.pylab_import_all configurable.
88 92 """
89 93 )
90 94 @magic_gui_arg
91 95 def pylab(self, line=''):
92 96 """Load numpy and matplotlib to work interactively.
93 97
94 98 This function lets you activate pylab (matplotlib, numpy and
95 99 interactive support) at any point during an IPython session.
96 100
97 101 %pylab makes the following imports::
98 102
99 103 import numpy
100 104 import matplotlib
101 105 from matplotlib import pylab, mlab, pyplot
102 106 np = numpy
103 107 plt = pyplot
104 108
105 109 from IPython.display import display
106 110 from IPython.core.pylabtools import figsize, getfigs
107 111
108 112 from pylab import *
109 113 from numpy import *
110 114
111 115 If you pass `--no-import-all`, the last two `*` imports will be excluded.
112 116
113 117 See the %matplotlib magic for more details about activating matplotlib
114 118 without affecting the interactive namespace.
115 119 """
116 120 args = magic_arguments.parse_argstring(self.pylab, line)
117 121 if args.no_import_all is None:
118 122 # get default from Application
119 123 if Application.initialized():
120 124 app = Application.instance()
121 125 try:
122 126 import_all = app.pylab_import_all
123 127 except AttributeError:
124 128 import_all = True
125 129 else:
126 130 # nothing specified, no app - default True
127 131 import_all = True
128 132 else:
129 133 # invert no-import flag
130 134 import_all = not args.no_import_all
131 135
132 136 gui, backend, clobbered = self.shell.enable_pylab(args.gui, import_all=import_all)
133 137 self._show_matplotlib_backend(args.gui, backend)
134 138 print ("Populating the interactive namespace from numpy and matplotlib")
135 139 if clobbered:
136 140 warn("pylab import has clobbered these variables: %s" % clobbered +
137 141 "\n`%pylab --no-import-all` prevents importing * from pylab and numpy"
138 142 )
139 143
140 144 def _show_matplotlib_backend(self, gui, backend):
141 145 """show matplotlib message backend message"""
142 146 if not gui or gui == 'auto':
143 147 print("Using matplotlib backend: %s" % backend)
@@ -1,346 +1,358 b''
1 1 # -*- coding: utf-8 -*-
2 2 """Pylab (matplotlib) support utilities.
3 3
4 4 Authors
5 5 -------
6 6
7 7 * Fernando Perez.
8 8 * Brian Granger
9 9 """
10 10 from __future__ import print_function
11 11
12 12 #-----------------------------------------------------------------------------
13 13 # Copyright (C) 2009 The IPython Development Team
14 14 #
15 15 # Distributed under the terms of the BSD License. The full license is in
16 16 # the file COPYING, distributed as part of this software.
17 17 #-----------------------------------------------------------------------------
18 18
19 19 #-----------------------------------------------------------------------------
20 20 # Imports
21 21 #-----------------------------------------------------------------------------
22 22
23 23 import sys
24 24 from io import BytesIO
25 25
26 26 from IPython.core.display import _pngxy
27 27 from IPython.utils.decorators import flag_calls
28 from IPython.utils import py3compat
28 29
29 30 # If user specifies a GUI, that dictates the backend, otherwise we read the
30 31 # user's mpl default from the mpl rc structure
31 32 backends = {'tk': 'TkAgg',
32 33 'gtk': 'GTKAgg',
33 34 'gtk3': 'GTK3Agg',
34 35 'wx': 'WXAgg',
35 36 'qt': 'Qt4Agg', # qt3 not supported
36 37 'qt4': 'Qt4Agg',
37 38 'osx': 'MacOSX',
38 39 'inline' : 'module://IPython.kernel.zmq.pylab.backend_inline'}
39 40
40 41 # We also need a reverse backends2guis mapping that will properly choose which
41 42 # GUI support to activate based on the desired matplotlib backend. For the
42 43 # most part it's just a reverse of the above dict, but we also need to add a
43 44 # few others that map to the same GUI manually:
44 45 backend2gui = dict(zip(backends.values(), backends.keys()))
45 46 # Our tests expect backend2gui to just return 'qt'
46 47 backend2gui['Qt4Agg'] = 'qt'
47 48 # In the reverse mapping, there are a few extra valid matplotlib backends that
48 49 # map to the same GUI support
49 50 backend2gui['GTK'] = backend2gui['GTKCairo'] = 'gtk'
50 51 backend2gui['GTK3Cairo'] = 'gtk3'
51 52 backend2gui['WX'] = 'wx'
52 53 backend2gui['CocoaAgg'] = 'osx'
53 54
54 55 #-----------------------------------------------------------------------------
55 56 # Matplotlib utilities
56 57 #-----------------------------------------------------------------------------
57 58
58 59
59 60 def getfigs(*fig_nums):
60 61 """Get a list of matplotlib figures by figure numbers.
61 62
62 63 If no arguments are given, all available figures are returned. If the
63 64 argument list contains references to invalid figures, a warning is printed
64 65 but the function continues pasting further figures.
65 66
66 67 Parameters
67 68 ----------
68 69 figs : tuple
69 70 A tuple of ints giving the figure numbers of the figures to return.
70 71 """
71 72 from matplotlib._pylab_helpers import Gcf
72 73 if not fig_nums:
73 74 fig_managers = Gcf.get_all_fig_managers()
74 75 return [fm.canvas.figure for fm in fig_managers]
75 76 else:
76 77 figs = []
77 78 for num in fig_nums:
78 79 f = Gcf.figs.get(num)
79 80 if f is None:
80 81 print('Warning: figure %s not available.' % num)
81 82 else:
82 83 figs.append(f.canvas.figure)
83 84 return figs
84 85
85 86
86 87 def figsize(sizex, sizey):
87 88 """Set the default figure size to be [sizex, sizey].
88 89
89 90 This is just an easy to remember, convenience wrapper that sets::
90 91
91 92 matplotlib.rcParams['figure.figsize'] = [sizex, sizey]
92 93 """
93 94 import matplotlib
94 95 matplotlib.rcParams['figure.figsize'] = [sizex, sizey]
95 96
96 97
97 98 def print_figure(fig, fmt='png', quality=90):
98 99 """Convert a figure to svg, png or jpg for inline display.
99 100 Quality is only relevant for jpg.
100 101 """
101 102 from matplotlib import rcParams
102 103 # When there's an empty figure, we shouldn't return anything, otherwise we
103 104 # get big blank areas in the qt console.
104 105 if not fig.axes and not fig.lines:
105 106 return
106 107
107 108 fc = fig.get_facecolor()
108 109 ec = fig.get_edgecolor()
109 110 bytes_io = BytesIO()
110 111 dpi = rcParams['savefig.dpi']
111 112 if fmt == 'retina':
112 113 dpi = dpi * 2
113 114 fmt = 'png'
114 115 fig.canvas.print_figure(bytes_io, format=fmt, bbox_inches='tight',
115 116 facecolor=fc, edgecolor=ec, dpi=dpi, quality=quality)
116 117 data = bytes_io.getvalue()
117 118 return data
118 119
119 120 def retina_figure(fig):
120 121 """format a figure as a pixel-doubled (retina) PNG"""
121 122 pngdata = print_figure(fig, fmt='retina')
122 123 w, h = _pngxy(pngdata)
123 124 metadata = dict(width=w//2, height=h//2)
124 125 return pngdata, metadata
125 126
126 127 # We need a little factory function here to create the closure where
127 128 # safe_execfile can live.
128 129 def mpl_runner(safe_execfile):
129 130 """Factory to return a matplotlib-enabled runner for %run.
130 131
131 132 Parameters
132 133 ----------
133 134 safe_execfile : function
134 135 This must be a function with the same interface as the
135 136 :meth:`safe_execfile` method of IPython.
136 137
137 138 Returns
138 139 -------
139 140 A function suitable for use as the ``runner`` argument of the %run magic
140 141 function.
141 142 """
142 143
143 144 def mpl_execfile(fname,*where,**kw):
144 145 """matplotlib-aware wrapper around safe_execfile.
145 146
146 147 Its interface is identical to that of the :func:`execfile` builtin.
147 148
148 149 This is ultimately a call to execfile(), but wrapped in safeties to
149 150 properly handle interactive rendering."""
150 151
151 152 import matplotlib
152 153 import matplotlib.pylab as pylab
153 154
154 155 #print '*** Matplotlib runner ***' # dbg
155 156 # turn off rendering until end of script
156 157 is_interactive = matplotlib.rcParams['interactive']
157 158 matplotlib.interactive(False)
158 159 safe_execfile(fname,*where,**kw)
159 160 matplotlib.interactive(is_interactive)
160 161 # make rendering call now, if the user tried to do it
161 162 if pylab.draw_if_interactive.called:
162 163 pylab.draw()
163 164 pylab.draw_if_interactive.called = False
164 165
165 166 return mpl_execfile
166 167
167 168
168 def select_figure_format(shell, fmt, quality=90):
169 """Select figure format for inline backend, can be 'png', 'retina', 'jpg', or 'svg'.
169 def select_figure_formats(shell, formats, quality=90):
170 """Select figure formats for the inline backend.
170 171
171 Using this method ensures only one figure format is active at a time.
172 Parameters
173 ==========
174 shell : InteractiveShell
175 The main IPython instance.
176 formats : list
177 One or a set of figure formats to enable: 'png', 'retina', 'jpeg', 'svg', 'pdf'.
178 quality : int
179 A percentage for the quality of JPEG figures.
172 180 """
173 181 from matplotlib.figure import Figure
174 182 from IPython.kernel.zmq.pylab import backend_inline
175 183
176 184 svg_formatter = shell.display_formatter.formatters['image/svg+xml']
177 185 png_formatter = shell.display_formatter.formatters['image/png']
178 186 jpg_formatter = shell.display_formatter.formatters['image/jpeg']
187 pdf_formatter = shell.display_formatter.formatters['application/pdf']
179 188
180 [ f.type_printers.pop(Figure, None) for f in {svg_formatter, png_formatter, jpg_formatter} ]
189 if isinstance(formats, py3compat.string_types):
190 formats = {formats}
181 191
182 if fmt == 'png':
183 png_formatter.for_type(Figure, lambda fig: print_figure(fig, 'png'))
184 elif fmt in ('png2x', 'retina'):
185 png_formatter.for_type(Figure, retina_figure)
186 elif fmt in ('jpg', 'jpeg'):
187 jpg_formatter.for_type(Figure, lambda fig: print_figure(fig, 'jpg', quality))
188 elif fmt == 'svg':
189 svg_formatter.for_type(Figure, lambda fig: print_figure(fig, 'svg'))
190 else:
191 raise ValueError("supported formats are: 'png', 'retina', 'svg', 'jpg', not %r" % fmt)
192 [ f.type_printers.pop(Figure, None) for f in {svg_formatter, png_formatter, jpg_formatter} ]
192 193
193 # set the format to be used in the backend()
194 backend_inline._figure_format = fmt
194 for fmt in formats:
195 if fmt == 'png':
196 png_formatter.for_type(Figure, lambda fig: print_figure(fig, 'png'))
197 elif fmt in ('png2x', 'retina'):
198 png_formatter.for_type(Figure, retina_figure)
199 elif fmt in ('jpg', 'jpeg'):
200 jpg_formatter.for_type(Figure, lambda fig: print_figure(fig, 'jpg', quality))
201 elif fmt == 'svg':
202 svg_formatter.for_type(Figure, lambda fig: print_figure(fig, 'svg'))
203 elif fmt == 'pdf':
204 pdf_formatter.for_type(Figure, lambda fig: print_figure(fig, 'pdf'))
205 else:
206 raise ValueError("supported formats are: 'png', 'retina', 'svg', 'jpg', 'pdf' not %r" % fmt)
195 207
196 208 #-----------------------------------------------------------------------------
197 209 # Code for initializing matplotlib and importing pylab
198 210 #-----------------------------------------------------------------------------
199 211
200 212
201 213 def find_gui_and_backend(gui=None, gui_select=None):
202 214 """Given a gui string return the gui and mpl backend.
203 215
204 216 Parameters
205 217 ----------
206 218 gui : str
207 219 Can be one of ('tk','gtk','wx','qt','qt4','inline').
208 220 gui_select : str
209 221 Can be one of ('tk','gtk','wx','qt','qt4','inline').
210 222 This is any gui already selected by the shell.
211 223
212 224 Returns
213 225 -------
214 226 A tuple of (gui, backend) where backend is one of ('TkAgg','GTKAgg',
215 227 'WXAgg','Qt4Agg','module://IPython.kernel.zmq.pylab.backend_inline').
216 228 """
217 229
218 230 import matplotlib
219 231
220 232 if gui and gui != 'auto':
221 233 # select backend based on requested gui
222 234 backend = backends[gui]
223 235 else:
224 236 # We need to read the backend from the original data structure, *not*
225 237 # from mpl.rcParams, since a prior invocation of %matplotlib may have
226 238 # overwritten that.
227 239 # WARNING: this assumes matplotlib 1.1 or newer!!
228 240 backend = matplotlib.rcParamsOrig['backend']
229 241 # In this case, we need to find what the appropriate gui selection call
230 242 # should be for IPython, so we can activate inputhook accordingly
231 243 gui = backend2gui.get(backend, None)
232 244
233 245 # If we have already had a gui active, we need it and inline are the
234 246 # ones allowed.
235 247 if gui_select and gui != gui_select:
236 248 gui = gui_select
237 249 backend = backends[gui]
238 250
239 251 return gui, backend
240 252
241 253
242 254 def activate_matplotlib(backend):
243 255 """Activate the given backend and set interactive to True."""
244 256
245 257 import matplotlib
246 258 matplotlib.interactive(True)
247 259
248 260 # Matplotlib had a bug where even switch_backend could not force
249 261 # the rcParam to update. This needs to be set *before* the module
250 262 # magic of switch_backend().
251 263 matplotlib.rcParams['backend'] = backend
252 264
253 265 import matplotlib.pyplot
254 266 matplotlib.pyplot.switch_backend(backend)
255 267
256 268 # This must be imported last in the matplotlib series, after
257 269 # backend/interactivity choices have been made
258 270 import matplotlib.pylab as pylab
259 271
260 272 pylab.show._needmain = False
261 273 # We need to detect at runtime whether show() is called by the user.
262 274 # For this, we wrap it into a decorator which adds a 'called' flag.
263 275 pylab.draw_if_interactive = flag_calls(pylab.draw_if_interactive)
264 276
265 277
266 278 def import_pylab(user_ns, import_all=True):
267 279 """Populate the namespace with pylab-related values.
268 280
269 281 Imports matplotlib, pylab, numpy, and everything from pylab and numpy.
270 282
271 283 Also imports a few names from IPython (figsize, display, getfigs)
272 284
273 285 """
274 286
275 287 # Import numpy as np/pyplot as plt are conventions we're trying to
276 288 # somewhat standardize on. Making them available to users by default
277 289 # will greatly help this.
278 290 s = ("import numpy\n"
279 291 "import matplotlib\n"
280 292 "from matplotlib import pylab, mlab, pyplot\n"
281 293 "np = numpy\n"
282 294 "plt = pyplot\n"
283 295 )
284 296 exec(s, user_ns)
285 297
286 298 if import_all:
287 299 s = ("from matplotlib.pylab import *\n"
288 300 "from numpy import *\n")
289 301 exec(s, user_ns)
290 302
291 303 # IPython symbols to add
292 304 user_ns['figsize'] = figsize
293 305 from IPython.core.display import display
294 306 # Add display and getfigs to the user's namespace
295 307 user_ns['display'] = display
296 308 user_ns['getfigs'] = getfigs
297 309
298 310
299 311 def configure_inline_support(shell, backend):
300 312 """Configure an IPython shell object for matplotlib use.
301 313
302 314 Parameters
303 315 ----------
304 316 shell : InteractiveShell instance
305 317
306 318 backend : matplotlib backend
307 319 """
308 320 # If using our svg payload backend, register the post-execution
309 321 # function that will pick up the results for display. This can only be
310 322 # done with access to the real shell object.
311 323
312 324 # Note: if we can't load the inline backend, then there's no point
313 325 # continuing (such as in terminal-only shells in environments without
314 326 # zeromq available).
315 327 try:
316 328 from IPython.kernel.zmq.pylab.backend_inline import InlineBackend
317 329 except ImportError:
318 330 return
319 331 from matplotlib import pyplot
320 332
321 333 cfg = InlineBackend.instance(parent=shell)
322 334 cfg.shell = shell
323 335 if cfg not in shell.configurables:
324 336 shell.configurables.append(cfg)
325 337
326 338 if backend == backends['inline']:
327 339 from IPython.kernel.zmq.pylab.backend_inline import flush_figures
328 340 shell.register_post_execute(flush_figures)
329 341
330 342 # Save rcParams that will be overwrittern
331 343 shell._saved_rcParams = dict()
332 344 for k in cfg.rc:
333 345 shell._saved_rcParams[k] = pyplot.rcParams[k]
334 346 # load inline_rc
335 347 pyplot.rcParams.update(cfg.rc)
336 348 else:
337 349 from IPython.kernel.zmq.pylab.backend_inline import flush_figures
338 350 if flush_figures in shell._post_execute:
339 351 shell._post_execute.pop(flush_figures)
340 352 if hasattr(shell, '_saved_rcParams'):
341 353 pyplot.rcParams.update(shell._saved_rcParams)
342 354 del shell._saved_rcParams
343 355
344 356 # Setup the default figure format
345 select_figure_format(shell, cfg.figure_format, cfg.quality)
357 select_figure_formats(shell, cfg.figure_formats, cfg.quality)
346 358
@@ -1,282 +1,291 b''
1 1 """Tests for the Formatters."""
2 2
3 3 from math import pi
4 4
5 5 try:
6 6 import numpy
7 7 except:
8 8 numpy = None
9 9 import nose.tools as nt
10 10
11 from IPython.core.formatters import PlainTextFormatter, HTMLFormatter, _mod_name_key
11 from IPython.core.formatters import (
12 PlainTextFormatter, HTMLFormatter, PDFFormatter, _mod_name_key
13 )
12 14 from IPython.utils.io import capture_output
13 15
14 16 class A(object):
15 17 def __repr__(self):
16 18 return 'A()'
17 19
18 20 class B(A):
19 21 def __repr__(self):
20 22 return 'B()'
21 23
22 24 class C:
23 25 pass
24 26
25 27 class BadPretty(object):
26 28 _repr_pretty_ = None
27 29
28 30 class GoodPretty(object):
29 31 def _repr_pretty_(self, pp, cycle):
30 32 pp.text('foo')
31 33
32 34 def __repr__(self):
33 35 return 'GoodPretty()'
34 36
35 37 def foo_printer(obj, pp, cycle):
36 38 pp.text('foo')
37 39
38 40 def test_pretty():
39 41 f = PlainTextFormatter()
40 42 f.for_type(A, foo_printer)
41 43 nt.assert_equal(f(A()), 'foo')
42 44 nt.assert_equal(f(B()), 'foo')
43 45 nt.assert_equal(f(GoodPretty()), 'foo')
44 46 # Just don't raise an exception for the following:
45 47 f(BadPretty())
46 48
47 49 f.pprint = False
48 50 nt.assert_equal(f(A()), 'A()')
49 51 nt.assert_equal(f(B()), 'B()')
50 52 nt.assert_equal(f(GoodPretty()), 'GoodPretty()')
51 53
52 54
53 55 def test_deferred():
54 56 f = PlainTextFormatter()
55 57
56 58 def test_precision():
57 59 """test various values for float_precision."""
58 60 f = PlainTextFormatter()
59 61 nt.assert_equal(f(pi), repr(pi))
60 62 f.float_precision = 0
61 63 if numpy:
62 64 po = numpy.get_printoptions()
63 65 nt.assert_equal(po['precision'], 0)
64 66 nt.assert_equal(f(pi), '3')
65 67 f.float_precision = 2
66 68 if numpy:
67 69 po = numpy.get_printoptions()
68 70 nt.assert_equal(po['precision'], 2)
69 71 nt.assert_equal(f(pi), '3.14')
70 72 f.float_precision = '%g'
71 73 if numpy:
72 74 po = numpy.get_printoptions()
73 75 nt.assert_equal(po['precision'], 2)
74 76 nt.assert_equal(f(pi), '3.14159')
75 77 f.float_precision = '%e'
76 78 nt.assert_equal(f(pi), '3.141593e+00')
77 79 f.float_precision = ''
78 80 if numpy:
79 81 po = numpy.get_printoptions()
80 82 nt.assert_equal(po['precision'], 8)
81 83 nt.assert_equal(f(pi), repr(pi))
82 84
83 85 def test_bad_precision():
84 86 """test various invalid values for float_precision."""
85 87 f = PlainTextFormatter()
86 88 def set_fp(p):
87 89 f.float_precision=p
88 90 nt.assert_raises(ValueError, set_fp, '%')
89 91 nt.assert_raises(ValueError, set_fp, '%.3f%i')
90 92 nt.assert_raises(ValueError, set_fp, 'foo')
91 93 nt.assert_raises(ValueError, set_fp, -1)
92 94
93 95 def test_for_type():
94 96 f = PlainTextFormatter()
95 97
96 98 # initial return, None
97 99 nt.assert_is(f.for_type(C, foo_printer), None)
98 100 # no func queries
99 101 nt.assert_is(f.for_type(C), foo_printer)
100 102 # shouldn't change anything
101 103 nt.assert_is(f.for_type(C), foo_printer)
102 104 # None should do the same
103 105 nt.assert_is(f.for_type(C, None), foo_printer)
104 106 nt.assert_is(f.for_type(C, None), foo_printer)
105 107
106 108 def test_for_type_string():
107 109 f = PlainTextFormatter()
108 110
109 111 mod = C.__module__
110 112
111 113 type_str = '%s.%s' % (C.__module__, 'C')
112 114
113 115 # initial return, None
114 116 nt.assert_is(f.for_type(type_str, foo_printer), None)
115 117 # no func queries
116 118 nt.assert_is(f.for_type(type_str), foo_printer)
117 119 nt.assert_in(_mod_name_key(C), f.deferred_printers)
118 120 nt.assert_is(f.for_type(C), foo_printer)
119 121 nt.assert_not_in(_mod_name_key(C), f.deferred_printers)
120 122 nt.assert_in(C, f.type_printers)
121 123
122 124 def test_for_type_by_name():
123 125 f = PlainTextFormatter()
124 126
125 127 mod = C.__module__
126 128
127 129 # initial return, None
128 130 nt.assert_is(f.for_type_by_name(mod, 'C', foo_printer), None)
129 131 # no func queries
130 132 nt.assert_is(f.for_type_by_name(mod, 'C'), foo_printer)
131 133 # shouldn't change anything
132 134 nt.assert_is(f.for_type_by_name(mod, 'C'), foo_printer)
133 135 # None should do the same
134 136 nt.assert_is(f.for_type_by_name(mod, 'C', None), foo_printer)
135 137 nt.assert_is(f.for_type_by_name(mod, 'C', None), foo_printer)
136 138
137 139 def test_lookup():
138 140 f = PlainTextFormatter()
139 141
140 142 f.for_type(C, foo_printer)
141 143 nt.assert_is(f.lookup(C()), foo_printer)
142 144 with nt.assert_raises(KeyError):
143 145 f.lookup(A())
144 146
145 147 def test_lookup_string():
146 148 f = PlainTextFormatter()
147 149 type_str = '%s.%s' % (C.__module__, 'C')
148 150
149 151 f.for_type(type_str, foo_printer)
150 152 nt.assert_is(f.lookup(C()), foo_printer)
151 153 # should move from deferred to imported dict
152 154 nt.assert_not_in(_mod_name_key(C), f.deferred_printers)
153 155 nt.assert_in(C, f.type_printers)
154 156
155 157 def test_lookup_by_type():
156 158 f = PlainTextFormatter()
157 159 f.for_type(C, foo_printer)
158 160 nt.assert_is(f.lookup_by_type(C), foo_printer)
159 161 type_str = '%s.%s' % (C.__module__, 'C')
160 162 with nt.assert_raises(KeyError):
161 163 f.lookup_by_type(A)
162 164
163 165 def test_lookup_by_type_string():
164 166 f = PlainTextFormatter()
165 167 type_str = '%s.%s' % (C.__module__, 'C')
166 168 f.for_type(type_str, foo_printer)
167 169
168 170 # verify insertion
169 171 nt.assert_in(_mod_name_key(C), f.deferred_printers)
170 172 nt.assert_not_in(C, f.type_printers)
171 173
172 174 nt.assert_is(f.lookup_by_type(type_str), foo_printer)
173 175 # lookup by string doesn't cause import
174 176 nt.assert_in(_mod_name_key(C), f.deferred_printers)
175 177 nt.assert_not_in(C, f.type_printers)
176 178
177 179 nt.assert_is(f.lookup_by_type(C), foo_printer)
178 180 # should move from deferred to imported dict
179 181 nt.assert_not_in(_mod_name_key(C), f.deferred_printers)
180 182 nt.assert_in(C, f.type_printers)
181 183
182 184 def test_in_formatter():
183 185 f = PlainTextFormatter()
184 186 f.for_type(C, foo_printer)
185 187 type_str = '%s.%s' % (C.__module__, 'C')
186 188 nt.assert_in(C, f)
187 189 nt.assert_in(type_str, f)
188 190
189 191 def test_string_in_formatter():
190 192 f = PlainTextFormatter()
191 193 type_str = '%s.%s' % (C.__module__, 'C')
192 194 f.for_type(type_str, foo_printer)
193 195 nt.assert_in(type_str, f)
194 196 nt.assert_in(C, f)
195 197
196 198 def test_pop():
197 199 f = PlainTextFormatter()
198 200 f.for_type(C, foo_printer)
199 201 nt.assert_is(f.lookup_by_type(C), foo_printer)
200 202 nt.assert_is(f.pop(C, None), foo_printer)
201 203 f.for_type(C, foo_printer)
202 204 nt.assert_is(f.pop(C), foo_printer)
203 205 with nt.assert_raises(KeyError):
204 206 f.lookup_by_type(C)
205 207 with nt.assert_raises(KeyError):
206 208 f.pop(C)
207 209 with nt.assert_raises(KeyError):
208 210 f.pop(A)
209 211 nt.assert_is(f.pop(A, None), None)
210 212
211 213 def test_pop_string():
212 214 f = PlainTextFormatter()
213 215 type_str = '%s.%s' % (C.__module__, 'C')
214 216
215 217 with nt.assert_raises(KeyError):
216 218 f.pop(type_str)
217 219
218 220 f.for_type(type_str, foo_printer)
219 221 f.pop(type_str)
220 222 with nt.assert_raises(KeyError):
221 223 f.lookup_by_type(C)
222 224 with nt.assert_raises(KeyError):
223 225 f.pop(type_str)
224 226
225 227 f.for_type(C, foo_printer)
226 228 nt.assert_is(f.pop(type_str, None), foo_printer)
227 229 with nt.assert_raises(KeyError):
228 230 f.lookup_by_type(C)
229 231 with nt.assert_raises(KeyError):
230 232 f.pop(type_str)
231 233 nt.assert_is(f.pop(type_str, None), None)
232 234
233 235
234 236 def test_warn_error_method():
235 237 f = HTMLFormatter()
236 238 class BadHTML(object):
237 239 def _repr_html_(self):
238 240 return 1/0
239 241 bad = BadHTML()
240 242 with capture_output() as captured:
241 243 result = f(bad)
242 244 nt.assert_is(result, None)
243 245 nt.assert_in("FormatterWarning", captured.stderr)
244 246 nt.assert_in("text/html", captured.stderr)
245 247 nt.assert_in("zero", captured.stderr)
246 248
247 249 def test_nowarn_notimplemented():
248 250 f = HTMLFormatter()
249 251 class HTMLNotImplemented(object):
250 252 def _repr_html_(self):
251 253 raise NotImplementedError
252 254 return 1/0
253 255 h = HTMLNotImplemented()
254 256 with capture_output() as captured:
255 257 result = f(h)
256 258 nt.assert_is(result, None)
257 259 nt.assert_not_in("FormatterWarning", captured.stderr)
258 260
259 261 def test_warn_error_for_type():
260 262 f = HTMLFormatter()
261 263 f.for_type(int, lambda i: name_error)
262 264 with capture_output() as captured:
263 265 result = f(5)
264 266 nt.assert_is(result, None)
265 267 nt.assert_in("FormatterWarning", captured.stderr)
266 268 nt.assert_in("text/html", captured.stderr)
267 269 nt.assert_in("name_error", captured.stderr)
268 270
269 271 def test_warn_error_pretty_method():
270 272 f = PlainTextFormatter()
271 273 class BadPretty(object):
272 274 def _repr_pretty_(self):
273 275 return "hello"
274 276 bad = BadPretty()
275 277 with capture_output() as captured:
276 278 result = f(bad)
277 279 nt.assert_is(result, None)
278 280 nt.assert_in("FormatterWarning", captured.stderr)
279 281 nt.assert_in("text/plain", captured.stderr)
280 282 nt.assert_in("argument", captured.stderr)
281 283
284 class MakePDF(object):
285 def _repr_pdf_(self):
286 return 'PDF'
282 287
288 def test_pdf_formatter():
289 pdf = MakePDF()
290 f = PDFFormatter()
291 nt.assert_equal(f(pdf), 'PDF')
@@ -1,62 +1,62 b''
1 1 """Tornado handlers logging into the notebook.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2011 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 uuid
20 20
21 21 from tornado.escape import url_escape
22 22
23 23 from IPython.lib.security import passwd_check
24 24
25 25 from ..base.handlers import IPythonHandler
26 26
27 27 #-----------------------------------------------------------------------------
28 28 # Handler
29 29 #-----------------------------------------------------------------------------
30 30
31 31 class LoginHandler(IPythonHandler):
32 32
33 33 def _render(self, message=None):
34 34 self.write(self.render_template('login.html',
35 next=url_escape(self.get_argument('next', default=self.base_project_url)),
35 next=url_escape(self.get_argument('next', default=self.base_url)),
36 36 message=message,
37 37 ))
38 38
39 39 def get(self):
40 40 if self.current_user:
41 self.redirect(self.get_argument('next', default=self.base_project_url))
41 self.redirect(self.get_argument('next', default=self.base_url))
42 42 else:
43 43 self._render()
44 44
45 45 def post(self):
46 46 pwd = self.get_argument('password', default=u'')
47 47 if self.login_available:
48 48 if passwd_check(self.password, pwd):
49 49 self.set_secure_cookie(self.cookie_name, str(uuid.uuid4()))
50 50 else:
51 51 self._render(message={'error': 'Invalid password'})
52 52 return
53 53
54 self.redirect(self.get_argument('next', default=self.base_project_url))
54 self.redirect(self.get_argument('next', default=self.base_url))
55 55
56 56
57 57 #-----------------------------------------------------------------------------
58 58 # URL to handler mappings
59 59 #-----------------------------------------------------------------------------
60 60
61 61
62 62 default_handlers = [(r"/login", LoginHandler)]
@@ -1,387 +1,387 b''
1 1 """Base Tornado handlers for the notebook.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2011 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
20 20 import functools
21 21 import json
22 22 import logging
23 23 import os
24 24 import re
25 25 import sys
26 26 import traceback
27 27 try:
28 28 # py3
29 29 from http.client import responses
30 30 except ImportError:
31 31 from httplib import responses
32 32
33 33 from jinja2 import TemplateNotFound
34 34 from tornado import web
35 35
36 36 try:
37 37 from tornado.log import app_log
38 38 except ImportError:
39 39 app_log = logging.getLogger()
40 40
41 41 from IPython.config import Application
42 42 from IPython.utils.path import filefind
43 43 from IPython.utils.py3compat import string_types
44 44 from IPython.html.utils import is_hidden
45 45
46 46 #-----------------------------------------------------------------------------
47 47 # Top-level handlers
48 48 #-----------------------------------------------------------------------------
49 49 non_alphanum = re.compile(r'[^A-Za-z0-9]')
50 50
51 51 class AuthenticatedHandler(web.RequestHandler):
52 52 """A RequestHandler with an authenticated user."""
53 53
54 54 def clear_login_cookie(self):
55 55 self.clear_cookie(self.cookie_name)
56 56
57 57 def get_current_user(self):
58 58 user_id = self.get_secure_cookie(self.cookie_name)
59 59 # For now the user_id should not return empty, but it could eventually
60 60 if user_id == '':
61 61 user_id = 'anonymous'
62 62 if user_id is None:
63 63 # prevent extra Invalid cookie sig warnings:
64 64 self.clear_login_cookie()
65 65 if not self.login_available:
66 66 user_id = 'anonymous'
67 67 return user_id
68 68
69 69 @property
70 70 def cookie_name(self):
71 71 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
72 72 self.request.host
73 73 ))
74 74 return self.settings.get('cookie_name', default_cookie_name)
75 75
76 76 @property
77 77 def password(self):
78 78 """our password"""
79 79 return self.settings.get('password', '')
80 80
81 81 @property
82 82 def logged_in(self):
83 83 """Is a user currently logged in?
84 84
85 85 """
86 86 user = self.get_current_user()
87 87 return (user and not user == 'anonymous')
88 88
89 89 @property
90 90 def login_available(self):
91 91 """May a user proceed to log in?
92 92
93 93 This returns True if login capability is available, irrespective of
94 94 whether the user is already logged in or not.
95 95
96 96 """
97 97 return bool(self.settings.get('password', ''))
98 98
99 99
100 100 class IPythonHandler(AuthenticatedHandler):
101 101 """IPython-specific extensions to authenticated handling
102 102
103 103 Mostly property shortcuts to IPython-specific settings.
104 104 """
105 105
106 106 @property
107 107 def config(self):
108 108 return self.settings.get('config', None)
109 109
110 110 @property
111 111 def log(self):
112 112 """use the IPython log by default, falling back on tornado's logger"""
113 113 if Application.initialized():
114 114 return Application.instance().log
115 115 else:
116 116 return app_log
117 117
118 118 #---------------------------------------------------------------
119 119 # URLs
120 120 #---------------------------------------------------------------
121 121
122 122 @property
123 123 def ws_url(self):
124 124 """websocket url matching the current request
125 125
126 126 By default, this is just `''`, indicating that it should match
127 127 the same host, protocol, port, etc.
128 128 """
129 129 return self.settings.get('websocket_url', '')
130 130
131 131 @property
132 132 def mathjax_url(self):
133 133 return self.settings.get('mathjax_url', '')
134 134
135 135 @property
136 def base_project_url(self):
137 return self.settings.get('base_project_url', '/')
136 def base_url(self):
137 return self.settings.get('base_url', '/')
138 138
139 139 @property
140 140 def base_kernel_url(self):
141 141 return self.settings.get('base_kernel_url', '/')
142 142
143 143 #---------------------------------------------------------------
144 144 # Manager objects
145 145 #---------------------------------------------------------------
146 146
147 147 @property
148 148 def kernel_manager(self):
149 149 return self.settings['kernel_manager']
150 150
151 151 @property
152 152 def notebook_manager(self):
153 153 return self.settings['notebook_manager']
154 154
155 155 @property
156 156 def cluster_manager(self):
157 157 return self.settings['cluster_manager']
158 158
159 159 @property
160 160 def session_manager(self):
161 161 return self.settings['session_manager']
162 162
163 163 @property
164 164 def project_dir(self):
165 165 return self.notebook_manager.notebook_dir
166 166
167 167 #---------------------------------------------------------------
168 168 # template rendering
169 169 #---------------------------------------------------------------
170 170
171 171 def get_template(self, name):
172 172 """Return the jinja template object for a given name"""
173 173 return self.settings['jinja2_env'].get_template(name)
174 174
175 175 def render_template(self, name, **ns):
176 176 ns.update(self.template_namespace)
177 177 template = self.get_template(name)
178 178 return template.render(**ns)
179 179
180 180 @property
181 181 def template_namespace(self):
182 182 return dict(
183 base_project_url=self.base_project_url,
183 base_url=self.base_url,
184 184 base_kernel_url=self.base_kernel_url,
185 185 logged_in=self.logged_in,
186 186 login_available=self.login_available,
187 187 static_url=self.static_url,
188 188 )
189 189
190 190 def get_json_body(self):
191 191 """Return the body of the request as JSON data."""
192 192 if not self.request.body:
193 193 return None
194 194 # Do we need to call body.decode('utf-8') here?
195 195 body = self.request.body.strip().decode(u'utf-8')
196 196 try:
197 197 model = json.loads(body)
198 198 except Exception:
199 199 self.log.debug("Bad JSON: %r", body)
200 200 self.log.error("Couldn't parse JSON", exc_info=True)
201 201 raise web.HTTPError(400, u'Invalid JSON in body of request')
202 202 return model
203 203
204 204 def get_error_html(self, status_code, **kwargs):
205 205 """render custom error pages"""
206 206 exception = kwargs.get('exception')
207 207 message = ''
208 208 status_message = responses.get(status_code, 'Unknown HTTP Error')
209 209 if exception:
210 210 # get the custom message, if defined
211 211 try:
212 212 message = exception.log_message % exception.args
213 213 except Exception:
214 214 pass
215 215
216 216 # construct the custom reason, if defined
217 217 reason = getattr(exception, 'reason', '')
218 218 if reason:
219 219 status_message = reason
220 220
221 221 # build template namespace
222 222 ns = dict(
223 223 status_code=status_code,
224 224 status_message=status_message,
225 225 message=message,
226 226 exception=exception,
227 227 )
228 228
229 229 # render the template
230 230 try:
231 231 html = self.render_template('%s.html' % status_code, **ns)
232 232 except TemplateNotFound:
233 233 self.log.debug("No template for %d", status_code)
234 234 html = self.render_template('error.html', **ns)
235 235 return html
236 236
237 237
238 238 class Template404(IPythonHandler):
239 239 """Render our 404 template"""
240 240 def prepare(self):
241 241 raise web.HTTPError(404)
242 242
243 243
244 244 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
245 245 """static files should only be accessible when logged in"""
246 246
247 247 @web.authenticated
248 248 def get(self, path):
249 249 if os.path.splitext(path)[1] == '.ipynb':
250 250 name = os.path.basename(path)
251 251 self.set_header('Content-Type', 'application/json')
252 252 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
253 253
254 254 return web.StaticFileHandler.get(self, path)
255 255
256 256 def compute_etag(self):
257 257 return None
258 258
259 259 def validate_absolute_path(self, root, absolute_path):
260 260 """Validate and return the absolute path.
261 261
262 262 Requires tornado 3.1
263 263
264 264 Adding to tornado's own handling, forbids the serving of hidden files.
265 265 """
266 266 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
267 267 abs_root = os.path.abspath(root)
268 268 if is_hidden(abs_path, abs_root):
269 269 raise web.HTTPError(404)
270 270 return abs_path
271 271
272 272
273 273 def json_errors(method):
274 274 """Decorate methods with this to return GitHub style JSON errors.
275 275
276 276 This should be used on any JSON API on any handler method that can raise HTTPErrors.
277 277
278 278 This will grab the latest HTTPError exception using sys.exc_info
279 279 and then:
280 280
281 281 1. Set the HTTP status code based on the HTTPError
282 282 2. Create and return a JSON body with a message field describing
283 283 the error in a human readable form.
284 284 """
285 285 @functools.wraps(method)
286 286 def wrapper(self, *args, **kwargs):
287 287 try:
288 288 result = method(self, *args, **kwargs)
289 289 except web.HTTPError as e:
290 290 status = e.status_code
291 291 message = e.log_message
292 292 self.set_status(e.status_code)
293 293 self.finish(json.dumps(dict(message=message)))
294 294 except Exception:
295 295 self.log.error("Unhandled error in API request", exc_info=True)
296 296 status = 500
297 297 message = "Unknown server error"
298 298 t, value, tb = sys.exc_info()
299 299 self.set_status(status)
300 300 tb_text = ''.join(traceback.format_exception(t, value, tb))
301 301 reply = dict(message=message, traceback=tb_text)
302 302 self.finish(json.dumps(reply))
303 303 else:
304 304 return result
305 305 return wrapper
306 306
307 307
308 308
309 309 #-----------------------------------------------------------------------------
310 310 # File handler
311 311 #-----------------------------------------------------------------------------
312 312
313 313 # to minimize subclass changes:
314 314 HTTPError = web.HTTPError
315 315
316 316 class FileFindHandler(web.StaticFileHandler):
317 317 """subclass of StaticFileHandler for serving files from a search path"""
318 318
319 319 # cache search results, don't search for files more than once
320 320 _static_paths = {}
321 321
322 322 def initialize(self, path, default_filename=None):
323 323 if isinstance(path, string_types):
324 324 path = [path]
325 325
326 326 self.root = tuple(
327 327 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
328 328 )
329 329 self.default_filename = default_filename
330 330
331 331 def compute_etag(self):
332 332 return None
333 333
334 334 @classmethod
335 335 def get_absolute_path(cls, roots, path):
336 336 """locate a file to serve on our static file search path"""
337 337 with cls._lock:
338 338 if path in cls._static_paths:
339 339 return cls._static_paths[path]
340 340 try:
341 341 abspath = os.path.abspath(filefind(path, roots))
342 342 except IOError:
343 343 # IOError means not found
344 344 return ''
345 345
346 346 cls._static_paths[path] = abspath
347 347 return abspath
348 348
349 349 def validate_absolute_path(self, root, absolute_path):
350 350 """check if the file should be served (raises 404, 403, etc.)"""
351 351 if absolute_path == '':
352 352 raise web.HTTPError(404)
353 353
354 354 for root in self.root:
355 355 if (absolute_path + os.sep).startswith(root):
356 356 break
357 357
358 358 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
359 359
360 360
361 361 class TrailingSlashHandler(web.RequestHandler):
362 362 """Simple redirect handler that strips trailing slashes
363 363
364 364 This should be the first, highest priority handler.
365 365 """
366 366
367 367 SUPPORTED_METHODS = ['GET']
368 368
369 369 def get(self):
370 370 self.redirect(self.request.uri.rstrip('/'))
371 371
372 372 #-----------------------------------------------------------------------------
373 373 # URL pattern fragments for re-use
374 374 #-----------------------------------------------------------------------------
375 375
376 376 path_regex = r"(?P<path>(?:/.*)*)"
377 377 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
378 378 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
379 379
380 380 #-----------------------------------------------------------------------------
381 381 # URL to handler mappings
382 382 #-----------------------------------------------------------------------------
383 383
384 384
385 385 default_handlers = [
386 386 (r".*/", TrailingSlashHandler)
387 387 ]
@@ -1,90 +1,90 b''
1 1 """Tornado handlers for the live notebook view.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2011 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 os
20 20 from tornado import web
21 21 HTTPError = web.HTTPError
22 22
23 23 from ..base.handlers import IPythonHandler, notebook_path_regex, path_regex
24 24 from ..utils import url_path_join, url_escape
25 25
26 26 #-----------------------------------------------------------------------------
27 27 # Handlers
28 28 #-----------------------------------------------------------------------------
29 29
30 30
31 31 class NotebookHandler(IPythonHandler):
32 32
33 33 @web.authenticated
34 34 def get(self, path='', name=None):
35 35 """get renders the notebook template if a name is given, or
36 36 redirects to the '/files/' handler if the name is not given."""
37 37 path = path.strip('/')
38 38 nbm = self.notebook_manager
39 39 if name is None:
40 40 raise web.HTTPError(500, "This shouldn't be accessible: %s" % self.request.uri)
41 41
42 42 # a .ipynb filename was given
43 43 if not nbm.notebook_exists(name, path):
44 44 raise web.HTTPError(404, u'Notebook does not exist: %s/%s' % (path, name))
45 45 name = url_escape(name)
46 46 path = url_escape(path)
47 47 self.write(self.render_template('notebook.html',
48 48 project=self.project_dir,
49 49 notebook_path=path,
50 50 notebook_name=name,
51 51 kill_kernel=False,
52 52 mathjax_url=self.mathjax_url,
53 53 )
54 54 )
55 55
56 56 class NotebookRedirectHandler(IPythonHandler):
57 57 def get(self, path=''):
58 58 nbm = self.notebook_manager
59 59 if nbm.path_exists(path):
60 60 # it's a *directory*, redirect to /tree
61 url = url_path_join(self.base_project_url, 'tree', path)
61 url = url_path_join(self.base_url, 'tree', path)
62 62 else:
63 63 # otherwise, redirect to /files
64 64 if '/files/' in path:
65 65 # redirect without files/ iff it would 404
66 66 # this preserves pre-2.0-style 'files/' links
67 67 # FIXME: this is hardcoded based on notebook_path,
68 68 # but so is the files handler itself,
69 69 # so it should work until both are cleaned up.
70 70 parts = path.split('/')
71 71 files_path = os.path.join(nbm.notebook_dir, *parts)
72 72 self.log.warn("filespath: %s", files_path)
73 73 if not os.path.exists(files_path):
74 74 path = path.replace('/files/', '/', 1)
75 75
76 url = url_path_join(self.base_project_url, 'files', path)
76 url = url_path_join(self.base_url, 'files', path)
77 77 url = url_escape(url)
78 78 self.log.debug("Redirecting %s to %s", self.request.path, url)
79 79 self.redirect(url)
80 80
81 81 #-----------------------------------------------------------------------------
82 82 # URL to handler mappings
83 83 #-----------------------------------------------------------------------------
84 84
85 85
86 86 default_handlers = [
87 87 (r"/notebooks%s" % notebook_path_regex, NotebookHandler),
88 88 (r"/notebooks%s" % path_regex, NotebookRedirectHandler),
89 89 ]
90 90
@@ -1,837 +1,842 b''
1 1 # coding: utf-8
2 2 """A tornado based IPython notebook server.
3 3
4 4 Authors:
5 5
6 6 * Brian Granger
7 7 """
8 8 from __future__ import print_function
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2013 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 # stdlib
21 21 import errno
22 22 import io
23 23 import json
24 24 import logging
25 25 import os
26 26 import random
27 27 import select
28 28 import signal
29 29 import socket
30 30 import sys
31 31 import threading
32 32 import time
33 33 import webbrowser
34 34
35 35
36 36 # Third party
37 37 # check for pyzmq 2.1.11
38 38 from IPython.utils.zmqrelated import check_for_zmq
39 39 check_for_zmq('2.1.11', 'IPython.html')
40 40
41 41 from jinja2 import Environment, FileSystemLoader
42 42
43 43 # Install the pyzmq ioloop. This has to be done before anything else from
44 44 # tornado is imported.
45 45 from zmq.eventloop import ioloop
46 46 ioloop.install()
47 47
48 48 # check for tornado 3.1.0
49 49 msg = "The IPython Notebook requires tornado >= 3.1.0"
50 50 try:
51 51 import tornado
52 52 except ImportError:
53 53 raise ImportError(msg)
54 54 try:
55 55 version_info = tornado.version_info
56 56 except AttributeError:
57 57 raise ImportError(msg + ", but you have < 1.1.0")
58 58 if version_info < (3,1,0):
59 59 raise ImportError(msg + ", but you have %s" % tornado.version)
60 60
61 61 from tornado import httpserver
62 62 from tornado import web
63 63
64 64 # Our own libraries
65 65 from IPython.html import DEFAULT_STATIC_FILES_PATH
66 66 from .base.handlers import Template404
67 67 from .log import log_request
68 68 from .services.kernels.kernelmanager import MappingKernelManager
69 69 from .services.notebooks.nbmanager import NotebookManager
70 70 from .services.notebooks.filenbmanager import FileNotebookManager
71 71 from .services.clusters.clustermanager import ClusterManager
72 72 from .services.sessions.sessionmanager import SessionManager
73 73
74 74 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
75 75
76 76 from IPython.config.application import catch_config_error, boolean_flag
77 77 from IPython.core.application import BaseIPythonApplication
78 78 from IPython.core.profiledir import ProfileDir
79 79 from IPython.consoleapp import IPythonConsoleApp
80 80 from IPython.kernel import swallow_argv
81 81 from IPython.kernel.zmq.session import default_secure
82 82 from IPython.kernel.zmq.kernelapp import (
83 83 kernel_flags,
84 84 kernel_aliases,
85 85 )
86 86 from IPython.utils.importstring import import_item
87 87 from IPython.utils.localinterfaces import localhost
88 88 from IPython.utils import submodule
89 89 from IPython.utils.traitlets import (
90 90 Dict, Unicode, Integer, List, Bool, Bytes,
91 91 DottedObjectName
92 92 )
93 93 from IPython.utils import py3compat
94 94 from IPython.utils.path import filefind, get_ipython_dir
95 95
96 96 from .utils import url_path_join
97 97
98 98 #-----------------------------------------------------------------------------
99 99 # Module globals
100 100 #-----------------------------------------------------------------------------
101 101
102 102 _examples = """
103 103 ipython notebook # start the notebook
104 104 ipython notebook --profile=sympy # use the sympy profile
105 105 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
106 106 """
107 107
108 108 #-----------------------------------------------------------------------------
109 109 # Helper functions
110 110 #-----------------------------------------------------------------------------
111 111
112 112 def random_ports(port, n):
113 113 """Generate a list of n random ports near the given port.
114 114
115 115 The first 5 ports will be sequential, and the remaining n-5 will be
116 116 randomly selected in the range [port-2*n, port+2*n].
117 117 """
118 118 for i in range(min(5, n)):
119 119 yield port + i
120 120 for i in range(n-5):
121 121 yield max(1, port + random.randint(-2*n, 2*n))
122 122
123 123 def load_handlers(name):
124 124 """Load the (URL pattern, handler) tuples for each component."""
125 125 name = 'IPython.html.' + name
126 126 mod = __import__(name, fromlist=['default_handlers'])
127 127 return mod.default_handlers
128 128
129 129 #-----------------------------------------------------------------------------
130 130 # The Tornado web application
131 131 #-----------------------------------------------------------------------------
132 132
133 133 class NotebookWebApplication(web.Application):
134 134
135 135 def __init__(self, ipython_app, kernel_manager, notebook_manager,
136 cluster_manager, session_manager, log, base_project_url,
136 cluster_manager, session_manager, log, base_url,
137 137 settings_overrides):
138 138
139 139 settings = self.init_settings(
140 140 ipython_app, kernel_manager, notebook_manager, cluster_manager,
141 session_manager, log, base_project_url, settings_overrides)
141 session_manager, log, base_url, settings_overrides)
142 142 handlers = self.init_handlers(settings)
143 143
144 144 super(NotebookWebApplication, self).__init__(handlers, **settings)
145 145
146 146 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
147 cluster_manager, session_manager, log, base_project_url,
147 cluster_manager, session_manager, log, base_url,
148 148 settings_overrides):
149 149 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
150 # base_project_url will always be unicode, which will in turn
150 # base_url will always be unicode, which will in turn
151 151 # make the patterns unicode, and ultimately result in unicode
152 152 # keys in kwargs to handler._execute(**kwargs) in tornado.
153 # This enforces that base_project_url be ascii in that situation.
153 # This enforces that base_url be ascii in that situation.
154 154 #
155 155 # Note that the URLs these patterns check against are escaped,
156 156 # and thus guaranteed to be ASCII: 'héllo' is really 'h%C3%A9llo'.
157 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
157 base_url = py3compat.unicode_to_str(base_url, 'ascii')
158 158 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
159 159 settings = dict(
160 160 # basics
161 161 log_function=log_request,
162 base_project_url=base_project_url,
162 base_url=base_url,
163 163 base_kernel_url=ipython_app.base_kernel_url,
164 164 template_path=template_path,
165 165 static_path=ipython_app.static_file_path,
166 166 static_handler_class = FileFindHandler,
167 static_url_prefix = url_path_join(base_project_url,'/static/'),
167 static_url_prefix = url_path_join(base_url,'/static/'),
168 168
169 169 # authentication
170 170 cookie_secret=ipython_app.cookie_secret,
171 login_url=url_path_join(base_project_url,'/login'),
171 login_url=url_path_join(base_url,'/login'),
172 172 password=ipython_app.password,
173 173
174 174 # managers
175 175 kernel_manager=kernel_manager,
176 176 notebook_manager=notebook_manager,
177 177 cluster_manager=cluster_manager,
178 178 session_manager=session_manager,
179 179
180 180 # IPython stuff
181 181 nbextensions_path = ipython_app.nbextensions_path,
182 182 mathjax_url=ipython_app.mathjax_url,
183 183 config=ipython_app.config,
184 184 jinja2_env=Environment(loader=FileSystemLoader(template_path)),
185 185 )
186 186
187 187 # allow custom overrides for the tornado web app.
188 188 settings.update(settings_overrides)
189 189 return settings
190 190
191 191 def init_handlers(self, settings):
192 192 # Load the (URL pattern, handler) tuples for each component.
193 193 handlers = []
194 194 handlers.extend(load_handlers('base.handlers'))
195 195 handlers.extend(load_handlers('tree.handlers'))
196 196 handlers.extend(load_handlers('auth.login'))
197 197 handlers.extend(load_handlers('auth.logout'))
198 198 handlers.extend(load_handlers('notebook.handlers'))
199 199 handlers.extend(load_handlers('nbconvert.handlers'))
200 200 handlers.extend(load_handlers('services.kernels.handlers'))
201 201 handlers.extend(load_handlers('services.notebooks.handlers'))
202 202 handlers.extend(load_handlers('services.clusters.handlers'))
203 203 handlers.extend(load_handlers('services.sessions.handlers'))
204 204 handlers.extend(load_handlers('services.nbconvert.handlers'))
205 205 handlers.extend([
206 206 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : settings['notebook_manager'].notebook_dir}),
207 207 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
208 208 ])
209 # prepend base_project_url onto the patterns that we match
209 # prepend base_url onto the patterns that we match
210 210 new_handlers = []
211 211 for handler in handlers:
212 pattern = url_path_join(settings['base_project_url'], handler[0])
212 pattern = url_path_join(settings['base_url'], handler[0])
213 213 new_handler = tuple([pattern] + list(handler[1:]))
214 214 new_handlers.append(new_handler)
215 215 # add 404 on the end, which will catch everything that falls through
216 216 new_handlers.append((r'(.*)', Template404))
217 217 return new_handlers
218 218
219 219
220 220 class NbserverListApp(BaseIPythonApplication):
221 221
222 222 description="List currently running notebook servers in this profile."
223 223
224 224 flags = dict(
225 225 json=({'NbserverListApp': {'json': True}},
226 226 "Produce machine-readable JSON output."),
227 227 )
228 228
229 229 json = Bool(False, config=True,
230 230 help="If True, each line of output will be a JSON object with the "
231 231 "details from the server info file.")
232 232
233 233 def start(self):
234 234 if not self.json:
235 235 print("Currently running servers:")
236 236 for serverinfo in list_running_servers(self.profile):
237 237 if self.json:
238 238 print(json.dumps(serverinfo))
239 239 else:
240 240 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
241 241
242 242 #-----------------------------------------------------------------------------
243 243 # Aliases and Flags
244 244 #-----------------------------------------------------------------------------
245 245
246 246 flags = dict(kernel_flags)
247 247 flags['no-browser']=(
248 248 {'NotebookApp' : {'open_browser' : False}},
249 249 "Don't open the notebook in a browser after startup."
250 250 )
251 251 flags['no-mathjax']=(
252 252 {'NotebookApp' : {'enable_mathjax' : False}},
253 253 """Disable MathJax
254 254
255 255 MathJax is the javascript library IPython uses to render math/LaTeX. It is
256 256 very large, so you may want to disable it if you have a slow internet
257 257 connection, or for offline use of the notebook.
258 258
259 259 When disabled, equations etc. will appear as their untransformed TeX source.
260 260 """
261 261 )
262 262
263 263 # Add notebook manager flags
264 264 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
265 265 'Auto-save a .py script everytime the .ipynb notebook is saved',
266 266 'Do not auto-save .py scripts for every notebook'))
267 267
268 268 # the flags that are specific to the frontend
269 269 # these must be scrubbed before being passed to the kernel,
270 270 # or it will raise an error on unrecognized flags
271 271 notebook_flags = ['no-browser', 'no-mathjax', 'script', 'no-script']
272 272
273 273 aliases = dict(kernel_aliases)
274 274
275 275 aliases.update({
276 276 'ip': 'NotebookApp.ip',
277 277 'port': 'NotebookApp.port',
278 278 'port-retries': 'NotebookApp.port_retries',
279 279 'transport': 'KernelManager.transport',
280 280 'keyfile': 'NotebookApp.keyfile',
281 281 'certfile': 'NotebookApp.certfile',
282 282 'notebook-dir': 'NotebookManager.notebook_dir',
283 283 'browser': 'NotebookApp.browser',
284 284 })
285 285
286 286 # remove ipkernel flags that are singletons, and don't make sense in
287 287 # multi-kernel evironment:
288 288 aliases.pop('f', None)
289 289
290 290 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
291 291 u'notebook-dir', u'profile', u'profile-dir']
292 292
293 293 #-----------------------------------------------------------------------------
294 294 # NotebookApp
295 295 #-----------------------------------------------------------------------------
296 296
297 297 class NotebookApp(BaseIPythonApplication):
298 298
299 299 name = 'ipython-notebook'
300 300
301 301 description = """
302 302 The IPython HTML Notebook.
303 303
304 304 This launches a Tornado based HTML Notebook Server that serves up an
305 305 HTML5/Javascript Notebook client.
306 306 """
307 307 examples = _examples
308 308
309 309 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
310 310 FileNotebookManager]
311 311 flags = Dict(flags)
312 312 aliases = Dict(aliases)
313 313
314 314 subcommands = dict(
315 315 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
316 316 )
317 317
318 318 kernel_argv = List(Unicode)
319 319
320 320 def _log_level_default(self):
321 321 return logging.INFO
322 322
323 323 def _log_format_default(self):
324 324 """override default log format to include time"""
325 325 return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"
326 326
327 327 # create requested profiles by default, if they don't exist:
328 328 auto_create = Bool(True)
329 329
330 330 # file to be opened in the notebook server
331 331 file_to_run = Unicode('')
332 332
333 333 # Network related information.
334 334
335 335 ip = Unicode(config=True,
336 336 help="The IP address the notebook server will listen on."
337 337 )
338 338 def _ip_default(self):
339 339 return localhost()
340 340
341 341 def _ip_changed(self, name, old, new):
342 342 if new == u'*': self.ip = u''
343 343
344 344 port = Integer(8888, config=True,
345 345 help="The port the notebook server will listen on."
346 346 )
347 347 port_retries = Integer(50, config=True,
348 348 help="The number of additional ports to try if the specified port is not available."
349 349 )
350 350
351 351 certfile = Unicode(u'', config=True,
352 352 help="""The full path to an SSL/TLS certificate file."""
353 353 )
354 354
355 355 keyfile = Unicode(u'', config=True,
356 356 help="""The full path to a private key file for usage with SSL/TLS."""
357 357 )
358 358
359 359 cookie_secret = Bytes(b'', config=True,
360 360 help="""The random bytes used to secure cookies.
361 361 By default this is a new random number every time you start the Notebook.
362 362 Set it to a value in a config file to enable logins to persist across server sessions.
363 363
364 364 Note: Cookie secrets should be kept private, do not share config files with
365 365 cookie_secret stored in plaintext (you can read the value from a file).
366 366 """
367 367 )
368 368 def _cookie_secret_default(self):
369 369 return os.urandom(1024)
370 370
371 371 password = Unicode(u'', config=True,
372 372 help="""Hashed password to use for web authentication.
373 373
374 374 To generate, type in a python/IPython shell:
375 375
376 376 from IPython.lib import passwd; passwd()
377 377
378 378 The string should be of the form type:salt:hashed-password.
379 379 """
380 380 )
381 381
382 382 open_browser = Bool(True, config=True,
383 383 help="""Whether to open in a browser after starting.
384 384 The specific browser used is platform dependent and
385 385 determined by the python standard library `webbrowser`
386 386 module, unless it is overridden using the --browser
387 387 (NotebookApp.browser) configuration option.
388 388 """)
389 389
390 390 browser = Unicode(u'', config=True,
391 391 help="""Specify what command to use to invoke a web
392 392 browser when opening the notebook. If not specified, the
393 393 default browser will be determined by the `webbrowser`
394 394 standard library module, which allows setting of the
395 395 BROWSER environment variable to override it.
396 396 """)
397 397
398 398 webapp_settings = Dict(config=True,
399 399 help="Supply overrides for the tornado.web.Application that the "
400 400 "IPython notebook uses.")
401 401
402 402 enable_mathjax = Bool(True, config=True,
403 403 help="""Whether to enable MathJax for typesetting math/TeX
404 404
405 405 MathJax is the javascript library IPython uses to render math/LaTeX. It is
406 406 very large, so you may want to disable it if you have a slow internet
407 407 connection, or for offline use of the notebook.
408 408
409 409 When disabled, equations etc. will appear as their untransformed TeX source.
410 410 """
411 411 )
412 412 def _enable_mathjax_changed(self, name, old, new):
413 413 """set mathjax url to empty if mathjax is disabled"""
414 414 if not new:
415 415 self.mathjax_url = u''
416 416
417 base_project_url = Unicode('/', config=True,
417 base_url = Unicode('/', config=True,
418 418 help='''The base URL for the notebook server.
419 419
420 420 Leading and trailing slashes can be omitted,
421 421 and will automatically be added.
422 422 ''')
423 def _base_project_url_changed(self, name, old, new):
423 def _base_url_changed(self, name, old, new):
424 424 if not new.startswith('/'):
425 self.base_project_url = '/'+new
425 self.base_url = '/'+new
426 426 elif not new.endswith('/'):
427 self.base_project_url = new+'/'
427 self.base_url = new+'/'
428
429 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
430 def _base_project_url_changed(self, name, old, new):
431 self.log.warn("base_project_url is deprecated, use base_url")
432 self.base_url = new
428 433
429 434 base_kernel_url = Unicode('/', config=True,
430 435 help='''The base URL for the kernel server
431 436
432 437 Leading and trailing slashes can be omitted,
433 438 and will automatically be added.
434 439 ''')
435 440 def _base_kernel_url_changed(self, name, old, new):
436 441 if not new.startswith('/'):
437 442 self.base_kernel_url = '/'+new
438 443 elif not new.endswith('/'):
439 444 self.base_kernel_url = new+'/'
440 445
441 446 websocket_url = Unicode("", config=True,
442 447 help="""The base URL for the websocket server,
443 448 if it differs from the HTTP server (hint: it almost certainly doesn't).
444 449
445 450 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
446 451 """
447 452 )
448 453
449 454 extra_static_paths = List(Unicode, config=True,
450 455 help="""Extra paths to search for serving static files.
451 456
452 457 This allows adding javascript/css to be available from the notebook server machine,
453 458 or overriding individual files in the IPython"""
454 459 )
455 460 def _extra_static_paths_default(self):
456 461 return [os.path.join(self.profile_dir.location, 'static')]
457 462
458 463 @property
459 464 def static_file_path(self):
460 465 """return extra paths + the default location"""
461 466 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
462 467
463 468 nbextensions_path = List(Unicode, config=True,
464 469 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
465 470 )
466 471 def _nbextensions_path_default(self):
467 472 return [os.path.join(get_ipython_dir(), 'nbextensions')]
468 473
469 474 mathjax_url = Unicode("", config=True,
470 475 help="""The url for MathJax.js."""
471 476 )
472 477 def _mathjax_url_default(self):
473 478 if not self.enable_mathjax:
474 479 return u''
475 480 static_url_prefix = self.webapp_settings.get("static_url_prefix",
476 url_path_join(self.base_project_url, "static")
481 url_path_join(self.base_url, "static")
477 482 )
478 483
479 484 # try local mathjax, either in nbextensions/mathjax or static/mathjax
480 485 for (url_prefix, search_path) in [
481 (url_path_join(self.base_project_url, "nbextensions"), self.nbextensions_path),
486 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
482 487 (static_url_prefix, self.static_file_path),
483 488 ]:
484 489 self.log.debug("searching for local mathjax in %s", search_path)
485 490 try:
486 491 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
487 492 except IOError:
488 493 continue
489 494 else:
490 495 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
491 496 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
492 497 return url
493 498
494 499 # no local mathjax, serve from CDN
495 500 if self.certfile:
496 501 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
497 502 host = u"https://c328740.ssl.cf1.rackcdn.com"
498 503 else:
499 504 host = u"http://cdn.mathjax.org"
500 505
501 506 url = host + u"/mathjax/latest/MathJax.js"
502 507 self.log.info("Using MathJax from CDN: %s", url)
503 508 return url
504 509
505 510 def _mathjax_url_changed(self, name, old, new):
506 511 if new and not self.enable_mathjax:
507 512 # enable_mathjax=False overrides mathjax_url
508 513 self.mathjax_url = u''
509 514 else:
510 515 self.log.info("Using MathJax: %s", new)
511 516
512 517 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
513 518 config=True,
514 519 help='The notebook manager class to use.')
515 520
516 521 trust_xheaders = Bool(False, config=True,
517 522 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
518 523 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
519 524 )
520 525
521 526 info_file = Unicode()
522 527
523 528 def _info_file_default(self):
524 529 info_file = "nbserver-%s.json"%os.getpid()
525 530 return os.path.join(self.profile_dir.security_dir, info_file)
526 531
527 532 def parse_command_line(self, argv=None):
528 533 super(NotebookApp, self).parse_command_line(argv)
529 534
530 535 if self.extra_args:
531 536 arg0 = self.extra_args[0]
532 537 f = os.path.abspath(arg0)
533 538 self.argv.remove(arg0)
534 539 if not os.path.exists(f):
535 540 self.log.critical("No such file or directory: %s", f)
536 541 self.exit(1)
537 542 if os.path.isdir(f):
538 543 self.config.FileNotebookManager.notebook_dir = f
539 544 elif os.path.isfile(f):
540 545 self.file_to_run = f
541 546
542 547 def init_kernel_argv(self):
543 548 """construct the kernel arguments"""
544 549 # Scrub frontend-specific flags
545 550 self.kernel_argv = swallow_argv(self.argv, notebook_aliases, notebook_flags)
546 551 if any(arg.startswith(u'--pylab') for arg in self.kernel_argv):
547 552 self.log.warn('\n '.join([
548 553 "Starting all kernels in pylab mode is not recommended,",
549 554 "and will be disabled in a future release.",
550 555 "Please use the %matplotlib magic to enable matplotlib instead.",
551 556 "pylab implies many imports, which can have confusing side effects",
552 557 "and harm the reproducibility of your notebooks.",
553 558 ]))
554 559 # Kernel should inherit default config file from frontend
555 560 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
556 561 # Kernel should get *absolute* path to profile directory
557 562 self.kernel_argv.extend(["--profile-dir", self.profile_dir.location])
558 563
559 564 def init_configurables(self):
560 565 # force Session default to be secure
561 566 default_secure(self.config)
562 567 self.kernel_manager = MappingKernelManager(
563 568 parent=self, log=self.log, kernel_argv=self.kernel_argv,
564 569 connection_dir = self.profile_dir.security_dir,
565 570 )
566 571 kls = import_item(self.notebook_manager_class)
567 572 self.notebook_manager = kls(parent=self, log=self.log)
568 573 self.session_manager = SessionManager(parent=self, log=self.log)
569 574 self.cluster_manager = ClusterManager(parent=self, log=self.log)
570 575 self.cluster_manager.update_profiles()
571 576
572 577 def init_logging(self):
573 578 # This prevents double log messages because tornado use a root logger that
574 579 # self.log is a child of. The logging module dipatches log messages to a log
575 580 # and all of its ancenstors until propagate is set to False.
576 581 self.log.propagate = False
577 582
578 583 # hook up tornado 3's loggers to our app handlers
579 584 for name in ('access', 'application', 'general'):
580 585 logger = logging.getLogger('tornado.%s' % name)
581 586 logger.parent = self.log
582 587 logger.setLevel(self.log.level)
583 588
584 589 def init_webapp(self):
585 590 """initialize tornado webapp and httpserver"""
586 591 self.web_app = NotebookWebApplication(
587 592 self, self.kernel_manager, self.notebook_manager,
588 593 self.cluster_manager, self.session_manager,
589 self.log, self.base_project_url, self.webapp_settings
594 self.log, self.base_url, self.webapp_settings
590 595 )
591 596 if self.certfile:
592 597 ssl_options = dict(certfile=self.certfile)
593 598 if self.keyfile:
594 599 ssl_options['keyfile'] = self.keyfile
595 600 else:
596 601 ssl_options = None
597 602 self.web_app.password = self.password
598 603 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
599 604 xheaders=self.trust_xheaders)
600 605 if not self.ip:
601 606 warning = "WARNING: The notebook server is listening on all IP addresses"
602 607 if ssl_options is None:
603 608 self.log.critical(warning + " and not using encryption. This "
604 609 "is not recommended.")
605 610 if not self.password:
606 611 self.log.critical(warning + " and not using authentication. "
607 612 "This is highly insecure and not recommended.")
608 613 success = None
609 614 for port in random_ports(self.port, self.port_retries+1):
610 615 try:
611 616 self.http_server.listen(port, self.ip)
612 617 except socket.error as e:
613 618 if e.errno == errno.EADDRINUSE:
614 619 self.log.info('The port %i is already in use, trying another random port.' % port)
615 620 continue
616 621 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
617 622 self.log.warn("Permission to listen on port %i denied" % port)
618 623 continue
619 624 else:
620 625 raise
621 626 else:
622 627 self.port = port
623 628 success = True
624 629 break
625 630 if not success:
626 631 self.log.critical('ERROR: the notebook server could not be started because '
627 632 'no available port could be found.')
628 633 self.exit(1)
629 634
630 635 @property
631 636 def display_url(self):
632 637 ip = self.ip if self.ip else '[all ip addresses on your system]'
633 638 return self._url(ip)
634 639
635 640 @property
636 641 def connection_url(self):
637 642 ip = self.ip if self.ip else localhost()
638 643 return self._url(ip)
639 644
640 645 def _url(self, ip):
641 646 proto = 'https' if self.certfile else 'http'
642 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_project_url)
647 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
643 648
644 649 def init_signal(self):
645 650 if not sys.platform.startswith('win'):
646 651 signal.signal(signal.SIGINT, self._handle_sigint)
647 652 signal.signal(signal.SIGTERM, self._signal_stop)
648 653 if hasattr(signal, 'SIGUSR1'):
649 654 # Windows doesn't support SIGUSR1
650 655 signal.signal(signal.SIGUSR1, self._signal_info)
651 656 if hasattr(signal, 'SIGINFO'):
652 657 # only on BSD-based systems
653 658 signal.signal(signal.SIGINFO, self._signal_info)
654 659
655 660 def _handle_sigint(self, sig, frame):
656 661 """SIGINT handler spawns confirmation dialog"""
657 662 # register more forceful signal handler for ^C^C case
658 663 signal.signal(signal.SIGINT, self._signal_stop)
659 664 # request confirmation dialog in bg thread, to avoid
660 665 # blocking the App
661 666 thread = threading.Thread(target=self._confirm_exit)
662 667 thread.daemon = True
663 668 thread.start()
664 669
665 670 def _restore_sigint_handler(self):
666 671 """callback for restoring original SIGINT handler"""
667 672 signal.signal(signal.SIGINT, self._handle_sigint)
668 673
669 674 def _confirm_exit(self):
670 675 """confirm shutdown on ^C
671 676
672 677 A second ^C, or answering 'y' within 5s will cause shutdown,
673 678 otherwise original SIGINT handler will be restored.
674 679
675 680 This doesn't work on Windows.
676 681 """
677 682 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
678 683 time.sleep(0.1)
679 684 info = self.log.info
680 685 info('interrupted')
681 686 print(self.notebook_info())
682 687 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
683 688 sys.stdout.flush()
684 689 r,w,x = select.select([sys.stdin], [], [], 5)
685 690 if r:
686 691 line = sys.stdin.readline()
687 692 if line.lower().startswith('y'):
688 693 self.log.critical("Shutdown confirmed")
689 694 ioloop.IOLoop.instance().stop()
690 695 return
691 696 else:
692 697 print("No answer for 5s:", end=' ')
693 698 print("resuming operation...")
694 699 # no answer, or answer is no:
695 700 # set it back to original SIGINT handler
696 701 # use IOLoop.add_callback because signal.signal must be called
697 702 # from main thread
698 703 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
699 704
700 705 def _signal_stop(self, sig, frame):
701 706 self.log.critical("received signal %s, stopping", sig)
702 707 ioloop.IOLoop.instance().stop()
703 708
704 709 def _signal_info(self, sig, frame):
705 710 print(self.notebook_info())
706 711
707 712 def init_components(self):
708 713 """Check the components submodule, and warn if it's unclean"""
709 714 status = submodule.check_submodule_status()
710 715 if status == 'missing':
711 716 self.log.warn("components submodule missing, running `git submodule update`")
712 717 submodule.update_submodules(submodule.ipython_parent())
713 718 elif status == 'unclean':
714 719 self.log.warn("components submodule unclean, you may see 404s on static/components")
715 720 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
716 721
717 722 @catch_config_error
718 723 def initialize(self, argv=None):
719 724 super(NotebookApp, self).initialize(argv)
720 725 self.init_logging()
721 726 self.init_kernel_argv()
722 727 self.init_configurables()
723 728 self.init_components()
724 729 self.init_webapp()
725 730 self.init_signal()
726 731
727 732 def cleanup_kernels(self):
728 733 """Shutdown all kernels.
729 734
730 735 The kernels will shutdown themselves when this process no longer exists,
731 736 but explicit shutdown allows the KernelManagers to cleanup the connection files.
732 737 """
733 738 self.log.info('Shutting down kernels')
734 739 self.kernel_manager.shutdown_all()
735 740
736 741 def notebook_info(self):
737 742 "Return the current working directory and the server url information"
738 743 info = self.notebook_manager.info_string() + "\n"
739 744 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
740 745 return info + "The IPython Notebook is running at: %s" % self.display_url
741 746
742 747 def server_info(self):
743 748 """Return a JSONable dict of information about this server."""
744 749 return {'url': self.connection_url,
745 750 'hostname': self.ip if self.ip else 'localhost',
746 751 'port': self.port,
747 752 'secure': bool(self.certfile),
748 'base_project_url': self.base_project_url,
753 'base_url': self.base_url,
749 754 'notebook_dir': os.path.abspath(self.notebook_manager.notebook_dir),
750 755 }
751 756
752 757 def write_server_info_file(self):
753 758 """Write the result of server_info() to the JSON file info_file."""
754 759 with open(self.info_file, 'w') as f:
755 760 json.dump(self.server_info(), f, indent=2)
756 761
757 762 def remove_server_info_file(self):
758 763 """Remove the nbserver-<pid>.json file created for this server.
759 764
760 765 Ignores the error raised when the file has already been removed.
761 766 """
762 767 try:
763 768 os.unlink(self.info_file)
764 769 except OSError as e:
765 770 if e.errno != errno.ENOENT:
766 771 raise
767 772
768 773 def start(self):
769 774 """ Start the IPython Notebook server app, after initialization
770 775
771 776 This method takes no arguments so all configuration and initialization
772 777 must be done prior to calling this method."""
773 778 if self.subapp is not None:
774 779 return self.subapp.start()
775 780
776 781 info = self.log.info
777 782 for line in self.notebook_info().split("\n"):
778 783 info(line)
779 784 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
780 785
781 786 self.write_server_info_file()
782 787
783 788 if self.open_browser or self.file_to_run:
784 789 try:
785 790 browser = webbrowser.get(self.browser or None)
786 791 except webbrowser.Error as e:
787 792 self.log.warn('No web browser found: %s.' % e)
788 793 browser = None
789 794
790 795 f = self.file_to_run
791 796 if f:
792 797 nbdir = os.path.abspath(self.notebook_manager.notebook_dir)
793 798 if f.startswith(nbdir):
794 799 f = f[len(nbdir):]
795 800 else:
796 801 self.log.warn(
797 802 "Probably won't be able to open notebook %s "
798 803 "because it is not in notebook_dir %s",
799 804 f, nbdir,
800 805 )
801 806
802 807 if os.path.isfile(self.file_to_run):
803 808 url = url_path_join('notebooks', f)
804 809 else:
805 810 url = url_path_join('tree', f)
806 811 if browser:
807 812 b = lambda : browser.open("%s%s" % (self.connection_url, url),
808 813 new=2)
809 814 threading.Thread(target=b).start()
810 815 try:
811 816 ioloop.IOLoop.instance().start()
812 817 except KeyboardInterrupt:
813 818 info("Interrupted...")
814 819 finally:
815 820 self.cleanup_kernels()
816 821 self.remove_server_info_file()
817 822
818 823
819 824 def list_running_servers(profile='default'):
820 825 """Iterate over the server info files of running notebook servers.
821 826
822 827 Given a profile name, find nbserver-* files in the security directory of
823 828 that profile, and yield dicts of their information, each one pertaining to
824 829 a currently running notebook server instance.
825 830 """
826 831 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
827 832 for file in os.listdir(pd.security_dir):
828 833 if file.startswith('nbserver-'):
829 834 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
830 835 yield json.load(f)
831 836
832 837 #-----------------------------------------------------------------------------
833 838 # Main entry point
834 839 #-----------------------------------------------------------------------------
835 840
836 841 launch_new_instance = NotebookApp.launch_instance
837 842
@@ -1,290 +1,290 b''
1 1 """Tornado handlers for the notebooks web service.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2011 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 tornado import web
22 22
23 23 from IPython.html.utils import url_path_join, url_escape
24 24 from IPython.utils.jsonutil import date_default
25 25
26 26 from IPython.html.base.handlers import (IPythonHandler, json_errors,
27 27 notebook_path_regex, path_regex,
28 28 notebook_name_regex)
29 29
30 30 #-----------------------------------------------------------------------------
31 31 # Notebook web service handlers
32 32 #-----------------------------------------------------------------------------
33 33
34 34
35 35 class NotebookHandler(IPythonHandler):
36 36
37 37 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
38 38
39 39 def notebook_location(self, name, path=''):
40 40 """Return the full URL location of a notebook based.
41 41
42 42 Parameters
43 43 ----------
44 44 name : unicode
45 45 The base name of the notebook, such as "foo.ipynb".
46 46 path : unicode
47 47 The URL path of the notebook.
48 48 """
49 49 return url_escape(url_path_join(
50 self.base_project_url, 'api', 'notebooks', path, name
50 self.base_url, 'api', 'notebooks', path, name
51 51 ))
52 52
53 53 def _finish_model(self, model, location=True):
54 54 """Finish a JSON request with a model, setting relevant headers, etc."""
55 55 if location:
56 56 location = self.notebook_location(model['name'], model['path'])
57 57 self.set_header('Location', location)
58 58 self.set_header('Last-Modified', model['last_modified'])
59 59 self.finish(json.dumps(model, default=date_default))
60 60
61 61 @web.authenticated
62 62 @json_errors
63 63 def get(self, path='', name=None):
64 64 """Return a Notebook or list of notebooks.
65 65
66 66 * GET with path and no notebook name lists notebooks in a directory
67 67 * GET with path and notebook name returns notebook JSON
68 68 """
69 69 nbm = self.notebook_manager
70 70 # Check to see if a notebook name was given
71 71 if name is None:
72 72 # TODO: Remove this after we create the contents web service and directories are
73 73 # no longer listed by the notebook web service. This should only handle notebooks
74 74 # and not directories.
75 75 dirs = nbm.list_dirs(path)
76 76 notebooks = []
77 77 index = []
78 78 for nb in nbm.list_notebooks(path):
79 79 if nb['name'].lower() == 'index.ipynb':
80 80 index.append(nb)
81 81 else:
82 82 notebooks.append(nb)
83 83 notebooks = index + dirs + notebooks
84 84 self.finish(json.dumps(notebooks, default=date_default))
85 85 return
86 86 # get and return notebook representation
87 87 model = nbm.get_notebook_model(name, path)
88 88 self._finish_model(model, location=False)
89 89
90 90 @web.authenticated
91 91 @json_errors
92 92 def patch(self, path='', name=None):
93 93 """PATCH renames a notebook without re-uploading content."""
94 94 nbm = self.notebook_manager
95 95 if name is None:
96 96 raise web.HTTPError(400, u'Notebook name missing')
97 97 model = self.get_json_body()
98 98 if model is None:
99 99 raise web.HTTPError(400, u'JSON body missing')
100 100 model = nbm.update_notebook_model(model, name, path)
101 101 self._finish_model(model)
102 102
103 103 def _copy_notebook(self, copy_from, path, copy_to=None):
104 104 """Copy a notebook in path, optionally specifying the new name.
105 105
106 106 Only support copying within the same directory.
107 107 """
108 108 self.log.info(u"Copying notebook from %s/%s to %s/%s",
109 109 path, copy_from,
110 110 path, copy_to or '',
111 111 )
112 112 model = self.notebook_manager.copy_notebook(copy_from, copy_to, path)
113 113 self.set_status(201)
114 114 self._finish_model(model)
115 115
116 116 def _upload_notebook(self, model, path, name=None):
117 117 """Upload a notebook
118 118
119 119 If name specified, create it in path/name.
120 120 """
121 121 self.log.info(u"Uploading notebook to %s/%s", path, name or '')
122 122 if name:
123 123 model['name'] = name
124 124
125 125 model = self.notebook_manager.create_notebook_model(model, path)
126 126 self.set_status(201)
127 127 self._finish_model(model)
128 128
129 129 def _create_empty_notebook(self, path, name=None):
130 130 """Create an empty notebook in path
131 131
132 132 If name specified, create it in path/name.
133 133 """
134 134 self.log.info(u"Creating new notebook in %s/%s", path, name or '')
135 135 model = {}
136 136 if name:
137 137 model['name'] = name
138 138 model = self.notebook_manager.create_notebook_model(model, path=path)
139 139 self.set_status(201)
140 140 self._finish_model(model)
141 141
142 142 def _save_notebook(self, model, path, name):
143 143 """Save an existing notebook."""
144 144 self.log.info(u"Saving notebook at %s/%s", path, name)
145 145 model = self.notebook_manager.save_notebook_model(model, name, path)
146 146 if model['path'] != path.strip('/') or model['name'] != name:
147 147 # a rename happened, set Location header
148 148 location = True
149 149 else:
150 150 location = False
151 151 self._finish_model(model, location)
152 152
153 153 @web.authenticated
154 154 @json_errors
155 155 def post(self, path='', name=None):
156 156 """Create a new notebook in the specified path.
157 157
158 158 POST creates new notebooks. The server always decides on the notebook name.
159 159
160 160 POST /api/notebooks/path
161 161 New untitled notebook in path. If content specified, upload a
162 162 notebook, otherwise start empty.
163 163 POST /api/notebooks/path?copy=OtherNotebook.ipynb
164 164 New copy of OtherNotebook in path
165 165 """
166 166
167 167 if name is not None:
168 168 raise web.HTTPError(400, "Only POST to directories. Use PUT for full names.")
169 169
170 170 model = self.get_json_body()
171 171
172 172 if model is not None:
173 173 copy_from = model.get('copy_from')
174 174 if copy_from:
175 175 if model.get('content'):
176 176 raise web.HTTPError(400, "Can't upload and copy at the same time.")
177 177 self._copy_notebook(copy_from, path)
178 178 else:
179 179 self._upload_notebook(model, path)
180 180 else:
181 181 self._create_empty_notebook(path)
182 182
183 183 @web.authenticated
184 184 @json_errors
185 185 def put(self, path='', name=None):
186 186 """Saves the notebook in the location specified by name and path.
187 187
188 188 PUT is very similar to POST, but the requester specifies the name,
189 189 whereas with POST, the server picks the name.
190 190
191 191 PUT /api/notebooks/path/Name.ipynb
192 192 Save notebook at ``path/Name.ipynb``. Notebook structure is specified
193 193 in `content` key of JSON request body. If content is not specified,
194 194 create a new empty notebook.
195 195 PUT /api/notebooks/path/Name.ipynb?copy=OtherNotebook.ipynb
196 196 Copy OtherNotebook to Name
197 197 """
198 198 if name is None:
199 199 raise web.HTTPError(400, "Only PUT to full names. Use POST for directories.")
200 200
201 201 model = self.get_json_body()
202 202 if model:
203 203 copy_from = model.get('copy_from')
204 204 if copy_from:
205 205 if model.get('content'):
206 206 raise web.HTTPError(400, "Can't upload and copy at the same time.")
207 207 self._copy_notebook(copy_from, path, name)
208 208 elif self.notebook_manager.notebook_exists(name, path):
209 209 self._save_notebook(model, path, name)
210 210 else:
211 211 self._upload_notebook(model, path, name)
212 212 else:
213 213 self._create_empty_notebook(path, name)
214 214
215 215 @web.authenticated
216 216 @json_errors
217 217 def delete(self, path='', name=None):
218 218 """delete the notebook in the given notebook path"""
219 219 nbm = self.notebook_manager
220 220 nbm.delete_notebook_model(name, path)
221 221 self.set_status(204)
222 222 self.finish()
223 223
224 224
225 225 class NotebookCheckpointsHandler(IPythonHandler):
226 226
227 227 SUPPORTED_METHODS = ('GET', 'POST')
228 228
229 229 @web.authenticated
230 230 @json_errors
231 231 def get(self, path='', name=None):
232 232 """get lists checkpoints for a notebook"""
233 233 nbm = self.notebook_manager
234 234 checkpoints = nbm.list_checkpoints(name, path)
235 235 data = json.dumps(checkpoints, default=date_default)
236 236 self.finish(data)
237 237
238 238 @web.authenticated
239 239 @json_errors
240 240 def post(self, path='', name=None):
241 241 """post creates a new checkpoint"""
242 242 nbm = self.notebook_manager
243 243 checkpoint = nbm.create_checkpoint(name, path)
244 244 data = json.dumps(checkpoint, default=date_default)
245 location = url_path_join(self.base_project_url, 'api/notebooks',
245 location = url_path_join(self.base_url, 'api/notebooks',
246 246 path, name, 'checkpoints', checkpoint['id'])
247 247 self.set_header('Location', url_escape(location))
248 248 self.set_status(201)
249 249 self.finish(data)
250 250
251 251
252 252 class ModifyNotebookCheckpointsHandler(IPythonHandler):
253 253
254 254 SUPPORTED_METHODS = ('POST', 'DELETE')
255 255
256 256 @web.authenticated
257 257 @json_errors
258 258 def post(self, path, name, checkpoint_id):
259 259 """post restores a notebook from a checkpoint"""
260 260 nbm = self.notebook_manager
261 261 nbm.restore_checkpoint(checkpoint_id, name, path)
262 262 self.set_status(204)
263 263 self.finish()
264 264
265 265 @web.authenticated
266 266 @json_errors
267 267 def delete(self, path, name, checkpoint_id):
268 268 """delete clears a checkpoint for a given notebook"""
269 269 nbm = self.notebook_manager
270 270 nbm.delete_checkpoint(checkpoint_id, name, path)
271 271 self.set_status(204)
272 272 self.finish()
273 273
274 274 #-----------------------------------------------------------------------------
275 275 # URL to handler mappings
276 276 #-----------------------------------------------------------------------------
277 277
278 278
279 279 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
280 280
281 281 default_handlers = [
282 282 (r"/api/notebooks%s/checkpoints" % notebook_path_regex, NotebookCheckpointsHandler),
283 283 (r"/api/notebooks%s/checkpoints/%s" % (notebook_path_regex, _checkpoint_id_regex),
284 284 ModifyNotebookCheckpointsHandler),
285 285 (r"/api/notebooks%s" % notebook_path_regex, NotebookHandler),
286 286 (r"/api/notebooks%s" % path_regex, NotebookHandler),
287 287 ]
288 288
289 289
290 290
@@ -1,45 +1,52 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // Login button
10 10 //============================================================================
11 11
12 12 var IPython = (function (IPython) {
13 "use strict";
13 14
14 15 var LoginWidget = function (selector, options) {
15 var options = options || {};
16 this.base_url = options.baseProjectUrl || $('body').data('baseProjectUrl') ;
16 options = options || {};
17 this.base_url = options.base_url || IPython.utils.get_body_data("baseUrl");
17 18 this.selector = selector;
18 19 if (this.selector !== undefined) {
19 20 this.element = $(selector);
20 21 this.style();
21 22 this.bind_events();
22 23 }
23 24 };
24 25
25 26 LoginWidget.prototype.style = function () {
26 27 this.element.find("button").addClass("btn btn-small");
27 28 };
28 29
29 30
30 31 LoginWidget.prototype.bind_events = function () {
31 32 var that = this;
32 33 this.element.find("button#logout").click(function () {
33 window.location = that.base_url+"logout";
34 window.location = IPythin.utils.url_join_encode(
35 that.base_url,
36 "logout"
37 );
34 38 });
35 39 this.element.find("button#login").click(function () {
36 window.location = that.base_url+"login";
40 window.location = IPythin.utils.url_join_encode(
41 that.base_url,
42 "login"
43 );
37 44 });
38 45 };
39 46
40 47 // Set module variables
41 48 IPython.LoginWidget = LoginWidget;
42 49
43 50 return IPython;
44 51
45 52 }(IPython));
@@ -1,523 +1,547 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2012 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // Utilities
10 10 //============================================================================
11 11 IPython.namespace('IPython.utils');
12 12
13 13 IPython.utils = (function (IPython) {
14 14 "use strict";
15 15
16 16 //============================================================================
17 17 // Cross-browser RegEx Split
18 18 //============================================================================
19 19
20 20 // This code has been MODIFIED from the code licensed below to not replace the
21 21 // default browser split. The license is reproduced here.
22 22
23 23 // see http://blog.stevenlevithan.com/archives/cross-browser-split for more info:
24 24 /*!
25 25 * Cross-Browser Split 1.1.1
26 26 * Copyright 2007-2012 Steven Levithan <stevenlevithan.com>
27 27 * Available under the MIT License
28 28 * ECMAScript compliant, uniform cross-browser split method
29 29 */
30 30
31 31 /**
32 32 * Splits a string into an array of strings using a regex or string
33 33 * separator. Matches of the separator are not included in the result array.
34 34 * However, if `separator` is a regex that contains capturing groups,
35 35 * backreferences are spliced into the result each time `separator` is
36 36 * matched. Fixes browser bugs compared to the native
37 37 * `String.prototype.split` and can be used reliably cross-browser.
38 38 * @param {String} str String to split.
39 39 * @param {RegExp|String} separator Regex or string to use for separating
40 40 * the string.
41 41 * @param {Number} [limit] Maximum number of items to include in the result
42 42 * array.
43 43 * @returns {Array} Array of substrings.
44 44 * @example
45 45 *
46 46 * // Basic use
47 47 * regex_split('a b c d', ' ');
48 48 * // -> ['a', 'b', 'c', 'd']
49 49 *
50 50 * // With limit
51 51 * regex_split('a b c d', ' ', 2);
52 52 * // -> ['a', 'b']
53 53 *
54 54 * // Backreferences in result array
55 55 * regex_split('..word1 word2..', /([a-z]+)(\d+)/i);
56 56 * // -> ['..', 'word', '1', ' ', 'word', '2', '..']
57 57 */
58 58 var regex_split = function (str, separator, limit) {
59 59 // If `separator` is not a regex, use `split`
60 60 if (Object.prototype.toString.call(separator) !== "[object RegExp]") {
61 61 return split.call(str, separator, limit);
62 62 }
63 63 var output = [],
64 64 flags = (separator.ignoreCase ? "i" : "") +
65 65 (separator.multiline ? "m" : "") +
66 66 (separator.extended ? "x" : "") + // Proposed for ES6
67 67 (separator.sticky ? "y" : ""), // Firefox 3+
68 68 lastLastIndex = 0,
69 69 // Make `global` and avoid `lastIndex` issues by working with a copy
70 70 separator = new RegExp(separator.source, flags + "g"),
71 71 separator2, match, lastIndex, lastLength;
72 72 str += ""; // Type-convert
73 73
74 74 var compliantExecNpcg = typeof(/()??/.exec("")[1]) === "undefined";
75 75 if (!compliantExecNpcg) {
76 76 // Doesn't need flags gy, but they don't hurt
77 77 separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags);
78 78 }
79 79 /* Values for `limit`, per the spec:
80 80 * If undefined: 4294967295 // Math.pow(2, 32) - 1
81 81 * If 0, Infinity, or NaN: 0
82 82 * If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296;
83 83 * If negative number: 4294967296 - Math.floor(Math.abs(limit))
84 84 * If other: Type-convert, then use the above rules
85 85 */
86 86 limit = typeof(limit) === "undefined" ?
87 87 -1 >>> 0 : // Math.pow(2, 32) - 1
88 88 limit >>> 0; // ToUint32(limit)
89 89 while (match = separator.exec(str)) {
90 90 // `separator.lastIndex` is not reliable cross-browser
91 91 lastIndex = match.index + match[0].length;
92 92 if (lastIndex > lastLastIndex) {
93 93 output.push(str.slice(lastLastIndex, match.index));
94 94 // Fix browsers whose `exec` methods don't consistently return `undefined` for
95 95 // nonparticipating capturing groups
96 96 if (!compliantExecNpcg && match.length > 1) {
97 97 match[0].replace(separator2, function () {
98 98 for (var i = 1; i < arguments.length - 2; i++) {
99 99 if (typeof(arguments[i]) === "undefined") {
100 100 match[i] = undefined;
101 101 }
102 102 }
103 103 });
104 104 }
105 105 if (match.length > 1 && match.index < str.length) {
106 106 Array.prototype.push.apply(output, match.slice(1));
107 107 }
108 108 lastLength = match[0].length;
109 109 lastLastIndex = lastIndex;
110 110 if (output.length >= limit) {
111 111 break;
112 112 }
113 113 }
114 114 if (separator.lastIndex === match.index) {
115 115 separator.lastIndex++; // Avoid an infinite loop
116 116 }
117 117 }
118 118 if (lastLastIndex === str.length) {
119 119 if (lastLength || !separator.test("")) {
120 120 output.push("");
121 121 }
122 122 } else {
123 123 output.push(str.slice(lastLastIndex));
124 124 }
125 125 return output.length > limit ? output.slice(0, limit) : output;
126 126 };
127 127
128 128 //============================================================================
129 129 // End contributed Cross-browser RegEx Split
130 130 //============================================================================
131 131
132 132
133 133 var uuid = function () {
134 134 // http://www.ietf.org/rfc/rfc4122.txt
135 135 var s = [];
136 136 var hexDigits = "0123456789ABCDEF";
137 137 for (var i = 0; i < 32; i++) {
138 138 s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
139 139 }
140 140 s[12] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
141 141 s[16] = hexDigits.substr((s[16] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
142 142
143 143 var uuid = s.join("");
144 144 return uuid;
145 145 };
146 146
147 147
148 148 //Fix raw text to parse correctly in crazy XML
149 149 function xmlencode(string) {
150 150 return string.replace(/\&/g,'&'+'amp;')
151 151 .replace(/</g,'&'+'lt;')
152 152 .replace(/>/g,'&'+'gt;')
153 153 .replace(/\'/g,'&'+'apos;')
154 154 .replace(/\"/g,'&'+'quot;')
155 155 .replace(/`/g,'&'+'#96;');
156 156 }
157 157
158 158
159 159 //Map from terminal commands to CSS classes
160 160 var ansi_colormap = {
161 161 "01":"ansibold",
162 162
163 163 "30":"ansiblack",
164 164 "31":"ansired",
165 165 "32":"ansigreen",
166 166 "33":"ansiyellow",
167 167 "34":"ansiblue",
168 168 "35":"ansipurple",
169 169 "36":"ansicyan",
170 170 "37":"ansigray",
171 171
172 172 "40":"ansibgblack",
173 173 "41":"ansibgred",
174 174 "42":"ansibggreen",
175 175 "43":"ansibgyellow",
176 176 "44":"ansibgblue",
177 177 "45":"ansibgpurple",
178 178 "46":"ansibgcyan",
179 179 "47":"ansibggray"
180 180 };
181 181
182 182 function _process_numbers(attrs, numbers) {
183 183 // process ansi escapes
184 184 var n = numbers.shift();
185 185 if (ansi_colormap[n]) {
186 186 if ( ! attrs["class"] ) {
187 187 attrs["class"] = ansi_colormap[n];
188 188 } else {
189 189 attrs["class"] += " " + ansi_colormap[n];
190 190 }
191 191 } else if (n == "38" || n == "48") {
192 192 // VT100 256 color or 24 bit RGB
193 193 if (numbers.length < 2) {
194 194 console.log("Not enough fields for VT100 color", numbers);
195 195 return;
196 196 }
197 197
198 198 var index_or_rgb = numbers.shift();
199 199 var r,g,b;
200 200 if (index_or_rgb == "5") {
201 201 // 256 color
202 202 var idx = parseInt(numbers.shift());
203 203 if (idx < 16) {
204 204 // indexed ANSI
205 205 // ignore bright / non-bright distinction
206 206 idx = idx % 8;
207 207 var ansiclass = ansi_colormap[n[0] + (idx % 8).toString()];
208 208 if ( ! attrs["class"] ) {
209 209 attrs["class"] = ansiclass;
210 210 } else {
211 211 attrs["class"] += " " + ansiclass;
212 212 }
213 213 return;
214 214 } else if (idx < 232) {
215 215 // 216 color 6x6x6 RGB
216 216 idx = idx - 16;
217 217 b = idx % 6;
218 218 g = Math.floor(idx / 6) % 6;
219 219 r = Math.floor(idx / 36) % 6;
220 220 // convert to rgb
221 221 r = (r * 51);
222 222 g = (g * 51);
223 223 b = (b * 51);
224 224 } else {
225 225 // grayscale
226 226 idx = idx - 231;
227 227 // it's 1-24 and should *not* include black or white,
228 228 // so a 26 point scale
229 229 r = g = b = Math.floor(idx * 256 / 26);
230 230 }
231 231 } else if (index_or_rgb == "2") {
232 232 // Simple 24 bit RGB
233 233 if (numbers.length > 3) {
234 234 console.log("Not enough fields for RGB", numbers);
235 235 return;
236 236 }
237 237 r = numbers.shift();
238 238 g = numbers.shift();
239 239 b = numbers.shift();
240 240 } else {
241 241 console.log("unrecognized control", numbers);
242 242 return;
243 243 }
244 244 if (r !== undefined) {
245 245 // apply the rgb color
246 246 var line;
247 247 if (n == "38") {
248 248 line = "color: ";
249 249 } else {
250 250 line = "background-color: ";
251 251 }
252 252 line = line + "rgb(" + r + "," + g + "," + b + ");"
253 253 if ( !attrs["style"] ) {
254 254 attrs["style"] = line;
255 255 } else {
256 256 attrs["style"] += " " + line;
257 257 }
258 258 }
259 259 }
260 260 }
261 261
262 262 function ansispan(str) {
263 263 // ansispan function adapted from github.com/mmalecki/ansispan (MIT License)
264 264 // regular ansi escapes (using the table above)
265 265 return str.replace(/\033\[(0?[01]|22|39)?([;\d]+)?m/g, function(match, prefix, pattern) {
266 266 if (!pattern) {
267 267 // [(01|22|39|)m close spans
268 268 return "</span>";
269 269 }
270 270 // consume sequence of color escapes
271 271 var numbers = pattern.match(/\d+/g);
272 272 var attrs = {};
273 273 while (numbers.length > 0) {
274 274 _process_numbers(attrs, numbers);
275 275 }
276 276
277 277 var span = "<span ";
278 278 for (var attr in attrs) {
279 279 var value = attrs[attr];
280 280 span = span + " " + attr + '="' + attrs[attr] + '"';
281 281 }
282 282 return span + ">";
283 283 });
284 284 };
285 285
286 286 // Transform ANSI color escape codes into HTML <span> tags with css
287 287 // classes listed in the above ansi_colormap object. The actual color used
288 288 // are set in the css file.
289 289 function fixConsole(txt) {
290 290 txt = xmlencode(txt);
291 291 var re = /\033\[([\dA-Fa-f;]*?)m/;
292 292 var opened = false;
293 293 var cmds = [];
294 294 var opener = "";
295 295 var closer = "";
296 296
297 297 // Strip all ANSI codes that are not color related. Matches
298 298 // all ANSI codes that do not end with "m".
299 299 var ignored_re = /(?=(\033\[[\d;=]*[a-ln-zA-Z]{1}))\1(?!m)/g;
300 300 txt = txt.replace(ignored_re, "");
301 301
302 302 // color ansi codes
303 303 txt = ansispan(txt);
304 304 return txt;
305 305 }
306 306
307 307 // Remove chunks that should be overridden by the effect of
308 308 // carriage return characters
309 309 function fixCarriageReturn(txt) {
310 310 var tmp = txt;
311 311 do {
312 312 txt = tmp;
313 313 tmp = txt.replace(/\r+\n/gm, '\n'); // \r followed by \n --> newline
314 314 tmp = tmp.replace(/^.*\r+/gm, ''); // Other \r --> clear line
315 315 } while (tmp.length < txt.length);
316 316 return txt;
317 317 }
318 318
319 319 // Locate any URLs and convert them to a anchor tag
320 320 function autoLinkUrls(txt) {
321 321 return txt.replace(/(^|\s)(https?|ftp)(:[^'">\s]+)/gi,
322 322 "$1<a target=\"_blank\" href=\"$2$3\">$2$3</a>");
323 323 }
324 324
325 325 // some keycodes that seem to be platform/browser independent
326 326 var keycodes = {
327 327 BACKSPACE: 8,
328 328 TAB : 9,
329 329 ENTER : 13,
330 330 SHIFT : 16,
331 331 CTRL : 17,
332 332 CONTROL : 17,
333 333 ALT : 18,
334 334 CAPS_LOCK: 20,
335 335 ESC : 27,
336 336 SPACE : 32,
337 337 PGUP : 33,
338 338 PGDOWN : 34,
339 339 END : 35,
340 340 HOME : 36,
341 341 LEFT_ARROW: 37,
342 342 LEFTARROW: 37,
343 343 LEFT : 37,
344 344 UP_ARROW : 38,
345 345 UPARROW : 38,
346 346 UP : 38,
347 347 RIGHT_ARROW:39,
348 348 RIGHTARROW:39,
349 349 RIGHT : 39,
350 350 DOWN_ARROW: 40,
351 351 DOWNARROW: 40,
352 352 DOWN : 40,
353 353 I : 73,
354 354 M : 77,
355 355 // all three of these keys may be COMMAND on OS X:
356 356 LEFT_SUPER : 91,
357 357 RIGHT_SUPER : 92,
358 358 COMMAND : 93,
359 359 };
360 360
361 361 // trigger a key press event
362 362 var press = function (key) {
363 363 var key_press = $.Event('keydown', {which: key});
364 364 $(document).trigger(key_press);
365 365 }
366 366
367 367 var press_up = function() { press(keycodes.UP); };
368 368 var press_down = function() { press(keycodes.DOWN); };
369 369
370 370 var press_ctrl_enter = function() {
371 371 $(document).trigger($.Event('keydown', {which: keycodes.ENTER, ctrlKey: true}));
372 372 };
373 373
374 374 var press_shift_enter = function() {
375 375 $(document).trigger($.Event('keydown', {which: keycodes.ENTER, shiftKey: true}));
376 376 };
377 377
378 378 // trigger the ctrl-m shortcut followed by one of our keys
379 379 var press_ghetto = function(key) {
380 380 $(document).trigger($.Event('keydown', {which: keycodes.M, ctrlKey: true}));
381 381 press(key);
382 382 };
383 383
384 384
385 385 var points_to_pixels = function (points) {
386 386 // A reasonably good way of converting between points and pixels.
387 387 var test = $('<div style="display: none; width: 10000pt; padding:0; border:0;"></div>');
388 388 $(body).append(test);
389 389 var pixel_per_point = test.width()/10000;
390 390 test.remove();
391 391 return Math.floor(points*pixel_per_point);
392 392 };
393 393
394 394 var always_new = function (constructor) {
395 395 // wrapper around contructor to avoid requiring `var a = new constructor()`
396 396 // useful for passing constructors as callbacks,
397 397 // not for programmer laziness.
398 398 // from http://programmers.stackexchange.com/questions/118798
399 399 return function () {
400 400 var obj = Object.create(constructor.prototype);
401 401 constructor.apply(obj, arguments);
402 402 return obj;
403 403 };
404 404 };
405 405
406 406
407 407 var url_path_join = function () {
408 408 // join a sequence of url components with '/'
409 409 var url = '';
410 410 for (var i = 0; i < arguments.length; i++) {
411 411 if (arguments[i] === '') {
412 412 continue;
413 413 }
414 414 if (url.length > 0 && url[url.length-1] != '/') {
415 415 url = url + '/' + arguments[i];
416 416 } else {
417 417 url = url + arguments[i];
418 418 }
419 419 }
420 url = url.replace(/\/\/+/, '/');
420 421 return url;
421 422 };
422 423
424 var parse_url = function (url) {
425 // an `a` element with an href allows attr-access to the parsed segments of a URL
426 // a = parse_url("http://localhost:8888/path/name#hash")
427 // a.protocol = "http:"
428 // a.host = "localhost:8888"
429 // a.hostname = "localhost"
430 // a.port = 8888
431 // a.pathname = "/path/name"
432 // a.hash = "#hash"
433 var a = document.createElement("a");
434 a.href = url;
435 return a;
436 };
423 437
424 438 var encode_uri_components = function (uri) {
425 439 // encode just the components of a multi-segment uri,
426 440 // leaving '/' separators
427 441 return uri.split('/').map(encodeURIComponent).join('/');
428 }
442 };
429 443
430 444 var url_join_encode = function () {
431 445 // join a sequence of url components with '/',
432 446 // encoding each component with encodeURIComponent
433 447 return encode_uri_components(url_path_join.apply(null, arguments));
434 448 };
435 449
436 450
437 451 var splitext = function (filename) {
438 452 // mimic Python os.path.splitext
439 453 // Returns ['base', '.ext']
440 454 var idx = filename.lastIndexOf('.');
441 455 if (idx > 0) {
442 456 return [filename.slice(0, idx), filename.slice(idx)];
443 457 } else {
444 458 return [filename, ''];
445 459 }
446 }
460 };
461
462
463 var get_body_data = function(key) {
464 // get a url-encoded item from body.data and decode it
465 // we should never have any encoded URLs anywhere else in code
466 // until we are building an actual request
467 return decodeURIComponent($('body').data(key));
468 };
447 469
448 470
449 471 // http://stackoverflow.com/questions/2400935/browser-detection-in-javascript
450 472 var browser = (function() {
451 473 if (typeof navigator === 'undefined') {
452 474 // navigator undefined in node
453 475 return 'None';
454 476 }
455 477 var N= navigator.appName, ua= navigator.userAgent, tem;
456 478 var M= ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i);
457 479 if (M && (tem= ua.match(/version\/([\.\d]+)/i))!= null) M[2]= tem[1];
458 480 M= M? [M[1], M[2]]: [N, navigator.appVersion,'-?'];
459 481 return M;
460 482 })();
461 483
462 484 // http://stackoverflow.com/questions/11219582/how-to-detect-my-browser-version-and-operating-system-using-javascript
463 485 var platform = (function () {
464 486 if (typeof navigator === 'undefined') {
465 487 // navigator undefined in node
466 488 return 'None';
467 489 }
468 490 var OSName="None";
469 491 if (navigator.appVersion.indexOf("Win")!=-1) OSName="Windows";
470 492 if (navigator.appVersion.indexOf("Mac")!=-1) OSName="MacOS";
471 493 if (navigator.appVersion.indexOf("X11")!=-1) OSName="UNIX";
472 494 if (navigator.appVersion.indexOf("Linux")!=-1) OSName="Linux";
473 495 return OSName
474 496 })();
475 497
476 498 var is_or_has = function (a, b) {
477 499 // Is b a child of a or a itself?
478 500 return a.has(b).length !==0 || a.is(b);
479 501 }
480 502
481 503 var is_focused = function (e) {
482 504 // Is element e, or one of its children focused?
483 505 e = $(e);
484 506 var target = $(document.activeElement);
485 507 if (target.length > 0) {
486 508 if (is_or_has(e, target)) {
487 509 return true;
488 510 } else {
489 511 return false;
490 512 }
491 513 } else {
492 514 return false;
493 515 }
494 516 }
495 517
496 518
497 519 return {
498 520 regex_split : regex_split,
499 521 uuid : uuid,
500 522 fixConsole : fixConsole,
501 523 keycodes : keycodes,
502 524 press : press,
503 525 press_up : press_up,
504 526 press_down : press_down,
505 527 press_ctrl_enter : press_ctrl_enter,
506 528 press_shift_enter : press_shift_enter,
507 529 press_ghetto : press_ghetto,
508 530 fixCarriageReturn : fixCarriageReturn,
509 531 autoLinkUrls : autoLinkUrls,
510 532 points_to_pixels : points_to_pixels,
533 get_body_data : get_body_data,
534 parse_url : parse_url,
511 535 url_path_join : url_path_join,
512 536 url_join_encode : url_join_encode,
513 537 encode_uri_components : encode_uri_components,
514 538 splitext : splitext,
515 539 always_new : always_new,
516 540 browser : browser,
517 541 platform: platform,
518 542 is_or_has : is_or_has,
519 543 is_focused : is_focused
520 544 };
521 545
522 546 }(IPython));
523 547
@@ -1,764 +1,772 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // Keyboard management
10 10 //============================================================================
11 11
12 12 var IPython = (function (IPython) {
13 13 "use strict";
14 14
15 15 // Setup global keycodes and inverse keycodes.
16 16
17 17 // See http://unixpapa.com/js/key.html for a complete description. The short of
18 18 // it is that there are different keycode sets. Firefox uses the "Mozilla keycodes"
19 19 // and Webkit/IE use the "IE keycodes". These keycode sets are mostly the same
20 20 // but have minor differences.
21 21
22 22 // These apply to Firefox, (Webkit and IE)
23 23 var _keycodes = {
24 24 'a': 65, 'b': 66, 'c': 67, 'd': 68, 'e': 69, 'f': 70, 'g': 71, 'h': 72, 'i': 73,
25 25 'j': 74, 'k': 75, 'l': 76, 'm': 77, 'n': 78, 'o': 79, 'p': 80, 'q': 81, 'r': 82,
26 26 's': 83, 't': 84, 'u': 85, 'v': 86, 'w': 87, 'x': 88, 'y': 89, 'z': 90,
27 27 '1 !': 49, '2 @': 50, '3 #': 51, '4 $': 52, '5 %': 53, '6 ^': 54,
28 28 '7 &': 55, '8 *': 56, '9 (': 57, '0 )': 48,
29 29 '[ {': 219, '] }': 221, '` ~': 192, ', <': 188, '. >': 190, '/ ?': 191,
30 30 '\\ |': 220, '\' "': 222,
31 31 'numpad0': 96, 'numpad1': 97, 'numpad2': 98, 'numpad3': 99, 'numpad4': 100,
32 32 'numpad5': 101, 'numpad6': 102, 'numpad7': 103, 'numpad8': 104, 'numpad9': 105,
33 33 'multiply': 106, 'add': 107, 'subtract': 109, 'decimal': 110, 'divide': 111,
34 34 'f1': 112, 'f2': 113, 'f3': 114, 'f4': 115, 'f5': 116, 'f6': 117, 'f7': 118,
35 35 'f8': 119, 'f9': 120, 'f11': 122, 'f12': 123, 'f13': 124, 'f14': 125, 'f15': 126,
36 36 'backspace': 8, 'tab': 9, 'enter': 13, 'shift': 16, 'ctrl': 17, 'alt': 18,
37 37 'meta': 91, 'capslock': 20, 'esc': 27, 'space': 32, 'pageup': 33, 'pagedown': 34,
38 38 'end': 35, 'home': 36, 'left': 37, 'up': 38, 'right': 39, 'down': 40,
39 39 'insert': 45, 'delete': 46, 'numlock': 144,
40 40 };
41 41
42 42 // These apply to Firefox and Opera
43 43 var _mozilla_keycodes = {
44 44 '; :': 59, '= +': 61, '- _': 173, 'meta': 224
45 45 }
46 46
47 47 // This apply to Webkit and IE
48 48 var _ie_keycodes = {
49 49 '; :': 186, '= +': 187, '- _': 189,
50 50 }
51 51
52 52 var browser = IPython.utils.browser[0];
53 53 var platform = IPython.utils.platform;
54 54
55 55 if (browser === 'Firefox' || browser === 'Opera') {
56 56 $.extend(_keycodes, _mozilla_keycodes);
57 57 } else if (browser === 'Safari' || browser === 'Chrome' || browser === 'MSIE') {
58 58 $.extend(_keycodes, _ie_keycodes);
59 59 }
60 60
61 61 var keycodes = {};
62 62 var inv_keycodes = {};
63 63 for (var name in _keycodes) {
64 64 var names = name.split(' ');
65 65 if (names.length === 1) {
66 66 var n = names[0]
67 67 keycodes[n] = _keycodes[n]
68 68 inv_keycodes[_keycodes[n]] = n
69 69 } else {
70 70 var primary = names[0];
71 71 var secondary = names[1];
72 72 keycodes[primary] = _keycodes[name]
73 73 keycodes[secondary] = _keycodes[name]
74 74 inv_keycodes[_keycodes[name]] = primary
75 75 }
76 76 }
77 77
78 78
79 79 // Default keyboard shortcuts
80 80
81 81 var default_common_shortcuts = {
82 82 'shift' : {
83 83 help : '',
84 84 help_index : '',
85 85 handler : function (event) {
86 86 // ignore shift keydown
87 87 return true;
88 88 }
89 89 },
90 90 'shift+enter' : {
91 91 help : 'run cell, select below',
92 92 help_index : 'ba',
93 93 handler : function (event) {
94 94 IPython.notebook.execute_cell_and_select_below();
95 95 return false;
96 96 }
97 97 },
98 98 'ctrl+enter' : {
99 99 help : 'run cell',
100 100 help_index : 'bb',
101 101 handler : function (event) {
102 102 IPython.notebook.execute_cell();
103 103 return false;
104 104 }
105 105 },
106 106 'alt+enter' : {
107 107 help : 'run cell, insert below',
108 108 help_index : 'bc',
109 109 handler : function (event) {
110 110 IPython.notebook.execute_cell_and_insert_below();
111 111 return false;
112 112 }
113 113 }
114 114 }
115 115
116 116 if (platform === 'MacOS') {
117 117 default_common_shortcuts['cmd+s'] =
118 118 {
119 119 help : 'save notebook',
120 120 help_index : 'fb',
121 121 handler : function (event) {
122 122 IPython.notebook.save_checkpoint();
123 123 event.preventDefault();
124 124 return false;
125 125 }
126 126 };
127 127 } else {
128 128 default_common_shortcuts['ctrl+s'] =
129 129 {
130 130 help : 'save notebook',
131 131 help_index : 'fb',
132 132 handler : function (event) {
133 133 IPython.notebook.save_checkpoint();
134 134 event.preventDefault();
135 135 return false;
136 136 }
137 137 };
138 138 }
139 139
140 140 // Edit mode defaults
141 141
142 142 var default_edit_shortcuts = {
143 143 'esc' : {
144 144 help : 'command mode',
145 145 help_index : 'aa',
146 146 handler : function (event) {
147 147 IPython.notebook.command_mode();
148 148 IPython.notebook.focus_cell();
149 149 return false;
150 150 }
151 151 },
152 152 'ctrl+m' : {
153 153 help : 'command mode',
154 154 help_index : 'ab',
155 155 handler : function (event) {
156 156 IPython.notebook.command_mode();
157 157 IPython.notebook.focus_cell();
158 158 return false;
159 159 }
160 160 },
161 161 'up' : {
162 162 help : '',
163 163 help_index : '',
164 164 handler : function (event) {
165 165 var cell = IPython.notebook.get_selected_cell();
166 166 if (cell && cell.at_top()) {
167 167 event.preventDefault();
168 168 IPython.notebook.command_mode()
169 169 IPython.notebook.select_prev();
170 170 IPython.notebook.edit_mode();
171 171 return false;
172 172 };
173 173 }
174 174 },
175 175 'down' : {
176 176 help : '',
177 177 help_index : '',
178 178 handler : function (event) {
179 179 var cell = IPython.notebook.get_selected_cell();
180 180 if (cell && cell.at_bottom()) {
181 181 event.preventDefault();
182 182 IPython.notebook.command_mode()
183 183 IPython.notebook.select_next();
184 184 IPython.notebook.edit_mode();
185 185 return false;
186 186 };
187 187 }
188 188 },
189 189 'alt+-' : {
190 190 help : 'split cell',
191 191 help_index : 'ea',
192 192 handler : function (event) {
193 193 IPython.notebook.split_cell();
194 194 return false;
195 195 }
196 196 },
197 197 'alt+subtract' : {
198 198 help : '',
199 199 help_index : 'eb',
200 200 handler : function (event) {
201 201 IPython.notebook.split_cell();
202 202 return false;
203 203 }
204 204 },
205 205 'tab' : {
206 206 help : 'indent or complete',
207 207 help_index : 'ec',
208 208 },
209 209 'shift+tab' : {
210 210 help : 'tooltip',
211 211 help_index : 'ed',
212 212 },
213 213 }
214 214
215 215 if (platform === 'MacOS') {
216 216 default_edit_shortcuts['cmd+/'] =
217 217 {
218 218 help : 'toggle comment',
219 219 help_index : 'ee'
220 220 };
221 221 default_edit_shortcuts['cmd+]'] =
222 222 {
223 223 help : 'indent',
224 224 help_index : 'ef'
225 225 };
226 226 default_edit_shortcuts['cmd+['] =
227 227 {
228 228 help : 'dedent',
229 229 help_index : 'eg'
230 230 };
231 231 } else {
232 232 default_edit_shortcuts['ctrl+/'] =
233 233 {
234 234 help : 'toggle comment',
235 235 help_index : 'ee'
236 236 };
237 237 default_edit_shortcuts['ctrl+]'] =
238 238 {
239 239 help : 'indent',
240 240 help_index : 'ef'
241 241 };
242 242 default_edit_shortcuts['ctrl+['] =
243 243 {
244 244 help : 'dedent',
245 245 help_index : 'eg'
246 246 };
247 247 }
248 248
249 249 // Command mode defaults
250 250
251 251 var default_command_shortcuts = {
252 252 'enter' : {
253 253 help : 'edit mode',
254 254 help_index : 'aa',
255 255 handler : function (event) {
256 256 IPython.notebook.edit_mode();
257 257 return false;
258 258 }
259 259 },
260 260 'up' : {
261 261 help : 'select previous cell',
262 262 help_index : 'da',
263 263 handler : function (event) {
264 264 var index = IPython.notebook.get_selected_index();
265 265 if (index !== 0 && index !== null) {
266 266 IPython.notebook.select_prev();
267 267 var cell = IPython.notebook.get_selected_cell();
268 268 cell.focus_cell();
269 269 };
270 270 return false;
271 271 }
272 272 },
273 273 'down' : {
274 274 help : 'select next cell',
275 275 help_index : 'db',
276 276 handler : function (event) {
277 277 var index = IPython.notebook.get_selected_index();
278 278 if (index !== (IPython.notebook.ncells()-1) && index !== null) {
279 279 IPython.notebook.select_next();
280 280 var cell = IPython.notebook.get_selected_cell();
281 281 cell.focus_cell();
282 282 };
283 283 return false;
284 284 }
285 285 },
286 286 'k' : {
287 287 help : 'select previous cell',
288 288 help_index : 'dc',
289 289 handler : function (event) {
290 290 var index = IPython.notebook.get_selected_index();
291 291 if (index !== 0 && index !== null) {
292 292 IPython.notebook.select_prev();
293 293 var cell = IPython.notebook.get_selected_cell();
294 294 cell.focus_cell();
295 295 };
296 296 return false;
297 297 }
298 298 },
299 299 'j' : {
300 300 help : 'select next cell',
301 301 help_index : 'dd',
302 302 handler : function (event) {
303 303 var index = IPython.notebook.get_selected_index();
304 304 if (index !== (IPython.notebook.ncells()-1) && index !== null) {
305 305 IPython.notebook.select_next();
306 306 var cell = IPython.notebook.get_selected_cell();
307 307 cell.focus_cell();
308 308 };
309 309 return false;
310 310 }
311 311 },
312 312 'x' : {
313 313 help : 'cut cell',
314 314 help_index : 'ee',
315 315 handler : function (event) {
316 316 IPython.notebook.cut_cell();
317 317 return false;
318 318 }
319 319 },
320 320 'c' : {
321 321 help : 'copy cell',
322 322 help_index : 'ef',
323 323 handler : function (event) {
324 324 IPython.notebook.copy_cell();
325 325 return false;
326 326 }
327 327 },
328 328 'shift+v' : {
329 329 help : 'paste cell above',
330 330 help_index : 'eg',
331 331 handler : function (event) {
332 332 IPython.notebook.paste_cell_above();
333 333 return false;
334 334 }
335 335 },
336 336 'v' : {
337 337 help : 'paste cell below',
338 338 help_index : 'eh',
339 339 handler : function (event) {
340 340 IPython.notebook.paste_cell_below();
341 341 return false;
342 342 }
343 343 },
344 344 'd' : {
345 345 help : 'delete cell (press twice)',
346 346 help_index : 'ej',
347 347 count: 2,
348 348 handler : function (event) {
349 349 IPython.notebook.delete_cell();
350 350 return false;
351 351 }
352 352 },
353 353 'a' : {
354 354 help : 'insert cell above',
355 355 help_index : 'ec',
356 356 handler : function (event) {
357 357 IPython.notebook.insert_cell_above('code');
358 358 IPython.notebook.select_prev();
359 359 IPython.notebook.focus_cell();
360 360 return false;
361 361 }
362 362 },
363 363 'b' : {
364 364 help : 'insert cell below',
365 365 help_index : 'ed',
366 366 handler : function (event) {
367 367 IPython.notebook.insert_cell_below('code');
368 368 IPython.notebook.select_next();
369 369 IPython.notebook.focus_cell();
370 370 return false;
371 371 }
372 372 },
373 373 'y' : {
374 374 help : 'to code',
375 375 help_index : 'ca',
376 376 handler : function (event) {
377 377 IPython.notebook.to_code();
378 378 return false;
379 379 }
380 380 },
381 381 'm' : {
382 382 help : 'to markdown',
383 383 help_index : 'cb',
384 384 handler : function (event) {
385 385 IPython.notebook.to_markdown();
386 386 return false;
387 387 }
388 388 },
389 389 'r' : {
390 390 help : 'to raw',
391 391 help_index : 'cc',
392 392 handler : function (event) {
393 393 IPython.notebook.to_raw();
394 394 return false;
395 395 }
396 396 },
397 397 '1' : {
398 398 help : 'to heading 1',
399 399 help_index : 'cd',
400 400 handler : function (event) {
401 401 IPython.notebook.to_heading(undefined, 1);
402 402 return false;
403 403 }
404 404 },
405 405 '2' : {
406 406 help : 'to heading 2',
407 407 help_index : 'ce',
408 408 handler : function (event) {
409 409 IPython.notebook.to_heading(undefined, 2);
410 410 return false;
411 411 }
412 412 },
413 413 '3' : {
414 414 help : 'to heading 3',
415 415 help_index : 'cf',
416 416 handler : function (event) {
417 417 IPython.notebook.to_heading(undefined, 3);
418 418 return false;
419 419 }
420 420 },
421 421 '4' : {
422 422 help : 'to heading 4',
423 423 help_index : 'cg',
424 424 handler : function (event) {
425 425 IPython.notebook.to_heading(undefined, 4);
426 426 return false;
427 427 }
428 428 },
429 429 '5' : {
430 430 help : 'to heading 5',
431 431 help_index : 'ch',
432 432 handler : function (event) {
433 433 IPython.notebook.to_heading(undefined, 5);
434 434 return false;
435 435 }
436 436 },
437 437 '6' : {
438 438 help : 'to heading 6',
439 439 help_index : 'ci',
440 440 handler : function (event) {
441 441 IPython.notebook.to_heading(undefined, 6);
442 442 return false;
443 443 }
444 444 },
445 445 'o' : {
446 446 help : 'toggle output',
447 447 help_index : 'gb',
448 448 handler : function (event) {
449 449 IPython.notebook.toggle_output();
450 450 return false;
451 451 }
452 452 },
453 453 'shift+o' : {
454 454 help : 'toggle output scrolling',
455 455 help_index : 'gc',
456 456 handler : function (event) {
457 457 IPython.notebook.toggle_output_scroll();
458 458 return false;
459 459 }
460 460 },
461 461 's' : {
462 462 help : 'save notebook',
463 463 help_index : 'fa',
464 464 handler : function (event) {
465 465 IPython.notebook.save_checkpoint();
466 466 return false;
467 467 }
468 468 },
469 469 'ctrl+j' : {
470 470 help : 'move cell down',
471 471 help_index : 'eb',
472 472 handler : function (event) {
473 473 IPython.notebook.move_cell_down();
474 474 return false;
475 475 }
476 476 },
477 477 'ctrl+k' : {
478 478 help : 'move cell up',
479 479 help_index : 'ea',
480 480 handler : function (event) {
481 481 IPython.notebook.move_cell_up();
482 482 return false;
483 483 }
484 484 },
485 485 'l' : {
486 486 help : 'toggle line numbers',
487 487 help_index : 'ga',
488 488 handler : function (event) {
489 489 IPython.notebook.cell_toggle_line_numbers();
490 490 return false;
491 491 }
492 492 },
493 493 'i' : {
494 494 help : 'interrupt kernel (press twice)',
495 495 help_index : 'ha',
496 496 count: 2,
497 497 handler : function (event) {
498 498 IPython.notebook.kernel.interrupt();
499 499 return false;
500 500 }
501 501 },
502 502 '0' : {
503 503 help : 'restart kernel (press twice)',
504 504 help_index : 'hb',
505 505 count: 2,
506 506 handler : function (event) {
507 507 IPython.notebook.restart_kernel();
508 508 return false;
509 509 }
510 510 },
511 511 'h' : {
512 512 help : 'keyboard shortcuts',
513 help_index : 'gd',
513 help_index : 'ge',
514 514 handler : function (event) {
515 515 IPython.quick_help.show_keyboard_shortcuts();
516 516 return false;
517 517 }
518 518 },
519 519 'z' : {
520 520 help : 'undo last delete',
521 521 help_index : 'ei',
522 522 handler : function (event) {
523 523 IPython.notebook.undelete_cell();
524 524 return false;
525 525 }
526 526 },
527 527 'shift+m' : {
528 528 help : 'merge cell below',
529 529 help_index : 'ek',
530 530 handler : function (event) {
531 531 IPython.notebook.merge_cell_below();
532 532 return false;
533 533 }
534 534 },
535 'q' : {
536 help : 'close pager',
537 help_index : 'gd',
538 handler : function (event) {
539 IPython.pager.collapse();
540 return false;
541 }
542 },
535 543 }
536 544
537 545
538 546 // Shortcut manager class
539 547
540 548 var ShortcutManager = function (delay) {
541 549 this._shortcuts = {}
542 550 this._counts = {}
543 551 this._timers = {}
544 552 this.delay = delay || 800; // delay in milliseconds
545 553 }
546 554
547 555 ShortcutManager.prototype.help = function () {
548 556 var help = [];
549 557 for (var shortcut in this._shortcuts) {
550 558 var help_string = this._shortcuts[shortcut]['help'];
551 559 var help_index = this._shortcuts[shortcut]['help_index'];
552 560 if (help_string) {
553 561 if (platform === 'MacOS') {
554 562 shortcut = shortcut.replace('meta', 'cmd');
555 563 }
556 564 help.push({
557 565 shortcut: shortcut,
558 566 help: help_string,
559 567 help_index: help_index}
560 568 );
561 569 }
562 570 }
563 571 help.sort(function (a, b) {
564 572 if (a.help_index > b.help_index)
565 573 return 1;
566 574 if (a.help_index < b.help_index)
567 575 return -1;
568 576 return 0;
569 577 });
570 578 return help;
571 579 }
572 580
573 581 ShortcutManager.prototype.normalize_key = function (key) {
574 582 return inv_keycodes[keycodes[key]];
575 583 }
576 584
577 585 ShortcutManager.prototype.normalize_shortcut = function (shortcut) {
578 586 // Sort a sequence of + separated modifiers into the order alt+ctrl+meta+shift
579 587 shortcut = shortcut.replace('cmd', 'meta').toLowerCase();
580 588 var values = shortcut.split("+");
581 589 if (values.length === 1) {
582 590 return this.normalize_key(values[0])
583 591 } else {
584 592 var modifiers = values.slice(0,-1);
585 593 var key = this.normalize_key(values[values.length-1]);
586 594 modifiers.sort();
587 595 return modifiers.join('+') + '+' + key;
588 596 }
589 597 }
590 598
591 599 ShortcutManager.prototype.event_to_shortcut = function (event) {
592 600 // Convert a jQuery keyboard event to a strong based keyboard shortcut
593 601 var shortcut = '';
594 602 var key = inv_keycodes[event.which]
595 603 if (event.altKey && key !== 'alt') {shortcut += 'alt+';}
596 604 if (event.ctrlKey && key !== 'ctrl') {shortcut += 'ctrl+';}
597 605 if (event.metaKey && key !== 'meta') {shortcut += 'meta+';}
598 606 if (event.shiftKey && key !== 'shift') {shortcut += 'shift+';}
599 607 shortcut += key;
600 608 return shortcut
601 609 }
602 610
603 611 ShortcutManager.prototype.clear_shortcuts = function () {
604 612 this._shortcuts = {};
605 613 }
606 614
607 615 ShortcutManager.prototype.add_shortcut = function (shortcut, data) {
608 616 if (typeof(data) === 'function') {
609 617 data = {help: '', help_index: '', handler: data}
610 618 }
611 619 data.help_index = data.help_index || '';
612 620 data.help = data.help || '';
613 621 data.count = data.count || 1;
614 622 if (data.help_index === '') {
615 623 data.help_index = 'zz';
616 624 }
617 625 shortcut = this.normalize_shortcut(shortcut);
618 626 this._counts[shortcut] = 0;
619 627 this._shortcuts[shortcut] = data;
620 628 }
621 629
622 630 ShortcutManager.prototype.add_shortcuts = function (data) {
623 631 for (var shortcut in data) {
624 632 this.add_shortcut(shortcut, data[shortcut]);
625 633 }
626 634 }
627 635
628 636 ShortcutManager.prototype.remove_shortcut = function (shortcut) {
629 637 shortcut = this.normalize_shortcut(shortcut);
630 638 delete this._counts[shortcut];
631 639 delete this._shortcuts[shortcut];
632 640 }
633 641
634 642 ShortcutManager.prototype.count_handler = function (shortcut, event, data) {
635 643 var that = this;
636 644 var c = this._counts;
637 645 var t = this._timers;
638 646 var timer = null;
639 647 if (c[shortcut] === data.count-1) {
640 648 c[shortcut] = 0;
641 649 var timer = t[shortcut];
642 650 if (timer) {clearTimeout(timer); delete t[shortcut];}
643 651 return data.handler(event);
644 652 } else {
645 653 c[shortcut] = c[shortcut] + 1;
646 654 timer = setTimeout(function () {
647 655 c[shortcut] = 0;
648 656 }, that.delay);
649 657 t[shortcut] = timer;
650 658 }
651 659 return false;
652 660 }
653 661
654 662 ShortcutManager.prototype.call_handler = function (event) {
655 663 var shortcut = this.event_to_shortcut(event);
656 664 var data = this._shortcuts[shortcut];
657 665 if (data) {
658 666 var handler = data['handler'];
659 667 if (handler) {
660 668 if (data.count === 1) {
661 669 return handler(event);
662 670 } else if (data.count > 1) {
663 671 return this.count_handler(shortcut, event, data);
664 672 }
665 673 }
666 674 }
667 675 return true;
668 676 }
669 677
670 678
671 679
672 680 // Main keyboard manager for the notebook
673 681
674 682 var KeyboardManager = function () {
675 683 this.mode = 'command';
676 684 this.enabled = true;
677 685 this.bind_events();
678 686 this.command_shortcuts = new ShortcutManager();
679 687 this.command_shortcuts.add_shortcuts(default_common_shortcuts);
680 688 this.command_shortcuts.add_shortcuts(default_command_shortcuts);
681 689 this.edit_shortcuts = new ShortcutManager();
682 690 this.edit_shortcuts.add_shortcuts(default_common_shortcuts);
683 691 this.edit_shortcuts.add_shortcuts(default_edit_shortcuts);
684 692 };
685 693
686 694 KeyboardManager.prototype.bind_events = function () {
687 695 var that = this;
688 696 $(document).keydown(function (event) {
689 697 return that.handle_keydown(event);
690 698 });
691 699 };
692 700
693 701 KeyboardManager.prototype.handle_keydown = function (event) {
694 702 var notebook = IPython.notebook;
695 703
696 704 if (event.which === keycodes['esc']) {
697 705 // Intercept escape at highest level to avoid closing
698 706 // websocket connection with firefox
699 707 event.preventDefault();
700 708 }
701 709
702 710 if (!this.enabled) {
703 711 if (event.which === keycodes['esc']) {
704 712 // ESC
705 713 notebook.command_mode();
706 714 return false;
707 715 }
708 716 return true;
709 717 }
710 718
711 719 if (this.mode === 'edit') {
712 720 return this.edit_shortcuts.call_handler(event);
713 721 } else if (this.mode === 'command') {
714 722 return this.command_shortcuts.call_handler(event);
715 723 }
716 724 return true;
717 725 }
718 726
719 727 KeyboardManager.prototype.edit_mode = function () {
720 728 this.last_mode = this.mode;
721 729 this.mode = 'edit';
722 730 }
723 731
724 732 KeyboardManager.prototype.command_mode = function () {
725 733 this.last_mode = this.mode;
726 734 this.mode = 'command';
727 735 }
728 736
729 737 KeyboardManager.prototype.enable = function () {
730 738 this.enabled = true;
731 739 }
732 740
733 741 KeyboardManager.prototype.disable = function () {
734 742 this.enabled = false;
735 743 }
736 744
737 745 KeyboardManager.prototype.register_events = function (e) {
738 746 var that = this;
739 747 e.on('focusin', function () {
740 748 that.disable();
741 749 });
742 750 e.on('focusout', function () {
743 751 that.enable();
744 752 });
745 753 // There are times (raw_input) where we remove the element from the DOM before
746 754 // focusout is called. In this case we bind to the remove event of jQueryUI,
747 755 // which gets triggered upon removal.
748 756 e.on('remove', function () {
749 757 that.enable();
750 758 });
751 759 }
752 760
753 761
754 762 IPython.keycodes = keycodes;
755 763 IPython.inv_keycodes = inv_keycodes;
756 764 IPython.default_common_shortcuts = default_common_shortcuts;
757 765 IPython.default_edit_shortcuts = default_edit_shortcuts;
758 766 IPython.default_command_shortcuts = default_command_shortcuts;
759 767 IPython.ShortcutManager = ShortcutManager;
760 768 IPython.KeyboardManager = KeyboardManager;
761 769
762 770 return IPython;
763 771
764 772 }(IPython));
@@ -1,128 +1,122 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // On document ready
10 10 //============================================================================
11 "use strict";
12 11
13 12 // for the time beeing, we have to pass marked as a parameter here,
14 13 // as injecting require.js make marked not to put itself in the globals,
15 14 // which make both this file fail at setting marked configuration, and textcell.js
16 15 // which search marked into global.
17 16 require(['components/marked/lib/marked',
18 17 'notebook/js/widgets/init'],
19 18
20 19 function (marked) {
20 "use strict";
21 21
22 window.marked = marked
22 window.marked = marked;
23 23
24 24 // monkey patch CM to be able to syntax highlight cell magics
25 25 // bug reported upstream,
26 26 // see https://github.com/marijnh/CodeMirror2/issues/670
27 if(CodeMirror.getMode(1,'text/plain').indent == undefined ){
27 if(CodeMirror.getMode(1,'text/plain').indent === undefined ){
28 28 console.log('patching CM for undefined indent');
29 29 CodeMirror.modes.null = function() {
30 return {token: function(stream) {stream.skipToEnd();},indent : function(){return 0}}
31 }
30 return {token: function(stream) {stream.skipToEnd();},indent : function(){return 0;}};
31 };
32 32 }
33 33
34 34 CodeMirror.patchedGetMode = function(config, mode){
35 35 var cmmode = CodeMirror.getMode(config, mode);
36 if(cmmode.indent == null)
37 {
36 if(cmmode.indent === null) {
38 37 console.log('patch mode "' , mode, '" on the fly');
39 cmmode.indent = function(){return 0};
38 cmmode.indent = function(){return 0;};
40 39 }
41 40 return cmmode;
42 }
41 };
43 42 // end monkey patching CodeMirror
44 43
45 44 IPython.mathjaxutils.init();
46 45
47 46 $('#ipython-main-app').addClass('border-box-sizing');
48 47 $('div#notebook_panel').addClass('border-box-sizing');
49 48
50 var baseProjectUrl = $('body').data('baseProjectUrl');
51 var notebookPath = $('body').data('notebookPath');
52 var notebookName = $('body').data('notebookName');
53 notebookName = decodeURIComponent(notebookName);
54 notebookPath = decodeURIComponent(notebookPath);
55 console.log(notebookName);
56 if (notebookPath == 'None'){
57 notebookPath = "";
58 }
49 var opts = {
50 base_url : IPython.utils.get_body_data("baseUrl"),
51 base_kernel_url : IPython.utils.get_body_data("baseKernelUrl"),
52 notebook_path : IPython.utils.get_body_data("notebookPath"),
53 notebook_name : IPython.utils.get_body_data('notebookName')
54 };
59 55
60 56 IPython.page = new IPython.Page();
61 57 IPython.layout_manager = new IPython.LayoutManager();
62 58 IPython.pager = new IPython.Pager('div#pager', 'div#pager_splitter');
63 59 IPython.quick_help = new IPython.QuickHelp();
64 IPython.login_widget = new IPython.LoginWidget('span#login_widget',{baseProjectUrl:baseProjectUrl});
65 IPython.notebook = new IPython.Notebook('div#notebook',{baseProjectUrl:baseProjectUrl, notebookPath:notebookPath, notebookName:notebookName});
60 IPython.login_widget = new IPython.LoginWidget('span#login_widget', opts);
61 IPython.notebook = new IPython.Notebook('div#notebook', opts);
66 62 IPython.keyboard_manager = new IPython.KeyboardManager();
67 63 IPython.save_widget = new IPython.SaveWidget('span#save_widget');
68 IPython.menubar = new IPython.MenuBar('#menubar',{baseProjectUrl:baseProjectUrl, notebookPath: notebookPath})
69 IPython.toolbar = new IPython.MainToolBar('#maintoolbar-container')
70 IPython.tooltip = new IPython.Tooltip()
71 IPython.notification_area = new IPython.NotificationArea('#notification_area')
64 IPython.menubar = new IPython.MenuBar('#menubar', opts);
65 IPython.toolbar = new IPython.MainToolBar('#maintoolbar-container');
66 IPython.tooltip = new IPython.Tooltip();
67 IPython.notification_area = new IPython.NotificationArea('#notification_area');
72 68 IPython.notification_area.init_notification_widgets();
73 69
74 70 IPython.layout_manager.do_resize();
75 71
76 72 $('body').append('<div id="fonttest"><pre><span id="test1">x</span>'+
77 73 '<span id="test2" style="font-weight: bold;">x</span>'+
78 '<span id="test3" style="font-style: italic;">x</span></pre></div>')
74 '<span id="test3" style="font-style: italic;">x</span></pre></div>');
79 75 var nh = $('#test1').innerHeight();
80 76 var bh = $('#test2').innerHeight();
81 77 var ih = $('#test3').innerHeight();
82 78 if(nh != bh || nh != ih) {
83 79 $('head').append('<style>.CodeMirror span { vertical-align: bottom; }</style>');
84 80 }
85 81 $('#fonttest').remove();
86 82
87 83 IPython.page.show();
88 84
89 85 IPython.layout_manager.do_resize();
90 86 var first_load = function () {
91 87 IPython.layout_manager.do_resize();
92 88 var hash = document.location.hash;
93 89 if (hash) {
94 90 document.location.hash = '';
95 91 document.location.hash = hash;
96 92 }
97 93 IPython.notebook.set_autosave_interval(IPython.notebook.minimum_autosave_interval);
98 94 // only do this once
99 95 $([IPython.events]).off('notebook_loaded.Notebook', first_load);
100 96 };
101 97
102 98 $([IPython.events]).on('notebook_loaded.Notebook', first_load);
103 99 $([IPython.events]).trigger('app_initialized.NotebookApp');
104 IPython.notebook.load_notebook(notebookName, notebookPath);
100 IPython.notebook.load_notebook(opts.notebook_name, opts.notebook_path);
105 101
106 102 if (marked) {
107 103 marked.setOptions({
108 104 gfm : true,
109 105 tables: true,
110 106 langPrefix: "language-",
111 107 highlight: function(code, lang) {
112 108 if (!lang) {
113 109 // no language, no highlight
114 110 return code;
115 111 }
116 112 var highlighted;
117 113 try {
118 114 highlighted = hljs.highlight(lang, code, false);
119 115 } catch(err) {
120 116 highlighted = hljs.highlightAuto(code);
121 117 }
122 118 return highlighted.value;
123 119 }
124 })
120 });
125 121 }
126 }
127
128 );
122 });
@@ -1,213 +1,214 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // ToolBar
10 10 //============================================================================
11 11
12 12 var IPython = (function (IPython) {
13 13 "use strict";
14 14
15 15 var MainToolBar = function (selector) {
16 16 IPython.ToolBar.apply(this, arguments);
17 17 this.construct();
18 18 this.add_celltype_list();
19 19 this.add_celltoolbar_list();
20 20 this.bind_events();
21 21 };
22 22
23 23 MainToolBar.prototype = new IPython.ToolBar();
24 24
25 25 MainToolBar.prototype.construct = function () {
26 26 this.add_buttons_group([
27 27 {
28 28 id : 'save_b',
29 29 label : 'Save and Checkpoint',
30 30 icon : 'icon-save',
31 31 callback : function () {
32 32 IPython.notebook.save_checkpoint();
33 33 }
34 34 }
35 35 ]);
36 36
37 37 this.add_buttons_group([
38 38 {
39 39 id : 'insert_below_b',
40 40 label : 'Insert Cell Below',
41 41 icon : 'icon-plus-sign',
42 42 callback : function () {
43 43 IPython.notebook.insert_cell_below('code');
44 44 IPython.notebook.select_next();
45 45 IPython.notebook.focus_cell();
46 46 }
47 47 }
48 48 ],'insert_above_below');
49 49
50 50 this.add_buttons_group([
51 51 {
52 52 id : 'cut_b',
53 53 label : 'Cut Cell',
54 54 icon : 'icon-cut',
55 55 callback : function () {
56 56 IPython.notebook.cut_cell();
57 57 }
58 58 },
59 59 {
60 60 id : 'copy_b',
61 61 label : 'Copy Cell',
62 62 icon : 'icon-copy',
63 63 callback : function () {
64 64 IPython.notebook.copy_cell();
65 65 }
66 66 },
67 67 {
68 68 id : 'paste_b',
69 69 label : 'Paste Cell Below',
70 70 icon : 'icon-paste',
71 71 callback : function () {
72 72 IPython.notebook.paste_cell_below();
73 73 }
74 74 }
75 75 ],'cut_copy_paste');
76 76
77 77 this.add_buttons_group([
78 78 {
79 79 id : 'move_up_b',
80 80 label : 'Move Cell Up',
81 81 icon : 'icon-arrow-up',
82 82 callback : function () {
83 83 IPython.notebook.move_cell_up();
84 84 }
85 85 },
86 86 {
87 87 id : 'move_down_b',
88 88 label : 'Move Cell Down',
89 89 icon : 'icon-arrow-down',
90 90 callback : function () {
91 91 IPython.notebook.move_cell_down();
92 92 }
93 93 }
94 94 ],'move_up_down');
95 95
96 96
97 97 this.add_buttons_group([
98 98 {
99 99 id : 'run_b',
100 100 label : 'Run Cell',
101 101 icon : 'icon-play',
102 102 callback : function () {
103 IPython.notebook.execute_cell();
104 }
103 // emulate default shift-enter behavior
104 IPython.notebook.execute_cell_and_select_below();
105 }
105 106 },
106 107 {
107 108 id : 'interrupt_b',
108 109 label : 'Interrupt',
109 110 icon : 'icon-stop',
110 111 callback : function () {
111 112 IPython.notebook.session.interrupt_kernel();
112 113 }
113 114 },
114 115 {
115 116 id : 'repeat_b',
116 117 label : 'Restart Kernel',
117 118 icon : 'icon-repeat',
118 119 callback : function () {
119 120 IPython.notebook.restart_kernel();
120 121 }
121 122 }
122 123 ],'run_int');
123 124 };
124 125
125 126 MainToolBar.prototype.add_celltype_list = function () {
126 127 this.element
127 128 .append($('<select/>')
128 129 .attr('id','cell_type')
129 130 // .addClass('ui-widget-content')
130 131 .append($('<option/>').attr('value','code').text('Code'))
131 132 .append($('<option/>').attr('value','markdown').text('Markdown'))
132 133 .append($('<option/>').attr('value','raw').text('Raw NBConvert'))
133 134 .append($('<option/>').attr('value','heading1').text('Heading 1'))
134 135 .append($('<option/>').attr('value','heading2').text('Heading 2'))
135 136 .append($('<option/>').attr('value','heading3').text('Heading 3'))
136 137 .append($('<option/>').attr('value','heading4').text('Heading 4'))
137 138 .append($('<option/>').attr('value','heading5').text('Heading 5'))
138 139 .append($('<option/>').attr('value','heading6').text('Heading 6'))
139 140 );
140 141 };
141 142
142 143
143 144 MainToolBar.prototype.add_celltoolbar_list = function () {
144 145 var label = $('<span/>').addClass("navbar-text").text('Cell Toolbar:');
145 146 var select = $('<select/>')
146 147 // .addClass('ui-widget-content')
147 148 .attr('id', 'ctb_select')
148 149 .append($('<option/>').attr('value', '').text('None'));
149 150 this.element.append(label).append(select);
150 151 select.change(function() {
151 152 var val = $(this).val()
152 153 if (val =='') {
153 154 IPython.CellToolbar.global_hide();
154 155 delete IPython.notebook.metadata.celltoolbar;
155 156 } else {
156 157 IPython.CellToolbar.global_show();
157 158 IPython.CellToolbar.activate_preset(val);
158 159 IPython.notebook.metadata.celltoolbar = val;
159 160 }
160 161 });
161 162 // Setup the currently registered presets.
162 163 var presets = IPython.CellToolbar.list_presets();
163 164 for (var i=0; i<presets.length; i++) {
164 165 var name = presets[i];
165 166 select.append($('<option/>').attr('value', name).text(name));
166 167 }
167 168 // Setup future preset registrations.
168 169 $([IPython.events]).on('preset_added.CellToolbar', function (event, data) {
169 170 var name = data.name;
170 171 select.append($('<option/>').attr('value', name).text(name));
171 172 });
172 173 };
173 174
174 175
175 176 MainToolBar.prototype.bind_events = function () {
176 177 var that = this;
177 178
178 179 this.element.find('#cell_type').change(function () {
179 180 var cell_type = $(this).val();
180 181 if (cell_type === 'code') {
181 182 IPython.notebook.to_code();
182 183 } else if (cell_type === 'markdown') {
183 184 IPython.notebook.to_markdown();
184 185 } else if (cell_type === 'raw') {
185 186 IPython.notebook.to_raw();
186 187 } else if (cell_type === 'heading1') {
187 188 IPython.notebook.to_heading(undefined, 1);
188 189 } else if (cell_type === 'heading2') {
189 190 IPython.notebook.to_heading(undefined, 2);
190 191 } else if (cell_type === 'heading3') {
191 192 IPython.notebook.to_heading(undefined, 3);
192 193 } else if (cell_type === 'heading4') {
193 194 IPython.notebook.to_heading(undefined, 4);
194 195 } else if (cell_type === 'heading5') {
195 196 IPython.notebook.to_heading(undefined, 5);
196 197 } else if (cell_type === 'heading6') {
197 198 IPython.notebook.to_heading(undefined, 6);
198 199 }
199 200 });
200 201 $([IPython.events]).on('selected_cell_type_changed.Notebook', function (event, data) {
201 202 if (data.cell_type === 'heading') {
202 203 that.element.find('#cell_type').val(data.cell_type+data.level);
203 204 } else {
204 205 that.element.find('#cell_type').val(data.cell_type);
205 206 }
206 207 });
207 208 };
208 209
209 210 IPython.MainToolBar = MainToolBar;
210 211
211 212 return IPython;
212 213
213 214 }(IPython));
@@ -1,327 +1,318 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // MenuBar
10 10 //============================================================================
11 11
12 12 /**
13 13 * @module IPython
14 14 * @namespace IPython
15 15 * @submodule MenuBar
16 16 */
17 17
18 18
19 19 var IPython = (function (IPython) {
20 20 "use strict";
21 21
22 22 var utils = IPython.utils;
23 23
24 24 /**
25 25 * A MenuBar Class to generate the menubar of IPython notebook
26 26 * @Class MenuBar
27 27 *
28 28 * @constructor
29 29 *
30 30 *
31 31 * @param selector {string} selector for the menubar element in DOM
32 32 * @param {object} [options]
33 * @param [options.baseProjectUrl] {String} String to use for the
34 * Base Project url, default would be to inspect
35 * $('body').data('baseProjectUrl');
33 * @param [options.base_url] {String} String to use for the
34 * base project url. Default is to inspect
35 * $('body').data('baseUrl');
36 36 * does not support change for now is set through this option
37 37 */
38 38 var MenuBar = function (selector, options) {
39 39 options = options || {};
40 if (options.baseProjectUrl !== undefined) {
41 this._baseProjectUrl = options.baseProjectUrl;
42 }
40 this.base_url = options.base_url || IPython.utils.get_body_data("baseUrl");
43 41 this.selector = selector;
44 42 if (this.selector !== undefined) {
45 43 this.element = $(selector);
46 44 this.style();
47 45 this.bind_events();
48 46 }
49 47 };
50 48
51 MenuBar.prototype.baseProjectUrl = function(){
52 return this._baseProjectUrl || $('body').data('baseProjectUrl');
53 };
54
55 MenuBar.prototype.notebookPath = function() {
56 var path = $('body').data('notebookPath');
57 path = decodeURIComponent(path);
58 return path;
59 };
60
61 49 MenuBar.prototype.style = function () {
62 50 this.element.addClass('border-box-sizing');
63 51 this.element.find("li").click(function (event, ui) {
64 52 // The selected cell loses focus when the menu is entered, so we
65 53 // re-select it upon selection.
66 54 var i = IPython.notebook.get_selected_index();
67 55 IPython.notebook.select(i);
68 56 }
69 57 );
70 58 };
71 59
72 60 MenuBar.prototype._nbconvert = function (format, download) {
73 61 download = download || false;
74 var notebook_name = IPython.notebook.get_notebook_name();
62 var notebook_path = IPython.notebook.notebook_path;
63 var notebook_name = IPython.notebook.notebook_name;
75 64 if (IPython.notebook.dirty) {
76 65 IPython.notebook.save_notebook({async : false});
77 66 }
78 var url = utils.url_path_join(
79 this.baseProjectUrl(),
67 var url = utils.url_join_encode(
68 this.base_url,
80 69 'nbconvert',
81 70 format,
82 this.notebookPath(),
83 notebook_name + '.ipynb'
71 notebook_path,
72 notebook_name
84 73 ) + "?download=" + download.toString();
85 74
86 75 window.open(url);
87 }
76 };
88 77
89 78 MenuBar.prototype.bind_events = function () {
90 79 // File
91 80 var that = this;
92 81 this.element.find('#new_notebook').click(function () {
93 82 IPython.notebook.new_notebook();
94 83 });
95 84 this.element.find('#open_notebook').click(function () {
96 85 window.open(utils.url_join_encode(
97 that.baseProjectUrl(),
86 IPython.notebook.base_url,
98 87 'tree',
99 that.notebookPath()
88 IPython.notebook.notebook_path
100 89 ));
101 90 });
102 91 this.element.find('#copy_notebook').click(function () {
103 92 IPython.notebook.copy_notebook();
104 93 return false;
105 94 });
106 95 this.element.find('#download_ipynb').click(function () {
107 var notebook_name = IPython.notebook.get_notebook_name();
96 var base_url = IPython.notebook.base_url;
97 var notebook_path = IPython.notebook.notebook_path;
98 var notebook_name = IPython.notebook.notebook_name;
108 99 if (IPython.notebook.dirty) {
109 100 IPython.notebook.save_notebook({async : false});
110 101 }
111 102
112 103 var url = utils.url_join_encode(
113 that.baseProjectUrl(),
104 base_url,
114 105 'files',
115 that.notebookPath(),
116 notebook_name + '.ipynb'
106 notebook_path,
107 notebook_name
117 108 );
118 109 window.location.assign(url);
119 110 });
120 111
121 112 this.element.find('#print_preview').click(function () {
122 113 that._nbconvert('html', false);
123 114 });
124 115
125 116 this.element.find('#download_py').click(function () {
126 117 that._nbconvert('python', true);
127 118 });
128 119
129 120 this.element.find('#download_html').click(function () {
130 121 that._nbconvert('html', true);
131 122 });
132 123
133 124 this.element.find('#download_rst').click(function () {
134 125 that._nbconvert('rst', true);
135 126 });
136 127
137 128 this.element.find('#rename_notebook').click(function () {
138 129 IPython.save_widget.rename_notebook();
139 130 });
140 131 this.element.find('#save_checkpoint').click(function () {
141 132 IPython.notebook.save_checkpoint();
142 133 });
143 134 this.element.find('#restore_checkpoint').click(function () {
144 135 });
145 136 this.element.find('#kill_and_exit').click(function () {
146 137 IPython.notebook.session.delete();
147 138 setTimeout(function(){
148 139 // allow closing of new tabs in Chromium, impossible in FF
149 140 window.open('', '_self', '');
150 141 window.close();
151 142 }, 500);
152 143 });
153 144 // Edit
154 145 this.element.find('#cut_cell').click(function () {
155 146 IPython.notebook.cut_cell();
156 147 });
157 148 this.element.find('#copy_cell').click(function () {
158 149 IPython.notebook.copy_cell();
159 150 });
160 151 this.element.find('#delete_cell').click(function () {
161 152 IPython.notebook.delete_cell();
162 153 });
163 154 this.element.find('#undelete_cell').click(function () {
164 155 IPython.notebook.undelete_cell();
165 156 });
166 157 this.element.find('#split_cell').click(function () {
167 158 IPython.notebook.split_cell();
168 159 });
169 160 this.element.find('#merge_cell_above').click(function () {
170 161 IPython.notebook.merge_cell_above();
171 162 });
172 163 this.element.find('#merge_cell_below').click(function () {
173 164 IPython.notebook.merge_cell_below();
174 165 });
175 166 this.element.find('#move_cell_up').click(function () {
176 167 IPython.notebook.move_cell_up();
177 168 });
178 169 this.element.find('#move_cell_down').click(function () {
179 170 IPython.notebook.move_cell_down();
180 171 });
181 172 this.element.find('#edit_nb_metadata').click(function () {
182 173 IPython.notebook.edit_metadata();
183 174 });
184 175
185 176 // View
186 177 this.element.find('#toggle_header').click(function () {
187 178 $('div#header').toggle();
188 179 IPython.layout_manager.do_resize();
189 180 });
190 181 this.element.find('#toggle_toolbar').click(function () {
191 182 $('div#maintoolbar').toggle();
192 183 IPython.layout_manager.do_resize();
193 184 });
194 185 // Insert
195 186 this.element.find('#insert_cell_above').click(function () {
196 187 IPython.notebook.insert_cell_above('code');
197 188 IPython.notebook.select_prev();
198 189 });
199 190 this.element.find('#insert_cell_below').click(function () {
200 191 IPython.notebook.insert_cell_below('code');
201 192 IPython.notebook.select_next();
202 193 });
203 194 // Cell
204 195 this.element.find('#run_cell').click(function () {
205 196 IPython.notebook.execute_cell();
206 197 });
207 198 this.element.find('#run_cell_select_below').click(function () {
208 199 IPython.notebook.execute_cell_and_select_below();
209 200 });
210 201 this.element.find('#run_cell_insert_below').click(function () {
211 202 IPython.notebook.execute_cell_and_insert_below();
212 203 });
213 204 this.element.find('#run_all_cells').click(function () {
214 205 IPython.notebook.execute_all_cells();
215 206 });
216 207 this.element.find('#run_all_cells_above').click(function () {
217 208 IPython.notebook.execute_cells_above();
218 209 });
219 210 this.element.find('#run_all_cells_below').click(function () {
220 211 IPython.notebook.execute_cells_below();
221 212 });
222 213 this.element.find('#to_code').click(function () {
223 214 IPython.notebook.to_code();
224 215 });
225 216 this.element.find('#to_markdown').click(function () {
226 217 IPython.notebook.to_markdown();
227 218 });
228 219 this.element.find('#to_raw').click(function () {
229 220 IPython.notebook.to_raw();
230 221 });
231 222 this.element.find('#to_heading1').click(function () {
232 223 IPython.notebook.to_heading(undefined, 1);
233 224 });
234 225 this.element.find('#to_heading2').click(function () {
235 226 IPython.notebook.to_heading(undefined, 2);
236 227 });
237 228 this.element.find('#to_heading3').click(function () {
238 229 IPython.notebook.to_heading(undefined, 3);
239 230 });
240 231 this.element.find('#to_heading4').click(function () {
241 232 IPython.notebook.to_heading(undefined, 4);
242 233 });
243 234 this.element.find('#to_heading5').click(function () {
244 235 IPython.notebook.to_heading(undefined, 5);
245 236 });
246 237 this.element.find('#to_heading6').click(function () {
247 238 IPython.notebook.to_heading(undefined, 6);
248 239 });
249 240
250 241 this.element.find('#toggle_current_output').click(function () {
251 242 IPython.notebook.toggle_output();
252 243 });
253 244 this.element.find('#toggle_current_output_scroll').click(function () {
254 245 IPython.notebook.toggle_output_scroll();
255 246 });
256 247 this.element.find('#clear_current_output').click(function () {
257 248 IPython.notebook.clear_output();
258 249 });
259 250
260 251 this.element.find('#toggle_all_output').click(function () {
261 252 IPython.notebook.toggle_all_output();
262 253 });
263 254 this.element.find('#toggle_all_output_scroll').click(function () {
264 255 IPython.notebook.toggle_all_output_scroll();
265 256 });
266 257 this.element.find('#clear_all_output').click(function () {
267 258 IPython.notebook.clear_all_output();
268 259 });
269 260
270 261 // Kernel
271 262 this.element.find('#int_kernel').click(function () {
272 263 IPython.notebook.session.interrupt_kernel();
273 264 });
274 265 this.element.find('#restart_kernel').click(function () {
275 266 IPython.notebook.restart_kernel();
276 267 });
277 268 // Help
278 269 this.element.find('#keyboard_shortcuts').click(function () {
279 270 IPython.quick_help.show_keyboard_shortcuts();
280 271 });
281 272
282 273 this.update_restore_checkpoint(null);
283 274
284 275 $([IPython.events]).on('checkpoints_listed.Notebook', function (event, data) {
285 276 that.update_restore_checkpoint(IPython.notebook.checkpoints);
286 277 });
287 278
288 279 $([IPython.events]).on('checkpoint_created.Notebook', function (event, data) {
289 280 that.update_restore_checkpoint(IPython.notebook.checkpoints);
290 281 });
291 282 };
292 283
293 284 MenuBar.prototype.update_restore_checkpoint = function(checkpoints) {
294 285 var ul = this.element.find("#restore_checkpoint").find("ul");
295 286 ul.empty();
296 287 if (!checkpoints || checkpoints.length === 0) {
297 288 ul.append(
298 289 $("<li/>")
299 290 .addClass("disabled")
300 291 .append(
301 292 $("<a/>")
302 293 .text("No checkpoints")
303 294 )
304 295 );
305 296 return;
306 297 }
307 298
308 299 checkpoints.map(function (checkpoint) {
309 300 var d = new Date(checkpoint.last_modified);
310 301 ul.append(
311 302 $("<li/>").append(
312 303 $("<a/>")
313 304 .attr("href", "#")
314 305 .text(d.format("mmm dd HH:MM:ss"))
315 306 .click(function () {
316 307 IPython.notebook.restore_checkpoint_dialog(checkpoint);
317 308 })
318 309 )
319 310 );
320 311 });
321 312 };
322 313
323 314 IPython.MenuBar = MenuBar;
324 315
325 316 return IPython;
326 317
327 318 }(IPython));
@@ -1,2301 +1,2288 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // Notebook
10 10 //============================================================================
11 11
12 12 var IPython = (function (IPython) {
13 13 "use strict";
14 14
15 15 var utils = IPython.utils;
16 16
17 17 /**
18 18 * A notebook contains and manages cells.
19 19 *
20 20 * @class Notebook
21 21 * @constructor
22 22 * @param {String} selector A jQuery selector for the notebook's DOM element
23 23 * @param {Object} [options] A config object
24 24 */
25 25 var Notebook = function (selector, options) {
26 var options = options || {};
27 this._baseProjectUrl = options.baseProjectUrl;
28 this.notebook_path = options.notebookPath;
29 this.notebook_name = options.notebookName;
26 this.options = options = options || {};
27 this.base_url = options.base_url;
28 this.notebook_path = options.notebook_path;
29 this.notebook_name = options.notebook_name;
30 30 this.element = $(selector);
31 31 this.element.scroll();
32 32 this.element.data("notebook", this);
33 33 this.next_prompt_number = 1;
34 34 this.session = null;
35 35 this.kernel = null;
36 36 this.clipboard = null;
37 37 this.undelete_backup = null;
38 38 this.undelete_index = null;
39 39 this.undelete_below = false;
40 40 this.paste_enabled = false;
41 41 // It is important to start out in command mode to match the intial mode
42 42 // of the KeyboardManager.
43 43 this.mode = 'command';
44 44 this.set_dirty(false);
45 45 this.metadata = {};
46 46 this._checkpoint_after_save = false;
47 47 this.last_checkpoint = null;
48 48 this.checkpoints = [];
49 49 this.autosave_interval = 0;
50 50 this.autosave_timer = null;
51 51 // autosave *at most* every two minutes
52 52 this.minimum_autosave_interval = 120000;
53 53 // single worksheet for now
54 54 this.worksheet_metadata = {};
55 55 this.notebook_name_blacklist_re = /[\/\\:]/;
56 this.nbformat = 3 // Increment this when changing the nbformat
57 this.nbformat_minor = 0 // Increment this when changing the nbformat
56 this.nbformat = 3; // Increment this when changing the nbformat
57 this.nbformat_minor = 0; // Increment this when changing the nbformat
58 58 this.style();
59 59 this.create_elements();
60 60 this.bind_events();
61 61 };
62 62
63 63 /**
64 64 * Tweak the notebook's CSS style.
65 65 *
66 66 * @method style
67 67 */
68 68 Notebook.prototype.style = function () {
69 69 $('div#notebook').addClass('border-box-sizing');
70 70 };
71 71
72 72 /**
73 * Get the root URL of the notebook server.
74 *
75 * @method baseProjectUrl
76 * @return {String} The base project URL
77 */
78 Notebook.prototype.baseProjectUrl = function() {
79 return this._baseProjectUrl || $('body').data('baseProjectUrl');
80 };
81
82 Notebook.prototype.notebookName = function() {
83 return $('body').data('notebookName');
84 };
85
86 Notebook.prototype.notebookPath = function() {
87 return $('body').data('notebookPath');
88 };
89
90 /**
91 73 * Create an HTML and CSS representation of the notebook.
92 74 *
93 75 * @method create_elements
94 76 */
95 77 Notebook.prototype.create_elements = function () {
96 78 var that = this;
97 79 this.element.attr('tabindex','-1');
98 80 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
99 81 // We add this end_space div to the end of the notebook div to:
100 82 // i) provide a margin between the last cell and the end of the notebook
101 83 // ii) to prevent the div from scrolling up when the last cell is being
102 84 // edited, but is too low on the page, which browsers will do automatically.
103 85 var end_space = $('<div/>').addClass('end_space');
104 86 end_space.dblclick(function (e) {
105 87 var ncells = that.ncells();
106 88 that.insert_cell_below('code',ncells-1);
107 89 });
108 90 this.element.append(this.container);
109 91 this.container.append(end_space);
110 92 };
111 93
112 94 /**
113 95 * Bind JavaScript events: key presses and custom IPython events.
114 96 *
115 97 * @method bind_events
116 98 */
117 99 Notebook.prototype.bind_events = function () {
118 100 var that = this;
119 101
120 102 $([IPython.events]).on('set_next_input.Notebook', function (event, data) {
121 103 var index = that.find_cell_index(data.cell);
122 104 var new_cell = that.insert_cell_below('code',index);
123 105 new_cell.set_text(data.text);
124 106 that.dirty = true;
125 107 });
126 108
127 109 $([IPython.events]).on('set_dirty.Notebook', function (event, data) {
128 110 that.dirty = data.value;
129 111 });
130 112
131 113 $([IPython.events]).on('select.Cell', function (event, data) {
132 114 var index = that.find_cell_index(data.cell);
133 115 that.select(index);
134 116 });
135 117
136 118 $([IPython.events]).on('edit_mode.Cell', function (event, data) {
137 119 var index = that.find_cell_index(data.cell);
138 120 that.select(index);
139 121 that.edit_mode();
140 122 });
141 123
142 124 $([IPython.events]).on('command_mode.Cell', function (event, data) {
143 125 that.command_mode();
144 126 });
145 127
146 128 $([IPython.events]).on('status_autorestarting.Kernel', function () {
147 129 IPython.dialog.modal({
148 130 title: "Kernel Restarting",
149 131 body: "The kernel appears to have died. It will restart automatically.",
150 132 buttons: {
151 133 OK : {
152 134 class : "btn-primary"
153 135 }
154 136 }
155 137 });
156 138 });
157 139
158 140 var collapse_time = function (time) {
159 141 var app_height = $('#ipython-main-app').height(); // content height
160 142 var splitter_height = $('div#pager_splitter').outerHeight(true);
161 143 var new_height = app_height - splitter_height;
162 144 that.element.animate({height : new_height + 'px'}, time);
163 145 };
164 146
165 147 this.element.bind('collapse_pager', function (event, extrap) {
166 var time = (extrap != undefined) ? ((extrap.duration != undefined ) ? extrap.duration : 'fast') : 'fast';
148 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
167 149 collapse_time(time);
168 150 });
169 151
170 152 var expand_time = function (time) {
171 153 var app_height = $('#ipython-main-app').height(); // content height
172 154 var splitter_height = $('div#pager_splitter').outerHeight(true);
173 155 var pager_height = $('div#pager').outerHeight(true);
174 156 var new_height = app_height - pager_height - splitter_height;
175 157 that.element.animate({height : new_height + 'px'}, time);
176 158 };
177 159
178 160 this.element.bind('expand_pager', function (event, extrap) {
179 var time = (extrap != undefined) ? ((extrap.duration != undefined ) ? extrap.duration : 'fast') : 'fast';
161 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
180 162 expand_time(time);
181 163 });
182 164
183 165 // Firefox 22 broke $(window).on("beforeunload")
184 166 // I'm not sure why or how.
185 167 window.onbeforeunload = function (e) {
186 168 // TODO: Make killing the kernel configurable.
187 169 var kill_kernel = false;
188 170 if (kill_kernel) {
189 171 that.session.kill_kernel();
190 172 }
191 173 // if we are autosaving, trigger an autosave on nav-away.
192 174 // still warn, because if we don't the autosave may fail.
193 175 if (that.dirty) {
194 176 if ( that.autosave_interval ) {
195 177 // schedule autosave in a timeout
196 178 // this gives you a chance to forcefully discard changes
197 179 // by reloading the page if you *really* want to.
198 180 // the timer doesn't start until you *dismiss* the dialog.
199 181 setTimeout(function () {
200 182 if (that.dirty) {
201 183 that.save_notebook();
202 184 }
203 185 }, 1000);
204 186 return "Autosave in progress, latest changes may be lost.";
205 187 } else {
206 188 return "Unsaved changes will be lost.";
207 189 }
208 };
190 }
209 191 // Null is the *only* return value that will make the browser not
210 192 // pop up the "don't leave" dialog.
211 193 return null;
212 194 };
213 195 };
214 196
215 197 /**
216 198 * Set the dirty flag, and trigger the set_dirty.Notebook event
217 199 *
218 200 * @method set_dirty
219 201 */
220 202 Notebook.prototype.set_dirty = function (value) {
221 203 if (value === undefined) {
222 204 value = true;
223 205 }
224 206 if (this.dirty == value) {
225 207 return;
226 208 }
227 209 $([IPython.events]).trigger('set_dirty.Notebook', {value: value});
228 210 };
229 211
230 212 /**
231 213 * Scroll the top of the page to a given cell.
232 214 *
233 215 * @method scroll_to_cell
234 216 * @param {Number} cell_number An index of the cell to view
235 217 * @param {Number} time Animation time in milliseconds
236 218 * @return {Number} Pixel offset from the top of the container
237 219 */
238 220 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
239 221 var cells = this.get_cells();
240 var time = time || 0;
222 time = time || 0;
241 223 cell_number = Math.min(cells.length-1,cell_number);
242 224 cell_number = Math.max(0 ,cell_number);
243 225 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
244 226 this.element.animate({scrollTop:scroll_value}, time);
245 227 return scroll_value;
246 228 };
247 229
248 230 /**
249 231 * Scroll to the bottom of the page.
250 232 *
251 233 * @method scroll_to_bottom
252 234 */
253 235 Notebook.prototype.scroll_to_bottom = function () {
254 236 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
255 237 };
256 238
257 239 /**
258 240 * Scroll to the top of the page.
259 241 *
260 242 * @method scroll_to_top
261 243 */
262 244 Notebook.prototype.scroll_to_top = function () {
263 245 this.element.animate({scrollTop:0}, 0);
264 246 };
265 247
266 248 // Edit Notebook metadata
267 249
268 250 Notebook.prototype.edit_metadata = function () {
269 251 var that = this;
270 252 IPython.dialog.edit_metadata(this.metadata, function (md) {
271 253 that.metadata = md;
272 254 }, 'Notebook');
273 255 };
274 256
275 257 // Cell indexing, retrieval, etc.
276 258
277 259 /**
278 260 * Get all cell elements in the notebook.
279 261 *
280 262 * @method get_cell_elements
281 263 * @return {jQuery} A selector of all cell elements
282 264 */
283 265 Notebook.prototype.get_cell_elements = function () {
284 266 return this.container.children("div.cell");
285 267 };
286 268
287 269 /**
288 270 * Get a particular cell element.
289 271 *
290 272 * @method get_cell_element
291 273 * @param {Number} index An index of a cell to select
292 274 * @return {jQuery} A selector of the given cell.
293 275 */
294 276 Notebook.prototype.get_cell_element = function (index) {
295 277 var result = null;
296 278 var e = this.get_cell_elements().eq(index);
297 279 if (e.length !== 0) {
298 280 result = e;
299 281 }
300 282 return result;
301 283 };
302 284
303 285 /**
304 286 * Try to get a particular cell by msg_id.
305 287 *
306 288 * @method get_msg_cell
307 289 * @param {String} msg_id A message UUID
308 290 * @return {Cell} Cell or null if no cell was found.
309 291 */
310 292 Notebook.prototype.get_msg_cell = function (msg_id) {
311 293 return IPython.CodeCell.msg_cells[msg_id] || null;
312 294 };
313 295
314 296 /**
315 297 * Count the cells in this notebook.
316 298 *
317 299 * @method ncells
318 300 * @return {Number} The number of cells in this notebook
319 301 */
320 302 Notebook.prototype.ncells = function () {
321 303 return this.get_cell_elements().length;
322 304 };
323 305
324 306 /**
325 307 * Get all Cell objects in this notebook.
326 308 *
327 309 * @method get_cells
328 310 * @return {Array} This notebook's Cell objects
329 311 */
330 312 // TODO: we are often calling cells as cells()[i], which we should optimize
331 313 // to cells(i) or a new method.
332 314 Notebook.prototype.get_cells = function () {
333 315 return this.get_cell_elements().toArray().map(function (e) {
334 316 return $(e).data("cell");
335 317 });
336 318 };
337 319
338 320 /**
339 321 * Get a Cell object from this notebook.
340 322 *
341 323 * @method get_cell
342 324 * @param {Number} index An index of a cell to retrieve
343 325 * @return {Cell} A particular cell
344 326 */
345 327 Notebook.prototype.get_cell = function (index) {
346 328 var result = null;
347 329 var ce = this.get_cell_element(index);
348 330 if (ce !== null) {
349 331 result = ce.data('cell');
350 332 }
351 333 return result;
352 }
334 };
353 335
354 336 /**
355 337 * Get the cell below a given cell.
356 338 *
357 339 * @method get_next_cell
358 340 * @param {Cell} cell The provided cell
359 341 * @return {Cell} The next cell
360 342 */
361 343 Notebook.prototype.get_next_cell = function (cell) {
362 344 var result = null;
363 345 var index = this.find_cell_index(cell);
364 346 if (this.is_valid_cell_index(index+1)) {
365 347 result = this.get_cell(index+1);
366 348 }
367 349 return result;
368 }
350 };
369 351
370 352 /**
371 353 * Get the cell above a given cell.
372 354 *
373 355 * @method get_prev_cell
374 356 * @param {Cell} cell The provided cell
375 357 * @return {Cell} The previous cell
376 358 */
377 359 Notebook.prototype.get_prev_cell = function (cell) {
378 360 // TODO: off-by-one
379 361 // nb.get_prev_cell(nb.get_cell(1)) is null
380 362 var result = null;
381 363 var index = this.find_cell_index(cell);
382 364 if (index !== null && index > 1) {
383 365 result = this.get_cell(index-1);
384 366 }
385 367 return result;
386 }
368 };
387 369
388 370 /**
389 371 * Get the numeric index of a given cell.
390 372 *
391 373 * @method find_cell_index
392 374 * @param {Cell} cell The provided cell
393 375 * @return {Number} The cell's numeric index
394 376 */
395 377 Notebook.prototype.find_cell_index = function (cell) {
396 378 var result = null;
397 379 this.get_cell_elements().filter(function (index) {
398 380 if ($(this).data("cell") === cell) {
399 381 result = index;
400 };
382 }
401 383 });
402 384 return result;
403 385 };
404 386
405 387 /**
406 388 * Get a given index , or the selected index if none is provided.
407 389 *
408 390 * @method index_or_selected
409 391 * @param {Number} index A cell's index
410 392 * @return {Number} The given index, or selected index if none is provided.
411 393 */
412 394 Notebook.prototype.index_or_selected = function (index) {
413 395 var i;
414 396 if (index === undefined || index === null) {
415 397 i = this.get_selected_index();
416 398 if (i === null) {
417 399 i = 0;
418 400 }
419 401 } else {
420 402 i = index;
421 403 }
422 404 return i;
423 405 };
424 406
425 407 /**
426 408 * Get the currently selected cell.
427 409 * @method get_selected_cell
428 410 * @return {Cell} The selected cell
429 411 */
430 412 Notebook.prototype.get_selected_cell = function () {
431 413 var index = this.get_selected_index();
432 414 return this.get_cell(index);
433 415 };
434 416
435 417 /**
436 418 * Check whether a cell index is valid.
437 419 *
438 420 * @method is_valid_cell_index
439 421 * @param {Number} index A cell index
440 422 * @return True if the index is valid, false otherwise
441 423 */
442 424 Notebook.prototype.is_valid_cell_index = function (index) {
443 425 if (index !== null && index >= 0 && index < this.ncells()) {
444 426 return true;
445 427 } else {
446 428 return false;
447 };
448 }
429 }
430 };
449 431
450 432 /**
451 433 * Get the index of the currently selected cell.
452 434
453 435 * @method get_selected_index
454 436 * @return {Number} The selected cell's numeric index
455 437 */
456 438 Notebook.prototype.get_selected_index = function () {
457 439 var result = null;
458 440 this.get_cell_elements().filter(function (index) {
459 441 if ($(this).data("cell").selected === true) {
460 442 result = index;
461 };
443 }
462 444 });
463 445 return result;
464 446 };
465 447
466 448
467 449 // Cell selection.
468 450
469 451 /**
470 452 * Programmatically select a cell.
471 453 *
472 454 * @method select
473 455 * @param {Number} index A cell's index
474 456 * @return {Notebook} This notebook
475 457 */
476 458 Notebook.prototype.select = function (index) {
477 459 if (this.is_valid_cell_index(index)) {
478 var sindex = this.get_selected_index()
460 var sindex = this.get_selected_index();
479 461 if (sindex !== null && index !== sindex) {
480 462 this.command_mode();
481 463 this.get_cell(sindex).unselect();
482 };
464 }
483 465 var cell = this.get_cell(index);
484 466 cell.select();
485 467 if (cell.cell_type === 'heading') {
486 468 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
487 469 {'cell_type':cell.cell_type,level:cell.level}
488 470 );
489 471 } else {
490 472 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
491 473 {'cell_type':cell.cell_type}
492 474 );
493 };
494 };
475 }
476 }
495 477 return this;
496 478 };
497 479
498 480 /**
499 481 * Programmatically select the next cell.
500 482 *
501 483 * @method select_next
502 484 * @return {Notebook} This notebook
503 485 */
504 486 Notebook.prototype.select_next = function () {
505 487 var index = this.get_selected_index();
506 488 this.select(index+1);
507 489 return this;
508 490 };
509 491
510 492 /**
511 493 * Programmatically select the previous cell.
512 494 *
513 495 * @method select_prev
514 496 * @return {Notebook} This notebook
515 497 */
516 498 Notebook.prototype.select_prev = function () {
517 499 var index = this.get_selected_index();
518 500 this.select(index-1);
519 501 return this;
520 502 };
521 503
522 504
523 505 // Edit/Command mode
524 506
525 507 Notebook.prototype.get_edit_index = function () {
526 508 var result = null;
527 509 this.get_cell_elements().filter(function (index) {
528 510 if ($(this).data("cell").mode === 'edit') {
529 511 result = index;
530 };
512 }
531 513 });
532 514 return result;
533 515 };
534 516
535 517 Notebook.prototype.command_mode = function () {
536 518 if (this.mode !== 'command') {
519 $([IPython.events]).trigger('command_mode.Notebook');
537 520 var index = this.get_edit_index();
538 521 var cell = this.get_cell(index);
539 522 if (cell) {
540 523 cell.command_mode();
541 };
524 }
542 525 this.mode = 'command';
543 526 IPython.keyboard_manager.command_mode();
544 };
527 }
545 528 };
546 529
547 530 Notebook.prototype.edit_mode = function () {
548 531 if (this.mode !== 'edit') {
532 $([IPython.events]).trigger('edit_mode.Notebook');
549 533 var cell = this.get_selected_cell();
550 534 if (cell === null) {return;} // No cell is selected
551 535 // We need to set the mode to edit to prevent reentering this method
552 536 // when cell.edit_mode() is called below.
553 537 this.mode = 'edit';
554 538 IPython.keyboard_manager.edit_mode();
555 539 cell.edit_mode();
556 };
540 }
557 541 };
558 542
559 543 Notebook.prototype.focus_cell = function () {
560 544 var cell = this.get_selected_cell();
561 545 if (cell === null) {return;} // No cell is selected
562 546 cell.focus_cell();
563 547 };
564 548
565 549 // Cell movement
566 550
567 551 /**
568 552 * Move given (or selected) cell up and select it.
569 553 *
570 554 * @method move_cell_up
571 555 * @param [index] {integer} cell index
572 556 * @return {Notebook} This notebook
573 557 **/
574 558 Notebook.prototype.move_cell_up = function (index) {
575 559 var i = this.index_or_selected(index);
576 560 if (this.is_valid_cell_index(i) && i > 0) {
577 561 var pivot = this.get_cell_element(i-1);
578 562 var tomove = this.get_cell_element(i);
579 563 if (pivot !== null && tomove !== null) {
580 564 tomove.detach();
581 565 pivot.before(tomove);
582 566 this.select(i-1);
583 567 var cell = this.get_selected_cell();
584 568 cell.focus_cell();
585 };
569 }
586 570 this.set_dirty(true);
587 };
571 }
588 572 return this;
589 573 };
590 574
591 575
592 576 /**
593 577 * Move given (or selected) cell down and select it
594 578 *
595 579 * @method move_cell_down
596 580 * @param [index] {integer} cell index
597 581 * @return {Notebook} This notebook
598 582 **/
599 583 Notebook.prototype.move_cell_down = function (index) {
600 584 var i = this.index_or_selected(index);
601 585 if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
602 586 var pivot = this.get_cell_element(i+1);
603 587 var tomove = this.get_cell_element(i);
604 588 if (pivot !== null && tomove !== null) {
605 589 tomove.detach();
606 590 pivot.after(tomove);
607 591 this.select(i+1);
608 592 var cell = this.get_selected_cell();
609 593 cell.focus_cell();
610 };
611 };
594 }
595 }
612 596 this.set_dirty();
613 597 return this;
614 598 };
615 599
616 600
617 601 // Insertion, deletion.
618 602
619 603 /**
620 604 * Delete a cell from the notebook.
621 605 *
622 606 * @method delete_cell
623 607 * @param [index] A cell's numeric index
624 608 * @return {Notebook} This notebook
625 609 */
626 610 Notebook.prototype.delete_cell = function (index) {
627 611 var i = this.index_or_selected(index);
628 612 var cell = this.get_selected_cell();
629 613 this.undelete_backup = cell.toJSON();
630 614 $('#undelete_cell').removeClass('disabled');
631 615 if (this.is_valid_cell_index(i)) {
632 616 var old_ncells = this.ncells();
633 617 var ce = this.get_cell_element(i);
634 618 ce.remove();
635 619 if (i === 0) {
636 620 // Always make sure we have at least one cell.
637 621 if (old_ncells === 1) {
638 622 this.insert_cell_below('code');
639 623 }
640 624 this.select(0);
641 625 this.undelete_index = 0;
642 626 this.undelete_below = false;
643 627 } else if (i === old_ncells-1 && i !== 0) {
644 628 this.select(i-1);
645 629 this.undelete_index = i - 1;
646 630 this.undelete_below = true;
647 631 } else {
648 632 this.select(i);
649 633 this.undelete_index = i;
650 634 this.undelete_below = false;
651 };
635 }
652 636 $([IPython.events]).trigger('delete.Cell', {'cell': cell, 'index': i});
653 637 this.set_dirty(true);
654 };
638 }
655 639 return this;
656 640 };
657 641
658 642 /**
659 643 * Restore the most recently deleted cell.
660 644 *
661 645 * @method undelete
662 646 */
663 647 Notebook.prototype.undelete_cell = function() {
664 648 if (this.undelete_backup !== null && this.undelete_index !== null) {
665 649 var current_index = this.get_selected_index();
666 650 if (this.undelete_index < current_index) {
667 651 current_index = current_index + 1;
668 652 }
669 653 if (this.undelete_index >= this.ncells()) {
670 654 this.select(this.ncells() - 1);
671 655 }
672 656 else {
673 657 this.select(this.undelete_index);
674 658 }
675 659 var cell_data = this.undelete_backup;
676 660 var new_cell = null;
677 661 if (this.undelete_below) {
678 662 new_cell = this.insert_cell_below(cell_data.cell_type);
679 663 } else {
680 664 new_cell = this.insert_cell_above(cell_data.cell_type);
681 665 }
682 666 new_cell.fromJSON(cell_data);
683 667 if (this.undelete_below) {
684 668 this.select(current_index+1);
685 669 } else {
686 670 this.select(current_index);
687 671 }
688 672 this.undelete_backup = null;
689 673 this.undelete_index = null;
690 674 }
691 675 $('#undelete_cell').addClass('disabled');
692 }
676 };
693 677
694 678 /**
695 679 * Insert a cell so that after insertion the cell is at given index.
696 680 *
697 681 * Similar to insert_above, but index parameter is mandatory
698 682 *
699 683 * Index will be brought back into the accissible range [0,n]
700 684 *
701 685 * @method insert_cell_at_index
702 686 * @param type {string} in ['code','markdown','heading']
703 687 * @param [index] {int} a valid index where to inser cell
704 688 *
705 689 * @return cell {cell|null} created cell or null
706 690 **/
707 691 Notebook.prototype.insert_cell_at_index = function(type, index){
708 692
709 693 var ncells = this.ncells();
710 var index = Math.min(index,ncells);
711 index = Math.max(index,0);
694 index = Math.min(index,ncells);
695 index = Math.max(index,0);
712 696 var cell = null;
713 697
714 698 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
715 699 if (type === 'code') {
716 700 cell = new IPython.CodeCell(this.kernel);
717 701 cell.set_input_prompt();
718 702 } else if (type === 'markdown') {
719 703 cell = new IPython.MarkdownCell();
720 704 } else if (type === 'raw') {
721 705 cell = new IPython.RawCell();
722 706 } else if (type === 'heading') {
723 707 cell = new IPython.HeadingCell();
724 708 }
725 709
726 710 if(this._insert_element_at_index(cell.element,index)) {
727 711 cell.render();
728 712 $([IPython.events]).trigger('create.Cell', {'cell': cell, 'index': index});
729 713 cell.refresh();
730 714 // We used to select the cell after we refresh it, but there
731 715 // are now cases were this method is called where select is
732 716 // not appropriate. The selection logic should be handled by the
733 717 // caller of the the top level insert_cell methods.
734 718 this.set_dirty(true);
735 719 }
736 720 }
737 721 return cell;
738 722
739 723 };
740 724
741 725 /**
742 726 * Insert an element at given cell index.
743 727 *
744 728 * @method _insert_element_at_index
745 729 * @param element {dom element} a cell element
746 730 * @param [index] {int} a valid index where to inser cell
747 731 * @private
748 732 *
749 733 * return true if everything whent fine.
750 734 **/
751 735 Notebook.prototype._insert_element_at_index = function(element, index){
752 736 if (element === undefined){
753 737 return false;
754 738 }
755 739
756 740 var ncells = this.ncells();
757 741
758 742 if (ncells === 0) {
759 743 // special case append if empty
760 744 this.element.find('div.end_space').before(element);
761 745 } else if ( ncells === index ) {
762 746 // special case append it the end, but not empty
763 747 this.get_cell_element(index-1).after(element);
764 748 } else if (this.is_valid_cell_index(index)) {
765 749 // otherwise always somewhere to append to
766 750 this.get_cell_element(index).before(element);
767 751 } else {
768 752 return false;
769 753 }
770 754
771 755 if (this.undelete_index !== null && index <= this.undelete_index) {
772 756 this.undelete_index = this.undelete_index + 1;
773 757 this.set_dirty(true);
774 758 }
775 759 return true;
776 760 };
777 761
778 762 /**
779 763 * Insert a cell of given type above given index, or at top
780 764 * of notebook if index smaller than 0.
781 765 *
782 766 * default index value is the one of currently selected cell
783 767 *
784 768 * @method insert_cell_above
785 769 * @param type {string} cell type
786 770 * @param [index] {integer}
787 771 *
788 772 * @return handle to created cell or null
789 773 **/
790 774 Notebook.prototype.insert_cell_above = function (type, index) {
791 775 index = this.index_or_selected(index);
792 776 return this.insert_cell_at_index(type, index);
793 777 };
794 778
795 779 /**
796 780 * Insert a cell of given type below given index, or at bottom
797 781 * of notebook if index greater thatn number of cell
798 782 *
799 783 * default index value is the one of currently selected cell
800 784 *
801 785 * @method insert_cell_below
802 786 * @param type {string} cell type
803 787 * @param [index] {integer}
804 788 *
805 789 * @return handle to created cell or null
806 790 *
807 791 **/
808 792 Notebook.prototype.insert_cell_below = function (type, index) {
809 793 index = this.index_or_selected(index);
810 794 return this.insert_cell_at_index(type, index+1);
811 795 };
812 796
813 797
814 798 /**
815 799 * Insert cell at end of notebook
816 800 *
817 801 * @method insert_cell_at_bottom
818 802 * @param {String} type cell type
819 803 *
820 804 * @return the added cell; or null
821 805 **/
822 806 Notebook.prototype.insert_cell_at_bottom = function (type){
823 807 var len = this.ncells();
824 808 return this.insert_cell_below(type,len-1);
825 809 };
826 810
827 811 /**
828 812 * Turn a cell into a code cell.
829 813 *
830 814 * @method to_code
831 815 * @param {Number} [index] A cell's index
832 816 */
833 817 Notebook.prototype.to_code = function (index) {
834 818 var i = this.index_or_selected(index);
835 819 if (this.is_valid_cell_index(i)) {
836 820 var source_element = this.get_cell_element(i);
837 821 var source_cell = source_element.data("cell");
838 822 if (!(source_cell instanceof IPython.CodeCell)) {
839 823 var target_cell = this.insert_cell_below('code',i);
840 824 var text = source_cell.get_text();
841 825 if (text === source_cell.placeholder) {
842 826 text = '';
843 827 }
844 828 target_cell.set_text(text);
845 829 // make this value the starting point, so that we can only undo
846 830 // to this state, instead of a blank cell
847 831 target_cell.code_mirror.clearHistory();
848 832 source_element.remove();
849 833 this.select(i);
850 834 this.set_dirty(true);
851 };
852 };
835 }
836 }
853 837 };
854 838
855 839 /**
856 840 * Turn a cell into a Markdown cell.
857 841 *
858 842 * @method to_markdown
859 843 * @param {Number} [index] A cell's index
860 844 */
861 845 Notebook.prototype.to_markdown = function (index) {
862 846 var i = this.index_or_selected(index);
863 847 if (this.is_valid_cell_index(i)) {
864 848 var source_element = this.get_cell_element(i);
865 849 var source_cell = source_element.data("cell");
866 850 if (!(source_cell instanceof IPython.MarkdownCell)) {
867 851 var target_cell = this.insert_cell_below('markdown',i);
868 852 var text = source_cell.get_text();
869 853 if (text === source_cell.placeholder) {
870 854 text = '';
871 };
855 }
872 856 // We must show the editor before setting its contents
873 857 target_cell.unrender();
874 858 target_cell.set_text(text);
875 859 // make this value the starting point, so that we can only undo
876 860 // to this state, instead of a blank cell
877 861 target_cell.code_mirror.clearHistory();
878 862 source_element.remove();
879 863 this.select(i);
880 864 if ((source_cell instanceof IPython.TextCell) && source_cell.rendered) {
881 865 target_cell.render();
882 866 }
883 867 this.set_dirty(true);
884 };
885 };
868 }
869 }
886 870 };
887 871
888 872 /**
889 873 * Turn a cell into a raw text cell.
890 874 *
891 875 * @method to_raw
892 876 * @param {Number} [index] A cell's index
893 877 */
894 878 Notebook.prototype.to_raw = function (index) {
895 879 var i = this.index_or_selected(index);
896 880 if (this.is_valid_cell_index(i)) {
897 881 var source_element = this.get_cell_element(i);
898 882 var source_cell = source_element.data("cell");
899 883 var target_cell = null;
900 884 if (!(source_cell instanceof IPython.RawCell)) {
901 885 target_cell = this.insert_cell_below('raw',i);
902 886 var text = source_cell.get_text();
903 887 if (text === source_cell.placeholder) {
904 888 text = '';
905 };
889 }
906 890 // We must show the editor before setting its contents
907 891 target_cell.unrender();
908 892 target_cell.set_text(text);
909 893 // make this value the starting point, so that we can only undo
910 894 // to this state, instead of a blank cell
911 895 target_cell.code_mirror.clearHistory();
912 896 source_element.remove();
913 897 this.select(i);
914 898 this.set_dirty(true);
915 };
916 };
899 }
900 }
917 901 };
918 902
919 903 /**
920 904 * Turn a cell into a heading cell.
921 905 *
922 906 * @method to_heading
923 907 * @param {Number} [index] A cell's index
924 908 * @param {Number} [level] A heading level (e.g., 1 becomes &lt;h1&gt;)
925 909 */
926 910 Notebook.prototype.to_heading = function (index, level) {
927 911 level = level || 1;
928 912 var i = this.index_or_selected(index);
929 913 if (this.is_valid_cell_index(i)) {
930 914 var source_element = this.get_cell_element(i);
931 915 var source_cell = source_element.data("cell");
932 916 var target_cell = null;
933 917 if (source_cell instanceof IPython.HeadingCell) {
934 918 source_cell.set_level(level);
935 919 } else {
936 920 target_cell = this.insert_cell_below('heading',i);
937 921 var text = source_cell.get_text();
938 922 if (text === source_cell.placeholder) {
939 923 text = '';
940 };
924 }
941 925 // We must show the editor before setting its contents
942 926 target_cell.set_level(level);
943 927 target_cell.unrender();
944 928 target_cell.set_text(text);
945 929 // make this value the starting point, so that we can only undo
946 930 // to this state, instead of a blank cell
947 931 target_cell.code_mirror.clearHistory();
948 932 source_element.remove();
949 933 this.select(i);
950 934 if ((source_cell instanceof IPython.TextCell) && source_cell.rendered) {
951 935 target_cell.render();
952 936 }
953 };
937 }
954 938 this.set_dirty(true);
955 939 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
956 940 {'cell_type':'heading',level:level}
957 941 );
958 };
942 }
959 943 };
960 944
961 945
962 946 // Cut/Copy/Paste
963 947
964 948 /**
965 949 * Enable UI elements for pasting cells.
966 950 *
967 951 * @method enable_paste
968 952 */
969 953 Notebook.prototype.enable_paste = function () {
970 954 var that = this;
971 955 if (!this.paste_enabled) {
972 956 $('#paste_cell_replace').removeClass('disabled')
973 957 .on('click', function () {that.paste_cell_replace();});
974 958 $('#paste_cell_above').removeClass('disabled')
975 959 .on('click', function () {that.paste_cell_above();});
976 960 $('#paste_cell_below').removeClass('disabled')
977 961 .on('click', function () {that.paste_cell_below();});
978 962 this.paste_enabled = true;
979 };
963 }
980 964 };
981 965
982 966 /**
983 967 * Disable UI elements for pasting cells.
984 968 *
985 969 * @method disable_paste
986 970 */
987 971 Notebook.prototype.disable_paste = function () {
988 972 if (this.paste_enabled) {
989 973 $('#paste_cell_replace').addClass('disabled').off('click');
990 974 $('#paste_cell_above').addClass('disabled').off('click');
991 975 $('#paste_cell_below').addClass('disabled').off('click');
992 976 this.paste_enabled = false;
993 };
977 }
994 978 };
995 979
996 980 /**
997 981 * Cut a cell.
998 982 *
999 983 * @method cut_cell
1000 984 */
1001 985 Notebook.prototype.cut_cell = function () {
1002 986 this.copy_cell();
1003 987 this.delete_cell();
1004 }
988 };
1005 989
1006 990 /**
1007 991 * Copy a cell.
1008 992 *
1009 993 * @method copy_cell
1010 994 */
1011 995 Notebook.prototype.copy_cell = function () {
1012 996 var cell = this.get_selected_cell();
1013 997 this.clipboard = cell.toJSON();
1014 998 this.enable_paste();
1015 999 };
1016 1000
1017 1001 /**
1018 1002 * Replace the selected cell with a cell in the clipboard.
1019 1003 *
1020 1004 * @method paste_cell_replace
1021 1005 */
1022 1006 Notebook.prototype.paste_cell_replace = function () {
1023 1007 if (this.clipboard !== null && this.paste_enabled) {
1024 1008 var cell_data = this.clipboard;
1025 1009 var new_cell = this.insert_cell_above(cell_data.cell_type);
1026 1010 new_cell.fromJSON(cell_data);
1027 1011 var old_cell = this.get_next_cell(new_cell);
1028 1012 this.delete_cell(this.find_cell_index(old_cell));
1029 1013 this.select(this.find_cell_index(new_cell));
1030 };
1014 }
1031 1015 };
1032 1016
1033 1017 /**
1034 1018 * Paste a cell from the clipboard above the selected cell.
1035 1019 *
1036 1020 * @method paste_cell_above
1037 1021 */
1038 1022 Notebook.prototype.paste_cell_above = function () {
1039 1023 if (this.clipboard !== null && this.paste_enabled) {
1040 1024 var cell_data = this.clipboard;
1041 1025 var new_cell = this.insert_cell_above(cell_data.cell_type);
1042 1026 new_cell.fromJSON(cell_data);
1043 1027 new_cell.focus_cell();
1044 };
1028 }
1045 1029 };
1046 1030
1047 1031 /**
1048 1032 * Paste a cell from the clipboard below the selected cell.
1049 1033 *
1050 1034 * @method paste_cell_below
1051 1035 */
1052 1036 Notebook.prototype.paste_cell_below = function () {
1053 1037 if (this.clipboard !== null && this.paste_enabled) {
1054 1038 var cell_data = this.clipboard;
1055 1039 var new_cell = this.insert_cell_below(cell_data.cell_type);
1056 1040 new_cell.fromJSON(cell_data);
1057 1041 new_cell.focus_cell();
1058 };
1042 }
1059 1043 };
1060 1044
1061 1045 // Split/merge
1062 1046
1063 1047 /**
1064 1048 * Split the selected cell into two, at the cursor.
1065 1049 *
1066 1050 * @method split_cell
1067 1051 */
1068 1052 Notebook.prototype.split_cell = function () {
1069 1053 var mdc = IPython.MarkdownCell;
1070 1054 var rc = IPython.RawCell;
1071 1055 var cell = this.get_selected_cell();
1072 1056 if (cell.is_splittable()) {
1073 1057 var texta = cell.get_pre_cursor();
1074 1058 var textb = cell.get_post_cursor();
1075 1059 if (cell instanceof IPython.CodeCell) {
1076 1060 // In this case the operations keep the notebook in its existing mode
1077 1061 // so we don't need to do any post-op mode changes.
1078 1062 cell.set_text(textb);
1079 1063 var new_cell = this.insert_cell_above('code');
1080 1064 new_cell.set_text(texta);
1081 1065 } else if ((cell instanceof mdc && !cell.rendered) || (cell instanceof rc)) {
1082 1066 // We know cell is !rendered so we can use set_text.
1083 1067 cell.set_text(textb);
1084 1068 var new_cell = this.insert_cell_above(cell.cell_type);
1085 1069 // Unrender the new cell so we can call set_text.
1086 1070 new_cell.unrender();
1087 1071 new_cell.set_text(texta);
1088 1072 }
1089 };
1073 }
1090 1074 };
1091 1075
1092 1076 /**
1093 1077 * Combine the selected cell into the cell above it.
1094 1078 *
1095 1079 * @method merge_cell_above
1096 1080 */
1097 1081 Notebook.prototype.merge_cell_above = function () {
1098 1082 var mdc = IPython.MarkdownCell;
1099 1083 var rc = IPython.RawCell;
1100 1084 var index = this.get_selected_index();
1101 1085 var cell = this.get_cell(index);
1102 1086 var render = cell.rendered;
1103 1087 if (!cell.is_mergeable()) {
1104 1088 return;
1105 1089 }
1106 1090 if (index > 0) {
1107 1091 var upper_cell = this.get_cell(index-1);
1108 1092 if (!upper_cell.is_mergeable()) {
1109 1093 return;
1110 1094 }
1111 1095 var upper_text = upper_cell.get_text();
1112 1096 var text = cell.get_text();
1113 1097 if (cell instanceof IPython.CodeCell) {
1114 1098 cell.set_text(upper_text+'\n'+text);
1115 1099 } else if ((cell instanceof mdc) || (cell instanceof rc)) {
1116 1100 cell.unrender(); // Must unrender before we set_text.
1117 1101 cell.set_text(upper_text+'\n\n'+text);
1118 1102 if (render) {
1119 1103 // The rendered state of the final cell should match
1120 1104 // that of the original selected cell;
1121 1105 cell.render();
1122 1106 }
1123 };
1107 }
1124 1108 this.delete_cell(index-1);
1125 1109 this.select(this.find_cell_index(cell));
1126 };
1110 }
1127 1111 };
1128 1112
1129 1113 /**
1130 1114 * Combine the selected cell into the cell below it.
1131 1115 *
1132 1116 * @method merge_cell_below
1133 1117 */
1134 1118 Notebook.prototype.merge_cell_below = function () {
1135 1119 var mdc = IPython.MarkdownCell;
1136 1120 var rc = IPython.RawCell;
1137 1121 var index = this.get_selected_index();
1138 1122 var cell = this.get_cell(index);
1139 1123 var render = cell.rendered;
1140 1124 if (!cell.is_mergeable()) {
1141 1125 return;
1142 1126 }
1143 1127 if (index < this.ncells()-1) {
1144 1128 var lower_cell = this.get_cell(index+1);
1145 1129 if (!lower_cell.is_mergeable()) {
1146 1130 return;
1147 1131 }
1148 1132 var lower_text = lower_cell.get_text();
1149 1133 var text = cell.get_text();
1150 1134 if (cell instanceof IPython.CodeCell) {
1151 1135 cell.set_text(text+'\n'+lower_text);
1152 1136 } else if ((cell instanceof mdc) || (cell instanceof rc)) {
1153 1137 cell.unrender(); // Must unrender before we set_text.
1154 1138 cell.set_text(text+'\n\n'+lower_text);
1155 1139 if (render) {
1156 1140 // The rendered state of the final cell should match
1157 1141 // that of the original selected cell;
1158 1142 cell.render();
1159 1143 }
1160 };
1144 }
1161 1145 this.delete_cell(index+1);
1162 1146 this.select(this.find_cell_index(cell));
1163 };
1147 }
1164 1148 };
1165 1149
1166 1150
1167 1151 // Cell collapsing and output clearing
1168 1152
1169 1153 /**
1170 1154 * Hide a cell's output.
1171 1155 *
1172 1156 * @method collapse_output
1173 1157 * @param {Number} index A cell's numeric index
1174 1158 */
1175 1159 Notebook.prototype.collapse_output = function (index) {
1176 1160 var i = this.index_or_selected(index);
1177 1161 var cell = this.get_cell(i);
1178 1162 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1179 1163 cell.collapse_output();
1180 1164 this.set_dirty(true);
1181 1165 }
1182 1166 };
1183 1167
1184 1168 /**
1185 1169 * Hide each code cell's output area.
1186 1170 *
1187 1171 * @method collapse_all_output
1188 1172 */
1189 1173 Notebook.prototype.collapse_all_output = function () {
1190 1174 $.map(this.get_cells(), function (cell, i) {
1191 1175 if (cell instanceof IPython.CodeCell) {
1192 1176 cell.collapse_output();
1193 1177 }
1194 1178 });
1195 1179 // this should not be set if the `collapse` key is removed from nbformat
1196 1180 this.set_dirty(true);
1197 1181 };
1198 1182
1199 1183 /**
1200 1184 * Show a cell's output.
1201 1185 *
1202 1186 * @method expand_output
1203 1187 * @param {Number} index A cell's numeric index
1204 1188 */
1205 1189 Notebook.prototype.expand_output = function (index) {
1206 1190 var i = this.index_or_selected(index);
1207 1191 var cell = this.get_cell(i);
1208 1192 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1209 1193 cell.expand_output();
1210 1194 this.set_dirty(true);
1211 1195 }
1212 1196 };
1213 1197
1214 1198 /**
1215 1199 * Expand each code cell's output area, and remove scrollbars.
1216 1200 *
1217 1201 * @method expand_all_output
1218 1202 */
1219 1203 Notebook.prototype.expand_all_output = function () {
1220 1204 $.map(this.get_cells(), function (cell, i) {
1221 1205 if (cell instanceof IPython.CodeCell) {
1222 1206 cell.expand_output();
1223 1207 }
1224 1208 });
1225 1209 // this should not be set if the `collapse` key is removed from nbformat
1226 1210 this.set_dirty(true);
1227 1211 };
1228 1212
1229 1213 /**
1230 1214 * Clear the selected CodeCell's output area.
1231 1215 *
1232 1216 * @method clear_output
1233 1217 * @param {Number} index A cell's numeric index
1234 1218 */
1235 1219 Notebook.prototype.clear_output = function (index) {
1236 1220 var i = this.index_or_selected(index);
1237 1221 var cell = this.get_cell(i);
1238 1222 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1239 1223 cell.clear_output();
1240 1224 this.set_dirty(true);
1241 1225 }
1242 1226 };
1243 1227
1244 1228 /**
1245 1229 * Clear each code cell's output area.
1246 1230 *
1247 1231 * @method clear_all_output
1248 1232 */
1249 1233 Notebook.prototype.clear_all_output = function () {
1250 1234 $.map(this.get_cells(), function (cell, i) {
1251 1235 if (cell instanceof IPython.CodeCell) {
1252 1236 cell.clear_output();
1253 1237 }
1254 1238 });
1255 1239 this.set_dirty(true);
1256 1240 };
1257 1241
1258 1242 /**
1259 1243 * Scroll the selected CodeCell's output area.
1260 1244 *
1261 1245 * @method scroll_output
1262 1246 * @param {Number} index A cell's numeric index
1263 1247 */
1264 1248 Notebook.prototype.scroll_output = function (index) {
1265 1249 var i = this.index_or_selected(index);
1266 1250 var cell = this.get_cell(i);
1267 1251 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1268 1252 cell.scroll_output();
1269 1253 this.set_dirty(true);
1270 1254 }
1271 1255 };
1272 1256
1273 1257 /**
1274 1258 * Expand each code cell's output area, and add a scrollbar for long output.
1275 1259 *
1276 1260 * @method scroll_all_output
1277 1261 */
1278 1262 Notebook.prototype.scroll_all_output = function () {
1279 1263 $.map(this.get_cells(), function (cell, i) {
1280 1264 if (cell instanceof IPython.CodeCell) {
1281 1265 cell.scroll_output();
1282 1266 }
1283 1267 });
1284 1268 // this should not be set if the `collapse` key is removed from nbformat
1285 1269 this.set_dirty(true);
1286 1270 };
1287 1271
1288 1272 /** Toggle whether a cell's output is collapsed or expanded.
1289 1273 *
1290 1274 * @method toggle_output
1291 1275 * @param {Number} index A cell's numeric index
1292 1276 */
1293 1277 Notebook.prototype.toggle_output = function (index) {
1294 1278 var i = this.index_or_selected(index);
1295 1279 var cell = this.get_cell(i);
1296 1280 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1297 1281 cell.toggle_output();
1298 1282 this.set_dirty(true);
1299 1283 }
1300 1284 };
1301 1285
1302 1286 /**
1303 1287 * Hide/show the output of all cells.
1304 1288 *
1305 1289 * @method toggle_all_output
1306 1290 */
1307 1291 Notebook.prototype.toggle_all_output = function () {
1308 1292 $.map(this.get_cells(), function (cell, i) {
1309 1293 if (cell instanceof IPython.CodeCell) {
1310 1294 cell.toggle_output();
1311 1295 }
1312 1296 });
1313 1297 // this should not be set if the `collapse` key is removed from nbformat
1314 1298 this.set_dirty(true);
1315 1299 };
1316 1300
1317 1301 /**
1318 1302 * Toggle a scrollbar for long cell outputs.
1319 1303 *
1320 1304 * @method toggle_output_scroll
1321 1305 * @param {Number} index A cell's numeric index
1322 1306 */
1323 1307 Notebook.prototype.toggle_output_scroll = function (index) {
1324 1308 var i = this.index_or_selected(index);
1325 1309 var cell = this.get_cell(i);
1326 1310 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1327 1311 cell.toggle_output_scroll();
1328 1312 this.set_dirty(true);
1329 1313 }
1330 1314 };
1331 1315
1332 1316 /**
1333 1317 * Toggle the scrolling of long output on all cells.
1334 1318 *
1335 1319 * @method toggle_all_output_scrolling
1336 1320 */
1337 1321 Notebook.prototype.toggle_all_output_scroll = function () {
1338 1322 $.map(this.get_cells(), function (cell, i) {
1339 1323 if (cell instanceof IPython.CodeCell) {
1340 1324 cell.toggle_output_scroll();
1341 1325 }
1342 1326 });
1343 1327 // this should not be set if the `collapse` key is removed from nbformat
1344 1328 this.set_dirty(true);
1345 1329 };
1346 1330
1347 1331 // Other cell functions: line numbers, ...
1348 1332
1349 1333 /**
1350 1334 * Toggle line numbers in the selected cell's input area.
1351 1335 *
1352 1336 * @method cell_toggle_line_numbers
1353 1337 */
1354 1338 Notebook.prototype.cell_toggle_line_numbers = function() {
1355 1339 this.get_selected_cell().toggle_line_numbers();
1356 1340 };
1357 1341
1358 1342 // Session related things
1359 1343
1360 1344 /**
1361 1345 * Start a new session and set it on each code cell.
1362 1346 *
1363 1347 * @method start_session
1364 1348 */
1365 1349 Notebook.prototype.start_session = function () {
1366 this.session = new IPython.Session(this.notebook_name, this.notebook_path, this);
1350 this.session = new IPython.Session(this, this.options);
1367 1351 this.session.start($.proxy(this._session_started, this));
1368 1352 };
1369 1353
1370 1354
1371 1355 /**
1372 1356 * Once a session is started, link the code cells to the kernel and pass the
1373 1357 * comm manager to the widget manager
1374 1358 *
1375 1359 */
1376 1360 Notebook.prototype._session_started = function(){
1377 1361 this.kernel = this.session.kernel;
1378 1362 var ncells = this.ncells();
1379 1363 for (var i=0; i<ncells; i++) {
1380 1364 var cell = this.get_cell(i);
1381 1365 if (cell instanceof IPython.CodeCell) {
1382 1366 cell.set_kernel(this.session.kernel);
1383 };
1384 };
1367 }
1368 }
1385 1369 };
1386 1370
1387 1371 /**
1388 1372 * Prompt the user to restart the IPython kernel.
1389 1373 *
1390 1374 * @method restart_kernel
1391 1375 */
1392 1376 Notebook.prototype.restart_kernel = function () {
1393 1377 var that = this;
1394 1378 IPython.dialog.modal({
1395 1379 title : "Restart kernel or continue running?",
1396 1380 body : $("<p/>").text(
1397 1381 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1398 1382 ),
1399 1383 buttons : {
1400 1384 "Continue running" : {},
1401 1385 "Restart" : {
1402 1386 "class" : "btn-danger",
1403 1387 "click" : function() {
1404 1388 that.session.restart_kernel();
1405 1389 }
1406 1390 }
1407 1391 }
1408 1392 });
1409 1393 };
1410 1394
1411 1395 /**
1412 1396 * Execute or render cell outputs and go into command mode.
1413 1397 *
1414 1398 * @method execute_cell
1415 1399 */
1416 1400 Notebook.prototype.execute_cell = function () {
1417 1401 // mode = shift, ctrl, alt
1418 1402 var cell = this.get_selected_cell();
1419 1403 var cell_index = this.find_cell_index(cell);
1420 1404
1421 1405 cell.execute();
1422 1406 this.command_mode();
1423 1407 cell.focus_cell();
1424 1408 this.set_dirty(true);
1425 }
1409 };
1426 1410
1427 1411 /**
1428 1412 * Execute or render cell outputs and insert a new cell below.
1429 1413 *
1430 1414 * @method execute_cell_and_insert_below
1431 1415 */
1432 1416 Notebook.prototype.execute_cell_and_insert_below = function () {
1433 1417 var cell = this.get_selected_cell();
1434 1418 var cell_index = this.find_cell_index(cell);
1435 1419
1436 1420 cell.execute();
1437 1421
1438 1422 // If we are at the end always insert a new cell and return
1439 1423 if (cell_index === (this.ncells()-1)) {
1440 1424 this.insert_cell_below('code');
1441 1425 this.select(cell_index+1);
1442 1426 this.edit_mode();
1443 1427 this.scroll_to_bottom();
1444 1428 this.set_dirty(true);
1445 1429 return;
1446 1430 }
1447 1431
1448 1432 this.insert_cell_below('code');
1449 1433 this.select(cell_index+1);
1450 1434 this.edit_mode();
1451 1435 this.set_dirty(true);
1452 1436 };
1453 1437
1454 1438 /**
1455 1439 * Execute or render cell outputs and select the next cell.
1456 1440 *
1457 1441 * @method execute_cell_and_select_below
1458 1442 */
1459 1443 Notebook.prototype.execute_cell_and_select_below = function () {
1460 1444
1461 1445 var cell = this.get_selected_cell();
1462 1446 var cell_index = this.find_cell_index(cell);
1463 1447
1464 1448 cell.execute();
1465 1449
1466 1450 // If we are at the end always insert a new cell and return
1467 1451 if (cell_index === (this.ncells()-1)) {
1468 1452 this.insert_cell_below('code');
1469 1453 this.select(cell_index+1);
1470 1454 this.edit_mode();
1471 1455 this.scroll_to_bottom();
1472 1456 this.set_dirty(true);
1473 1457 return;
1474 1458 }
1475 1459
1476 1460 this.select(cell_index+1);
1477 1461 this.get_cell(cell_index+1).focus_cell();
1478 1462 this.set_dirty(true);
1479 1463 };
1480 1464
1481 1465 /**
1482 1466 * Execute all cells below the selected cell.
1483 1467 *
1484 1468 * @method execute_cells_below
1485 1469 */
1486 1470 Notebook.prototype.execute_cells_below = function () {
1487 1471 this.execute_cell_range(this.get_selected_index(), this.ncells());
1488 1472 this.scroll_to_bottom();
1489 1473 };
1490 1474
1491 1475 /**
1492 1476 * Execute all cells above the selected cell.
1493 1477 *
1494 1478 * @method execute_cells_above
1495 1479 */
1496 1480 Notebook.prototype.execute_cells_above = function () {
1497 1481 this.execute_cell_range(0, this.get_selected_index());
1498 1482 };
1499 1483
1500 1484 /**
1501 1485 * Execute all cells.
1502 1486 *
1503 1487 * @method execute_all_cells
1504 1488 */
1505 1489 Notebook.prototype.execute_all_cells = function () {
1506 1490 this.execute_cell_range(0, this.ncells());
1507 1491 this.scroll_to_bottom();
1508 1492 };
1509 1493
1510 1494 /**
1511 1495 * Execute a contiguous range of cells.
1512 1496 *
1513 1497 * @method execute_cell_range
1514 1498 * @param {Number} start Index of the first cell to execute (inclusive)
1515 1499 * @param {Number} end Index of the last cell to execute (exclusive)
1516 1500 */
1517 1501 Notebook.prototype.execute_cell_range = function (start, end) {
1518 1502 for (var i=start; i<end; i++) {
1519 1503 this.select(i);
1520 1504 this.execute_cell();
1521 };
1505 }
1522 1506 };
1523 1507
1524 1508 // Persistance and loading
1525 1509
1526 1510 /**
1527 1511 * Getter method for this notebook's name.
1528 1512 *
1529 1513 * @method get_notebook_name
1530 * @return {String} This notebook's name
1514 * @return {String} This notebook's name (excluding file extension)
1531 1515 */
1532 1516 Notebook.prototype.get_notebook_name = function () {
1533 1517 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1534 1518 return nbname;
1535 1519 };
1536 1520
1537 1521 /**
1538 1522 * Setter method for this notebook's name.
1539 1523 *
1540 1524 * @method set_notebook_name
1541 1525 * @param {String} name A new name for this notebook
1542 1526 */
1543 1527 Notebook.prototype.set_notebook_name = function (name) {
1544 1528 this.notebook_name = name;
1545 1529 };
1546 1530
1547 1531 /**
1548 1532 * Check that a notebook's name is valid.
1549 1533 *
1550 1534 * @method test_notebook_name
1551 1535 * @param {String} nbname A name for this notebook
1552 1536 * @return {Boolean} True if the name is valid, false if invalid
1553 1537 */
1554 1538 Notebook.prototype.test_notebook_name = function (nbname) {
1555 1539 nbname = nbname || '';
1556 if (this.notebook_name_blacklist_re.test(nbname) == false && nbname.length>0) {
1540 if (nbname.length>0 && !this.notebook_name_blacklist_re.test(nbname)) {
1557 1541 return true;
1558 1542 } else {
1559 1543 return false;
1560 };
1544 }
1561 1545 };
1562 1546
1563 1547 /**
1564 1548 * Load a notebook from JSON (.ipynb).
1565 1549 *
1566 1550 * This currently handles one worksheet: others are deleted.
1567 1551 *
1568 1552 * @method fromJSON
1569 1553 * @param {Object} data JSON representation of a notebook
1570 1554 */
1571 1555 Notebook.prototype.fromJSON = function (data) {
1572 1556 var content = data.content;
1573 1557 var ncells = this.ncells();
1574 1558 var i;
1575 1559 for (i=0; i<ncells; i++) {
1576 1560 // Always delete cell 0 as they get renumbered as they are deleted.
1577 1561 this.delete_cell(0);
1578 };
1562 }
1579 1563 // Save the metadata and name.
1580 1564 this.metadata = content.metadata;
1581 1565 this.notebook_name = data.name;
1582 1566 // Only handle 1 worksheet for now.
1583 1567 var worksheet = content.worksheets[0];
1584 1568 if (worksheet !== undefined) {
1585 1569 if (worksheet.metadata) {
1586 1570 this.worksheet_metadata = worksheet.metadata;
1587 1571 }
1588 1572 var new_cells = worksheet.cells;
1589 1573 ncells = new_cells.length;
1590 1574 var cell_data = null;
1591 1575 var new_cell = null;
1592 1576 for (i=0; i<ncells; i++) {
1593 1577 cell_data = new_cells[i];
1594 1578 // VERSIONHACK: plaintext -> raw
1595 1579 // handle never-released plaintext name for raw cells
1596 1580 if (cell_data.cell_type === 'plaintext'){
1597 1581 cell_data.cell_type = 'raw';
1598 1582 }
1599 1583
1600 1584 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1601 1585 new_cell.fromJSON(cell_data);
1602 };
1603 };
1586 }
1587 }
1604 1588 if (content.worksheets.length > 1) {
1605 1589 IPython.dialog.modal({
1606 1590 title : "Multiple worksheets",
1607 1591 body : "This notebook has " + data.worksheets.length + " worksheets, " +
1608 1592 "but this version of IPython can only handle the first. " +
1609 1593 "If you save this notebook, worksheets after the first will be lost.",
1610 1594 buttons : {
1611 1595 OK : {
1612 1596 class : "btn-danger"
1613 1597 }
1614 1598 }
1615 1599 });
1616 1600 }
1617 1601 };
1618 1602
1619 1603 /**
1620 1604 * Dump this notebook into a JSON-friendly object.
1621 1605 *
1622 1606 * @method toJSON
1623 1607 * @return {Object} A JSON-friendly representation of this notebook.
1624 1608 */
1625 1609 Notebook.prototype.toJSON = function () {
1626 1610 var cells = this.get_cells();
1627 1611 var ncells = cells.length;
1628 1612 var cell_array = new Array(ncells);
1629 1613 for (var i=0; i<ncells; i++) {
1630 1614 cell_array[i] = cells[i].toJSON();
1631 };
1615 }
1632 1616 var data = {
1633 1617 // Only handle 1 worksheet for now.
1634 1618 worksheets : [{
1635 1619 cells: cell_array,
1636 1620 metadata: this.worksheet_metadata
1637 1621 }],
1638 1622 metadata : this.metadata
1639 1623 };
1640 1624 return data;
1641 1625 };
1642 1626
1643 1627 /**
1644 1628 * Start an autosave timer, for periodically saving the notebook.
1645 1629 *
1646 1630 * @method set_autosave_interval
1647 1631 * @param {Integer} interval the autosave interval in milliseconds
1648 1632 */
1649 1633 Notebook.prototype.set_autosave_interval = function (interval) {
1650 1634 var that = this;
1651 1635 // clear previous interval, so we don't get simultaneous timers
1652 1636 if (this.autosave_timer) {
1653 1637 clearInterval(this.autosave_timer);
1654 1638 }
1655 1639
1656 1640 this.autosave_interval = this.minimum_autosave_interval = interval;
1657 1641 if (interval) {
1658 1642 this.autosave_timer = setInterval(function() {
1659 1643 if (that.dirty) {
1660 1644 that.save_notebook();
1661 1645 }
1662 1646 }, interval);
1663 1647 $([IPython.events]).trigger("autosave_enabled.Notebook", interval);
1664 1648 } else {
1665 1649 this.autosave_timer = null;
1666 1650 $([IPython.events]).trigger("autosave_disabled.Notebook");
1667 };
1651 }
1668 1652 };
1669 1653
1670 1654 /**
1671 1655 * Save this notebook on the server.
1672 1656 *
1673 1657 * @method save_notebook
1674 1658 */
1675 1659 Notebook.prototype.save_notebook = function (extra_settings) {
1676 1660 // Create a JSON model to be sent to the server.
1677 1661 var model = {};
1678 1662 model.name = this.notebook_name;
1679 1663 model.path = this.notebook_path;
1680 1664 model.content = this.toJSON();
1681 1665 model.content.nbformat = this.nbformat;
1682 1666 model.content.nbformat_minor = this.nbformat_minor;
1683 1667 // time the ajax call for autosave tuning purposes.
1684 1668 var start = new Date().getTime();
1685 1669 // We do the call with settings so we can set cache to false.
1686 1670 var settings = {
1687 1671 processData : false,
1688 1672 cache : false,
1689 1673 type : "PUT",
1690 1674 data : JSON.stringify(model),
1691 1675 headers : {'Content-Type': 'application/json'},
1692 1676 success : $.proxy(this.save_notebook_success, this, start),
1693 1677 error : $.proxy(this.save_notebook_error, this)
1694 1678 };
1695 1679 if (extra_settings) {
1696 1680 for (var key in extra_settings) {
1697 1681 settings[key] = extra_settings[key];
1698 1682 }
1699 1683 }
1700 1684 $([IPython.events]).trigger('notebook_saving.Notebook');
1701 1685 var url = utils.url_join_encode(
1702 this._baseProjectUrl,
1686 this.base_url,
1703 1687 'api/notebooks',
1704 1688 this.notebook_path,
1705 1689 this.notebook_name
1706 1690 );
1707 1691 $.ajax(url, settings);
1708 1692 };
1709 1693
1710 1694 /**
1711 1695 * Success callback for saving a notebook.
1712 1696 *
1713 1697 * @method save_notebook_success
1714 1698 * @param {Integer} start the time when the save request started
1715 1699 * @param {Object} data JSON representation of a notebook
1716 1700 * @param {String} status Description of response status
1717 1701 * @param {jqXHR} xhr jQuery Ajax object
1718 1702 */
1719 1703 Notebook.prototype.save_notebook_success = function (start, data, status, xhr) {
1720 1704 this.set_dirty(false);
1721 1705 $([IPython.events]).trigger('notebook_saved.Notebook');
1722 1706 this._update_autosave_interval(start);
1723 1707 if (this._checkpoint_after_save) {
1724 1708 this.create_checkpoint();
1725 1709 this._checkpoint_after_save = false;
1726 };
1710 }
1727 1711 };
1728 1712
1729 1713 /**
1730 1714 * update the autosave interval based on how long the last save took
1731 1715 *
1732 1716 * @method _update_autosave_interval
1733 1717 * @param {Integer} timestamp when the save request started
1734 1718 */
1735 1719 Notebook.prototype._update_autosave_interval = function (start) {
1736 1720 var duration = (new Date().getTime() - start);
1737 1721 if (this.autosave_interval) {
1738 1722 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
1739 1723 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
1740 1724 // round to 10 seconds, otherwise we will be setting a new interval too often
1741 1725 interval = 10000 * Math.round(interval / 10000);
1742 1726 // set new interval, if it's changed
1743 1727 if (interval != this.autosave_interval) {
1744 1728 this.set_autosave_interval(interval);
1745 1729 }
1746 1730 }
1747 1731 };
1748 1732
1749 1733 /**
1750 1734 * Failure callback for saving a notebook.
1751 1735 *
1752 1736 * @method save_notebook_error
1753 1737 * @param {jqXHR} xhr jQuery Ajax object
1754 1738 * @param {String} status Description of response status
1755 1739 * @param {String} error HTTP error message
1756 1740 */
1757 1741 Notebook.prototype.save_notebook_error = function (xhr, status, error) {
1758 1742 $([IPython.events]).trigger('notebook_save_failed.Notebook', [xhr, status, error]);
1759 1743 };
1760 1744
1761 1745 Notebook.prototype.new_notebook = function(){
1762 1746 var path = this.notebook_path;
1763 var base_project_url = this._baseProjectUrl;
1747 var base_url = this.base_url;
1764 1748 var settings = {
1765 1749 processData : false,
1766 1750 cache : false,
1767 1751 type : "POST",
1768 1752 dataType : "json",
1769 1753 async : false,
1770 1754 success : function (data, status, xhr){
1771 1755 var notebook_name = data.name;
1772 1756 window.open(
1773 1757 utils.url_join_encode(
1774 base_project_url,
1758 base_url,
1775 1759 'notebooks',
1776 1760 path,
1777 1761 notebook_name
1778 1762 ),
1779 1763 '_blank'
1780 1764 );
1781 1765 }
1782 1766 };
1783 1767 var url = utils.url_join_encode(
1784 base_project_url,
1768 base_url,
1785 1769 'api/notebooks',
1786 1770 path
1787 1771 );
1788 1772 $.ajax(url,settings);
1789 1773 };
1790 1774
1791 1775
1792 1776 Notebook.prototype.copy_notebook = function(){
1793 1777 var path = this.notebook_path;
1794 var base_project_url = this._baseProjectUrl;
1778 var base_url = this.base_url;
1795 1779 var settings = {
1796 1780 processData : false,
1797 1781 cache : false,
1798 1782 type : "POST",
1799 1783 dataType : "json",
1800 1784 data : JSON.stringify({copy_from : this.notebook_name}),
1801 1785 async : false,
1802 1786 success : function (data, status, xhr) {
1803 1787 window.open(utils.url_join_encode(
1804 base_project_url,
1788 base_url,
1805 1789 'notebooks',
1806 1790 data.path,
1807 1791 data.name
1808 1792 ), '_blank');
1809 1793 }
1810 1794 };
1811 1795 var url = utils.url_join_encode(
1812 base_project_url,
1796 base_url,
1813 1797 'api/notebooks',
1814 1798 path
1815 1799 );
1816 1800 $.ajax(url,settings);
1817 1801 };
1818 1802
1819 1803 Notebook.prototype.rename = function (nbname) {
1820 1804 var that = this;
1821 var data = {name: nbname + '.ipynb'};
1805 if (!nbname.match(/\.ipynb$/)) {
1806 nbname = nbname + ".ipynb";
1807 }
1808 var data = {name: nbname};
1822 1809 var settings = {
1823 1810 processData : false,
1824 1811 cache : false,
1825 1812 type : "PATCH",
1826 1813 data : JSON.stringify(data),
1827 1814 dataType: "json",
1828 1815 headers : {'Content-Type': 'application/json'},
1829 1816 success : $.proxy(that.rename_success, this),
1830 1817 error : $.proxy(that.rename_error, this)
1831 1818 };
1832 1819 $([IPython.events]).trigger('rename_notebook.Notebook', data);
1833 1820 var url = utils.url_join_encode(
1834 this._baseProjectUrl,
1821 this.base_url,
1835 1822 'api/notebooks',
1836 1823 this.notebook_path,
1837 1824 this.notebook_name
1838 1825 );
1839 1826 $.ajax(url, settings);
1840 1827 };
1841 1828
1842 1829 Notebook.prototype.delete = function () {
1843 1830 var that = this;
1844 1831 var settings = {
1845 1832 processData : false,
1846 1833 cache : false,
1847 1834 type : "DELETE",
1848 1835 dataType: "json",
1849 1836 };
1850 1837 var url = utils.url_join_encode(
1851 this._baseProjectUrl,
1838 this.base_url,
1852 1839 'api/notebooks',
1853 1840 this.notebook_path,
1854 1841 this.notebook_name
1855 1842 );
1856 1843 $.ajax(url, settings);
1857 1844 };
1858 1845
1859 1846
1860 1847 Notebook.prototype.rename_success = function (json, status, xhr) {
1861 this.notebook_name = json.name;
1862 var name = this.notebook_name;
1848 var name = this.notebook_name = json.name;
1863 1849 var path = json.path;
1864 1850 this.session.rename_notebook(name, path);
1865 1851 $([IPython.events]).trigger('notebook_renamed.Notebook', json);
1866 }
1852 };
1867 1853
1868 1854 Notebook.prototype.rename_error = function (xhr, status, error) {
1869 1855 var that = this;
1870 1856 var dialog = $('<div/>').append(
1871 1857 $("<p/>").addClass("rename-message")
1872 1858 .text('This notebook name already exists.')
1873 )
1859 );
1874 1860 $([IPython.events]).trigger('notebook_rename_failed.Notebook', [xhr, status, error]);
1875 1861 IPython.dialog.modal({
1876 1862 title: "Notebook Rename Error!",
1877 1863 body: dialog,
1878 1864 buttons : {
1879 1865 "Cancel": {},
1880 1866 "OK": {
1881 1867 class: "btn-primary",
1882 1868 click: function () {
1883 1869 IPython.save_widget.rename_notebook();
1884 1870 }}
1885 1871 },
1886 1872 open : function (event, ui) {
1887 1873 var that = $(this);
1888 1874 // Upon ENTER, click the OK button.
1889 1875 that.find('input[type="text"]').keydown(function (event, ui) {
1890 1876 if (event.which === utils.keycodes.ENTER) {
1891 1877 that.find('.btn-primary').first().click();
1892 1878 }
1893 1879 });
1894 1880 that.find('input[type="text"]').focus();
1895 1881 }
1896 1882 });
1897 }
1883 };
1898 1884
1899 1885 /**
1900 1886 * Request a notebook's data from the server.
1901 1887 *
1902 1888 * @method load_notebook
1903 1889 * @param {String} notebook_name and path A notebook to load
1904 1890 */
1905 1891 Notebook.prototype.load_notebook = function (notebook_name, notebook_path) {
1906 1892 var that = this;
1907 1893 this.notebook_name = notebook_name;
1908 1894 this.notebook_path = notebook_path;
1909 1895 // We do the call with settings so we can set cache to false.
1910 1896 var settings = {
1911 1897 processData : false,
1912 1898 cache : false,
1913 1899 type : "GET",
1914 1900 dataType : "json",
1915 1901 success : $.proxy(this.load_notebook_success,this),
1916 1902 error : $.proxy(this.load_notebook_error,this),
1917 1903 };
1918 1904 $([IPython.events]).trigger('notebook_loading.Notebook');
1919 1905 var url = utils.url_join_encode(
1920 this._baseProjectUrl,
1906 this.base_url,
1921 1907 'api/notebooks',
1922 1908 this.notebook_path,
1923 1909 this.notebook_name
1924 1910 );
1925 1911 $.ajax(url, settings);
1926 1912 };
1927 1913
1928 1914 /**
1929 1915 * Success callback for loading a notebook from the server.
1930 1916 *
1931 1917 * Load notebook data from the JSON response.
1932 1918 *
1933 1919 * @method load_notebook_success
1934 1920 * @param {Object} data JSON representation of a notebook
1935 1921 * @param {String} status Description of response status
1936 1922 * @param {jqXHR} xhr jQuery Ajax object
1937 1923 */
1938 1924 Notebook.prototype.load_notebook_success = function (data, status, xhr) {
1939 1925 this.fromJSON(data);
1940 1926 if (this.ncells() === 0) {
1941 1927 this.insert_cell_below('code');
1942 1928 this.select(0);
1943 1929 this.edit_mode();
1944 1930 } else {
1945 1931 this.select(0);
1946 1932 this.command_mode();
1947 };
1933 }
1948 1934 this.set_dirty(false);
1949 1935 this.scroll_to_top();
1950 1936 if (data.orig_nbformat !== undefined && data.nbformat !== data.orig_nbformat) {
1951 1937 var msg = "This notebook has been converted from an older " +
1952 1938 "notebook format (v"+data.orig_nbformat+") to the current notebook " +
1953 1939 "format (v"+data.nbformat+"). The next time you save this notebook, the " +
1954 1940 "newer notebook format will be used and older versions of IPython " +
1955 1941 "may not be able to read it. To keep the older version, close the " +
1956 1942 "notebook without saving it.";
1957 1943 IPython.dialog.modal({
1958 1944 title : "Notebook converted",
1959 1945 body : msg,
1960 1946 buttons : {
1961 1947 OK : {
1962 1948 class : "btn-primary"
1963 1949 }
1964 1950 }
1965 1951 });
1966 1952 } else if (data.orig_nbformat_minor !== undefined && data.nbformat_minor !== data.orig_nbformat_minor) {
1967 1953 var that = this;
1968 1954 var orig_vs = 'v' + data.nbformat + '.' + data.orig_nbformat_minor;
1969 1955 var this_vs = 'v' + data.nbformat + '.' + this.nbformat_minor;
1970 1956 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
1971 1957 this_vs + ". You can still work with this notebook, but some features " +
1972 "introduced in later notebook versions may not be available."
1958 "introduced in later notebook versions may not be available.";
1973 1959
1974 1960 IPython.dialog.modal({
1975 1961 title : "Newer Notebook",
1976 1962 body : msg,
1977 1963 buttons : {
1978 1964 OK : {
1979 1965 class : "btn-danger"
1980 1966 }
1981 1967 }
1982 1968 });
1983 1969
1984 1970 }
1985 1971
1986 1972 // Create the session after the notebook is completely loaded to prevent
1987 1973 // code execution upon loading, which is a security risk.
1988 if (this.session == null) {
1974 if (this.session === null) {
1989 1975 this.start_session();
1990 1976 }
1991 1977 // load our checkpoint list
1992 1978 this.list_checkpoints();
1993 1979
1994 1980 // load toolbar state
1995 1981 if (this.metadata.celltoolbar) {
1996 1982 IPython.CellToolbar.global_show();
1997 1983 IPython.CellToolbar.activate_preset(this.metadata.celltoolbar);
1998 1984 }
1999 1985
2000 1986 $([IPython.events]).trigger('notebook_loaded.Notebook');
2001 1987 };
2002 1988
2003 1989 /**
2004 1990 * Failure callback for loading a notebook from the server.
2005 1991 *
2006 1992 * @method load_notebook_error
2007 1993 * @param {jqXHR} xhr jQuery Ajax object
2008 1994 * @param {String} status Description of response status
2009 1995 * @param {String} error HTTP error message
2010 1996 */
2011 1997 Notebook.prototype.load_notebook_error = function (xhr, status, error) {
2012 1998 $([IPython.events]).trigger('notebook_load_failed.Notebook', [xhr, status, error]);
1999 var msg;
2013 2000 if (xhr.status === 400) {
2014 var msg = error;
2001 msg = error;
2015 2002 } else if (xhr.status === 500) {
2016 var msg = "An unknown error occurred while loading this notebook. " +
2003 msg = "An unknown error occurred while loading this notebook. " +
2017 2004 "This version can load notebook formats " +
2018 2005 "v" + this.nbformat + " or earlier.";
2019 2006 }
2020 2007 IPython.dialog.modal({
2021 2008 title: "Error loading notebook",
2022 2009 body : msg,
2023 2010 buttons : {
2024 2011 "OK": {}
2025 2012 }
2026 2013 });
2027 }
2014 };
2028 2015
2029 2016 /********************* checkpoint-related *********************/
2030 2017
2031 2018 /**
2032 2019 * Save the notebook then immediately create a checkpoint.
2033 2020 *
2034 2021 * @method save_checkpoint
2035 2022 */
2036 2023 Notebook.prototype.save_checkpoint = function () {
2037 2024 this._checkpoint_after_save = true;
2038 2025 this.save_notebook();
2039 2026 };
2040 2027
2041 2028 /**
2042 2029 * Add a checkpoint for this notebook.
2043 2030 * for use as a callback from checkpoint creation.
2044 2031 *
2045 2032 * @method add_checkpoint
2046 2033 */
2047 2034 Notebook.prototype.add_checkpoint = function (checkpoint) {
2048 2035 var found = false;
2049 2036 for (var i = 0; i < this.checkpoints.length; i++) {
2050 2037 var existing = this.checkpoints[i];
2051 2038 if (existing.id == checkpoint.id) {
2052 2039 found = true;
2053 2040 this.checkpoints[i] = checkpoint;
2054 2041 break;
2055 2042 }
2056 2043 }
2057 2044 if (!found) {
2058 2045 this.checkpoints.push(checkpoint);
2059 2046 }
2060 2047 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
2061 2048 };
2062 2049
2063 2050 /**
2064 2051 * List checkpoints for this notebook.
2065 2052 *
2066 2053 * @method list_checkpoints
2067 2054 */
2068 2055 Notebook.prototype.list_checkpoints = function () {
2069 2056 var url = utils.url_join_encode(
2070 this._baseProjectUrl,
2057 this.base_url,
2071 2058 'api/notebooks',
2072 2059 this.notebook_path,
2073 2060 this.notebook_name,
2074 2061 'checkpoints'
2075 2062 );
2076 2063 $.get(url).done(
2077 2064 $.proxy(this.list_checkpoints_success, this)
2078 2065 ).fail(
2079 2066 $.proxy(this.list_checkpoints_error, this)
2080 2067 );
2081 2068 };
2082 2069
2083 2070 /**
2084 2071 * Success callback for listing checkpoints.
2085 2072 *
2086 2073 * @method list_checkpoint_success
2087 2074 * @param {Object} data JSON representation of a checkpoint
2088 2075 * @param {String} status Description of response status
2089 2076 * @param {jqXHR} xhr jQuery Ajax object
2090 2077 */
2091 2078 Notebook.prototype.list_checkpoints_success = function (data, status, xhr) {
2092 var data = $.parseJSON(data);
2079 data = $.parseJSON(data);
2093 2080 this.checkpoints = data;
2094 2081 if (data.length) {
2095 2082 this.last_checkpoint = data[data.length - 1];
2096 2083 } else {
2097 2084 this.last_checkpoint = null;
2098 2085 }
2099 2086 $([IPython.events]).trigger('checkpoints_listed.Notebook', [data]);
2100 2087 };
2101 2088
2102 2089 /**
2103 2090 * Failure callback for listing a checkpoint.
2104 2091 *
2105 2092 * @method list_checkpoint_error
2106 2093 * @param {jqXHR} xhr jQuery Ajax object
2107 2094 * @param {String} status Description of response status
2108 2095 * @param {String} error_msg HTTP error message
2109 2096 */
2110 2097 Notebook.prototype.list_checkpoints_error = function (xhr, status, error_msg) {
2111 2098 $([IPython.events]).trigger('list_checkpoints_failed.Notebook');
2112 2099 };
2113 2100
2114 2101 /**
2115 2102 * Create a checkpoint of this notebook on the server from the most recent save.
2116 2103 *
2117 2104 * @method create_checkpoint
2118 2105 */
2119 2106 Notebook.prototype.create_checkpoint = function () {
2120 2107 var url = utils.url_join_encode(
2121 this._baseProjectUrl,
2108 this.base_url,
2122 2109 'api/notebooks',
2123 this.notebookPath(),
2110 this.notebook_path,
2124 2111 this.notebook_name,
2125 2112 'checkpoints'
2126 2113 );
2127 2114 $.post(url).done(
2128 2115 $.proxy(this.create_checkpoint_success, this)
2129 2116 ).fail(
2130 2117 $.proxy(this.create_checkpoint_error, this)
2131 2118 );
2132 2119 };
2133 2120
2134 2121 /**
2135 2122 * Success callback for creating a checkpoint.
2136 2123 *
2137 2124 * @method create_checkpoint_success
2138 2125 * @param {Object} data JSON representation of a checkpoint
2139 2126 * @param {String} status Description of response status
2140 2127 * @param {jqXHR} xhr jQuery Ajax object
2141 2128 */
2142 2129 Notebook.prototype.create_checkpoint_success = function (data, status, xhr) {
2143 var data = $.parseJSON(data);
2130 data = $.parseJSON(data);
2144 2131 this.add_checkpoint(data);
2145 2132 $([IPython.events]).trigger('checkpoint_created.Notebook', data);
2146 2133 };
2147 2134
2148 2135 /**
2149 2136 * Failure callback for creating a checkpoint.
2150 2137 *
2151 2138 * @method create_checkpoint_error
2152 2139 * @param {jqXHR} xhr jQuery Ajax object
2153 2140 * @param {String} status Description of response status
2154 2141 * @param {String} error_msg HTTP error message
2155 2142 */
2156 2143 Notebook.prototype.create_checkpoint_error = function (xhr, status, error_msg) {
2157 2144 $([IPython.events]).trigger('checkpoint_failed.Notebook');
2158 2145 };
2159 2146
2160 2147 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2161 2148 var that = this;
2162 var checkpoint = checkpoint || this.last_checkpoint;
2149 checkpoint = checkpoint || this.last_checkpoint;
2163 2150 if ( ! checkpoint ) {
2164 2151 console.log("restore dialog, but no checkpoint to restore to!");
2165 2152 return;
2166 2153 }
2167 2154 var body = $('<div/>').append(
2168 2155 $('<p/>').addClass("p-space").text(
2169 2156 "Are you sure you want to revert the notebook to " +
2170 2157 "the latest checkpoint?"
2171 2158 ).append(
2172 2159 $("<strong/>").text(
2173 2160 " This cannot be undone."
2174 2161 )
2175 2162 )
2176 2163 ).append(
2177 2164 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2178 2165 ).append(
2179 2166 $('<p/>').addClass("p-space").text(
2180 2167 Date(checkpoint.last_modified)
2181 2168 ).css("text-align", "center")
2182 2169 );
2183 2170
2184 2171 IPython.dialog.modal({
2185 2172 title : "Revert notebook to checkpoint",
2186 2173 body : body,
2187 2174 buttons : {
2188 2175 Revert : {
2189 2176 class : "btn-danger",
2190 2177 click : function () {
2191 2178 that.restore_checkpoint(checkpoint.id);
2192 2179 }
2193 2180 },
2194 2181 Cancel : {}
2195 2182 }
2196 2183 });
2197 }
2184 };
2198 2185
2199 2186 /**
2200 2187 * Restore the notebook to a checkpoint state.
2201 2188 *
2202 2189 * @method restore_checkpoint
2203 2190 * @param {String} checkpoint ID
2204 2191 */
2205 2192 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2206 2193 $([IPython.events]).trigger('notebook_restoring.Notebook', checkpoint);
2207 2194 var url = utils.url_join_encode(
2208 this._baseProjectUrl,
2195 this.base_url,
2209 2196 'api/notebooks',
2210 this.notebookPath(),
2197 this.notebook_path,
2211 2198 this.notebook_name,
2212 2199 'checkpoints',
2213 2200 checkpoint
2214 2201 );
2215 2202 $.post(url).done(
2216 2203 $.proxy(this.restore_checkpoint_success, this)
2217 2204 ).fail(
2218 2205 $.proxy(this.restore_checkpoint_error, this)
2219 2206 );
2220 2207 };
2221 2208
2222 2209 /**
2223 2210 * Success callback for restoring a notebook to a checkpoint.
2224 2211 *
2225 2212 * @method restore_checkpoint_success
2226 2213 * @param {Object} data (ignored, should be empty)
2227 2214 * @param {String} status Description of response status
2228 2215 * @param {jqXHR} xhr jQuery Ajax object
2229 2216 */
2230 2217 Notebook.prototype.restore_checkpoint_success = function (data, status, xhr) {
2231 2218 $([IPython.events]).trigger('checkpoint_restored.Notebook');
2232 2219 this.load_notebook(this.notebook_name, this.notebook_path);
2233 2220 };
2234 2221
2235 2222 /**
2236 2223 * Failure callback for restoring a notebook to a checkpoint.
2237 2224 *
2238 2225 * @method restore_checkpoint_error
2239 2226 * @param {jqXHR} xhr jQuery Ajax object
2240 2227 * @param {String} status Description of response status
2241 2228 * @param {String} error_msg HTTP error message
2242 2229 */
2243 2230 Notebook.prototype.restore_checkpoint_error = function (xhr, status, error_msg) {
2244 2231 $([IPython.events]).trigger('checkpoint_restore_failed.Notebook');
2245 2232 };
2246 2233
2247 2234 /**
2248 2235 * Delete a notebook checkpoint.
2249 2236 *
2250 2237 * @method delete_checkpoint
2251 2238 * @param {String} checkpoint ID
2252 2239 */
2253 2240 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2254 2241 $([IPython.events]).trigger('notebook_restoring.Notebook', checkpoint);
2255 2242 var url = utils.url_join_encode(
2256 this._baseProjectUrl,
2243 this.base_url,
2257 2244 'api/notebooks',
2258 this.notebookPath(),
2245 this.notebook_path,
2259 2246 this.notebook_name,
2260 2247 'checkpoints',
2261 2248 checkpoint
2262 2249 );
2263 2250 $.ajax(url, {
2264 2251 type: 'DELETE',
2265 2252 success: $.proxy(this.delete_checkpoint_success, this),
2266 2253 error: $.proxy(this.delete_notebook_error,this)
2267 2254 });
2268 2255 };
2269 2256
2270 2257 /**
2271 2258 * Success callback for deleting a notebook checkpoint
2272 2259 *
2273 2260 * @method delete_checkpoint_success
2274 2261 * @param {Object} data (ignored, should be empty)
2275 2262 * @param {String} status Description of response status
2276 2263 * @param {jqXHR} xhr jQuery Ajax object
2277 2264 */
2278 2265 Notebook.prototype.delete_checkpoint_success = function (data, status, xhr) {
2279 2266 $([IPython.events]).trigger('checkpoint_deleted.Notebook', data);
2280 2267 this.load_notebook(this.notebook_name, this.notebook_path);
2281 2268 };
2282 2269
2283 2270 /**
2284 2271 * Failure callback for deleting a notebook checkpoint.
2285 2272 *
2286 2273 * @method delete_checkpoint_error
2287 2274 * @param {jqXHR} xhr jQuery Ajax object
2288 2275 * @param {String} status Description of response status
2289 2276 * @param {String} error_msg HTTP error message
2290 2277 */
2291 2278 Notebook.prototype.delete_checkpoint_error = function (xhr, status, error_msg) {
2292 2279 $([IPython.events]).trigger('checkpoint_delete_failed.Notebook');
2293 2280 };
2294 2281
2295 2282
2296 2283 IPython.Notebook = Notebook;
2297 2284
2298 2285
2299 2286 return IPython;
2300 2287
2301 2288 }(IPython));
@@ -1,210 +1,222 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2012 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // Notification widget
10 10 //============================================================================
11 11
12 12 var IPython = (function (IPython) {
13 13 "use strict";
14 14 var utils = IPython.utils;
15 15
16 16
17 17 var NotificationArea = function (selector) {
18 18 this.selector = selector;
19 19 if (this.selector !== undefined) {
20 20 this.element = $(selector);
21 21 }
22 22 this.widget_dict = {};
23 23 };
24 24
25 25 NotificationArea.prototype.temp_message = function (msg, timeout, css_class) {
26 26 var uuid = utils.uuid();
27 27 if( css_class == 'danger') {css_class = 'ui-state-error';}
28 28 if( css_class == 'warning') {css_class = 'ui-state-highlight';}
29 29 var tdiv = $('<div>')
30 30 .attr('id',uuid)
31 31 .addClass('notification_widget ui-widget ui-widget-content ui-corner-all')
32 32 .addClass('border-box-sizing')
33 33 .addClass(css_class)
34 34 .hide()
35 35 .text(msg);
36 36
37 37 $(this.selector).append(tdiv);
38 38 var tmout = Math.max(1500,(timeout||1500));
39 39 tdiv.fadeIn(100);
40 40
41 41 setTimeout(function () {
42 42 tdiv.fadeOut(100, function () {tdiv.remove();});
43 43 }, tmout);
44 44 };
45 45
46 46 NotificationArea.prototype.widget = function(name) {
47 47 if(this.widget_dict[name] == undefined) {
48 48 return this.new_notification_widget(name);
49 49 }
50 50 return this.get_widget(name);
51 51 };
52 52
53 53 NotificationArea.prototype.get_widget = function(name) {
54 54 if(this.widget_dict[name] == undefined) {
55 55 throw('no widgets with this name');
56 56 }
57 57 return this.widget_dict[name];
58 58 };
59 59
60 60 NotificationArea.prototype.new_notification_widget = function(name) {
61 61 if(this.widget_dict[name] != undefined) {
62 62 throw('widget with that name already exists ! ');
63 63 }
64 64 var div = $('<div/>').attr('id','notification_'+name);
65 65 $(this.selector).append(div);
66 66 this.widget_dict[name] = new IPython.NotificationWidget('#notification_'+name);
67 67 return this.widget_dict[name];
68 68 };
69 69
70 70 NotificationArea.prototype.init_notification_widgets = function() {
71 71 var knw = this.new_notification_widget('kernel');
72 var $kernel_indic = $("#kernel_indicator");
72 var $kernel_ind_icon = $("#kernel_indicator_icon");
73 var $modal_ind_icon = $("#modal_indicator_icon");
74
75 // Command/Edit mode
76 $([IPython.events]).on('edit_mode.Notebook',function () {
77 IPython.save_widget.update_document_title();
78 $modal_ind_icon.attr('class','icon-pencil').attr('title','Edit Mode');
79 });
80
81 $([IPython.events]).on('command_mode.Notebook',function () {
82 IPython.save_widget.update_document_title();
83 $modal_ind_icon.attr('class','').attr('title','Command Mode');
84 });
73 85
74 86 // Kernel events
75 87 $([IPython.events]).on('status_idle.Kernel',function () {
76 88 IPython.save_widget.update_document_title();
77 $kernel_indic.attr('class','icon-circle-blank').attr('title','Kernel Idle');
89 $kernel_ind_icon.attr('class','icon-circle-blank').attr('title','Kernel Idle');
78 90 });
79 91
80 92 $([IPython.events]).on('status_busy.Kernel',function () {
81 93 window.document.title='(Busy) '+window.document.title;
82 $kernel_indic.attr('class','icon-circle').attr('title','Kernel Busy');
94 $kernel_ind_icon.attr('class','icon-circle').attr('title','Kernel Busy');
83 95 });
84 96
85 97 $([IPython.events]).on('status_restarting.Kernel',function () {
86 98 IPython.save_widget.update_document_title();
87 99 knw.set_message("Restarting kernel", 2000);
88 100 });
89 101
90 102 $([IPython.events]).on('status_interrupting.Kernel',function () {
91 103 knw.set_message("Interrupting kernel", 2000);
92 104 });
93 105
94 106 $([IPython.events]).on('status_dead.Kernel',function () {
95 107 var msg = 'The kernel has died, and the automatic restart has failed.' +
96 108 ' It is possible the kernel cannot be restarted.' +
97 109 ' If you are not able to restart the kernel, you will still be able to save' +
98 110 ' the notebook, but running code will no longer work until the notebook' +
99 111 ' is reopened.';
100 112
101 113 IPython.dialog.modal({
102 114 title: "Dead kernel",
103 115 body : msg,
104 116 buttons : {
105 117 "Manual Restart": {
106 118 class: "btn-danger",
107 119 click: function () {
108 120 $([IPython.events]).trigger('status_restarting.Kernel');
109 121 IPython.notebook.start_kernel();
110 122 }
111 123 },
112 124 "Don't restart": {}
113 125 }
114 126 });
115 127 });
116 128
117 129 $([IPython.events]).on('websocket_closed.Kernel', function (event, data) {
118 130 var kernel = data.kernel;
119 131 var ws_url = data.ws_url;
120 132 var early = data.early;
121 133 var msg;
122 134 if (!early) {
123 135 knw.set_message('Reconnecting WebSockets', 1000);
124 136 setTimeout(function () {
125 137 kernel.start_channels();
126 138 }, 5000);
127 139 return;
128 140 }
129 141 console.log('WebSocket connection failed: ', ws_url)
130 142 msg = "A WebSocket connection could not be established." +
131 143 " You will NOT be able to run code. Check your" +
132 144 " network connection or notebook server configuration.";
133 145 IPython.dialog.modal({
134 146 title: "WebSocket connection failed",
135 147 body: msg,
136 148 buttons : {
137 149 "OK": {},
138 150 "Reconnect": {
139 151 click: function () {
140 152 knw.set_message('Reconnecting WebSockets', 1000);
141 153 setTimeout(function () {
142 154 kernel.start_channels();
143 155 }, 5000);
144 156 }
145 157 }
146 158 }
147 159 });
148 160 });
149 161
150 162
151 163 var nnw = this.new_notification_widget('notebook');
152 164
153 165 // Notebook events
154 166 $([IPython.events]).on('notebook_loading.Notebook', function () {
155 167 nnw.set_message("Loading notebook",500);
156 168 });
157 169 $([IPython.events]).on('notebook_loaded.Notebook', function () {
158 170 nnw.set_message("Notebook loaded",500);
159 171 });
160 172 $([IPython.events]).on('notebook_saving.Notebook', function () {
161 173 nnw.set_message("Saving notebook",500);
162 174 });
163 175 $([IPython.events]).on('notebook_saved.Notebook', function () {
164 176 nnw.set_message("Notebook saved",2000);
165 177 });
166 178 $([IPython.events]).on('notebook_save_failed.Notebook', function () {
167 179 nnw.set_message("Notebook save failed");
168 180 });
169 181
170 182 // Checkpoint events
171 183 $([IPython.events]).on('checkpoint_created.Notebook', function (evt, data) {
172 184 var msg = "Checkpoint created";
173 185 if (data.last_modified) {
174 186 var d = new Date(data.last_modified);
175 187 msg = msg + ": " + d.format("HH:MM:ss");
176 188 }
177 189 nnw.set_message(msg, 2000);
178 190 });
179 191 $([IPython.events]).on('checkpoint_failed.Notebook', function () {
180 192 nnw.set_message("Checkpoint failed");
181 193 });
182 194 $([IPython.events]).on('checkpoint_deleted.Notebook', function () {
183 195 nnw.set_message("Checkpoint deleted", 500);
184 196 });
185 197 $([IPython.events]).on('checkpoint_delete_failed.Notebook', function () {
186 198 nnw.set_message("Checkpoint delete failed");
187 199 });
188 200 $([IPython.events]).on('checkpoint_restoring.Notebook', function () {
189 201 nnw.set_message("Restoring to checkpoint...", 500);
190 202 });
191 203 $([IPython.events]).on('checkpoint_restore_failed.Notebook', function () {
192 204 nnw.set_message("Checkpoint restore failed");
193 205 });
194 206
195 207 // Autosave events
196 208 $([IPython.events]).on('autosave_disabled.Notebook', function () {
197 209 nnw.set_message("Autosave disabled", 2000);
198 210 });
199 211 $([IPython.events]).on('autosave_enabled.Notebook', function (evt, interval) {
200 212 nnw.set_message("Saving every " + interval / 1000 + "s", 1000);
201 213 });
202 214
203 215 };
204 216
205 217 IPython.NotificationArea = NotificationArea;
206 218
207 219 return IPython;
208 220
209 221 }(IPython));
210 222
@@ -1,851 +1,867 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // OutputArea
10 10 //============================================================================
11 11
12 12 /**
13 13 * @module IPython
14 14 * @namespace IPython
15 15 * @submodule OutputArea
16 16 */
17 17 var IPython = (function (IPython) {
18 18 "use strict";
19 19
20 20 var utils = IPython.utils;
21 21
22 22 /**
23 23 * @class OutputArea
24 24 *
25 25 * @constructor
26 26 */
27 27
28 28 var OutputArea = function (selector, prompt_area) {
29 29 this.selector = selector;
30 30 this.wrapper = $(selector);
31 31 this.outputs = [];
32 32 this.collapsed = false;
33 33 this.scrolled = false;
34 34 this.trusted = true;
35 35 this.clear_queued = null;
36 36 if (prompt_area === undefined) {
37 37 this.prompt_area = true;
38 38 } else {
39 39 this.prompt_area = prompt_area;
40 40 }
41 41 this.create_elements();
42 42 this.style();
43 43 this.bind_events();
44 44 };
45 45
46 46
47 47 /**
48 48 * Class prototypes
49 49 **/
50 50
51 51 OutputArea.prototype.create_elements = function () {
52 52 this.element = $("<div/>");
53 53 this.collapse_button = $("<div/>");
54 54 this.prompt_overlay = $("<div/>");
55 55 this.wrapper.append(this.prompt_overlay);
56 56 this.wrapper.append(this.element);
57 57 this.wrapper.append(this.collapse_button);
58 58 };
59 59
60 60
61 61 OutputArea.prototype.style = function () {
62 62 this.collapse_button.hide();
63 63 this.prompt_overlay.hide();
64 64
65 65 this.wrapper.addClass('output_wrapper');
66 66 this.element.addClass('output');
67 67
68 68 this.collapse_button.addClass("btn output_collapsed");
69 69 this.collapse_button.attr('title', 'click to expand output');
70 70 this.collapse_button.text('. . .');
71 71
72 72 this.prompt_overlay.addClass('out_prompt_overlay prompt');
73 73 this.prompt_overlay.attr('title', 'click to expand output; double click to hide output');
74 74
75 75 this.collapse();
76 76 };
77 77
78 78 /**
79 79 * Should the OutputArea scroll?
80 80 * Returns whether the height (in lines) exceeds a threshold.
81 81 *
82 82 * @private
83 83 * @method _should_scroll
84 84 * @param [lines=100]{Integer}
85 85 * @return {Bool}
86 86 *
87 87 */
88 88 OutputArea.prototype._should_scroll = function (lines) {
89 89 if (lines <=0 ){ return }
90 90 if (!lines) {
91 91 lines = 100;
92 92 }
93 93 // line-height from http://stackoverflow.com/questions/1185151
94 94 var fontSize = this.element.css('font-size');
95 95 var lineHeight = Math.floor(parseInt(fontSize.replace('px','')) * 1.5);
96 96
97 97 return (this.element.height() > lines * lineHeight);
98 98 };
99 99
100 100
101 101 OutputArea.prototype.bind_events = function () {
102 102 var that = this;
103 103 this.prompt_overlay.dblclick(function () { that.toggle_output(); });
104 104 this.prompt_overlay.click(function () { that.toggle_scroll(); });
105 105
106 106 this.element.resize(function () {
107 107 // FIXME: Firefox on Linux misbehaves, so automatic scrolling is disabled
108 108 if ( IPython.utils.browser[0] === "Firefox" ) {
109 109 return;
110 110 }
111 111 // maybe scroll output,
112 112 // if it's grown large enough and hasn't already been scrolled.
113 113 if ( !that.scrolled && that._should_scroll(OutputArea.auto_scroll_threshold)) {
114 114 that.scroll_area();
115 115 }
116 116 });
117 117 this.collapse_button.click(function () {
118 118 that.expand();
119 119 });
120 120 };
121 121
122 122
123 123 OutputArea.prototype.collapse = function () {
124 124 if (!this.collapsed) {
125 125 this.element.hide();
126 126 this.prompt_overlay.hide();
127 127 if (this.element.html()){
128 128 this.collapse_button.show();
129 129 }
130 130 this.collapsed = true;
131 131 }
132 132 };
133 133
134 134
135 135 OutputArea.prototype.expand = function () {
136 136 if (this.collapsed) {
137 137 this.collapse_button.hide();
138 138 this.element.show();
139 139 this.prompt_overlay.show();
140 140 this.collapsed = false;
141 141 }
142 142 };
143 143
144 144
145 145 OutputArea.prototype.toggle_output = function () {
146 146 if (this.collapsed) {
147 147 this.expand();
148 148 } else {
149 149 this.collapse();
150 150 }
151 151 };
152 152
153 153
154 154 OutputArea.prototype.scroll_area = function () {
155 155 this.element.addClass('output_scroll');
156 156 this.prompt_overlay.attr('title', 'click to unscroll output; double click to hide');
157 157 this.scrolled = true;
158 158 };
159 159
160 160
161 161 OutputArea.prototype.unscroll_area = function () {
162 162 this.element.removeClass('output_scroll');
163 163 this.prompt_overlay.attr('title', 'click to scroll output; double click to hide');
164 164 this.scrolled = false;
165 165 };
166 166
167 167 /**
168 168 *
169 169 * Scroll OutputArea if height supperior than a threshold (in lines).
170 170 *
171 171 * Threshold is a maximum number of lines. If unspecified, defaults to
172 172 * OutputArea.minimum_scroll_threshold.
173 173 *
174 174 * Negative threshold will prevent the OutputArea from ever scrolling.
175 175 *
176 176 * @method scroll_if_long
177 177 *
178 178 * @param [lines=20]{Number} Default to 20 if not set,
179 179 * behavior undefined for value of `0`.
180 180 *
181 181 **/
182 182 OutputArea.prototype.scroll_if_long = function (lines) {
183 183 var n = lines | OutputArea.minimum_scroll_threshold;
184 184 if(n <= 0){
185 185 return
186 186 }
187 187
188 188 if (this._should_scroll(n)) {
189 189 // only allow scrolling long-enough output
190 190 this.scroll_area();
191 191 }
192 192 };
193 193
194 194
195 195 OutputArea.prototype.toggle_scroll = function () {
196 196 if (this.scrolled) {
197 197 this.unscroll_area();
198 198 } else {
199 199 // only allow scrolling long-enough output
200 200 this.scroll_if_long();
201 201 }
202 202 };
203 203
204 204
205 205 // typeset with MathJax if MathJax is available
206 206 OutputArea.prototype.typeset = function () {
207 207 if (window.MathJax){
208 208 MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
209 209 }
210 210 };
211 211
212 212
213 213 OutputArea.prototype.handle_output = function (msg) {
214 214 var json = {};
215 215 var msg_type = json.output_type = msg.header.msg_type;
216 216 var content = msg.content;
217 217 if (msg_type === "stream") {
218 218 json.text = content.data;
219 219 json.stream = content.name;
220 220 } else if (msg_type === "display_data") {
221 221 json = content.data;
222 222 json.output_type = msg_type;
223 223 json.metadata = content.metadata;
224 224 } else if (msg_type === "pyout") {
225 225 json = content.data;
226 226 json.output_type = msg_type;
227 227 json.metadata = content.metadata;
228 228 json.prompt_number = content.execution_count;
229 229 } else if (msg_type === "pyerr") {
230 230 json.ename = content.ename;
231 231 json.evalue = content.evalue;
232 232 json.traceback = content.traceback;
233 233 }
234 234 this.append_output(json);
235 235 };
236 236
237 237
238 238 OutputArea.prototype.rename_keys = function (data, key_map) {
239 239 var remapped = {};
240 240 for (var key in data) {
241 241 var new_key = key_map[key] || key;
242 242 remapped[new_key] = data[key];
243 243 }
244 244 return remapped;
245 245 };
246 246
247 247
248 248 OutputArea.output_types = [
249 249 'application/javascript',
250 250 'text/html',
251 251 'text/latex',
252 252 'image/svg+xml',
253 253 'image/png',
254 254 'image/jpeg',
255 'application/pdf',
255 256 'text/plain'
256 257 ];
257 258
258 259 OutputArea.prototype.validate_output = function (json) {
259 260 // scrub invalid outputs
260 261 // TODO: right now everything is a string, but JSON really shouldn't be.
261 262 // nbformat 4 will fix that.
262 263 $.map(OutputArea.output_types, function(key){
263 264 if (json[key] !== undefined && typeof json[key] !== 'string') {
264 265 console.log("Invalid type for " + key, json[key]);
265 266 delete json[key];
266 267 }
267 268 });
268 269 return json;
269 270 };
270 271
271 272 OutputArea.prototype.append_output = function (json) {
272 273 this.expand();
273 274 // Clear the output if clear is queued.
274 275 var needs_height_reset = false;
275 276 if (this.clear_queued) {
276 277 this.clear_output(false);
277 278 needs_height_reset = true;
278 279 }
279 280
280 281 // validate output data types
281 282 json = this.validate_output(json);
282 283
283 284 if (json.output_type === 'pyout') {
284 285 this.append_pyout(json);
285 286 } else if (json.output_type === 'pyerr') {
286 287 this.append_pyerr(json);
287 288 } else if (json.output_type === 'display_data') {
288 289 this.append_display_data(json);
289 290 } else if (json.output_type === 'stream') {
290 291 this.append_stream(json);
291 292 }
292 293
293 294 this.outputs.push(json);
294 295
295 296 // Only reset the height to automatic if the height is currently
296 297 // fixed (done by wait=True flag on clear_output).
297 298 if (needs_height_reset) {
298 299 this.element.height('');
299 300 }
300 301
301 302 var that = this;
302 303 setTimeout(function(){that.element.trigger('resize');}, 100);
303 304 };
304 305
305 306
306 307 OutputArea.prototype.create_output_area = function () {
307 308 var oa = $("<div/>").addClass("output_area");
308 309 if (this.prompt_area) {
309 310 oa.append($('<div/>').addClass('prompt'));
310 311 }
311 312 return oa;
312 313 };
313 314
314 315
315 316 function _get_metadata_key(metadata, key, mime) {
316 317 var mime_md = metadata[mime];
317 318 // mime-specific higher priority
318 319 if (mime_md && mime_md[key] !== undefined) {
319 320 return mime_md[key];
320 321 }
321 322 // fallback on global
322 323 return metadata[key];
323 324 }
324 325
325 326 OutputArea.prototype.create_output_subarea = function(md, classes, mime) {
326 327 var subarea = $('<div/>').addClass('output_subarea').addClass(classes);
327 328 if (_get_metadata_key(md, 'isolated', mime)) {
328 329 // Create an iframe to isolate the subarea from the rest of the
329 330 // document
330 331 var iframe = $('<iframe/>').addClass('box-flex1');
331 332 iframe.css({'height':1, 'width':'100%', 'display':'block'});
332 333 iframe.attr('frameborder', 0);
333 334 iframe.attr('scrolling', 'auto');
334 335
335 336 // Once the iframe is loaded, the subarea is dynamically inserted
336 337 iframe.on('load', function() {
337 338 // Workaround needed by Firefox, to properly render svg inside
338 339 // iframes, see http://stackoverflow.com/questions/10177190/
339 340 // svg-dynamically-added-to-iframe-does-not-render-correctly
340 341 this.contentDocument.open();
341 342
342 343 // Insert the subarea into the iframe
343 344 // We must directly write the html. When using Jquery's append
344 345 // method, javascript is evaluated in the parent document and
345 346 // not in the iframe document.
346 347 this.contentDocument.write(subarea.html());
347 348
348 349 this.contentDocument.close();
349 350
350 351 var body = this.contentDocument.body;
351 352 // Adjust the iframe height automatically
352 353 iframe.height(body.scrollHeight + 'px');
353 354 });
354 355
355 356 // Elements should be appended to the inner subarea and not to the
356 357 // iframe
357 358 iframe.append = function(that) {
358 359 subarea.append(that);
359 360 };
360 361
361 362 return iframe;
362 363 } else {
363 364 return subarea;
364 365 }
365 366 }
366 367
367 368
368 369 OutputArea.prototype._append_javascript_error = function (err, element) {
369 370 // display a message when a javascript error occurs in display output
370 371 var msg = "Javascript error adding output!"
371 372 if ( element === undefined ) return;
372 373 element.append(
373 374 $('<div/>').html(msg + "<br/>" +
374 375 err.toString() +
375 376 '<br/>See your browser Javascript console for more details.'
376 377 ).addClass('js-error')
377 378 );
378 379 };
379 380
380 381 OutputArea.prototype._safe_append = function (toinsert) {
381 382 // safely append an item to the document
382 383 // this is an object created by user code,
383 384 // and may have errors, which should not be raised
384 385 // under any circumstances.
385 386 try {
386 387 this.element.append(toinsert);
387 388 } catch(err) {
388 389 console.log(err);
389 390 // Create an actual output_area and output_subarea, which creates
390 391 // the prompt area and the proper indentation.
391 392 var toinsert = this.create_output_area();
392 393 var subarea = $('<div/>').addClass('output_subarea');
393 394 toinsert.append(subarea);
394 395 this._append_javascript_error(err, subarea);
395 396 this.element.append(toinsert);
396 397 }
397 398 };
398 399
399 400
400 401 OutputArea.prototype.append_pyout = function (json) {
401 402 var n = json.prompt_number || ' ';
402 403 var toinsert = this.create_output_area();
403 404 if (this.prompt_area) {
404 405 toinsert.find('div.prompt').addClass('output_prompt').text('Out[' + n + ']:');
405 406 }
406 407 this.append_mime_type(json, toinsert);
407 408 this._safe_append(toinsert);
408 409 // If we just output latex, typeset it.
409 410 if ((json['text/latex'] !== undefined) || (json['text/html'] !== undefined)) {
410 411 this.typeset();
411 412 }
412 413 };
413 414
414 415
415 416 OutputArea.prototype.append_pyerr = function (json) {
416 417 var tb = json.traceback;
417 418 if (tb !== undefined && tb.length > 0) {
418 419 var s = '';
419 420 var len = tb.length;
420 421 for (var i=0; i<len; i++) {
421 422 s = s + tb[i] + '\n';
422 423 }
423 424 s = s + '\n';
424 425 var toinsert = this.create_output_area();
425 426 this.append_text(s, {}, toinsert);
426 427 this._safe_append(toinsert);
427 428 }
428 429 };
429 430
430 431
431 432 OutputArea.prototype.append_stream = function (json) {
432 433 // temporary fix: if stream undefined (json file written prior to this patch),
433 434 // default to most likely stdout:
434 435 if (json.stream == undefined){
435 436 json.stream = 'stdout';
436 437 }
437 438 var text = json.text;
438 439 var subclass = "output_"+json.stream;
439 440 if (this.outputs.length > 0){
440 441 // have at least one output to consider
441 442 var last = this.outputs[this.outputs.length-1];
442 443 if (last.output_type == 'stream' && json.stream == last.stream){
443 444 // latest output was in the same stream,
444 445 // so append directly into its pre tag
445 446 // escape ANSI & HTML specials:
446 447 var pre = this.element.find('div.'+subclass).last().find('pre');
447 448 var html = utils.fixCarriageReturn(
448 449 pre.html() + utils.fixConsole(text));
449 450 pre.html(html);
450 451 return;
451 452 }
452 453 }
453 454
454 455 if (!text.replace("\r", "")) {
455 456 // text is nothing (empty string, \r, etc.)
456 457 // so don't append any elements, which might add undesirable space
457 458 return;
458 459 }
459 460
460 461 // If we got here, attach a new div
461 462 var toinsert = this.create_output_area();
462 463 this.append_text(text, {}, toinsert, "output_stream "+subclass);
463 464 this._safe_append(toinsert);
464 465 };
465 466
466 467
467 468 OutputArea.prototype.append_display_data = function (json) {
468 469 var toinsert = this.create_output_area();
469 470 if (this.append_mime_type(json, toinsert)) {
470 471 this._safe_append(toinsert);
471 472 // If we just output latex, typeset it.
472 473 if ((json['text/latex'] !== undefined) || (json['text/html'] !== undefined)) {
473 474 this.typeset();
474 475 }
475 476 }
476 477 };
477 478
478 479
479 480 OutputArea.safe_outputs = {
480 481 'text/plain' : true,
481 482 'image/png' : true,
482 483 'image/jpeg' : true
483 484 };
484 485
485 486 OutputArea.prototype.append_mime_type = function (json, element) {
486 487 for (var type_i in OutputArea.display_order) {
487 488 var type = OutputArea.display_order[type_i];
488 489 var append = OutputArea.append_map[type];
489 490 if ((json[type] !== undefined) && append) {
490 491 if (!this.trusted && !OutputArea.safe_outputs[type]) {
491 492 // not trusted show warning and do not display
492 493 var content = {
493 494 text : "Untrusted " + type + " output ignored.",
494 495 stream : "stderr"
495 496 }
496 497 this.append_stream(content);
497 498 continue;
498 499 }
499 500 var md = json.metadata || {};
500 501 var toinsert = append.apply(this, [json[type], md, element]);
501 502 $([IPython.events]).trigger('output_appended.OutputArea', [type, json[type], md, toinsert]);
502 503 return true;
503 504 }
504 505 }
505 506 return false;
506 507 };
507 508
508 509
509 510 OutputArea.prototype.append_html = function (html, md, element) {
510 511 var type = 'text/html';
511 512 var toinsert = this.create_output_subarea(md, "output_html rendered_html", type);
512 513 IPython.keyboard_manager.register_events(toinsert);
513 514 toinsert.append(html);
514 515 element.append(toinsert);
515 516 return toinsert;
516 517 };
517 518
518 519
519 520 OutputArea.prototype.append_javascript = function (js, md, element) {
520 521 // We just eval the JS code, element appears in the local scope.
521 522 var type = 'application/javascript';
522 523 var toinsert = this.create_output_subarea(md, "output_javascript", type);
523 524 IPython.keyboard_manager.register_events(toinsert);
524 525 element.append(toinsert);
525 526 // FIXME TODO : remove `container element for 3.0`
526 527 //backward compat, js should be eval'ed in a context where `container` is defined.
527 528 var container = element;
528 529 container.show = function(){console.log('Warning "container.show()" is deprecated.')};
529 530 // end backward compat
530 531 try {
531 532 eval(js);
532 533 } catch(err) {
533 534 console.log(err);
534 535 this._append_javascript_error(err, toinsert);
535 536 }
536 537 return toinsert;
537 538 };
538 539
539 540
540 541 OutputArea.prototype.append_text = function (data, md, element, extra_class) {
541 542 var type = 'text/plain';
542 543 var toinsert = this.create_output_subarea(md, "output_text", type);
543 544 // escape ANSI & HTML specials in plaintext:
544 545 data = utils.fixConsole(data);
545 546 data = utils.fixCarriageReturn(data);
546 547 data = utils.autoLinkUrls(data);
547 548 if (extra_class){
548 549 toinsert.addClass(extra_class);
549 550 }
550 551 toinsert.append($("<pre/>").html(data));
551 552 element.append(toinsert);
552 553 return toinsert;
553 554 };
554 555
555 556
556 557 OutputArea.prototype.append_svg = function (svg, md, element) {
557 558 var type = 'image/svg+xml';
558 559 var toinsert = this.create_output_subarea(md, "output_svg", type);
559 560 toinsert.append(svg);
560 561 element.append(toinsert);
561 562 return toinsert;
562 563 };
563 564
564 565
565 566 OutputArea.prototype._dblclick_to_reset_size = function (img) {
566 567 // schedule wrapping image in resizable after a delay,
567 568 // so we don't end up calling resize on a zero-size object
568 569 var that = this;
569 570 setTimeout(function () {
570 571 var h0 = img.height();
571 572 var w0 = img.width();
572 573 if (!(h0 && w0)) {
573 574 // zero size, schedule another timeout
574 575 that._dblclick_to_reset_size(img);
575 576 return;
576 577 }
577 578 img.resizable({
578 579 aspectRatio: true,
579 580 autoHide: true
580 581 });
581 582 img.dblclick(function () {
582 583 // resize wrapper & image together for some reason:
583 584 img.parent().height(h0);
584 585 img.height(h0);
585 586 img.parent().width(w0);
586 587 img.width(w0);
587 588 });
588 589 }, 250);
589 590 };
590 591
591 592 var set_width_height = function (img, md, mime) {
592 593 // set width and height of an img element from metadata
593 594 var height = _get_metadata_key(md, 'height', mime);
594 595 if (height !== undefined) img.attr('height', height);
595 596 var width = _get_metadata_key(md, 'width', mime);
596 597 if (width !== undefined) img.attr('width', width);
597 598 };
598 599
599 600 OutputArea.prototype.append_png = function (png, md, element) {
600 601 var type = 'image/png';
601 602 var toinsert = this.create_output_subarea(md, "output_png", type);
602 603 var img = $("<img/>").attr('src','data:image/png;base64,'+png);
603 604 set_width_height(img, md, 'image/png');
604 605 this._dblclick_to_reset_size(img);
605 606 toinsert.append(img);
606 607 element.append(toinsert);
607 608 return toinsert;
608 609 };
609 610
610 611
611 612 OutputArea.prototype.append_jpeg = function (jpeg, md, element) {
612 613 var type = 'image/jpeg';
613 614 var toinsert = this.create_output_subarea(md, "output_jpeg", type);
614 615 var img = $("<img/>").attr('src','data:image/jpeg;base64,'+jpeg);
615 616 set_width_height(img, md, 'image/jpeg');
616 617 this._dblclick_to_reset_size(img);
617 618 toinsert.append(img);
618 619 element.append(toinsert);
619 620 return toinsert;
620 621 };
621 622
622 623
624 OutputArea.prototype.append_pdf = function (pdf, md, element) {
625 var type = 'application/pdf';
626 var toinsert = this.create_output_subarea(md, "output_pdf", type);
627 var a = $('<a/>').attr('href', 'data:application/pdf;base64,'+pdf);
628 a.attr('target', '_blank');
629 a.text('View PDF')
630 toinsert.append(a);
631 element.append(toinsert);
632 return toinsert;
633 }
634
623 635 OutputArea.prototype.append_latex = function (latex, md, element) {
624 636 // This method cannot do the typesetting because the latex first has to
625 637 // be on the page.
626 638 var type = 'text/latex';
627 639 var toinsert = this.create_output_subarea(md, "output_latex", type);
628 640 toinsert.append(latex);
629 641 element.append(toinsert);
630 642 return toinsert;
631 643 };
632 644
633 645
634 646 OutputArea.prototype.append_raw_input = function (msg) {
635 647 var that = this;
636 648 this.expand();
637 649 var content = msg.content;
638 650 var area = this.create_output_area();
639 651
640 652 // disable any other raw_inputs, if they are left around
641 653 $("div.output_subarea.raw_input").remove();
642 654
643 655 area.append(
644 656 $("<div/>")
645 657 .addClass("box-flex1 output_subarea raw_input")
646 658 .append(
647 659 $("<span/>")
648 660 .addClass("input_prompt")
649 661 .text(content.prompt)
650 662 )
651 663 .append(
652 664 $("<input/>")
653 665 .addClass("raw_input")
654 666 .attr('type', 'text')
655 667 .attr("size", 47)
656 668 .keydown(function (event, ui) {
657 669 // make sure we submit on enter,
658 670 // and don't re-execute the *cell* on shift-enter
659 671 if (event.which === utils.keycodes.ENTER) {
660 672 that._submit_raw_input();
661 673 return false;
662 674 }
663 675 })
664 676 )
665 677 );
666 678
667 679 this.element.append(area);
668 680 var raw_input = area.find('input.raw_input');
669 681 // Register events that enable/disable the keyboard manager while raw
670 682 // input is focused.
671 683 IPython.keyboard_manager.register_events(raw_input);
672 684 // Note, the following line used to read raw_input.focus().focus().
673 685 // This seemed to be needed otherwise only the cell would be focused.
674 686 // But with the modal UI, this seems to work fine with one call to focus().
675 687 raw_input.focus();
676 688 }
677 689
678 690 OutputArea.prototype._submit_raw_input = function (evt) {
679 691 var container = this.element.find("div.raw_input");
680 692 var theprompt = container.find("span.input_prompt");
681 693 var theinput = container.find("input.raw_input");
682 694 var value = theinput.val();
683 695 var content = {
684 696 output_type : 'stream',
685 697 name : 'stdout',
686 698 text : theprompt.text() + value + '\n'
687 699 }
688 700 // remove form container
689 701 container.parent().remove();
690 702 // replace with plaintext version in stdout
691 703 this.append_output(content, false);
692 704 $([IPython.events]).trigger('send_input_reply.Kernel', value);
693 705 }
694 706
695 707
696 708 OutputArea.prototype.handle_clear_output = function (msg) {
697 709 // msg spec v4 had stdout, stderr, display keys
698 710 // v4.1 replaced these with just wait
699 711 // The default behavior is the same (stdout=stderr=display=True, wait=False),
700 712 // so v4 messages will still be properly handled,
701 713 // except for the rarely used clearing less than all output.
702 714 this.clear_output(msg.content.wait || false);
703 715 };
704 716
705 717
706 718 OutputArea.prototype.clear_output = function(wait) {
707 719 if (wait) {
708 720
709 721 // If a clear is queued, clear before adding another to the queue.
710 722 if (this.clear_queued) {
711 723 this.clear_output(false);
712 724 };
713 725
714 726 this.clear_queued = true;
715 727 } else {
716 728
717 729 // Fix the output div's height if the clear_output is waiting for
718 730 // new output (it is being used in an animation).
719 731 if (this.clear_queued) {
720 732 var height = this.element.height();
721 733 this.element.height(height);
722 734 this.clear_queued = false;
723 735 }
724 736
725 737 // clear all, no need for logic
726 738 this.element.html("");
727 739 this.outputs = [];
728 740 this.trusted = true;
729 741 this.unscroll_area();
730 742 return;
731 743 };
732 744 };
733 745
734 746
735 747 // JSON serialization
736 748
737 749 OutputArea.prototype.fromJSON = function (outputs) {
738 750 var len = outputs.length;
739 751 var data;
740 752
741 753 for (var i=0; i<len; i++) {
742 754 data = outputs[i];
743 755 var msg_type = data.output_type;
744 756 if (msg_type === "display_data" || msg_type === "pyout") {
745 757 // convert short keys to mime keys
746 758 // TODO: remove mapping of short keys when we update to nbformat 4
747 759 data = this.rename_keys(data, OutputArea.mime_map_r);
748 760 data.metadata = this.rename_keys(data.metadata, OutputArea.mime_map_r);
749 761 }
750 762
751 763 this.append_output(data);
752 764 }
753 765 };
754 766
755 767
756 768 OutputArea.prototype.toJSON = function () {
757 769 var outputs = [];
758 770 var len = this.outputs.length;
759 771 var data;
760 772 for (var i=0; i<len; i++) {
761 773 data = this.outputs[i];
762 774 var msg_type = data.output_type;
763 775 if (msg_type === "display_data" || msg_type === "pyout") {
764 776 // convert mime keys to short keys
765 777 data = this.rename_keys(data, OutputArea.mime_map);
766 778 data.metadata = this.rename_keys(data.metadata, OutputArea.mime_map);
767 779 }
768 780 outputs[i] = data;
769 781 }
770 782 return outputs;
771 783 };
772 784
773 785 /**
774 786 * Class properties
775 787 **/
776 788
777 789 /**
778 790 * Threshold to trigger autoscroll when the OutputArea is resized,
779 791 * typically when new outputs are added.
780 792 *
781 793 * Behavior is undefined if autoscroll is lower than minimum_scroll_threshold,
782 794 * unless it is < 0, in which case autoscroll will never be triggered
783 795 *
784 796 * @property auto_scroll_threshold
785 797 * @type Number
786 798 * @default 100
787 799 *
788 800 **/
789 801 OutputArea.auto_scroll_threshold = 100;
790 802
791 803 /**
792 804 * Lower limit (in lines) for OutputArea to be made scrollable. OutputAreas
793 805 * shorter than this are never scrolled.
794 806 *
795 807 * @property minimum_scroll_threshold
796 808 * @type Number
797 809 * @default 20
798 810 *
799 811 **/
800 812 OutputArea.minimum_scroll_threshold = 20;
801 813
802 814
803 815
804 816 OutputArea.mime_map = {
805 817 "text/plain" : "text",
806 818 "text/html" : "html",
807 819 "image/svg+xml" : "svg",
808 820 "image/png" : "png",
809 821 "image/jpeg" : "jpeg",
822 "application/pdf" : "pdf",
810 823 "text/latex" : "latex",
811 824 "application/json" : "json",
812 825 "application/javascript" : "javascript",
813 826 };
814 827
815 828 OutputArea.mime_map_r = {
816 829 "text" : "text/plain",
817 830 "html" : "text/html",
818 831 "svg" : "image/svg+xml",
819 832 "png" : "image/png",
820 833 "jpeg" : "image/jpeg",
834 "pdf" : "application/pdf",
821 835 "latex" : "text/latex",
822 836 "json" : "application/json",
823 837 "javascript" : "application/javascript",
824 838 };
825 839
826 840 OutputArea.display_order = [
827 841 'application/javascript',
828 842 'text/html',
829 843 'text/latex',
830 844 'image/svg+xml',
831 845 'image/png',
832 846 'image/jpeg',
847 'application/pdf',
833 848 'text/plain'
834 849 ];
835 850
836 851 OutputArea.append_map = {
837 852 "text/plain" : OutputArea.prototype.append_text,
838 853 "text/html" : OutputArea.prototype.append_html,
839 854 "image/svg+xml" : OutputArea.prototype.append_svg,
840 855 "image/png" : OutputArea.prototype.append_png,
841 856 "image/jpeg" : OutputArea.prototype.append_jpeg,
842 857 "text/latex" : OutputArea.prototype.append_latex,
843 858 "application/json" : OutputArea.prototype.append_json,
844 859 "application/javascript" : OutputArea.prototype.append_javascript,
860 "application/pdf" : OutputArea.prototype.append_pdf
845 861 };
846 862
847 863 IPython.OutputArea = OutputArea;
848 864
849 865 return IPython;
850 866
851 867 }(IPython));
@@ -1,173 +1,173 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // SaveWidget
10 10 //============================================================================
11 11
12 12 var IPython = (function (IPython) {
13 13 "use strict";
14 14
15 15 var utils = IPython.utils;
16 16
17 17 var SaveWidget = function (selector) {
18 18 this.selector = selector;
19 19 if (this.selector !== undefined) {
20 20 this.element = $(selector);
21 21 this.style();
22 22 this.bind_events();
23 23 }
24 24 };
25 25
26 26
27 27 SaveWidget.prototype.style = function () {
28 28 };
29 29
30 30
31 31 SaveWidget.prototype.bind_events = function () {
32 32 var that = this;
33 33 this.element.find('span#notebook_name').click(function () {
34 34 that.rename_notebook();
35 35 });
36 36 this.element.find('span#notebook_name').hover(function () {
37 37 $(this).addClass("ui-state-hover");
38 38 }, function () {
39 39 $(this).removeClass("ui-state-hover");
40 40 });
41 41 $([IPython.events]).on('notebook_loaded.Notebook', function () {
42 42 that.update_notebook_name();
43 43 that.update_document_title();
44 44 });
45 45 $([IPython.events]).on('notebook_saved.Notebook', function () {
46 46 that.update_notebook_name();
47 47 that.update_document_title();
48 48 });
49 49 $([IPython.events]).on('notebook_renamed.Notebook', function () {
50 50 that.update_notebook_name();
51 51 that.update_document_title();
52 52 that.update_address_bar();
53 53 });
54 54 $([IPython.events]).on('notebook_save_failed.Notebook', function () {
55 55 that.set_save_status('Autosave Failed!');
56 56 });
57 57 $([IPython.events]).on('checkpoints_listed.Notebook', function (event, data) {
58 58 that.set_last_checkpoint(data[0]);
59 59 });
60 60
61 61 $([IPython.events]).on('checkpoint_created.Notebook', function (event, data) {
62 62 that.set_last_checkpoint(data);
63 63 });
64 64 $([IPython.events]).on('set_dirty.Notebook', function (event, data) {
65 65 that.set_autosaved(data.value);
66 66 });
67 67 };
68 68
69 69
70 70 SaveWidget.prototype.rename_notebook = function () {
71 71 var that = this;
72 72 var dialog = $('<div/>').append(
73 73 $("<p/>").addClass("rename-message")
74 74 .text('Enter a new notebook name:')
75 75 ).append(
76 76 $("<br/>")
77 77 ).append(
78 78 $('<input/>').attr('type','text').attr('size','25')
79 79 .val(IPython.notebook.get_notebook_name())
80 80 );
81 81 IPython.dialog.modal({
82 82 title: "Rename Notebook",
83 83 body: dialog,
84 84 buttons : {
85 85 "Cancel": {},
86 86 "OK": {
87 87 class: "btn-primary",
88 88 click: function () {
89 89 var new_name = $(this).find('input').val();
90 90 if (!IPython.notebook.test_notebook_name(new_name)) {
91 91 $(this).find('.rename-message').text(
92 92 "Invalid notebook name. Notebook names must "+
93 93 "have 1 or more characters and can contain any characters " +
94 94 "except :/\\. Please enter a new notebook name:"
95 95 );
96 96 return false;
97 97 } else {
98 98 IPython.notebook.rename(new_name);
99 99 }
100 100 }}
101 101 },
102 102 open : function (event, ui) {
103 103 var that = $(this);
104 104 // Upon ENTER, click the OK button.
105 105 that.find('input[type="text"]').keydown(function (event, ui) {
106 106 if (event.which === utils.keycodes.ENTER) {
107 107 that.find('.btn-primary').first().click();
108 108 return false;
109 109 }
110 110 });
111 111 that.find('input[type="text"]').focus().select();
112 112 }
113 113 });
114 114 }
115 115
116 116
117 117 SaveWidget.prototype.update_notebook_name = function () {
118 118 var nbname = IPython.notebook.get_notebook_name();
119 119 this.element.find('span#notebook_name').text(nbname);
120 120 };
121 121
122 122
123 123 SaveWidget.prototype.update_document_title = function () {
124 124 var nbname = IPython.notebook.get_notebook_name();
125 125 document.title = nbname;
126 126 };
127 127
128 128 SaveWidget.prototype.update_address_bar = function(){
129 129 var nbname = IPython.notebook.notebook_name;
130 var path = IPython.notebook.notebookPath();
130 var path = IPython.notebook.notebook_path;
131 131 var state = {path : utils.url_join_encode(path, nbname)};
132 132 window.history.replaceState(state, "", utils.url_join_encode(
133 133 "/notebooks",
134 134 path,
135 135 nbname)
136 136 );
137 137 }
138 138
139 139
140 140 SaveWidget.prototype.set_save_status = function (msg) {
141 141 this.element.find('span#autosave_status').text(msg);
142 142 }
143 143
144 144 SaveWidget.prototype.set_checkpoint_status = function (msg) {
145 145 this.element.find('span#checkpoint_status').text(msg);
146 146 }
147 147
148 148 SaveWidget.prototype.set_last_checkpoint = function (checkpoint) {
149 149 if (!checkpoint) {
150 150 this.set_checkpoint_status("");
151 151 return;
152 152 }
153 153 var d = new Date(checkpoint.last_modified);
154 154 this.set_checkpoint_status(
155 155 "Last Checkpoint: " + d.format('mmm dd HH:MM')
156 156 );
157 157 }
158 158
159 159 SaveWidget.prototype.set_autosaved = function (dirty) {
160 160 if (dirty) {
161 161 this.set_save_status("(unsaved changes)");
162 162 } else {
163 163 this.set_save_status("(autosaved)");
164 164 }
165 165 };
166 166
167 167
168 168 IPython.SaveWidget = SaveWidget;
169 169
170 170 return IPython;
171 171
172 172 }(IPython));
173 173
@@ -1,3 +1,18 b''
1 1 #notification_area {
2 2 z-index: 10;
3 3 }
4
5 .indicator_area {
6 color: @navbarLinkColor;
7 padding: 4px 3px;
8 margin: 0px;
9 width: 11px;
10 z-index: 10;
11 text-align: center;
12 }
13
14 #kernel_indicator {
15 // Pull it to the right, outside the container boundary
16 margin-right: -16px;
17 }
18
@@ -1,22 +1,13 b''
1 .notification_widget{
1 .notification_widget {
2 2 color: @navbarLinkColor;
3 3 padding: 1px 12px;
4 4 margin: 2px 4px;
5 5 z-index: 10;
6 6 border: 1px solid #ccc;
7 7 border-radius: @baseBorderRadius;
8 8 background: rgba(240, 240, 240, 0.5);
9 9
10 10 &.span {
11 11 padding-right:2px;
12 12 }
13
14 }
15
16 #indicator_area{
17 color: @navbarLinkColor;
18 padding: 2px 2px;
19 margin: 2px -9px 2px 4px;
20 z-index: 10;
21
22 13 }
@@ -1,603 +1,609 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // Kernel
10 10 //============================================================================
11 11
12 12 /**
13 13 * @module IPython
14 14 * @namespace IPython
15 15 * @submodule Kernel
16 16 */
17 17
18 18 var IPython = (function (IPython) {
19 19 "use strict";
20 20
21 21 var utils = IPython.utils;
22 22
23 23 // Initialization and connection.
24 24 /**
25 25 * A Kernel Class to communicate with the Python kernel
26 26 * @Class Kernel
27 27 */
28 var Kernel = function (base_url) {
28 var Kernel = function (kernel_service_url) {
29 29 this.kernel_id = null;
30 30 this.shell_channel = null;
31 31 this.iopub_channel = null;
32 32 this.stdin_channel = null;
33 this.base_url = base_url;
33 this.kernel_service_url = kernel_service_url;
34 34 this.running = false;
35 35 this.username = "username";
36 36 this.session_id = utils.uuid();
37 37 this._msg_callbacks = {};
38 38
39 39 if (typeof(WebSocket) !== 'undefined') {
40 40 this.WebSocket = WebSocket;
41 41 } else if (typeof(MozWebSocket) !== 'undefined') {
42 42 this.WebSocket = MozWebSocket;
43 43 } else {
44 44 alert('Your browser does not have WebSocket support, please try Chrome, Safari or Firefox ≥ 6. Firefox 4 and 5 are also supported by you have to enable WebSockets in about:config.');
45 45 }
46 46
47 47 this.bind_events();
48 48 this.init_iopub_handlers();
49 49 this.comm_manager = new IPython.CommManager(this);
50 50 this.widget_manager = new IPython.WidgetManager(this.comm_manager);
51 51 };
52 52
53 53
54 54 Kernel.prototype._get_msg = function (msg_type, content, metadata) {
55 55 var msg = {
56 56 header : {
57 57 msg_id : utils.uuid(),
58 58 username : this.username,
59 59 session : this.session_id,
60 60 msg_type : msg_type
61 61 },
62 62 metadata : metadata || {},
63 63 content : content,
64 64 parent_header : {}
65 65 };
66 66 return msg;
67 67 };
68 68
69 69 Kernel.prototype.bind_events = function () {
70 70 var that = this;
71 71 $([IPython.events]).on('send_input_reply.Kernel', function(evt, data) {
72 72 that.send_input_reply(data);
73 73 });
74 74 };
75 75
76 76 // Initialize the iopub handlers
77 77
78 78 Kernel.prototype.init_iopub_handlers = function () {
79 79 var output_types = ['stream', 'display_data', 'pyout', 'pyerr'];
80 80 this._iopub_handlers = {};
81 81 this.register_iopub_handler('status', $.proxy(this._handle_status_message, this));
82 82 this.register_iopub_handler('clear_output', $.proxy(this._handle_clear_output, this));
83 83
84 84 for (var i=0; i < output_types.length; i++) {
85 85 this.register_iopub_handler(output_types[i], $.proxy(this._handle_output_message, this));
86 86 }
87 87 };
88 88
89 89 /**
90 90 * Start the Python kernel
91 91 * @method start
92 92 */
93 93 Kernel.prototype.start = function (params) {
94 94 params = params || {};
95 95 if (!this.running) {
96 96 var qs = $.param(params);
97 var url = this.base_url + '?' + qs;
98 $.post(url,
97 $.post(utils.url_join_encode(this.kernel_service_url) + '?' + qs,
99 98 $.proxy(this._kernel_started, this),
100 99 'json'
101 100 );
102 101 }
103 102 };
104 103
105 104 /**
106 105 * Restart the python kernel.
107 106 *
108 107 * Emit a 'status_restarting.Kernel' event with
109 108 * the current object as parameter
110 109 *
111 110 * @method restart
112 111 */
113 112 Kernel.prototype.restart = function () {
114 113 $([IPython.events]).trigger('status_restarting.Kernel', {kernel: this});
115 114 if (this.running) {
116 115 this.stop_channels();
117 var url = utils.url_join_encode(this.kernel_url, "restart");
118 $.post(url,
116 $.post(utils.url_join_encode(this.kernel_url, "restart"),
119 117 $.proxy(this._kernel_started, this),
120 118 'json'
121 119 );
122 120 }
123 121 };
124 122
125 123
126 124 Kernel.prototype._kernel_started = function (json) {
127 125 console.log("Kernel started: ", json.id);
128 126 this.running = true;
129 127 this.kernel_id = json.id;
130 128 var ws_url = json.ws_url;
131 129 if (ws_url.match(/wss?:\/\//) === null) {
132 130 // trailing 's' in https will become wss for secure web sockets
133 131 var prot = location.protocol.replace('http', 'ws') + "//";
134 132 ws_url = prot + location.host + ws_url;
135 133 }
136 this.ws_url = ws_url;
137 this.kernel_url = utils.url_join_encode(this.base_url, this.kernel_id);
134 var parsed = utils.parse_url(ws_url);
135 this.ws_host = parsed.protocol + "//" + parsed.host;
136 this.kernel_url = utils.url_path_join(this.kernel_service_url, this.kernel_id);
137 this.ws_url = utils.url_path_join(parsed.pathname, this.kernel_url);
138 138 this.start_channels();
139 139 };
140 140
141 141
142 142 Kernel.prototype._websocket_closed = function(ws_url, early) {
143 143 this.stop_channels();
144 144 $([IPython.events]).trigger('websocket_closed.Kernel',
145 145 {ws_url: ws_url, kernel: this, early: early}
146 146 );
147 147 };
148 148
149 149 /**
150 150 * Start the `shell`and `iopub` channels.
151 151 * Will stop and restart them if they already exist.
152 152 *
153 153 * @method start_channels
154 154 */
155 155 Kernel.prototype.start_channels = function () {
156 156 var that = this;
157 157 this.stop_channels();
158 var ws_url = this.ws_url + this.kernel_url;
159 console.log("Starting WebSockets:", ws_url);
160 this.shell_channel = new this.WebSocket(ws_url + "/shell");
161 this.stdin_channel = new this.WebSocket(ws_url + "/stdin");
162 this.iopub_channel = new this.WebSocket(ws_url + "/iopub");
158 console.log("Starting WebSockets:", this.ws_host + this.ws_url);
159 this.shell_channel = new this.WebSocket(
160 this.ws_host + utils.url_join_encode(this.ws_url, "shell")
161 );
162 this.stdin_channel = new this.WebSocket(
163 this.ws_host + utils.url_join_encode(this.ws_url, "stdin")
164 );
165 this.iopub_channel = new this.WebSocket(
166 this.ws_host + utils.url_join_encode(this.ws_url, "iopub")
167 );
163 168
169 var ws_host_url = this.ws_host + this.ws_url;
164 170 var already_called_onclose = false; // only alert once
165 171 var ws_closed_early = function(evt){
166 172 if (already_called_onclose){
167 173 return;
168 174 }
169 175 already_called_onclose = true;
170 176 if ( ! evt.wasClean ){
171 that._websocket_closed(ws_url, true);
177 that._websocket_closed(ws_host_url, true);
172 178 }
173 179 };
174 180 var ws_closed_late = function(evt){
175 181 if (already_called_onclose){
176 182 return;
177 183 }
178 184 already_called_onclose = true;
179 185 if ( ! evt.wasClean ){
180 that._websocket_closed(ws_url, false);
186 that._websocket_closed(ws_host_url, false);
181 187 }
182 188 };
183 189 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
184 190 for (var i=0; i < channels.length; i++) {
185 191 channels[i].onopen = $.proxy(this._ws_opened, this);
186 192 channels[i].onclose = ws_closed_early;
187 193 }
188 194 // switch from early-close to late-close message after 1s
189 195 setTimeout(function() {
190 196 for (var i=0; i < channels.length; i++) {
191 197 if (channels[i] !== null) {
192 198 channels[i].onclose = ws_closed_late;
193 199 }
194 200 }
195 201 }, 1000);
196 202 this.shell_channel.onmessage = $.proxy(this._handle_shell_reply, this);
197 203 this.iopub_channel.onmessage = $.proxy(this._handle_iopub_message, this);
198 204 this.stdin_channel.onmessage = $.proxy(this._handle_input_request, this);
199 205 };
200 206
201 207 /**
202 208 * Handle a websocket entering the open state
203 209 * sends session and cookie authentication info as first message.
204 210 * Once all sockets are open, signal the Kernel.status_started event.
205 211 * @method _ws_opened
206 212 */
207 213 Kernel.prototype._ws_opened = function (evt) {
208 214 // send the session id so the Session object Python-side
209 215 // has the same identity
210 216 evt.target.send(this.session_id + ':' + document.cookie);
211 217
212 218 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
213 219 for (var i=0; i < channels.length; i++) {
214 220 // if any channel is not ready, don't trigger event.
215 221 if ( !channels[i].readyState ) return;
216 222 }
217 223 // all events ready, trigger started event.
218 224 $([IPython.events]).trigger('status_started.Kernel', {kernel: this});
219 225 };
220 226
221 227 /**
222 228 * Stop the websocket channels.
223 229 * @method stop_channels
224 230 */
225 231 Kernel.prototype.stop_channels = function () {
226 232 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
227 233 for (var i=0; i < channels.length; i++) {
228 234 if ( channels[i] !== null ) {
229 235 channels[i].onclose = null;
230 236 channels[i].close();
231 237 }
232 238 }
233 239 this.shell_channel = this.iopub_channel = this.stdin_channel = null;
234 240 };
235 241
236 242 // Main public methods.
237 243
238 244 // send a message on the Kernel's shell channel
239 245 Kernel.prototype.send_shell_message = function (msg_type, content, callbacks, metadata) {
240 246 var msg = this._get_msg(msg_type, content, metadata);
241 247 this.shell_channel.send(JSON.stringify(msg));
242 248 this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
243 249 return msg.header.msg_id;
244 250 };
245 251
246 252 /**
247 253 * Get kernel info
248 254 *
249 255 * @param callback {function}
250 256 * @method object_info
251 257 *
252 258 * When calling this method, pass a callback function that expects one argument.
253 259 * The callback will be passed the complete `kernel_info_reply` message documented
254 260 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#kernel-info)
255 261 */
256 262 Kernel.prototype.kernel_info = function (callback) {
257 263 var callbacks;
258 264 if (callback) {
259 265 callbacks = { shell : { reply : callback } };
260 266 }
261 267 return this.send_shell_message("kernel_info_request", {}, callbacks);
262 268 };
263 269
264 270 /**
265 271 * Get info on an object
266 272 *
267 273 * @param objname {string}
268 274 * @param callback {function}
269 275 * @method object_info
270 276 *
271 277 * When calling this method, pass a callback function that expects one argument.
272 278 * The callback will be passed the complete `object_info_reply` message documented
273 279 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#object-information)
274 280 */
275 281 Kernel.prototype.object_info = function (objname, callback) {
276 282 var callbacks;
277 283 if (callback) {
278 284 callbacks = { shell : { reply : callback } };
279 285 }
280 286
281 287 if (typeof(objname) !== null && objname !== null) {
282 288 var content = {
283 289 oname : objname.toString(),
284 290 detail_level : 0,
285 291 };
286 292 return this.send_shell_message("object_info_request", content, callbacks);
287 293 }
288 294 return;
289 295 };
290 296
291 297 /**
292 298 * Execute given code into kernel, and pass result to callback.
293 299 *
294 300 * @async
295 301 * @method execute
296 302 * @param {string} code
297 303 * @param [callbacks] {Object} With the following keys (all optional)
298 304 * @param callbacks.shell.reply {function}
299 305 * @param callbacks.shell.payload.[payload_name] {function}
300 306 * @param callbacks.iopub.output {function}
301 307 * @param callbacks.iopub.clear_output {function}
302 308 * @param callbacks.input {function}
303 309 * @param {object} [options]
304 310 * @param [options.silent=false] {Boolean}
305 311 * @param [options.user_expressions=empty_dict] {Dict}
306 312 * @param [options.user_variables=empty_list] {List od Strings}
307 313 * @param [options.allow_stdin=false] {Boolean} true|false
308 314 *
309 315 * @example
310 316 *
311 317 * The options object should contain the options for the execute call. Its default
312 318 * values are:
313 319 *
314 320 * options = {
315 321 * silent : true,
316 322 * user_variables : [],
317 323 * user_expressions : {},
318 324 * allow_stdin : false
319 325 * }
320 326 *
321 327 * When calling this method pass a callbacks structure of the form:
322 328 *
323 329 * callbacks = {
324 330 * shell : {
325 331 * reply : execute_reply_callback,
326 332 * payload : {
327 333 * set_next_input : set_next_input_callback,
328 334 * }
329 335 * },
330 336 * iopub : {
331 337 * output : output_callback,
332 338 * clear_output : clear_output_callback,
333 339 * },
334 340 * input : raw_input_callback
335 341 * }
336 342 *
337 343 * Each callback will be passed the entire message as a single arugment.
338 344 * Payload handlers will be passed the corresponding payload and the execute_reply message.
339 345 */
340 346 Kernel.prototype.execute = function (code, callbacks, options) {
341 347
342 348 var content = {
343 349 code : code,
344 350 silent : true,
345 351 store_history : false,
346 352 user_variables : [],
347 353 user_expressions : {},
348 354 allow_stdin : false
349 355 };
350 356 callbacks = callbacks || {};
351 357 if (callbacks.input !== undefined) {
352 358 content.allow_stdin = true;
353 359 }
354 360 $.extend(true, content, options);
355 361 $([IPython.events]).trigger('execution_request.Kernel', {kernel: this, content:content});
356 362 return this.send_shell_message("execute_request", content, callbacks);
357 363 };
358 364
359 365 /**
360 366 * When calling this method, pass a function to be called with the `complete_reply` message
361 367 * as its only argument when it arrives.
362 368 *
363 369 * `complete_reply` is documented
364 370 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#complete)
365 371 *
366 372 * @method complete
367 373 * @param line {integer}
368 374 * @param cursor_pos {integer}
369 375 * @param callback {function}
370 376 *
371 377 */
372 378 Kernel.prototype.complete = function (line, cursor_pos, callback) {
373 379 var callbacks;
374 380 if (callback) {
375 381 callbacks = { shell : { reply : callback } };
376 382 }
377 383 var content = {
378 384 text : '',
379 385 line : line,
380 386 block : null,
381 387 cursor_pos : cursor_pos
382 388 };
383 389 return this.send_shell_message("complete_request", content, callbacks);
384 390 };
385 391
386 392
387 393 Kernel.prototype.interrupt = function () {
388 394 if (this.running) {
389 395 $([IPython.events]).trigger('status_interrupting.Kernel', {kernel: this});
390 $.post(this.kernel_url + "/interrupt");
396 $.post(utils.url_join_encode(this.kernel_url, "interrupt"));
391 397 }
392 398 };
393 399
394 400
395 401 Kernel.prototype.kill = function () {
396 402 if (this.running) {
397 403 this.running = false;
398 404 var settings = {
399 405 cache : false,
400 406 type : "DELETE"
401 407 };
402 $.ajax(this.kernel_url, settings);
408 $.ajax(utils.url_join_encode(this.kernel_url), settings);
403 409 }
404 410 };
405 411
406 412 Kernel.prototype.send_input_reply = function (input) {
407 413 var content = {
408 414 value : input,
409 415 };
410 416 $([IPython.events]).trigger('input_reply.Kernel', {kernel: this, content:content});
411 417 var msg = this._get_msg("input_reply", content);
412 418 this.stdin_channel.send(JSON.stringify(msg));
413 419 return msg.header.msg_id;
414 420 };
415 421
416 422
417 423 // Reply handlers
418 424
419 425 Kernel.prototype.register_iopub_handler = function (msg_type, callback) {
420 426 this._iopub_handlers[msg_type] = callback;
421 427 };
422 428
423 429 Kernel.prototype.get_iopub_handler = function (msg_type) {
424 430 // get iopub handler for a specific message type
425 431 return this._iopub_handlers[msg_type];
426 432 };
427 433
428 434
429 435 Kernel.prototype.get_callbacks_for_msg = function (msg_id) {
430 436 // get callbacks for a specific message
431 437 return this._msg_callbacks[msg_id];
432 438 };
433 439
434 440
435 441 Kernel.prototype.clear_callbacks_for_msg = function (msg_id) {
436 442 if (this._msg_callbacks[msg_id] !== undefined ) {
437 443 delete this._msg_callbacks[msg_id];
438 444 }
439 445 };
440 446
441 447 /* Set callbacks for a particular message.
442 448 * Callbacks should be a struct of the following form:
443 449 * shell : {
444 450 *
445 451 * }
446 452
447 453 */
448 454 Kernel.prototype.set_callbacks_for_msg = function (msg_id, callbacks) {
449 455 if (callbacks) {
450 456 // shallow-copy mapping, because we will modify it at the top level
451 457 var cbcopy = this._msg_callbacks[msg_id] = {};
452 458 cbcopy.shell = callbacks.shell;
453 459 cbcopy.iopub = callbacks.iopub;
454 460 cbcopy.input = callbacks.input;
455 461 this._msg_callbacks[msg_id] = cbcopy;
456 462 }
457 463 };
458 464
459 465
460 466 Kernel.prototype._handle_shell_reply = function (e) {
461 467 var reply = $.parseJSON(e.data);
462 468 $([IPython.events]).trigger('shell_reply.Kernel', {kernel: this, reply:reply});
463 469 var content = reply.content;
464 470 var metadata = reply.metadata;
465 471 var parent_id = reply.parent_header.msg_id;
466 472 var callbacks = this.get_callbacks_for_msg(parent_id);
467 473 if (!callbacks || !callbacks.shell) {
468 474 return;
469 475 }
470 476 var shell_callbacks = callbacks.shell;
471 477
472 478 // clear callbacks on shell
473 479 delete callbacks.shell;
474 480 delete callbacks.input;
475 481 if (!callbacks.iopub) {
476 482 this.clear_callbacks_for_msg(parent_id);
477 483 }
478 484
479 485 if (shell_callbacks.reply !== undefined) {
480 486 shell_callbacks.reply(reply);
481 487 }
482 488 if (content.payload && shell_callbacks.payload) {
483 489 this._handle_payloads(content.payload, shell_callbacks.payload, reply);
484 490 }
485 491 };
486 492
487 493
488 494 Kernel.prototype._handle_payloads = function (payloads, payload_callbacks, msg) {
489 495 var l = payloads.length;
490 496 // Payloads are handled by triggering events because we don't want the Kernel
491 497 // to depend on the Notebook or Pager classes.
492 498 for (var i=0; i<l; i++) {
493 499 var payload = payloads[i];
494 500 var callback = payload_callbacks[payload.source];
495 501 if (callback) {
496 502 callback(payload, msg);
497 503 }
498 504 }
499 505 };
500 506
501 507 Kernel.prototype._handle_status_message = function (msg) {
502 508 var execution_state = msg.content.execution_state;
503 509 var parent_id = msg.parent_header.msg_id;
504 510
505 511 // dispatch status msg callbacks, if any
506 512 var callbacks = this.get_callbacks_for_msg(parent_id);
507 513 if (callbacks && callbacks.iopub && callbacks.iopub.status) {
508 514 try {
509 515 callbacks.iopub.status(msg);
510 516 } catch (e) {
511 517 console.log("Exception in status msg handler", e, e.stack);
512 518 }
513 519 }
514 520
515 521 if (execution_state === 'busy') {
516 522 $([IPython.events]).trigger('status_busy.Kernel', {kernel: this});
517 523 } else if (execution_state === 'idle') {
518 524 // clear callbacks on idle, there can be no more
519 525 if (callbacks !== undefined) {
520 526 delete callbacks.iopub;
521 527 delete callbacks.input;
522 528 if (!callbacks.shell) {
523 529 this.clear_callbacks_for_msg(parent_id);
524 530 }
525 531 }
526 532 // trigger status_idle event
527 533 $([IPython.events]).trigger('status_idle.Kernel', {kernel: this});
528 534 } else if (execution_state === 'restarting') {
529 535 // autorestarting is distinct from restarting,
530 536 // in that it means the kernel died and the server is restarting it.
531 537 // status_restarting sets the notification widget,
532 538 // autorestart shows the more prominent dialog.
533 539 $([IPython.events]).trigger('status_autorestarting.Kernel', {kernel: this});
534 540 $([IPython.events]).trigger('status_restarting.Kernel', {kernel: this});
535 541 } else if (execution_state === 'dead') {
536 542 this.stop_channels();
537 543 $([IPython.events]).trigger('status_dead.Kernel', {kernel: this});
538 544 }
539 545 };
540 546
541 547
542 548 // handle clear_output message
543 549 Kernel.prototype._handle_clear_output = function (msg) {
544 550 var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
545 551 if (!callbacks || !callbacks.iopub) {
546 552 return;
547 553 }
548 554 var callback = callbacks.iopub.clear_output;
549 555 if (callback) {
550 556 callback(msg);
551 557 }
552 558 };
553 559
554 560
555 561 // handle an output message (pyout, display_data, etc.)
556 562 Kernel.prototype._handle_output_message = function (msg) {
557 563 var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
558 564 if (!callbacks || !callbacks.iopub) {
559 565 return;
560 566 }
561 567 var callback = callbacks.iopub.output;
562 568 if (callback) {
563 569 callback(msg);
564 570 }
565 571 };
566 572
567 573 // dispatch IOPub messages to respective handlers.
568 574 // each message type should have a handler.
569 575 Kernel.prototype._handle_iopub_message = function (e) {
570 576 var msg = $.parseJSON(e.data);
571 577
572 578 var handler = this.get_iopub_handler(msg.header.msg_type);
573 579 if (handler !== undefined) {
574 580 handler(msg);
575 581 }
576 582 };
577 583
578 584
579 585 Kernel.prototype._handle_input_request = function (e) {
580 586 var request = $.parseJSON(e.data);
581 587 var header = request.header;
582 588 var content = request.content;
583 589 var metadata = request.metadata;
584 590 var msg_type = header.msg_type;
585 591 if (msg_type !== 'input_request') {
586 592 console.log("Invalid input request!", request);
587 593 return;
588 594 }
589 595 var callbacks = this.get_callbacks_for_msg(request.parent_header.msg_id);
590 596 if (callbacks) {
591 597 if (callbacks.input) {
592 598 callbacks.input(request);
593 599 }
594 600 }
595 601 };
596 602
597 603
598 604 IPython.Kernel = Kernel;
599 605
600 606 return IPython;
601 607
602 608 }(IPython));
603 609
@@ -1,118 +1,119 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2013 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // Notebook
10 10 //============================================================================
11 11
12 12 var IPython = (function (IPython) {
13 13 "use strict";
14 14
15 15 var utils = IPython.utils;
16 16
17 var Session = function(notebook_name, notebook_path, notebook){
17 var Session = function(notebook, options){
18 18 this.kernel = null;
19 19 this.id = null;
20 this.name = notebook_name;
21 this.path = notebook_path;
22 20 this.notebook = notebook;
23 this._baseProjectUrl = notebook.baseProjectUrl();
21 this.name = notebook.notebook_name;
22 this.path = notebook.notebook_path;
23 this.base_url = notebook.base_url;
24 this.base_kernel_url = options.base_kernel_url || utils.get_body_data("baseKernelUrl");
24 25 };
25 26
26 27 Session.prototype.start = function(callback) {
27 28 var that = this;
28 29 var model = {
29 30 notebook : {
30 31 name : this.name,
31 32 path : this.path
32 33 }
33 34 };
34 35 var settings = {
35 36 processData : false,
36 37 cache : false,
37 38 type : "POST",
38 39 data: JSON.stringify(model),
39 40 dataType : "json",
40 41 success : function (data, status, xhr) {
41 42 that._handle_start_success(data);
42 43 if (callback) {
43 44 callback(data, status, xhr);
44 45 }
45 46 },
46 47 };
47 var url = utils.url_join_encode(this._baseProjectUrl, 'api/sessions');
48 var url = utils.url_join_encode(this.base_url, 'api/sessions');
48 49 $.ajax(url, settings);
49 50 };
50 51
51 52 Session.prototype.rename_notebook = function (name, path) {
52 53 this.name = name;
53 54 this.path = path;
54 55 var model = {
55 56 notebook : {
56 57 name : this.name,
57 58 path : this.path
58 59 }
59 60 };
60 61 var settings = {
61 62 processData : false,
62 63 cache : false,
63 64 type : "PATCH",
64 65 data: JSON.stringify(model),
65 66 dataType : "json",
66 67 };
67 var url = utils.url_join_encode(this._baseProjectUrl, 'api/sessions', this.id);
68 var url = utils.url_join_encode(this.base_url, 'api/sessions', this.id);
68 69 $.ajax(url, settings);
69 70 };
70 71
71 72 Session.prototype.delete = function() {
72 73 var settings = {
73 74 processData : false,
74 75 cache : false,
75 76 type : "DELETE",
76 77 dataType : "json",
77 78 };
78 79 this.kernel.running = false;
79 var url = utils.url_join_encode(this._baseProjectUrl, 'api/sessions', this.id);
80 var url = utils.url_join_encode(this.base_url, 'api/sessions', this.id);
80 81 $.ajax(url, settings);
81 82 };
82 83
83 84 // Kernel related things
84 85 /**
85 86 * Create the Kernel object associated with this Session.
86 87 *
87 88 * @method _handle_start_success
88 89 */
89 90 Session.prototype._handle_start_success = function (data, status, xhr) {
90 91 this.id = data.id;
91 var base_url = utils.url_path_join($('body').data('baseKernelUrl'), "api/kernels");
92 this.kernel = new IPython.Kernel(base_url);
92 var kernel_service_url = utils.url_path_join(this.base_kernel_url, "api/kernels");
93 this.kernel = new IPython.Kernel(kernel_service_url);
93 94 this.kernel._kernel_started(data.kernel);
94 95 };
95 96
96 97 /**
97 98 * Prompt the user to restart the IPython kernel.
98 99 *
99 100 * @method restart_kernel
100 101 */
101 102 Session.prototype.restart_kernel = function () {
102 103 this.kernel.restart();
103 104 };
104 105
105 106 Session.prototype.interrupt_kernel = function() {
106 107 this.kernel.interrupt();
107 108 };
108 109
109 110
110 111 Session.prototype.kill_kernel = function() {
111 112 this.kernel.kill();
112 113 };
113 114
114 115 IPython.Session = Session;
115 116
116 117 return IPython;
117 118
118 119 }(IPython));
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from IPython/html/tests/casperjs/README.md to IPython/html/tests/README.md
1 NO CONTENT: file renamed from IPython/html/tests/casperjs/test_cases/misc_tests.js to IPython/html/tests/base/misc.js
1 NO CONTENT: file renamed from IPython/html/tests/casperjs/test_cases/nb_arrow_keys.js to IPython/html/tests/notebook/arrow_keys.js
1 NO CONTENT: file renamed from IPython/html/tests/casperjs/test_cases/display_image.js to IPython/html/tests/notebook/display_image.js
1 NO CONTENT: file renamed from IPython/html/tests/casperjs/test_cases/empty_nb_arrow_keys.js to IPython/html/tests/notebook/empty_arrow_keys.js
1 NO CONTENT: file renamed from IPython/html/tests/casperjs/test_cases/execute_code_cell.js to IPython/html/tests/notebook/execute_code.js
1 NO CONTENT: file renamed from IPython/html/tests/casperjs/test_cases/inject_js.js to IPython/html/tests/notebook/inject_js.js
1 NO CONTENT: file renamed from IPython/html/tests/casperjs/test_cases/check_interrupt.js to IPython/html/tests/notebook/interrupt.js
1 NO CONTENT: file renamed from IPython/html/tests/casperjs/test_cases/isolated_svg.js to IPython/html/tests/notebook/isolated_svg.js
1 NO CONTENT: file renamed from IPython/html/tests/casperjs/test_cases/render_markdown.js to IPython/html/tests/notebook/markdown.js
1 NO CONTENT: file renamed from IPython/html/tests/casperjs/test_cases/merge_cells.js to IPython/html/tests/notebook/merge_cells.js
1 NO CONTENT: file renamed from IPython/html/tests/casperjs/test_cases/nb_roundtrip.js to IPython/html/tests/notebook/roundtrip.js
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from IPython/html/tests/casperjs/test_cases/safe_append_output.js to IPython/html/tests/notebook/safe_append_output.js
1 NO CONTENT: file renamed from IPython/html/tests/casperjs/test_cases/save_notebook.js to IPython/html/tests/notebook/save.js
1 NO CONTENT: file renamed from IPython/html/tests/casperjs/test_cases/shutdown_notebook.js to IPython/html/tests/notebook/shutdown.js
1 NO CONTENT: file renamed from IPython/html/tests/casperjs/test_cases/tooltip.js to IPython/html/tests/notebook/tooltip.js
1 NO CONTENT: file renamed from IPython/html/tests/casperjs/test_cases/kernel_test.js to IPython/html/tests/services/kernel.js
1 NO CONTENT: file renamed from IPython/html/tests/casperjs/test_cases/dashboard_nav.js to IPython/html/tests/tree/dashboard_nav.js
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from IPython/html/tests/casperjs/util.js to IPython/html/tests/util.js
1 NO CONTENT: file renamed from IPython/html/tests/casperjs/test_cases/widgets.js to IPython/html/tests/widgets/widget.js
1 NO CONTENT: file renamed from IPython/html/tests/casperjs/test_cases/widgets_bool.js to IPython/html/tests/widgets/widget_bool.js
1 NO CONTENT: file renamed from IPython/html/tests/casperjs/test_cases/widgets_button.js to IPython/html/tests/widgets/widget_button.js
1 NO CONTENT: file renamed from IPython/html/tests/casperjs/test_cases/widgets_container.js to IPython/html/tests/widgets/widget_container.js
1 NO CONTENT: file renamed from IPython/html/tests/casperjs/test_cases/widgets_float.js to IPython/html/tests/widgets/widget_float.js
1 NO CONTENT: file renamed from IPython/html/tests/casperjs/test_cases/widgets_image.js to IPython/html/tests/widgets/widget_image.js
1 NO CONTENT: file renamed from IPython/html/tests/casperjs/test_cases/widgets_int.js to IPython/html/tests/widgets/widget_int.js
1 NO CONTENT: file renamed from IPython/html/tests/casperjs/test_cases/widgets_multicontainer.js to IPython/html/tests/widgets/widget_multicontainer.js
1 NO CONTENT: file renamed from IPython/html/tests/casperjs/test_cases/widgets_selection.js to IPython/html/tests/widgets/widget_selection.js
1 NO CONTENT: file renamed from IPython/html/tests/casperjs/test_cases/widgets_string.js to IPython/html/tests/widgets/widget_string.js
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from examples/parallel/parallel_pylab.ipy to examples/parallel/plotting/parallel_plot.ipy
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from examples/tests/pylab_figshow.py to examples/tests/inline_figshow.py
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now