##// END OF EJS Templates
discard invalid stream output...
Min RK -
Show More
@@ -1,984 +1,988 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jqueryui',
7 7 'base/js/utils',
8 8 'base/js/security',
9 9 'base/js/keyboard',
10 10 'notebook/js/mathjaxutils',
11 11 'components/marked/lib/marked',
12 12 ], function(IPython, $, utils, security, keyboard, mathjaxutils, marked) {
13 13 "use strict";
14 14
15 15 /**
16 16 * @class OutputArea
17 17 *
18 18 * @constructor
19 19 */
20 20
21 21 var OutputArea = function (options) {
22 22 this.selector = options.selector;
23 23 this.events = options.events;
24 24 this.keyboard_manager = options.keyboard_manager;
25 25 this.wrapper = $(options.selector);
26 26 this.outputs = [];
27 27 this.collapsed = false;
28 28 this.scrolled = false;
29 29 this.trusted = true;
30 30 this.clear_queued = null;
31 31 if (options.prompt_area === undefined) {
32 32 this.prompt_area = true;
33 33 } else {
34 34 this.prompt_area = options.prompt_area;
35 35 }
36 36 this.create_elements();
37 37 this.style();
38 38 this.bind_events();
39 39 };
40 40
41 41
42 42 /**
43 43 * Class prototypes
44 44 **/
45 45
46 46 OutputArea.prototype.create_elements = function () {
47 47 this.element = $("<div/>");
48 48 this.collapse_button = $("<div/>");
49 49 this.prompt_overlay = $("<div/>");
50 50 this.wrapper.append(this.prompt_overlay);
51 51 this.wrapper.append(this.element);
52 52 this.wrapper.append(this.collapse_button);
53 53 };
54 54
55 55
56 56 OutputArea.prototype.style = function () {
57 57 this.collapse_button.hide();
58 58 this.prompt_overlay.hide();
59 59
60 60 this.wrapper.addClass('output_wrapper');
61 61 this.element.addClass('output');
62 62
63 63 this.collapse_button.addClass("btn btn-default output_collapsed");
64 64 this.collapse_button.attr('title', 'click to expand output');
65 65 this.collapse_button.text('. . .');
66 66
67 67 this.prompt_overlay.addClass('out_prompt_overlay prompt');
68 68 this.prompt_overlay.attr('title', 'click to expand output; double click to hide output');
69 69
70 70 this.collapse();
71 71 };
72 72
73 73 /**
74 74 * Should the OutputArea scroll?
75 75 * Returns whether the height (in lines) exceeds a threshold.
76 76 *
77 77 * @private
78 78 * @method _should_scroll
79 79 * @param [lines=100]{Integer}
80 80 * @return {Bool}
81 81 *
82 82 */
83 83 OutputArea.prototype._should_scroll = function (lines) {
84 84 if (lines <=0 ){ return; }
85 85 if (!lines) {
86 86 lines = 100;
87 87 }
88 88 // line-height from http://stackoverflow.com/questions/1185151
89 89 var fontSize = this.element.css('font-size');
90 90 var lineHeight = Math.floor(parseInt(fontSize.replace('px','')) * 1.5);
91 91
92 92 return (this.element.height() > lines * lineHeight);
93 93 };
94 94
95 95
96 96 OutputArea.prototype.bind_events = function () {
97 97 var that = this;
98 98 this.prompt_overlay.dblclick(function () { that.toggle_output(); });
99 99 this.prompt_overlay.click(function () { that.toggle_scroll(); });
100 100
101 101 this.element.resize(function () {
102 102 // FIXME: Firefox on Linux misbehaves, so automatic scrolling is disabled
103 103 if ( utils.browser[0] === "Firefox" ) {
104 104 return;
105 105 }
106 106 // maybe scroll output,
107 107 // if it's grown large enough and hasn't already been scrolled.
108 108 if ( !that.scrolled && that._should_scroll(OutputArea.auto_scroll_threshold)) {
109 109 that.scroll_area();
110 110 }
111 111 });
112 112 this.collapse_button.click(function () {
113 113 that.expand();
114 114 });
115 115 };
116 116
117 117
118 118 OutputArea.prototype.collapse = function () {
119 119 if (!this.collapsed) {
120 120 this.element.hide();
121 121 this.prompt_overlay.hide();
122 122 if (this.element.html()){
123 123 this.collapse_button.show();
124 124 }
125 125 this.collapsed = true;
126 126 }
127 127 };
128 128
129 129
130 130 OutputArea.prototype.expand = function () {
131 131 if (this.collapsed) {
132 132 this.collapse_button.hide();
133 133 this.element.show();
134 134 this.prompt_overlay.show();
135 135 this.collapsed = false;
136 136 }
137 137 };
138 138
139 139
140 140 OutputArea.prototype.toggle_output = function () {
141 141 if (this.collapsed) {
142 142 this.expand();
143 143 } else {
144 144 this.collapse();
145 145 }
146 146 };
147 147
148 148
149 149 OutputArea.prototype.scroll_area = function () {
150 150 this.element.addClass('output_scroll');
151 151 this.prompt_overlay.attr('title', 'click to unscroll output; double click to hide');
152 152 this.scrolled = true;
153 153 };
154 154
155 155
156 156 OutputArea.prototype.unscroll_area = function () {
157 157 this.element.removeClass('output_scroll');
158 158 this.prompt_overlay.attr('title', 'click to scroll output; double click to hide');
159 159 this.scrolled = false;
160 160 };
161 161
162 162 /**
163 163 *
164 164 * Scroll OutputArea if height supperior than a threshold (in lines).
165 165 *
166 166 * Threshold is a maximum number of lines. If unspecified, defaults to
167 167 * OutputArea.minimum_scroll_threshold.
168 168 *
169 169 * Negative threshold will prevent the OutputArea from ever scrolling.
170 170 *
171 171 * @method scroll_if_long
172 172 *
173 173 * @param [lines=20]{Number} Default to 20 if not set,
174 174 * behavior undefined for value of `0`.
175 175 *
176 176 **/
177 177 OutputArea.prototype.scroll_if_long = function (lines) {
178 178 var n = lines || OutputArea.minimum_scroll_threshold;
179 179 if(n <= 0){
180 180 return;
181 181 }
182 182
183 183 if (this._should_scroll(n)) {
184 184 // only allow scrolling long-enough output
185 185 this.scroll_area();
186 186 }
187 187 };
188 188
189 189
190 190 OutputArea.prototype.toggle_scroll = function () {
191 191 if (this.scrolled) {
192 192 this.unscroll_area();
193 193 } else {
194 194 // only allow scrolling long-enough output
195 195 this.scroll_if_long();
196 196 }
197 197 };
198 198
199 199
200 200 // typeset with MathJax if MathJax is available
201 201 OutputArea.prototype.typeset = function () {
202 202 utils.typeset(this.element);
203 203 };
204 204
205 205
206 206 OutputArea.prototype.handle_output = function (msg) {
207 207 var json = {};
208 208 var msg_type = json.output_type = msg.header.msg_type;
209 209 var content = msg.content;
210 210 if (msg_type === "stream") {
211 211 json.text = content.text;
212 212 json.name = content.name;
213 213 } else if (msg_type === "display_data") {
214 214 json.data = content.data;
215 215 json.output_type = msg_type;
216 216 json.metadata = content.metadata;
217 217 } else if (msg_type === "execute_result") {
218 218 json.data = content.data;
219 219 json.output_type = msg_type;
220 220 json.metadata = content.metadata;
221 221 json.execution_count = content.execution_count;
222 222 } else if (msg_type === "error") {
223 223 json.ename = content.ename;
224 224 json.evalue = content.evalue;
225 225 json.traceback = content.traceback;
226 226 } else {
227 227 console.log("unhandled output message", msg);
228 228 return;
229 229 }
230 230 this.append_output(json);
231 231 };
232 232
233 233
234 234 OutputArea.output_types = [
235 235 'application/javascript',
236 236 'text/html',
237 237 'text/markdown',
238 238 'text/latex',
239 239 'image/svg+xml',
240 240 'image/png',
241 241 'image/jpeg',
242 242 'application/pdf',
243 243 'text/plain'
244 244 ];
245 245
246 246 OutputArea.prototype.validate_mimebundle = function (bundle) {
247 247 /** scrub invalid outputs */
248 248 if (typeof bundle.data !== 'object') {
249 249 console.warn("mimebundle missing data", bundle);
250 250 bundle.data = {};
251 251 }
252 252 if (typeof bundle.metadata !== 'object') {
253 253 console.warn("mimebundle missing metadata", bundle);
254 254 bundle.metadata = {};
255 255 }
256 256 var data = bundle.data;
257 257 $.map(OutputArea.output_types, function(key){
258 258 if (key !== 'application/json' &&
259 259 data[key] !== undefined &&
260 260 typeof data[key] !== 'string'
261 261 ) {
262 262 console.log("Invalid type for " + key, data[key]);
263 263 delete data[key];
264 264 }
265 265 });
266 266 return bundle;
267 267 };
268 268
269 269 OutputArea.prototype.append_output = function (json) {
270 270 this.expand();
271 271
272 272 // Clear the output if clear is queued.
273 273 var needs_height_reset = false;
274 274 if (this.clear_queued) {
275 275 this.clear_output(false);
276 276 needs_height_reset = true;
277 277 }
278 278
279 279 var record_output = true;
280 280 switch(json.output_type) {
281 281 case 'execute_result':
282 282 json = this.validate_mimebundle(json);
283 283 this.append_execute_result(json);
284 284 break;
285 285 case 'stream':
286 286 // append_stream might have merged the output with earlier stream output
287 287 record_output = this.append_stream(json);
288 288 break;
289 289 case 'error':
290 290 this.append_error(json);
291 291 break;
292 292 case 'display_data':
293 293 // append handled below
294 294 json = this.validate_mimebundle(json);
295 295 break;
296 296 default:
297 297 console.log("unrecognized output type: " + json.output_type);
298 298 this.append_unrecognized(json);
299 299 }
300 300
301 301 // We must release the animation fixed height in a callback since Gecko
302 302 // (FireFox) doesn't render the image immediately as the data is
303 303 // available.
304 304 var that = this;
305 305 var handle_appended = function ($el) {
306 306 /**
307 307 * Only reset the height to automatic if the height is currently
308 308 * fixed (done by wait=True flag on clear_output).
309 309 */
310 310 if (needs_height_reset) {
311 311 that.element.height('');
312 312 }
313 313 that.element.trigger('resize');
314 314 };
315 315 if (json.output_type === 'display_data') {
316 316 this.append_display_data(json, handle_appended);
317 317 } else {
318 318 handle_appended();
319 319 }
320 320
321 321 if (record_output) {
322 322 this.outputs.push(json);
323 323 }
324 324 };
325 325
326 326
327 327 OutputArea.prototype.create_output_area = function () {
328 328 var oa = $("<div/>").addClass("output_area");
329 329 if (this.prompt_area) {
330 330 oa.append($('<div/>').addClass('prompt'));
331 331 }
332 332 return oa;
333 333 };
334 334
335 335
336 336 function _get_metadata_key(metadata, key, mime) {
337 337 var mime_md = metadata[mime];
338 338 // mime-specific higher priority
339 339 if (mime_md && mime_md[key] !== undefined) {
340 340 return mime_md[key];
341 341 }
342 342 // fallback on global
343 343 return metadata[key];
344 344 }
345 345
346 346 OutputArea.prototype.create_output_subarea = function(md, classes, mime) {
347 347 var subarea = $('<div/>').addClass('output_subarea').addClass(classes);
348 348 if (_get_metadata_key(md, 'isolated', mime)) {
349 349 // Create an iframe to isolate the subarea from the rest of the
350 350 // document
351 351 var iframe = $('<iframe/>').addClass('box-flex1');
352 352 iframe.css({'height':1, 'width':'100%', 'display':'block'});
353 353 iframe.attr('frameborder', 0);
354 354 iframe.attr('scrolling', 'auto');
355 355
356 356 // Once the iframe is loaded, the subarea is dynamically inserted
357 357 iframe.on('load', function() {
358 358 // Workaround needed by Firefox, to properly render svg inside
359 359 // iframes, see http://stackoverflow.com/questions/10177190/
360 360 // svg-dynamically-added-to-iframe-does-not-render-correctly
361 361 this.contentDocument.open();
362 362
363 363 // Insert the subarea into the iframe
364 364 // We must directly write the html. When using Jquery's append
365 365 // method, javascript is evaluated in the parent document and
366 366 // not in the iframe document. At this point, subarea doesn't
367 367 // contain any user content.
368 368 this.contentDocument.write(subarea.html());
369 369
370 370 this.contentDocument.close();
371 371
372 372 var body = this.contentDocument.body;
373 373 // Adjust the iframe height automatically
374 374 iframe.height(body.scrollHeight + 'px');
375 375 });
376 376
377 377 // Elements should be appended to the inner subarea and not to the
378 378 // iframe
379 379 iframe.append = function(that) {
380 380 subarea.append(that);
381 381 };
382 382
383 383 return iframe;
384 384 } else {
385 385 return subarea;
386 386 }
387 387 };
388 388
389 389
390 390 OutputArea.prototype._append_javascript_error = function (err, element) {
391 391 /**
392 392 * display a message when a javascript error occurs in display output
393 393 */
394 394 var msg = "Javascript error adding output!";
395 395 if ( element === undefined ) return;
396 396 element
397 397 .append($('<div/>').text(msg).addClass('js-error'))
398 398 .append($('<div/>').text(err.toString()).addClass('js-error'))
399 399 .append($('<div/>').text('See your browser Javascript console for more details.').addClass('js-error'));
400 400 };
401 401
402 402 OutputArea.prototype._safe_append = function (toinsert) {
403 403 /**
404 404 * safely append an item to the document
405 405 * this is an object created by user code,
406 406 * and may have errors, which should not be raised
407 407 * under any circumstances.
408 408 */
409 409 try {
410 410 this.element.append(toinsert);
411 411 } catch(err) {
412 412 console.log(err);
413 413 // Create an actual output_area and output_subarea, which creates
414 414 // the prompt area and the proper indentation.
415 415 var toinsert = this.create_output_area();
416 416 var subarea = $('<div/>').addClass('output_subarea');
417 417 toinsert.append(subarea);
418 418 this._append_javascript_error(err, subarea);
419 419 this.element.append(toinsert);
420 420 }
421 421
422 422 // Notify others of changes.
423 423 this.element.trigger('changed');
424 424 };
425 425
426 426
427 427 OutputArea.prototype.append_execute_result = function (json) {
428 428 var n = json.execution_count || ' ';
429 429 var toinsert = this.create_output_area();
430 430 if (this.prompt_area) {
431 431 toinsert.find('div.prompt').addClass('output_prompt').text('Out[' + n + ']:');
432 432 }
433 433 var inserted = this.append_mime_type(json, toinsert);
434 434 if (inserted) {
435 435 inserted.addClass('output_result');
436 436 }
437 437 this._safe_append(toinsert);
438 438 // If we just output latex, typeset it.
439 439 if ((json.data['text/latex'] !== undefined) ||
440 440 (json.data['text/html'] !== undefined) ||
441 441 (json.data['text/markdown'] !== undefined)) {
442 442 this.typeset();
443 443 }
444 444 };
445 445
446 446
447 447 OutputArea.prototype.append_error = function (json) {
448 448 var tb = json.traceback;
449 449 if (tb !== undefined && tb.length > 0) {
450 450 var s = '';
451 451 var len = tb.length;
452 452 for (var i=0; i<len; i++) {
453 453 s = s + tb[i] + '\n';
454 454 }
455 455 s = s + '\n';
456 456 var toinsert = this.create_output_area();
457 457 var append_text = OutputArea.append_map['text/plain'];
458 458 if (append_text) {
459 459 append_text.apply(this, [s, {}, toinsert]).addClass('output_error');
460 460 }
461 461 this._safe_append(toinsert);
462 462 }
463 463 };
464 464
465 465
466 466 OutputArea.prototype.append_stream = function (json) {
467 467 var text = json.text;
468 if (typeof text !== 'string') {
469 console.error("Stream output is invalid (missing text)", json);
470 return false;
471 }
468 472 var subclass = "output_"+json.name;
469 473 if (this.outputs.length > 0){
470 474 // have at least one output to consider
471 475 var last = this.outputs[this.outputs.length-1];
472 476 if (last.output_type == 'stream' && json.name == last.name){
473 477 // latest output was in the same stream,
474 478 // so append directly into its pre tag
475 479 // escape ANSI & HTML specials:
476 480 last.text = utils.fixCarriageReturn(last.text + json.text);
477 481 var pre = this.element.find('div.'+subclass).last().find('pre');
478 482 var html = utils.fixConsole(last.text);
479 483 // The only user content injected with this HTML call is
480 484 // escaped by the fixConsole() method.
481 485 pre.html(html);
482 486 // return false signals that we merged this output with the previous one,
483 487 // and the new output shouldn't be recorded.
484 488 return false;
485 489 }
486 490 }
487 491
488 492 if (!text.replace("\r", "")) {
489 493 // text is nothing (empty string, \r, etc.)
490 494 // so don't append any elements, which might add undesirable space
491 495 // return true to indicate the output should be recorded.
492 496 return true;
493 497 }
494 498
495 499 // If we got here, attach a new div
496 500 var toinsert = this.create_output_area();
497 501 var append_text = OutputArea.append_map['text/plain'];
498 502 if (append_text) {
499 503 append_text.apply(this, [text, {}, toinsert]).addClass("output_stream " + subclass);
500 504 }
501 505 this._safe_append(toinsert);
502 506 return true;
503 507 };
504 508
505 509
506 510 OutputArea.prototype.append_unrecognized = function (json) {
507 511 var that = this;
508 512 var toinsert = this.create_output_area();
509 513 var subarea = $('<div/>').addClass('output_subarea output_unrecognized');
510 514 toinsert.append(subarea);
511 515 subarea.append(
512 516 $("<a>")
513 517 .attr("href", "#")
514 518 .text("Unrecognized output: " + json.output_type)
515 519 .click(function () {
516 520 that.events.trigger('unrecognized_output.OutputArea', {output: json})
517 521 })
518 522 );
519 523 this._safe_append(toinsert);
520 524 };
521 525
522 526
523 527 OutputArea.prototype.append_display_data = function (json, handle_inserted) {
524 528 var toinsert = this.create_output_area();
525 529 if (this.append_mime_type(json, toinsert, handle_inserted)) {
526 530 this._safe_append(toinsert);
527 531 // If we just output latex, typeset it.
528 532 if ((json.data['text/latex'] !== undefined) ||
529 533 (json.data['text/html'] !== undefined) ||
530 534 (json.data['text/markdown'] !== undefined)) {
531 535 this.typeset();
532 536 }
533 537 }
534 538 };
535 539
536 540
537 541 OutputArea.safe_outputs = {
538 542 'text/plain' : true,
539 543 'text/latex' : true,
540 544 'image/png' : true,
541 545 'image/jpeg' : true
542 546 };
543 547
544 548 OutputArea.prototype.append_mime_type = function (json, element, handle_inserted) {
545 549 for (var i=0; i < OutputArea.display_order.length; i++) {
546 550 var type = OutputArea.display_order[i];
547 551 var append = OutputArea.append_map[type];
548 552 if ((json.data[type] !== undefined) && append) {
549 553 var value = json.data[type];
550 554 if (!this.trusted && !OutputArea.safe_outputs[type]) {
551 555 // not trusted, sanitize HTML
552 556 if (type==='text/html' || type==='text/svg') {
553 557 value = security.sanitize_html(value);
554 558 } else {
555 559 // don't display if we don't know how to sanitize it
556 560 console.log("Ignoring untrusted " + type + " output.");
557 561 continue;
558 562 }
559 563 }
560 564 var md = json.metadata || {};
561 565 var toinsert = append.apply(this, [value, md, element, handle_inserted]);
562 566 // Since only the png and jpeg mime types call the inserted
563 567 // callback, if the mime type is something other we must call the
564 568 // inserted callback only when the element is actually inserted
565 569 // into the DOM. Use a timeout of 0 to do this.
566 570 if (['image/png', 'image/jpeg'].indexOf(type) < 0 && handle_inserted !== undefined) {
567 571 setTimeout(handle_inserted, 0);
568 572 }
569 573 this.events.trigger('output_appended.OutputArea', [type, value, md, toinsert]);
570 574 return toinsert;
571 575 }
572 576 }
573 577 return null;
574 578 };
575 579
576 580
577 581 var append_html = function (html, md, element) {
578 582 var type = 'text/html';
579 583 var toinsert = this.create_output_subarea(md, "output_html rendered_html", type);
580 584 this.keyboard_manager.register_events(toinsert);
581 585 toinsert.append(html);
582 586 element.append(toinsert);
583 587 return toinsert;
584 588 };
585 589
586 590
587 591 var append_markdown = function(markdown, md, element) {
588 592 var type = 'text/markdown';
589 593 var toinsert = this.create_output_subarea(md, "output_markdown", type);
590 594 var text_and_math = mathjaxutils.remove_math(markdown);
591 595 var text = text_and_math[0];
592 596 var math = text_and_math[1];
593 597 marked(text, function (err, html) {
594 598 html = mathjaxutils.replace_math(html, math);
595 599 toinsert.append(html);
596 600 });
597 601 element.append(toinsert);
598 602 return toinsert;
599 603 };
600 604
601 605
602 606 var append_javascript = function (js, md, element) {
603 607 /**
604 608 * We just eval the JS code, element appears in the local scope.
605 609 */
606 610 var type = 'application/javascript';
607 611 var toinsert = this.create_output_subarea(md, "output_javascript", type);
608 612 this.keyboard_manager.register_events(toinsert);
609 613 element.append(toinsert);
610 614
611 615 // Fix for ipython/issues/5293, make sure `element` is the area which
612 616 // output can be inserted into at the time of JS execution.
613 617 element = toinsert;
614 618 try {
615 619 eval(js);
616 620 } catch(err) {
617 621 console.log(err);
618 622 this._append_javascript_error(err, toinsert);
619 623 }
620 624 return toinsert;
621 625 };
622 626
623 627
624 628 var append_text = function (data, md, element) {
625 629 var type = 'text/plain';
626 630 var toinsert = this.create_output_subarea(md, "output_text", type);
627 631 // escape ANSI & HTML specials in plaintext:
628 632 data = utils.fixConsole(data);
629 633 data = utils.fixCarriageReturn(data);
630 634 data = utils.autoLinkUrls(data);
631 635 // The only user content injected with this HTML call is
632 636 // escaped by the fixConsole() method.
633 637 toinsert.append($("<pre/>").html(data));
634 638 element.append(toinsert);
635 639 return toinsert;
636 640 };
637 641
638 642
639 643 var append_svg = function (svg_html, md, element) {
640 644 var type = 'image/svg+xml';
641 645 var toinsert = this.create_output_subarea(md, "output_svg", type);
642 646
643 647 // Get the svg element from within the HTML.
644 648 var svg = $('<div />').html(svg_html).find('svg');
645 649 var svg_area = $('<div />');
646 650 var width = svg.attr('width');
647 651 var height = svg.attr('height');
648 652 svg
649 653 .width('100%')
650 654 .height('100%');
651 655 svg_area
652 656 .width(width)
653 657 .height(height);
654 658
655 659 // The jQuery resize handlers don't seem to work on the svg element.
656 660 // When the svg renders completely, measure it's size and set the parent
657 661 // div to that size. Then set the svg to 100% the size of the parent
658 662 // div and make the parent div resizable.
659 663 this._dblclick_to_reset_size(svg_area, true, false);
660 664
661 665 svg_area.append(svg);
662 666 toinsert.append(svg_area);
663 667 element.append(toinsert);
664 668
665 669 return toinsert;
666 670 };
667 671
668 672 OutputArea.prototype._dblclick_to_reset_size = function (img, immediately, resize_parent) {
669 673 /**
670 674 * Add a resize handler to an element
671 675 *
672 676 * img: jQuery element
673 677 * immediately: bool=False
674 678 * Wait for the element to load before creating the handle.
675 679 * resize_parent: bool=True
676 680 * Should the parent of the element be resized when the element is
677 681 * reset (by double click).
678 682 */
679 683 var callback = function (){
680 684 var h0 = img.height();
681 685 var w0 = img.width();
682 686 if (!(h0 && w0)) {
683 687 // zero size, don't make it resizable
684 688 return;
685 689 }
686 690 img.resizable({
687 691 aspectRatio: true,
688 692 autoHide: true
689 693 });
690 694 img.dblclick(function () {
691 695 // resize wrapper & image together for some reason:
692 696 img.height(h0);
693 697 img.width(w0);
694 698 if (resize_parent === undefined || resize_parent) {
695 699 img.parent().height(h0);
696 700 img.parent().width(w0);
697 701 }
698 702 });
699 703 };
700 704
701 705 if (immediately) {
702 706 callback();
703 707 } else {
704 708 img.on("load", callback);
705 709 }
706 710 };
707 711
708 712 var set_width_height = function (img, md, mime) {
709 713 /**
710 714 * set width and height of an img element from metadata
711 715 */
712 716 var height = _get_metadata_key(md, 'height', mime);
713 717 if (height !== undefined) img.attr('height', height);
714 718 var width = _get_metadata_key(md, 'width', mime);
715 719 if (width !== undefined) img.attr('width', width);
716 720 };
717 721
718 722 var append_png = function (png, md, element, handle_inserted) {
719 723 var type = 'image/png';
720 724 var toinsert = this.create_output_subarea(md, "output_png", type);
721 725 var img = $("<img/>");
722 726 if (handle_inserted !== undefined) {
723 727 img.on('load', function(){
724 728 handle_inserted(img);
725 729 });
726 730 }
727 731 img[0].src = 'data:image/png;base64,'+ png;
728 732 set_width_height(img, md, 'image/png');
729 733 this._dblclick_to_reset_size(img);
730 734 toinsert.append(img);
731 735 element.append(toinsert);
732 736 return toinsert;
733 737 };
734 738
735 739
736 740 var append_jpeg = function (jpeg, md, element, handle_inserted) {
737 741 var type = 'image/jpeg';
738 742 var toinsert = this.create_output_subarea(md, "output_jpeg", type);
739 743 var img = $("<img/>");
740 744 if (handle_inserted !== undefined) {
741 745 img.on('load', function(){
742 746 handle_inserted(img);
743 747 });
744 748 }
745 749 img[0].src = 'data:image/jpeg;base64,'+ jpeg;
746 750 set_width_height(img, md, 'image/jpeg');
747 751 this._dblclick_to_reset_size(img);
748 752 toinsert.append(img);
749 753 element.append(toinsert);
750 754 return toinsert;
751 755 };
752 756
753 757
754 758 var append_pdf = function (pdf, md, element) {
755 759 var type = 'application/pdf';
756 760 var toinsert = this.create_output_subarea(md, "output_pdf", type);
757 761 var a = $('<a/>').attr('href', 'data:application/pdf;base64,'+pdf);
758 762 a.attr('target', '_blank');
759 763 a.text('View PDF');
760 764 toinsert.append(a);
761 765 element.append(toinsert);
762 766 return toinsert;
763 767 };
764 768
765 769 var append_latex = function (latex, md, element) {
766 770 /**
767 771 * This method cannot do the typesetting because the latex first has to
768 772 * be on the page.
769 773 */
770 774 var type = 'text/latex';
771 775 var toinsert = this.create_output_subarea(md, "output_latex", type);
772 776 toinsert.append(latex);
773 777 element.append(toinsert);
774 778 return toinsert;
775 779 };
776 780
777 781
778 782 OutputArea.prototype.append_raw_input = function (msg) {
779 783 var that = this;
780 784 this.expand();
781 785 var content = msg.content;
782 786 var area = this.create_output_area();
783 787
784 788 // disable any other raw_inputs, if they are left around
785 789 $("div.output_subarea.raw_input_container").remove();
786 790
787 791 var input_type = content.password ? 'password' : 'text';
788 792
789 793 area.append(
790 794 $("<div/>")
791 795 .addClass("box-flex1 output_subarea raw_input_container")
792 796 .append(
793 797 $("<span/>")
794 798 .addClass("raw_input_prompt")
795 799 .text(content.prompt)
796 800 )
797 801 .append(
798 802 $("<input/>")
799 803 .addClass("raw_input")
800 804 .attr('type', input_type)
801 805 .attr("size", 47)
802 806 .keydown(function (event, ui) {
803 807 // make sure we submit on enter,
804 808 // and don't re-execute the *cell* on shift-enter
805 809 if (event.which === keyboard.keycodes.enter) {
806 810 that._submit_raw_input();
807 811 return false;
808 812 }
809 813 })
810 814 )
811 815 );
812 816
813 817 this.element.append(area);
814 818 var raw_input = area.find('input.raw_input');
815 819 // Register events that enable/disable the keyboard manager while raw
816 820 // input is focused.
817 821 this.keyboard_manager.register_events(raw_input);
818 822 // Note, the following line used to read raw_input.focus().focus().
819 823 // This seemed to be needed otherwise only the cell would be focused.
820 824 // But with the modal UI, this seems to work fine with one call to focus().
821 825 raw_input.focus();
822 826 };
823 827
824 828 OutputArea.prototype._submit_raw_input = function (evt) {
825 829 var container = this.element.find("div.raw_input_container");
826 830 var theprompt = container.find("span.raw_input_prompt");
827 831 var theinput = container.find("input.raw_input");
828 832 var value = theinput.val();
829 833 var echo = value;
830 834 // don't echo if it's a password
831 835 if (theinput.attr('type') == 'password') {
832 836 echo = 'Β·Β·Β·Β·Β·Β·Β·Β·';
833 837 }
834 838 var content = {
835 839 output_type : 'stream',
836 840 name : 'stdout',
837 841 text : theprompt.text() + echo + '\n'
838 842 };
839 843 // remove form container
840 844 container.parent().remove();
841 845 // replace with plaintext version in stdout
842 846 this.append_output(content, false);
843 847 this.events.trigger('send_input_reply.Kernel', value);
844 848 };
845 849
846 850
847 851 OutputArea.prototype.handle_clear_output = function (msg) {
848 852 /**
849 853 * msg spec v4 had stdout, stderr, display keys
850 854 * v4.1 replaced these with just wait
851 855 * The default behavior is the same (stdout=stderr=display=True, wait=False),
852 856 * so v4 messages will still be properly handled,
853 857 * except for the rarely used clearing less than all output.
854 858 */
855 859 this.clear_output(msg.content.wait || false);
856 860 };
857 861
858 862
859 863 OutputArea.prototype.clear_output = function(wait, ignore_que) {
860 864 if (wait) {
861 865
862 866 // If a clear is queued, clear before adding another to the queue.
863 867 if (this.clear_queued) {
864 868 this.clear_output(false);
865 869 }
866 870
867 871 this.clear_queued = true;
868 872 } else {
869 873
870 874 // Fix the output div's height if the clear_output is waiting for
871 875 // new output (it is being used in an animation).
872 876 if (!ignore_que && this.clear_queued) {
873 877 var height = this.element.height();
874 878 this.element.height(height);
875 879 this.clear_queued = false;
876 880 }
877 881
878 882 // Clear all
879 883 // Remove load event handlers from img tags because we don't want
880 884 // them to fire if the image is never added to the page.
881 885 this.element.find('img').off('load');
882 886 this.element.html("");
883 887
884 888 // Notify others of changes.
885 889 this.element.trigger('changed');
886 890
887 891 this.outputs = [];
888 892 this.trusted = true;
889 893 this.unscroll_area();
890 894 return;
891 895 }
892 896 };
893 897
894 898
895 899 // JSON serialization
896 900
897 901 OutputArea.prototype.fromJSON = function (outputs, metadata) {
898 902 var len = outputs.length;
899 903 metadata = metadata || {};
900 904
901 905 for (var i=0; i<len; i++) {
902 906 this.append_output(outputs[i]);
903 907 }
904 908
905 909 if (metadata.collapsed !== undefined) {
906 910 this.collapsed = metadata.collapsed;
907 911 if (metadata.collapsed) {
908 912 this.collapse_output();
909 913 }
910 914 }
911 915 if (metadata.autoscroll !== undefined) {
912 916 this.collapsed = metadata.collapsed;
913 917 if (metadata.collapsed) {
914 918 this.collapse_output();
915 919 } else {
916 920 this.expand_output();
917 921 }
918 922 }
919 923 };
920 924
921 925
922 926 OutputArea.prototype.toJSON = function () {
923 927 return this.outputs;
924 928 };
925 929
926 930 /**
927 931 * Class properties
928 932 **/
929 933
930 934 /**
931 935 * Threshold to trigger autoscroll when the OutputArea is resized,
932 936 * typically when new outputs are added.
933 937 *
934 938 * Behavior is undefined if autoscroll is lower than minimum_scroll_threshold,
935 939 * unless it is < 0, in which case autoscroll will never be triggered
936 940 *
937 941 * @property auto_scroll_threshold
938 942 * @type Number
939 943 * @default 100
940 944 *
941 945 **/
942 946 OutputArea.auto_scroll_threshold = 100;
943 947
944 948 /**
945 949 * Lower limit (in lines) for OutputArea to be made scrollable. OutputAreas
946 950 * shorter than this are never scrolled.
947 951 *
948 952 * @property minimum_scroll_threshold
949 953 * @type Number
950 954 * @default 20
951 955 *
952 956 **/
953 957 OutputArea.minimum_scroll_threshold = 20;
954 958
955 959
956 960 OutputArea.display_order = [
957 961 'application/javascript',
958 962 'text/html',
959 963 'text/markdown',
960 964 'text/latex',
961 965 'image/svg+xml',
962 966 'image/png',
963 967 'image/jpeg',
964 968 'application/pdf',
965 969 'text/plain'
966 970 ];
967 971
968 972 OutputArea.append_map = {
969 973 "text/plain" : append_text,
970 974 "text/html" : append_html,
971 975 "text/markdown": append_markdown,
972 976 "image/svg+xml" : append_svg,
973 977 "image/png" : append_png,
974 978 "image/jpeg" : append_jpeg,
975 979 "text/latex" : append_latex,
976 980 "application/javascript" : append_javascript,
977 981 "application/pdf" : append_pdf
978 982 };
979 983
980 984 // For backwards compatability.
981 985 IPython.OutputArea = OutputArea;
982 986
983 987 return {'OutputArea': OutputArea};
984 988 });
General Comments 0
You need to be logged in to leave comments. Login now