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