##// END OF EJS Templates
Fix style and typo
Pablo de Oliveira -
Show More
@@ -1,708 +1,708 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
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 raw:
114 114 for obj in objs:
115 115 publish_display_data('display', obj, metadata)
116 116 else:
117 117 format = InteractiveShell.instance().display_formatter.format
118 118 for obj in objs:
119 119 format_dict, md_dict = format(obj, include=include, exclude=exclude)
120 120 if metadata:
121 121 # kwarg-specified metadata gets precedence
122 122 _merge(md_dict, metadata)
123 123 publish_display_data('display', format_dict, md_dict)
124 124
125 125
126 126 def display_pretty(*objs, **kwargs):
127 127 """Display the pretty (default) representation of an object.
128 128
129 129 Parameters
130 130 ----------
131 131 objs : tuple of objects
132 132 The Python objects to display, or if raw=True raw text data to
133 133 display.
134 134 raw : bool
135 135 Are the data objects raw data or Python objects that need to be
136 136 formatted before display? [default: False]
137 137 metadata : dict (optional)
138 138 Metadata to be associated with the specific mimetype output.
139 139 """
140 140 _display_mimetype('text/plain', objs, **kwargs)
141 141
142 142
143 143 def display_html(*objs, **kwargs):
144 144 """Display the HTML representation of an object.
145 145
146 146 Parameters
147 147 ----------
148 148 objs : tuple of objects
149 149 The Python objects to display, or if raw=True raw HTML data to
150 150 display.
151 151 raw : bool
152 152 Are the data objects raw data or Python objects that need to be
153 153 formatted before display? [default: False]
154 154 metadata : dict (optional)
155 155 Metadata to be associated with the specific mimetype output.
156 156 """
157 157 _display_mimetype('text/html', objs, **kwargs)
158 158
159 159
160 160 def display_svg(*objs, **kwargs):
161 161 """Display the SVG representation of an object.
162 162
163 163 Parameters
164 164 ----------
165 165 objs : tuple of objects
166 166 The Python objects to display, or if raw=True raw svg data to
167 167 display.
168 168 raw : bool
169 169 Are the data objects raw data or Python objects that need to be
170 170 formatted before display? [default: False]
171 171 metadata : dict (optional)
172 172 Metadata to be associated with the specific mimetype output.
173 173 """
174 174 _display_mimetype('image/svg+xml', objs, **kwargs)
175 175
176 176
177 177 def display_png(*objs, **kwargs):
178 178 """Display the PNG representation of an object.
179 179
180 180 Parameters
181 181 ----------
182 182 objs : tuple of objects
183 183 The Python objects to display, or if raw=True raw png data to
184 184 display.
185 185 raw : bool
186 186 Are the data objects raw data or Python objects that need to be
187 187 formatted before display? [default: False]
188 188 metadata : dict (optional)
189 189 Metadata to be associated with the specific mimetype output.
190 190 """
191 191 _display_mimetype('image/png', objs, **kwargs)
192 192
193 193
194 194 def display_jpeg(*objs, **kwargs):
195 195 """Display the JPEG representation of an object.
196 196
197 197 Parameters
198 198 ----------
199 199 objs : tuple of objects
200 200 The Python objects to display, or if raw=True raw JPEG data to
201 201 display.
202 202 raw : bool
203 203 Are the data objects raw data or Python objects that need to be
204 204 formatted before display? [default: False]
205 205 metadata : dict (optional)
206 206 Metadata to be associated with the specific mimetype output.
207 207 """
208 208 _display_mimetype('image/jpeg', objs, **kwargs)
209 209
210 210
211 211 def display_latex(*objs, **kwargs):
212 212 """Display the LaTeX representation of an object.
213 213
214 214 Parameters
215 215 ----------
216 216 objs : tuple of objects
217 217 The Python objects to display, or if raw=True raw latex data to
218 218 display.
219 219 raw : bool
220 220 Are the data objects raw data or Python objects that need to be
221 221 formatted before display? [default: False]
222 222 metadata : dict (optional)
223 223 Metadata to be associated with the specific mimetype output.
224 224 """
225 225 _display_mimetype('text/latex', objs, **kwargs)
226 226
227 227
228 228 def display_json(*objs, **kwargs):
229 229 """Display the JSON representation of an object.
230 230
231 231 Note that not many frontends support displaying JSON.
232 232
233 233 Parameters
234 234 ----------
235 235 objs : tuple of objects
236 236 The Python objects to display, or if raw=True raw json data to
237 237 display.
238 238 raw : bool
239 239 Are the data objects raw data or Python objects that need to be
240 240 formatted before display? [default: False]
241 241 metadata : dict (optional)
242 242 Metadata to be associated with the specific mimetype output.
243 243 """
244 244 _display_mimetype('application/json', objs, **kwargs)
245 245
246 246
247 247 def display_javascript(*objs, **kwargs):
248 248 """Display the Javascript representation of an object.
249 249
250 250 Parameters
251 251 ----------
252 252 objs : tuple of objects
253 253 The Python objects to display, or if raw=True raw javascript data to
254 254 display.
255 255 raw : bool
256 256 Are the data objects raw data or Python objects that need to be
257 257 formatted before display? [default: False]
258 258 metadata : dict (optional)
259 259 Metadata to be associated with the specific mimetype output.
260 260 """
261 261 _display_mimetype('application/javascript', objs, **kwargs)
262 262
263 263 #-----------------------------------------------------------------------------
264 264 # Smart classes
265 265 #-----------------------------------------------------------------------------
266 266
267 267
268 268 class DisplayObject(object):
269 269 """An object that wraps data to be displayed."""
270 270
271 271 _read_flags = 'r'
272 272
273 273 def __init__(self, data=None, url=None, filename=None):
274 274 """Create a display object given raw data.
275 275
276 276 When this object is returned by an expression or passed to the
277 277 display function, it will result in the data being displayed
278 278 in the frontend. The MIME type of the data should match the
279 279 subclasses used, so the Png subclass should be used for 'image/png'
280 280 data. If the data is a URL, the data will first be downloaded
281 281 and then displayed. If
282 282
283 283 Parameters
284 284 ----------
285 285 data : unicode, str or bytes
286 286 The raw data or a URL or file to load the data from
287 287 url : unicode
288 288 A URL to download the data from.
289 289 filename : unicode
290 290 Path to a local file to load the data from.
291 291 """
292 292 if data is not None and isinstance(data, string_types):
293 293 if data.startswith('http') and url is None:
294 294 url = data
295 295 filename = None
296 296 data = None
297 297 elif _safe_exists(data) and filename is None:
298 298 url = None
299 299 filename = data
300 300 data = None
301 301
302 302 self.data = data
303 303 self.url = url
304 304 self.filename = None if filename is None else unicode_type(filename)
305 305
306 306 self.reload()
307 307
308 308 def reload(self):
309 309 """Reload the raw data from file or URL."""
310 310 if self.filename is not None:
311 311 with open(self.filename, self._read_flags) as f:
312 312 self.data = f.read()
313 313 elif self.url is not None:
314 314 try:
315 315 import urllib2
316 316 response = urllib2.urlopen(self.url)
317 317 self.data = response.read()
318 318 # extract encoding from header, if there is one:
319 319 encoding = None
320 320 for sub in response.headers['content-type'].split(';'):
321 321 sub = sub.strip()
322 322 if sub.startswith('charset'):
323 323 encoding = sub.split('=')[-1].strip()
324 324 break
325 325 # decode data, if an encoding was specified
326 326 if encoding:
327 327 self.data = self.data.decode(encoding, 'replace')
328 328 except:
329 329 self.data = None
330 330
331 331 class Pretty(DisplayObject):
332 332
333 333 def _repr_pretty_(self):
334 334 return self.data
335 335
336 336
337 337 class HTML(DisplayObject):
338 338
339 339 def _repr_html_(self):
340 340 return self.data
341 341
342 342 def __html__(self):
343 343 """
344 344 This method exists to inform other HTML-using modules (e.g. Markupsafe,
345 345 htmltag, etc) that this object is HTML and does not need things like
346 346 special characters (<>&) escaped.
347 347 """
348 348 return self._repr_html_()
349 349
350 350
351 351 class Math(DisplayObject):
352 352
353 353 def _repr_latex_(self):
354 354 s = self.data.strip('$')
355 355 return "$$%s$$" % s
356 356
357 357
358 358 class Latex(DisplayObject):
359 359
360 360 def _repr_latex_(self):
361 361 return self.data
362 362
363 363
364 364 class SVG(DisplayObject):
365 365
366 366 def __init__(self, data=None, url=None, filename=None, scoped=False):
367 367 """Create a SVG display object given raw data.
368 368
369 369 When this object is returned by an expression or passed to the
370 370 display function, it will result in the data being displayed
371 371 in the frontend. If the data is a URL, the data will first be
372 372 downloaded and then displayed.
373 373
374 374 Parameters
375 375 ----------
376 376 data : unicode, str or bytes
377 377 The Javascript source code or a URL to download it from.
378 378 url : unicode
379 379 A URL to download the data from.
380 380 filename : unicode
381 381 Path to a local file to load the data from.
382 382 scoped : bool
383 383 Should the SVG declarations be scoped.
384 384 """
385 385 if not isinstance(scoped, (bool)):
386 386 raise TypeError('expected bool, got: %r' % scoped)
387 387 self.scoped = scoped
388 388 super(SVG, self).__init__(data=data, url=url, filename=filename)
389 389
390 390 # wrap data in a property, which extracts the <svg> tag, discarding
391 391 # document headers
392 392 _data = None
393 393
394 394 @property
395 395 def data(self):
396 396 return self._data
397 397
398 398 _scoped_class = "ipython-scoped"
399 399
400 400 @data.setter
401 401 def data(self, svg):
402 402 if svg is None:
403 403 self._data = None
404 404 return
405 405 # parse into dom object
406 406 from xml.dom import minidom
407 407 svg = cast_bytes_py2(svg)
408 408 x = minidom.parseString(svg)
409 409 # get svg tag (should be 1)
410 410 found_svg = x.getElementsByTagName('svg')
411 411 if found_svg:
412 # If the user request scoping, tag the svg with the
412 # If the user requests scoping, tag the svg with the
413 413 # ipython-scoped class
414 414 if self.scoped:
415 415 classes = (found_svg[0].getAttribute('class') +
416 416 " " + self._scoped_class)
417 417 found_svg[0].setAttribute('class', classes)
418 418 svg = found_svg[0].toxml()
419 419 else:
420 420 # fallback on the input, trust the user
421 421 # but this is probably an error.
422 422 pass
423 423 svg = cast_unicode(svg)
424 424 self._data = svg
425 425
426 426 def _repr_svg_(self):
427 427 return self.data
428 428
429 429
430 430 class JSON(DisplayObject):
431 431
432 432 def _repr_json_(self):
433 433 return self.data
434 434
435 435 css_t = """$("head").append($("<link/>").attr({
436 436 rel: "stylesheet",
437 437 type: "text/css",
438 438 href: "%s"
439 439 }));
440 440 """
441 441
442 442 lib_t1 = """$.getScript("%s", function () {
443 443 """
444 444 lib_t2 = """});
445 445 """
446 446
447 447 class Javascript(DisplayObject):
448 448
449 449 def __init__(self, data=None, url=None, filename=None, lib=None, css=None):
450 450 """Create a Javascript display object given raw data.
451 451
452 452 When this object is returned by an expression or passed to the
453 453 display function, it will result in the data being displayed
454 454 in the frontend. If the data is a URL, the data will first be
455 455 downloaded and then displayed.
456 456
457 457 In the Notebook, the containing element will be available as `element`,
458 458 and jQuery will be available. The output area starts hidden, so if
459 459 the js appends content to `element` that should be visible, then
460 460 it must call `container.show()` to unhide the area.
461 461
462 462 Parameters
463 463 ----------
464 464 data : unicode, str or bytes
465 465 The Javascript source code or a URL to download it from.
466 466 url : unicode
467 467 A URL to download the data from.
468 468 filename : unicode
469 469 Path to a local file to load the data from.
470 470 lib : list or str
471 471 A sequence of Javascript library URLs to load asynchronously before
472 472 running the source code. The full URLs of the libraries should
473 473 be given. A single Javascript library URL can also be given as a
474 474 string.
475 475 css: : list or str
476 476 A sequence of css files to load before running the source code.
477 477 The full URLs of the css files should be given. A single css URL
478 478 can also be given as a string.
479 479 """
480 480 if isinstance(lib, string_types):
481 481 lib = [lib]
482 482 elif lib is None:
483 483 lib = []
484 484 if isinstance(css, string_types):
485 485 css = [css]
486 486 elif css is None:
487 487 css = []
488 488 if not isinstance(lib, (list,tuple)):
489 489 raise TypeError('expected sequence, got: %r' % lib)
490 490 if not isinstance(css, (list,tuple)):
491 491 raise TypeError('expected sequence, got: %r' % css)
492 492 self.lib = lib
493 493 self.css = css
494 494 super(Javascript, self).__init__(data=data, url=url, filename=filename)
495 495
496 496 def _repr_javascript_(self):
497 497 r = ''
498 498 for c in self.css:
499 499 r += css_t % c
500 500 for l in self.lib:
501 501 r += lib_t1 % l
502 502 r += self.data
503 503 r += lib_t2*len(self.lib)
504 504 return r
505 505
506 506 # constants for identifying png/jpeg data
507 507 _PNG = b'\x89PNG\r\n\x1a\n'
508 508 _JPEG = b'\xff\xd8'
509 509
510 510 def _pngxy(data):
511 511 """read the (width, height) from a PNG header"""
512 512 ihdr = data.index(b'IHDR')
513 513 # next 8 bytes are width/height
514 514 w4h4 = data[ihdr+4:ihdr+12]
515 515 return struct.unpack('>ii', w4h4)
516 516
517 517 def _jpegxy(data):
518 518 """read the (width, height) from a JPEG header"""
519 519 # adapted from http://www.64lines.com/jpeg-width-height
520 520
521 521 idx = 4
522 522 while True:
523 523 block_size = struct.unpack('>H', data[idx:idx+2])[0]
524 524 idx = idx + block_size
525 525 if data[idx:idx+2] == b'\xFF\xC0':
526 526 # found Start of Frame
527 527 iSOF = idx
528 528 break
529 529 else:
530 530 # read another block
531 531 idx += 2
532 532
533 533 h, w = struct.unpack('>HH', data[iSOF+5:iSOF+9])
534 534 return w, h
535 535
536 536 class Image(DisplayObject):
537 537
538 538 _read_flags = 'rb'
539 539 _FMT_JPEG = u'jpeg'
540 540 _FMT_PNG = u'png'
541 541 _ACCEPTABLE_EMBEDDINGS = [_FMT_JPEG, _FMT_PNG]
542 542
543 543 def __init__(self, data=None, url=None, filename=None, format=u'png', embed=None, width=None, height=None, retina=False):
544 544 """Create a PNG/JPEG image object given raw data.
545 545
546 546 When this object is returned by an input cell or passed to the
547 547 display function, it will result in the image being displayed
548 548 in the frontend.
549 549
550 550 Parameters
551 551 ----------
552 552 data : unicode, str or bytes
553 553 The raw image data or a URL or filename to load the data from.
554 554 This always results in embedded image data.
555 555 url : unicode
556 556 A URL to download the data from. If you specify `url=`,
557 557 the image data will not be embedded unless you also specify `embed=True`.
558 558 filename : unicode
559 559 Path to a local file to load the data from.
560 560 Images from a file are always embedded.
561 561 format : unicode
562 562 The format of the image data (png/jpeg/jpg). If a filename or URL is given
563 563 for format will be inferred from the filename extension.
564 564 embed : bool
565 565 Should the image data be embedded using a data URI (True) or be
566 566 loaded using an <img> tag. Set this to True if you want the image
567 567 to be viewable later with no internet connection in the notebook.
568 568
569 569 Default is `True`, unless the keyword argument `url` is set, then
570 570 default value is `False`.
571 571
572 572 Note that QtConsole is not able to display images if `embed` is set to `False`
573 573 width : int
574 574 Width to which to constrain the image in html
575 575 height : int
576 576 Height to which to constrain the image in html
577 577 retina : bool
578 578 Automatically set the width and height to half of the measured
579 579 width and height.
580 580 This only works for embedded images because it reads the width/height
581 581 from image data.
582 582 For non-embedded images, you can just set the desired display width
583 583 and height directly.
584 584
585 585 Examples
586 586 --------
587 587 # embedded image data, works in qtconsole and notebook
588 588 # when passed positionally, the first arg can be any of raw image data,
589 589 # a URL, or a filename from which to load image data.
590 590 # The result is always embedding image data for inline images.
591 591 Image('http://www.google.fr/images/srpr/logo3w.png')
592 592 Image('/path/to/image.jpg')
593 593 Image(b'RAW_PNG_DATA...')
594 594
595 595 # Specifying Image(url=...) does not embed the image data,
596 596 # it only generates `<img>` tag with a link to the source.
597 597 # This will not work in the qtconsole or offline.
598 598 Image(url='http://www.google.fr/images/srpr/logo3w.png')
599 599
600 600 """
601 601 if filename is not None:
602 602 ext = self._find_ext(filename)
603 603 elif url is not None:
604 604 ext = self._find_ext(url)
605 605 elif data is None:
606 606 raise ValueError("No image data found. Expecting filename, url, or data.")
607 607 elif isinstance(data, string_types) and (
608 608 data.startswith('http') or _safe_exists(data)
609 609 ):
610 610 ext = self._find_ext(data)
611 611 else:
612 612 ext = None
613 613
614 614 if ext is not None:
615 615 format = ext.lower()
616 616 if ext == u'jpg' or ext == u'jpeg':
617 617 format = self._FMT_JPEG
618 618 if ext == u'png':
619 619 format = self._FMT_PNG
620 620 elif isinstance(data, bytes) and format == 'png':
621 621 # infer image type from image data header,
622 622 # only if format might not have been specified.
623 623 if data[:2] == _JPEG:
624 624 format = 'jpeg'
625 625
626 626 self.format = unicode_type(format).lower()
627 627 self.embed = embed if embed is not None else (url is None)
628 628
629 629 if self.embed and self.format not in self._ACCEPTABLE_EMBEDDINGS:
630 630 raise ValueError("Cannot embed the '%s' image format" % (self.format))
631 631 self.width = width
632 632 self.height = height
633 633 self.retina = retina
634 634 super(Image, self).__init__(data=data, url=url, filename=filename)
635 635
636 636 if retina:
637 637 self._retina_shape()
638 638
639 639 def _retina_shape(self):
640 640 """load pixel-doubled width and height from image data"""
641 641 if not self.embed:
642 642 return
643 643 if self.format == 'png':
644 644 w, h = _pngxy(self.data)
645 645 elif self.format == 'jpeg':
646 646 w, h = _jpegxy(self.data)
647 647 else:
648 648 # retina only supports png
649 649 return
650 650 self.width = w // 2
651 651 self.height = h // 2
652 652
653 653 def reload(self):
654 654 """Reload the raw data from file or URL."""
655 655 if self.embed:
656 656 super(Image,self).reload()
657 657 if self.retina:
658 658 self._retina_shape()
659 659
660 660 def _repr_html_(self):
661 661 if not self.embed:
662 662 width = height = ''
663 663 if self.width:
664 664 width = ' width="%d"' % self.width
665 665 if self.height:
666 666 height = ' height="%d"' % self.height
667 667 return u'<img src="%s"%s%s/>' % (self.url, width, height)
668 668
669 669 def _data_and_metadata(self):
670 670 """shortcut for returning metadata with shape information, if defined"""
671 671 md = {}
672 672 if self.width:
673 673 md['width'] = self.width
674 674 if self.height:
675 675 md['height'] = self.height
676 676 if md:
677 677 return self.data, md
678 678 else:
679 679 return self.data
680 680
681 681 def _repr_png_(self):
682 682 if self.embed and self.format == u'png':
683 683 return self._data_and_metadata()
684 684
685 685 def _repr_jpeg_(self):
686 686 if self.embed and (self.format == u'jpeg' or self.format == u'jpg'):
687 687 return self._data_and_metadata()
688 688
689 689 def _find_ext(self, s):
690 690 return unicode_type(s.split('.')[-1].lower())
691 691
692 692
693 693 def clear_output(wait=False):
694 694 """Clear the output of the current cell receiving output.
695 695
696 696 Parameters
697 697 ----------
698 698 wait : bool [default: false]
699 699 Wait to clear the output until new output is available to replace it."""
700 700 from IPython.core.interactiveshell import InteractiveShell
701 701 if InteractiveShell.initialized():
702 702 InteractiveShell.instance().display_pub.clear_output(wait)
703 703 else:
704 704 from IPython.utils import io
705 705 print('\033[2K\r', file=io.stdout, end='')
706 706 io.stdout.flush()
707 707 print('\033[2K\r', file=io.stderr, end='')
708 708 io.stderr.flush()
@@ -1,712 +1,712 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.clear_queued = null;
35 35 if (prompt_area === undefined) {
36 36 this.prompt_area = true;
37 37 } else {
38 38 this.prompt_area = prompt_area;
39 39 }
40 40 this.create_elements();
41 41 this.style();
42 42 this.bind_events();
43 43 };
44 44
45 45 OutputArea.prototype.create_elements = function () {
46 46 this.element = $("<div/>");
47 47 this.collapse_button = $("<div/>");
48 48 this.prompt_overlay = $("<div/>");
49 49 this.wrapper.append(this.prompt_overlay);
50 50 this.wrapper.append(this.element);
51 51 this.wrapper.append(this.collapse_button);
52 52 };
53 53
54 54
55 55 OutputArea.prototype.style = function () {
56 56 this.collapse_button.hide();
57 57 this.prompt_overlay.hide();
58 58
59 59 this.wrapper.addClass('output_wrapper');
60 60 this.element.addClass('output vbox');
61 61
62 62 this.collapse_button.addClass("btn output_collapsed");
63 63 this.collapse_button.attr('title', 'click to expand output');
64 64 this.collapse_button.html('. . .');
65 65
66 66 this.prompt_overlay.addClass('out_prompt_overlay prompt');
67 67 this.prompt_overlay.attr('title', 'click to expand output; double click to hide output');
68 68
69 69 this.collapse();
70 70 };
71 71
72 72 /**
73 73 * Should the OutputArea scroll?
74 74 * Returns whether the height (in lines) exceeds a threshold.
75 75 *
76 76 * @private
77 77 * @method _should_scroll
78 78 * @param [lines=100]{Integer}
79 79 * @return {Bool}
80 80 *
81 81 */
82 82 OutputArea.prototype._should_scroll = function (lines) {
83 83 if (lines <=0 ){ return }
84 84 if (!lines) {
85 85 lines = 100;
86 86 }
87 87 // line-height from http://stackoverflow.com/questions/1185151
88 88 var fontSize = this.element.css('font-size');
89 89 var lineHeight = Math.floor(parseInt(fontSize.replace('px','')) * 1.5);
90 90
91 91 return (this.element.height() > lines * lineHeight);
92 92 };
93 93
94 94
95 95 OutputArea.prototype.bind_events = function () {
96 96 var that = this;
97 97 this.prompt_overlay.dblclick(function () { that.toggle_output(); });
98 98 this.prompt_overlay.click(function () { that.toggle_scroll(); });
99 99
100 100 this.element.resize(function () {
101 101 // FIXME: Firefox on Linux misbehaves, so automatic scrolling is disabled
102 102 if ( IPython.utils.browser[0] === "Firefox" ) {
103 103 return;
104 104 }
105 105 // maybe scroll output,
106 106 // if it's grown large enough and hasn't already been scrolled.
107 107 if ( !that.scrolled && that._should_scroll(OutputArea.auto_scroll_threshold)) {
108 108 that.scroll_area();
109 109 }
110 110 });
111 111 this.collapse_button.click(function () {
112 112 that.expand();
113 113 });
114 114 };
115 115
116 116
117 117 OutputArea.prototype.collapse = function () {
118 118 if (!this.collapsed) {
119 119 this.element.hide();
120 120 this.prompt_overlay.hide();
121 121 if (this.element.html()){
122 122 this.collapse_button.show();
123 123 }
124 124 this.collapsed = true;
125 125 }
126 126 };
127 127
128 128
129 129 OutputArea.prototype.expand = function () {
130 130 if (this.collapsed) {
131 131 this.collapse_button.hide();
132 132 this.element.show();
133 133 this.prompt_overlay.show();
134 134 this.collapsed = false;
135 135 }
136 136 };
137 137
138 138
139 139 OutputArea.prototype.toggle_output = function () {
140 140 if (this.collapsed) {
141 141 this.expand();
142 142 } else {
143 143 this.collapse();
144 144 }
145 145 };
146 146
147 147
148 148 OutputArea.prototype.scroll_area = function () {
149 149 this.element.addClass('output_scroll');
150 150 this.prompt_overlay.attr('title', 'click to unscroll output; double click to hide');
151 151 this.scrolled = true;
152 152 };
153 153
154 154
155 155 OutputArea.prototype.unscroll_area = function () {
156 156 this.element.removeClass('output_scroll');
157 157 this.prompt_overlay.attr('title', 'click to scroll output; double click to hide');
158 158 this.scrolled = false;
159 159 };
160 160
161 161 /**
162 162 * Threshold to trigger autoscroll when the OutputArea is resized,
163 163 * typically when new outputs are added.
164 164 *
165 165 * Behavior is undefined if autoscroll is lower than minimum_scroll_threshold,
166 166 * unless it is < 0, in which case autoscroll will never be triggered
167 167 *
168 168 * @property auto_scroll_threshold
169 169 * @type Number
170 170 * @default 100
171 171 *
172 172 **/
173 173 OutputArea.auto_scroll_threshold = 100;
174 174
175 175
176 176 /**
177 177 * Lower limit (in lines) for OutputArea to be made scrollable. OutputAreas
178 178 * shorter than this are never scrolled.
179 179 *
180 180 * @property minimum_scroll_threshold
181 181 * @type Number
182 182 * @default 20
183 183 *
184 184 **/
185 185 OutputArea.minimum_scroll_threshold = 20;
186 186
187 187
188 188 /**
189 189 *
190 190 * Scroll OutputArea if height supperior than a threshold (in lines).
191 191 *
192 192 * Threshold is a maximum number of lines. If unspecified, defaults to
193 193 * OutputArea.minimum_scroll_threshold.
194 194 *
195 195 * Negative threshold will prevent the OutputArea from ever scrolling.
196 196 *
197 197 * @method scroll_if_long
198 198 *
199 199 * @param [lines=20]{Number} Default to 20 if not set,
200 200 * behavior undefined for value of `0`.
201 201 *
202 202 **/
203 203 OutputArea.prototype.scroll_if_long = function (lines) {
204 204 var n = lines | OutputArea.minimum_scroll_threshold;
205 205 if(n <= 0){
206 206 return
207 207 }
208 208
209 209 if (this._should_scroll(n)) {
210 210 // only allow scrolling long-enough output
211 211 this.scroll_area();
212 212 }
213 213 };
214 214
215 215
216 216 OutputArea.prototype.toggle_scroll = function () {
217 217 if (this.scrolled) {
218 218 this.unscroll_area();
219 219 } else {
220 220 // only allow scrolling long-enough output
221 221 this.scroll_if_long();
222 222 }
223 223 };
224 224
225 225
226 226 // typeset with MathJax if MathJax is available
227 227 OutputArea.prototype.typeset = function () {
228 228 if (window.MathJax){
229 229 MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
230 230 }
231 231 };
232 232
233 233
234 234 OutputArea.prototype.handle_output = function (msg) {
235 235 var json = {};
236 236 var msg_type = json.output_type = msg.header.msg_type;
237 237 var content = msg.content;
238 238 if (msg_type === "stream") {
239 239 json.text = content.data;
240 240 json.stream = content.name;
241 241 } else if (msg_type === "display_data") {
242 242 json = this.convert_mime_types(json, content.data);
243 243 json.metadata = this.convert_mime_types({}, content.metadata);
244 244 } else if (msg_type === "pyout") {
245 245 json.prompt_number = content.execution_count;
246 246 json = this.convert_mime_types(json, content.data);
247 247 json.metadata = this.convert_mime_types({}, content.metadata);
248 248 } else if (msg_type === "pyerr") {
249 249 json.ename = content.ename;
250 250 json.evalue = content.evalue;
251 251 json.traceback = content.traceback;
252 252 }
253 253 // append with dynamic=true
254 254 this.append_output(json, true);
255 255 };
256 256
257 257
258 258 OutputArea.prototype.convert_mime_types = function (json, data) {
259 259 if (data === undefined) {
260 260 return json;
261 261 }
262 262 if (data['text/plain'] !== undefined) {
263 263 json.text = data['text/plain'];
264 264 }
265 265 if (data['text/html'] !== undefined) {
266 266 json.html = data['text/html'];
267 267 }
268 268 if (data['image/svg+xml'] !== undefined) {
269 269 json.svg = data['image/svg+xml'];
270 270 }
271 271 if (data['image/png'] !== undefined) {
272 272 json.png = data['image/png'];
273 273 }
274 274 if (data['image/jpeg'] !== undefined) {
275 275 json.jpeg = data['image/jpeg'];
276 276 }
277 277 if (data['text/latex'] !== undefined) {
278 278 json.latex = data['text/latex'];
279 279 }
280 280 if (data['application/json'] !== undefined) {
281 281 json.json = data['application/json'];
282 282 }
283 283 if (data['application/javascript'] !== undefined) {
284 284 json.javascript = data['application/javascript'];
285 285 }
286 286 return json;
287 287 };
288 288
289 289
290 290 OutputArea.prototype.append_output = function (json, dynamic) {
291 291 // If dynamic is true, javascript output will be eval'd.
292 292 this.expand();
293 293
294 294 // Clear the output if clear is queued.
295 295 var needs_height_reset = false;
296 296 if (this.clear_queued) {
297 297 this.clear_output(false);
298 298 needs_height_reset = true;
299 299 }
300 300
301 301 if (json.output_type === 'pyout') {
302 302 this.append_pyout(json, dynamic);
303 303 } else if (json.output_type === 'pyerr') {
304 304 this.append_pyerr(json);
305 305 } else if (json.output_type === 'display_data') {
306 306 this.append_display_data(json, dynamic);
307 307 } else if (json.output_type === 'stream') {
308 308 this.append_stream(json);
309 309 }
310 310 this.outputs.push(json);
311 311
312 312 // Only reset the height to automatic if the height is currently
313 313 // fixed (done by wait=True flag on clear_output).
314 314 if (needs_height_reset) {
315 315 this.element.height('');
316 316 }
317 317
318 318 var that = this;
319 319 setTimeout(function(){that.element.trigger('resize');}, 100);
320 320 };
321 321
322 322
323 323 OutputArea.prototype.create_output_area = function () {
324 324 var oa = $("<div/>").addClass("output_area");
325 325 if (this.prompt_area) {
326 326 oa.append($('<div/>').addClass('prompt'));
327 327 }
328 328 return oa;
329 329 };
330 330
331 331 OutputArea.prototype._append_javascript_error = function (err, container) {
332 332 // display a message when a javascript error occurs in display output
333 333 var msg = "Javascript error adding output!"
334 334 console.log(msg, err);
335 335 if ( container === undefined ) return;
336 336 container.append(
337 337 $('<div/>').html(msg + "<br/>" +
338 338 err.toString() +
339 339 '<br/>See your browser Javascript console for more details.'
340 340 ).addClass('js-error')
341 341 );
342 342 container.show();
343 343 };
344 344
345 345 OutputArea.prototype._safe_append = function (toinsert) {
346 346 // safely append an item to the document
347 347 // this is an object created by user code,
348 348 // and may have errors, which should not be raised
349 349 // under any circumstances.
350 350 try {
351 351 this.element.append(toinsert);
352 352 } catch(err) {
353 353 console.log(err);
354 354 this._append_javascript_error(err, this.element);
355 355 }
356 356 };
357 357
358 358
359 359 OutputArea.prototype.append_pyout = function (json, dynamic) {
360 360 var n = json.prompt_number || ' ';
361 361 var toinsert = this.create_output_area();
362 362 if (this.prompt_area) {
363 363 toinsert.find('div.prompt').addClass('output_prompt').html('Out[' + n + ']:');
364 364 }
365 365 this.append_mime_type(json, toinsert, dynamic);
366 366 this._safe_append(toinsert);
367 367 // If we just output latex, typeset it.
368 368 if ((json.latex !== undefined) || (json.html !== undefined)) {
369 369 this.typeset();
370 370 }
371 371 };
372 372
373 373
374 374 OutputArea.prototype.append_pyerr = function (json) {
375 375 var tb = json.traceback;
376 376 if (tb !== undefined && tb.length > 0) {
377 377 var s = '';
378 378 var len = tb.length;
379 379 for (var i=0; i<len; i++) {
380 380 s = s + tb[i] + '\n';
381 381 }
382 382 s = s + '\n';
383 383 var toinsert = this.create_output_area();
384 384 this.append_text(s, {}, toinsert);
385 385 this._safe_append(toinsert);
386 386 }
387 387 };
388 388
389 389
390 390 OutputArea.prototype.append_stream = function (json) {
391 391 // temporary fix: if stream undefined (json file written prior to this patch),
392 392 // default to most likely stdout:
393 393 if (json.stream == undefined){
394 394 json.stream = 'stdout';
395 395 }
396 396 var text = json.text;
397 397 var subclass = "output_"+json.stream;
398 398 if (this.outputs.length > 0){
399 399 // have at least one output to consider
400 400 var last = this.outputs[this.outputs.length-1];
401 401 if (last.output_type == 'stream' && json.stream == last.stream){
402 402 // latest output was in the same stream,
403 403 // so append directly into its pre tag
404 404 // escape ANSI & HTML specials:
405 405 var pre = this.element.find('div.'+subclass).last().find('pre');
406 406 var html = utils.fixCarriageReturn(
407 407 pre.html() + utils.fixConsole(text));
408 408 pre.html(html);
409 409 return;
410 410 }
411 411 }
412 412
413 413 if (!text.replace("\r", "")) {
414 414 // text is nothing (empty string, \r, etc.)
415 415 // so don't append any elements, which might add undesirable space
416 416 return;
417 417 }
418 418
419 419 // If we got here, attach a new div
420 420 var toinsert = this.create_output_area();
421 421 this.append_text(text, {}, toinsert, "output_stream "+subclass);
422 422 this._safe_append(toinsert);
423 423 };
424 424
425 425
426 426 OutputArea.prototype.append_display_data = function (json, dynamic) {
427 427 var toinsert = this.create_output_area();
428 428 this.append_mime_type(json, toinsert, dynamic);
429 429 this._safe_append(toinsert);
430 430 // If we just output latex, typeset it.
431 431 if ( (json.latex !== undefined) || (json.html !== undefined) ) {
432 432 this.typeset();
433 433 }
434 434 };
435 435
436 436 OutputArea.display_order = ['javascript','html','latex','svg','png','jpeg','text'];
437 437
438 438 OutputArea.prototype.append_mime_type = function (json, element, dynamic) {
439 439 for(var type_i in OutputArea.display_order){
440 440 var type = OutputArea.display_order[type_i];
441 441 if(json[type] != undefined ){
442 442 var md = {};
443 443 if (json.metadata && json.metadata[type]) {
444 444 md = json.metadata[type];
445 445 };
446 446 if(type == 'javascript'){
447 447 if (dynamic) {
448 448 this.append_javascript(json.javascript, md, element, dynamic);
449 449 }
450 450 } else {
451 451 this['append_'+type](json[type], md, element);
452 452 }
453 453 return;
454 454 }
455 455 }
456 456 };
457 457
458 458
459 459 OutputArea.prototype.append_html = function (html, md, element) {
460 460 var toinsert = $("<div/>").addClass("output_subarea output_html rendered_html");
461 461 toinsert.append(html);
462 462 element.append(toinsert);
463 463 };
464 464
465 465
466 466 OutputArea.prototype.append_javascript = function (js, md, container) {
467 467 // We just eval the JS code, element appears in the local scope.
468 468 var element = $("<div/>").addClass("output_subarea");
469 469 container.append(element);
470 470 // Div for js shouldn't be drawn, as it will add empty height to the area.
471 471 container.hide();
472 472 // If the Javascript appends content to `element` that should be drawn, then
473 473 // it must also call `container.show()`.
474 474 try {
475 475 eval(js);
476 476 } catch(err) {
477 477 this._append_javascript_error(err, container);
478 478 }
479 479 };
480 480
481 481
482 482 OutputArea.prototype.append_text = function (data, md, element, extra_class) {
483 483 var toinsert = $("<div/>").addClass("output_subarea output_text");
484 484 // escape ANSI & HTML specials in plaintext:
485 485 data = utils.fixConsole(data);
486 486 data = utils.fixCarriageReturn(data);
487 487 data = utils.autoLinkUrls(data);
488 488 if (extra_class){
489 489 toinsert.addClass(extra_class);
490 490 }
491 491 toinsert.append($("<pre/>").html(data));
492 492 element.append(toinsert);
493 493 };
494 494
495 495
496 496 OutputArea.prototype.append_svg = function (svg, md, element) {
497 497 var wrapper = $('<div/>').addClass('output_subarea output_svg');
498 498 wrapper.append(svg);
499 499 var svg_element = wrapper.children()[0];
500 500
501 501 if (svg_element.classList.contains('ipython-scoped')) {
502 502 // To avoid style or use collisions between multiple svg figures,
503 503 // svg figures are wrapped inside an iframe.
504 504 var iframe = $('<iframe/>')
505 iframe.attr('frameborder', 0);
505 iframe.attr('frameborder', 0);
506 506 iframe.attr('scrolling', 'no');
507 507
508 508 // Once the iframe is loaded, the svg is dynamically inserted
509 509 iframe.on('load', function() {
510 510 // Set the iframe height and width to fit the svg
511 511 // (the +10 pixel offset handles the default body margins
512 512 // in Chrome)
513 513 iframe.width(svg_element.width.baseVal.value + 10);
514 514 iframe.height(svg_element.height.baseVal.value + 10);
515 515
516 516 // Workaround needed by Firefox, to properly render svg inside
517 517 // iframes, see http://stackoverflow.com/questions/10177190/
518 518 // svg-dynamically-added-to-iframe-does-not-render-correctly
519 519 iframe.contents()[0].open();
520 520 iframe.contents()[0].close();
521 521
522 522 // Insert the svg inside the iframe
523 523 var body = iframe.contents().find('body');
524 524 body.html(wrapper.html());
525 525 });
526 526
527 527 element.append(iframe);
528 528 } else {
529 529 element.append(wrapper);
530 530 }
531 531 };
532 532
533 533
534 534 OutputArea.prototype._dblclick_to_reset_size = function (img) {
535 535 // schedule wrapping image in resizable after a delay,
536 536 // so we don't end up calling resize on a zero-size object
537 537 var that = this;
538 538 setTimeout(function () {
539 539 var h0 = img.height();
540 540 var w0 = img.width();
541 541 if (!(h0 && w0)) {
542 542 // zero size, schedule another timeout
543 543 that._dblclick_to_reset_size(img);
544 544 return;
545 545 }
546 546 img.resizable({
547 547 aspectRatio: true,
548 548 autoHide: true
549 549 });
550 550 img.dblclick(function () {
551 551 // resize wrapper & image together for some reason:
552 552 img.parent().height(h0);
553 553 img.height(h0);
554 554 img.parent().width(w0);
555 555 img.width(w0);
556 556 });
557 557 }, 250);
558 558 };
559 559
560 560
561 561 OutputArea.prototype.append_png = function (png, md, element) {
562 562 var toinsert = $("<div/>").addClass("output_subarea output_png");
563 563 var img = $("<img/>").attr('src','data:image/png;base64,'+png);
564 564 if (md['height']) {
565 565 img.attr('height', md['height']);
566 566 }
567 567 if (md['width']) {
568 568 img.attr('width', md['width']);
569 569 }
570 570 this._dblclick_to_reset_size(img);
571 571 toinsert.append(img);
572 572 element.append(toinsert);
573 573 };
574 574
575 575
576 576 OutputArea.prototype.append_jpeg = function (jpeg, md, element) {
577 577 var toinsert = $("<div/>").addClass("output_subarea output_jpeg");
578 578 var img = $("<img/>").attr('src','data:image/jpeg;base64,'+jpeg);
579 579 if (md['height']) {
580 580 img.attr('height', md['height']);
581 581 }
582 582 if (md['width']) {
583 583 img.attr('width', md['width']);
584 584 }
585 585 this._dblclick_to_reset_size(img);
586 586 toinsert.append(img);
587 587 element.append(toinsert);
588 588 };
589 589
590 590
591 591 OutputArea.prototype.append_latex = function (latex, md, element) {
592 592 // This method cannot do the typesetting because the latex first has to
593 593 // be on the page.
594 594 var toinsert = $("<div/>").addClass("output_subarea output_latex");
595 595 toinsert.append(latex);
596 596 element.append(toinsert);
597 597 };
598 598
599 599 OutputArea.prototype.append_raw_input = function (msg) {
600 600 var that = this;
601 601 this.expand();
602 602 var content = msg.content;
603 603 var area = this.create_output_area();
604 604
605 605 // disable any other raw_inputs, if they are left around
606 606 $("div.output_subarea.raw_input").remove();
607 607
608 608 area.append(
609 609 $("<div/>")
610 610 .addClass("box-flex1 output_subarea raw_input")
611 611 .append(
612 612 $("<span/>")
613 613 .addClass("input_prompt")
614 614 .text(content.prompt)
615 615 )
616 616 .append(
617 617 $("<input/>")
618 618 .addClass("raw_input")
619 619 .attr('type', 'text')
620 620 .attr("size", 47)
621 621 .keydown(function (event, ui) {
622 622 // make sure we submit on enter,
623 623 // and don't re-execute the *cell* on shift-enter
624 624 if (event.which === utils.keycodes.ENTER) {
625 625 that._submit_raw_input();
626 626 return false;
627 627 }
628 628 })
629 629 )
630 630 );
631 631 this.element.append(area);
632 632 // weirdly need double-focus now,
633 633 // otherwise only the cell will be focused
634 634 area.find("input.raw_input").focus().focus();
635 635 }
636 636 OutputArea.prototype._submit_raw_input = function (evt) {
637 637 var container = this.element.find("div.raw_input");
638 638 var theprompt = container.find("span.input_prompt");
639 639 var theinput = container.find("input.raw_input");
640 640 var value = theinput.val();
641 641 var content = {
642 642 output_type : 'stream',
643 643 name : 'stdout',
644 644 text : theprompt.text() + value + '\n'
645 645 }
646 646 // remove form container
647 647 container.parent().remove();
648 648 // replace with plaintext version in stdout
649 649 this.append_output(content, false);
650 650 $([IPython.events]).trigger('send_input_reply.Kernel', value);
651 651 }
652 652
653 653
654 654 OutputArea.prototype.handle_clear_output = function (msg) {
655 655 this.clear_output(msg.content.wait);
656 656 };
657 657
658 658
659 659 OutputArea.prototype.clear_output = function(wait) {
660 660 if (wait) {
661 661
662 662 // If a clear is queued, clear before adding another to the queue.
663 663 if (this.clear_queued) {
664 664 this.clear_output(false);
665 665 };
666 666
667 667 this.clear_queued = true;
668 668 } else {
669 669
670 670 // Fix the output div's height if the clear_output is waiting for
671 671 // new output (it is being used in an animation).
672 672 if (this.clear_queued) {
673 673 var height = this.element.height();
674 674 this.element.height(height);
675 675 this.clear_queued = false;
676 676 }
677 677
678 678 // clear all, no need for logic
679 679 this.element.html("");
680 680 this.outputs = [];
681 681 this.unscroll_area();
682 682 return;
683 683 };
684 684 };
685 685
686 686
687 687 // JSON serialization
688 688
689 689 OutputArea.prototype.fromJSON = function (outputs) {
690 690 var len = outputs.length;
691 691 for (var i=0; i<len; i++) {
692 692 // append with dynamic=false.
693 693 this.append_output(outputs[i], false);
694 694 }
695 695 };
696 696
697 697
698 698 OutputArea.prototype.toJSON = function () {
699 699 var outputs = [];
700 700 var len = this.outputs.length;
701 701 for (var i=0; i<len; i++) {
702 702 outputs[i] = this.outputs[i];
703 703 }
704 704 return outputs;
705 705 };
706 706
707 707
708 708 IPython.OutputArea = OutputArea;
709 709
710 710 return IPython;
711 711
712 712 }(IPython));
General Comments 0
You need to be logged in to leave comments. Login now