Merge pull request #4533 from ivanov/display-isolated

propagate display metadata to all mimetypes

Additionally, this PR removes in-memory short keys in favor of using mimetype keys within the notebook javascript code, and tests toJSON/fromJSON codepaths for all of the supported mimetypes.

The change fixes the dropping of unrecognized mime-types from the notebook document.
pull/37/head
Min RK 12 years ago
commit 2d871ff682

@ -239,56 +239,55 @@ var IPython = (function (IPython) {
json.text = content.data;
json.stream = content.name;
} else if (msg_type === "display_data") {
json = this.convert_mime_types(json, content.data);
json.metadata = this.convert_mime_types({}, content.metadata);
json = content.data;
json.output_type = msg_type;
json.metadata = content.metadata;
} else if (msg_type === "pyout") {
json = content.data;
json.output_type = msg_type;
json.metadata = content.metadata;
json.prompt_number = content.execution_count;
json = this.convert_mime_types(json, content.data);
json.metadata = this.convert_mime_types({}, content.metadata);
} else if (msg_type === "pyerr") {
json.ename = content.ename;
json.evalue = content.evalue;
json.traceback = content.traceback;
}
// append with dynamic=true
this.append_output(json, true);
this.append_output(json);
};
OutputArea.mime_map = {
"text/plain" : "text",
"text/html" : "html",
"image/svg+xml" : "svg",
"image/png" : "png",
"image/jpeg" : "jpeg",
"text/latex" : "latex",
"application/json" : "json",
"application/javascript" : "javascript",
};
OutputArea.mime_map_r = {
"text" : "text/plain",
"html" : "text/html",
"svg" : "image/svg+xml",
"png" : "image/png",
"jpeg" : "image/jpeg",
"latex" : "text/latex",
"json" : "application/json",
"javascript" : "application/javascript",
};
OutputArea.prototype.convert_mime_types = function (json, data) {
if (data === undefined) {
return json;
}
if (data['text/plain'] !== undefined) {
json.text = data['text/plain'];
}
if (data['text/html'] !== undefined) {
json.html = data['text/html'];
}
if (data['image/svg+xml'] !== undefined) {
json.svg = data['image/svg+xml'];
}
if (data['image/png'] !== undefined) {
json.png = data['image/png'];
OutputArea.prototype.rename_keys = function (data, key_map) {
var remapped = {};
for (var key in data) {
var new_key = key_map[key] || key;
remapped[new_key] = data[key];
}
if (data['image/jpeg'] !== undefined) {
json.jpeg = data['image/jpeg'];
}
if (data['text/latex'] !== undefined) {
json.latex = data['text/latex'];
}
if (data['application/json'] !== undefined) {
json.json = data['application/json'];
}
if (data['application/javascript'] !== undefined) {
json.javascript = data['application/javascript'];
}
return json;
return remapped;
};
OutputArea.prototype.append_output = function (json, dynamic) {
// If dynamic is true, javascript output will be eval'd.
OutputArea.prototype.append_output = function (json) {
this.expand();
// Clear the output if clear is queued.
var needs_height_reset = false;
@ -298,11 +297,11 @@ var IPython = (function (IPython) {
}
if (json.output_type === 'pyout') {
this.append_pyout(json, dynamic);
this.append_pyout(json);
} else if (json.output_type === 'pyerr') {
this.append_pyerr(json);
} else if (json.output_type === 'display_data') {
this.append_display_data(json, dynamic);
this.append_display_data(json);
} else if (json.output_type === 'stream') {
this.append_stream(json);
}
@ -328,9 +327,19 @@ var IPython = (function (IPython) {
};
OutputArea.prototype.create_output_subarea = function(md, classes) {
function _get_metadata_key(metadata, key, mime) {
var mime_md = metadata[mime];
// mime-specific higher priority
if (mime_md && mime_md[key] !== undefined) {
return mime_md[key];
}
// fallback on global
return metadata[key];
}
OutputArea.prototype.create_output_subarea = function(md, classes, mime) {
var subarea = $('<div/>').addClass('output_subarea').addClass(classes);
if (md['isolated']) {
if (_get_metadata_key(md, 'isolated', mime)) {
// Create an iframe to isolate the subarea from the rest of the
// document
var iframe = $('<iframe/>').addClass('box-flex1');
@ -403,16 +412,16 @@ var IPython = (function (IPython) {
};
OutputArea.prototype.append_pyout = function (json, dynamic) {
OutputArea.prototype.append_pyout = function (json) {
var n = json.prompt_number || ' ';
var toinsert = this.create_output_area();
if (this.prompt_area) {
toinsert.find('div.prompt').addClass('output_prompt').html('Out[' + n + ']:');
}
this.append_mime_type(json, toinsert, dynamic);
this.append_mime_type(json, toinsert);
this._safe_append(toinsert);
// If we just output latex, typeset it.
if ((json.latex !== undefined) || (json.html !== undefined)) {
if ((json['text/latex'] !== undefined) || (json['text/html'] !== undefined)) {
this.typeset();
}
};
@ -470,37 +479,36 @@ var IPython = (function (IPython) {
};
OutputArea.prototype.append_display_data = function (json, dynamic) {
OutputArea.prototype.append_display_data = function (json) {
var toinsert = this.create_output_area();
if (this.append_mime_type(json, toinsert, dynamic)) {
if (this.append_mime_type(json, toinsert)) {
this._safe_append(toinsert);
// If we just output latex, typeset it.
if ( (json.latex !== undefined) || (json.html !== undefined) ) {
if ((json['text/latex'] !== undefined) || (json['text/html'] !== undefined)) {
this.typeset();
}
}
};
OutputArea.display_order = ['javascript','html','latex','svg','png','jpeg','text'];
OutputArea.display_order = [
'application/javascript',
'text/html',
'text/latex',
'image/svg+xml',
'image/png',
'image/jpeg',
'text/plain'
];
OutputArea.prototype.append_mime_type = function (json, element) {
OutputArea.prototype.append_mime_type = function (json, element, dynamic) {
for(var type_i in OutputArea.display_order){
for (var type_i in OutputArea.display_order) {
var type = OutputArea.display_order[type_i];
if(json[type] != undefined ){
var md = {};
if (json.metadata && json.metadata[type]) {
md = json.metadata[type];
};
if(type == 'javascript'){
if (dynamic) {
this.append_javascript(json.javascript, md, element, dynamic);
return true;
}
} else {
this['append_'+type](json[type], md, element);
return true;
}
return false;
var append = OutputArea.append_map[type];
if ((json[type] !== undefined) && append) {
var md = json.metadata || {};
append.apply(this, [json[type], md, element]);
return true;
}
}
return false;
@ -508,7 +516,8 @@ var IPython = (function (IPython) {
OutputArea.prototype.append_html = function (html, md, element) {
var toinsert = this.create_output_subarea(md, "output_html rendered_html");
var type = 'text/html';
var toinsert = this.create_output_subarea(md, "output_html rendered_html", type);
IPython.keyboard_manager.register_events(toinsert);
toinsert.append(html);
element.append(toinsert);
@ -517,7 +526,8 @@ var IPython = (function (IPython) {
OutputArea.prototype.append_javascript = function (js, md, container) {
// We just eval the JS code, element appears in the local scope.
var element = this.create_output_subarea(md, "output_javascript");
var type = 'application/javascript';
var element = this.create_output_subarea(md, "output_javascript", type);
IPython.keyboard_manager.register_events(element);
container.append(element);
try {
@ -530,7 +540,8 @@ var IPython = (function (IPython) {
OutputArea.prototype.append_text = function (data, md, element, extra_class) {
var toinsert = this.create_output_subarea(md, "output_text");
var type = 'text/plain';
var toinsert = this.create_output_subarea(md, "output_text", type);
// escape ANSI & HTML specials in plaintext:
data = utils.fixConsole(data);
data = utils.fixCarriageReturn(data);
@ -544,7 +555,8 @@ var IPython = (function (IPython) {
OutputArea.prototype.append_svg = function (svg, md, element) {
var toinsert = this.create_output_subarea(md, "output_svg");
var type = 'image/svg+xml';
var toinsert = this.create_output_subarea(md, "output_svg", type);
toinsert.append(svg);
element.append(toinsert);
};
@ -578,7 +590,8 @@ var IPython = (function (IPython) {
OutputArea.prototype.append_png = function (png, md, element) {
var toinsert = this.create_output_subarea(md, "output_png");
var type = 'image/png';
var toinsert = this.create_output_subarea(md, "output_png", type);
var img = $("<img/>");
img[0].setAttribute('src','data:image/png;base64,'+png);
if (md['height']) {
@ -594,7 +607,8 @@ var IPython = (function (IPython) {
OutputArea.prototype.append_jpeg = function (jpeg, md, element) {
var toinsert = this.create_output_subarea(md, "output_jpeg");
var type = 'image/jpeg';
var toinsert = this.create_output_subarea(md, "output_jpeg", type);
var img = $("<img/>").attr('src','data:image/jpeg;base64,'+jpeg);
if (md['height']) {
img.attr('height', md['height']);
@ -611,11 +625,23 @@ var IPython = (function (IPython) {
OutputArea.prototype.append_latex = function (latex, md, element) {
// This method cannot do the typesetting because the latex first has to
// be on the page.
var toinsert = this.create_output_subarea(md, "output_latex");
var type = 'text/latex';
var toinsert = this.create_output_subarea(md, "output_latex", type);
toinsert.append(latex);
element.append(toinsert);
};
OutputArea.append_map = {
"text/plain" : OutputArea.prototype.append_text,
"text/html" : OutputArea.prototype.append_html,
"image/svg+xml" : OutputArea.prototype.append_svg,
"image/png" : OutputArea.prototype.append_png,
"image/jpeg" : OutputArea.prototype.append_jpeg,
"text/latex" : OutputArea.prototype.append_latex,
"application/json" : OutputArea.prototype.append_json,
"application/javascript" : OutputArea.prototype.append_javascript,
};
OutputArea.prototype.append_raw_input = function (msg) {
var that = this;
this.expand();
@ -715,18 +741,46 @@ var IPython = (function (IPython) {
OutputArea.prototype.fromJSON = function (outputs) {
var len = outputs.length;
var data;
// We don't want to display javascript on load, so remove it from the
// display order for the duration of this function call, but be sure to
// put it back in there so incoming messages that contain javascript
// representations get displayed
var js_index = OutputArea.display_order.indexOf('application/javascript');
OutputArea.display_order.splice(js_index, 1);
for (var i=0; i<len; i++) {
// append with dynamic=false.
this.append_output(outputs[i], false);
data = outputs[i];
var msg_type = data.output_type;
if (msg_type === "display_data" || msg_type === "pyout") {
// convert short keys to mime keys
// TODO: remove mapping of short keys when we update to nbformat 4
data = this.rename_keys(data, OutputArea.mime_map_r);
data.metadata = this.rename_keys(data.metadata, OutputArea.mime_map_r);
}
this.append_output(data);
}
// reinsert javascript into display order, see note above
OutputArea.display_order.splice(js_index, 0, 'application/javascript');
};
OutputArea.prototype.toJSON = function () {
var outputs = [];
var len = this.outputs.length;
var data;
for (var i=0; i<len; i++) {
outputs[i] = this.outputs[i];
data = this.outputs[i];
var msg_type = data.output_type;
if (msg_type === "display_data" || msg_type === "pyout") {
// convert mime keys to short keys
data = this.rename_keys(data, OutputArea.mime_map);
data.metadata = this.rename_keys(data.metadata, OutputArea.mime_map);
}
outputs[i] = data;
}
return outputs;
};

@ -1,5 +1,5 @@
//
// Test svg isolation
// Test display isolation
// An object whose metadata contains an "isolated" tag must be isolated
// from the rest of the document. In the case of inline SVGs, this means
// that multiple SVGs have different scopes. This test checks that there
@ -21,10 +21,65 @@ casper.notebook_test(function () {
+ "display_svg(SVG(s2), metadata=dict(isolated=True))\n"
);
cell.execute();
console.log("hello" );
});
this.then(function() {
var fname=this.test.currentTestFile.split('/').pop().toLowerCase();
this.echo(fname)
this.echo(this.currentUrl)
this.evaluate(function (n) {
IPython.notebook.rename(n);
console.write("hello" + n);
IPython.notebook.save_notebook();
}, {n : fname});
this.echo(this.currentUrl)
});
this.then(function() {
url = this.evaluate(function() {
IPython.notebook.rename("foo");
//$("span#notebook_name")[0].click();
//$("input")[0].value = "please-work";
//$(".btn-primary")[0].click();
return document.location.href;
})
this.echo("renamed" + url);
this.echo(this.currentUrl);
});
this.wait_for_output(0);
this.then(function () {
var colors = this.evaluate(function () {
var colors = [];
var ifr = __utils__.findAll("iframe");
var svg1 = ifr[0].contentWindow.document.getElementById('r1');
colors[0] = window.getComputedStyle(svg1)["fill"];
var svg2 = ifr[1].contentWindow.document.getElementById('r2');
colors[1] = window.getComputedStyle(svg2)["fill"];
return colors;
});
this.test.assertEquals(colors && colors[0], '#ff0000', 'display_svg() First svg should be red');
this.test.assertEquals(colors && colors[1], '#000000', 'display_svg() Second svg should be black');
});
// now ensure that we can pass the same metadata dict to plain old display()
this.thenEvaluate(function () {
var cell = IPython.notebook.get_cell(0);
cell.clear_output();
cell.set_text( "from IPython.display import display\n"
+ "display(SVG(s1), metadata=dict(isolated=True))\n"
+ "display(SVG(s2), metadata=dict(isolated=True))\n"
);
cell.execute();
});
this.wait_for_output(0);
// same test as original
this.then(function () {
var colors = this.evaluate(function () {
var colors = [];
@ -36,7 +91,7 @@ casper.notebook_test(function () {
return colors;
});
this.test.assertEquals(colors[0], '#ff0000', 'First svg should be red');
this.test.assertEquals(colors[1], '#000000', 'Second svg should be black');
this.test.assertEquals(colors && colors[0], '#ff0000', 'display() First svg should be red');
this.test.assertEquals(colors && colors[1], '#000000', 'display() Second svg should be black');
});
});

@ -0,0 +1,235 @@
// Test opening a rich notebook, saving it, and reopening it again.
//
//toJSON fromJSON toJSON and do a string comparison
// this is just a copy of OutputArea.mime_mape_r in IPython/html/static/notebook/js/outputarea.js
mime = {
"text" : "text/plain",
"html" : "text/html",
"svg" : "image/svg+xml",
"png" : "image/png",
"jpeg" : "image/jpeg",
"latex" : "text/latex",
"json" : "application/json",
"javascript" : "application/javascript",
};
var black_dot_jpeg="\"\"\"/9j/4AAQSkZJRgABAQEASABIAAD/2wBDACodICUgGiolIiUvLSoyP2lEPzo6P4FcYUxpmYagnpaG\nk5GovfLNqLPltZGT0v/V5fr/////o8v///////L/////2wBDAS0vLz83P3xERHz/rpOu////////\n////////////////////////////////////////////////////////////wgARCAABAAEDAREA\nAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAABP/EABQBAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEA\nAhADEAAAARn/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBAAEFAn//xAAUEQEAAAAAAAAAAAAA\nAAAAAAAA/9oACAEDAQE/AX//xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oACAECAQE/AX//xAAUEAEA\nAAAAAAAAAAAAAAAAAAAA/9oACAEBAAY/An//xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBAAE/\nIX//2gAMAwEAAgADAAAAEB//xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oACAEDAQE/EH//xAAUEQEA\nAAAAAAAAAAAAAAAAAAAA/9oACAECAQE/EH//xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBAAE/\nEH//2Q==\"\"\"";
var black_dot_png = '\"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAWJLR0QA\\niAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB94BCRQnOqNu0b4AAAAKSURBVAjXY2AA\\nAAACAAHiIbwzAAAAAElFTkSuQmCC\"';
var svg = "\"<svg width='1cm' height='1cm' viewBox='0 0 1000 500'><defs><style>rect {fill:red;}; </style></defs><rect id='r1' x='200' y='100' width='600' height='300' /></svg>\"";
// helper function to ensure that the short_name is found in the toJSON
// represetnation, while the original in-memory cell retains its long mimetype
// name, and that fromJSON also gets its long mimetype name
function assert_has(short_name, json, result, result2) {
long_name = mime[short_name];
this.test.assertTrue(json[0].hasOwnProperty(short_name),
'toJSON() representation uses ' + short_name);
this.test.assertTrue(result.hasOwnProperty(long_name),
'toJSON() original embeded JSON keeps ' + long_name);
this.test.assertTrue(result2.hasOwnProperty(long_name),
'fromJSON() embeded ' + short_name + ' gets mime key ' + long_name);
}
// helper function for checkout that the first two cells have a particular
// output_type (either 'pyout' or 'display_data'), and checks the to/fromJSON
// for a set of mimetype keys, using their short names ('javascript', 'text',
// 'png', etc).
function check_output_area(output_type, keys) {
this.wait_for_output(0);
json = this.evaluate(function() {
var json = IPython.notebook.get_cell(0).output_area.toJSON();
// appended cell will initially be empty, lets add it some output
var cell = IPython.notebook.get_cell(1).output_area.fromJSON(json);
return json;
});
var result = this.get_output_cell(0);
var result2 = this.get_output_cell(1);
this.test.assertEquals(result.output_type, output_type,
'testing ' + output_type + ' for ' + keys.join(' and '));
for (var idx in keys) {
assert_has.apply(this, [keys[idx], json, result, result2]);
}
}
// helper function to clear the first two cells, set the text of and execute
// the first one
function clear_and_execute(that, code) {
that.evaluate(function() {
IPython.notebook.get_cell(0).clear_output();
IPython.notebook.get_cell(1).clear_output();
});
that.set_cell_text(0, code);
that.execute_cell(0);
}
casper.notebook_test(function () {
this.evaluate(function () {
var cell = IPython.notebook.get_cell(0);
// "we have to make messes to find out who we are"
cell.set_text([
"%%javascript",
"IPython.notebook.insert_cell_below('code')"
].join('\n')
);
cell.execute();
});
this.wait_for_output(0);
this.then(function ( ) {
var result = this.get_output_cell(0);
var num_cells = this.get_cells_length();
this.test.assertEquals(num_cells, 2, '%%javascript magic works');
this.test.assertTrue(result.hasOwnProperty('application/javascript'),
'testing JS embeded with mime key');
});
//this.thenEvaluate(function() { IPython.notebook.save_notebook(); });
this.then(function ( ) {
check_output_area.apply(this, ['display_data', ['javascript']]);
});
this.then(function() {
clear_and_execute(this, '%lsmagic');
});
this.then(function () {
check_output_area.apply(this, ['pyout', ['text', 'json']]);
});
this.then(function() {
clear_and_execute(this,
"x = %lsmagic\nfrom IPython.display import display; display(x)");
});
this.then(function ( ) {
check_output_area.apply(this, ['display_data', ['text', 'json']]);
});
this.then(function() {
clear_and_execute(this,
"from IPython.display import Latex; Latex('$X^2$')");
});
this.then(function ( ) {
check_output_area.apply(this, ['pyout', ['text', 'latex']]);
});
this.then(function() {
clear_and_execute(this,
"from IPython.display import Latex, display; display(Latex('$X^2$'))");
});
this.then(function ( ) {
check_output_area.apply(this, ['display_data', ['text', 'latex']]);
});
this.then(function() {
clear_and_execute(this,
"from IPython.display import HTML; HTML('<b>it works!</b>')");
});
this.then(function ( ) {
check_output_area.apply(this, ['pyout', ['text', 'html']]);
});
this.then(function() {
clear_and_execute(this,
"from IPython.display import HTML, display; display(HTML('<b>it works!</b>'))");
});
this.then(function ( ) {
check_output_area.apply(this, ['display_data', ['text', 'html']]);
});
this.then(function() {
clear_and_execute(this,
"from IPython.display import Image; Image(" + black_dot_png + ")");
});
this.thenEvaluate(function() { IPython.notebook.save_notebook(); });
this.then(function ( ) {
check_output_area.apply(this, ['pyout', ['text', 'png']]);
});
this.then(function() {
clear_and_execute(this,
"from IPython.display import Image, display; display(Image(" + black_dot_png + "))");
});
this.then(function ( ) {
check_output_area.apply(this, ['display_data', ['text', 'png']]);
});
this.then(function() {
clear_and_execute(this,
"from IPython.display import Image; Image(" + black_dot_jpeg + ", format='jpeg')");
});
this.then(function ( ) {
check_output_area.apply(this, ['pyout', ['text', 'jpeg']]);
});
this.then(function() {
clear_and_execute(this,
"from IPython.display import Image, display; display(Image(" + black_dot_jpeg + ", format='jpeg'))");
});
this.then(function ( ) {
check_output_area.apply(this, ['display_data', ['text', 'jpeg']]);
});
this.then(function() {
clear_and_execute(this,
"from IPython.core.display import SVG; SVG(" + svg + ")");
});
this.then(function ( ) {
check_output_area.apply(this, ['pyout', ['text', 'svg']]);
});
this.then(function() {
clear_and_execute(this,
"from IPython.core.display import SVG, display; display(SVG(" + svg + "))");
});
this.then(function ( ) {
check_output_area.apply(this, ['display_data', ['text', 'svg']]);
});
this.thenEvaluate(function() { IPython.notebook.save_notebook(); });
this.then(function() {
clear_and_execute(this, [
"from IPython.core.formatters import HTMLFormatter",
"x = HTMLFormatter()",
"x.format_type = 'text/superfancymimetype'",
"get_ipython().display_formatter.formatters['text/superfancymimetype'] = x",
"from IPython.display import HTML, display",
'display(HTML("yo"))',
"HTML('hello')"].join('\n')
);
});
this.then(function ( ) {
var long_name = 'text/superfancymimetype';
var result = this.get_output_cell(0);
this.test.assertTrue(result.hasOwnProperty(long_name),
'display_data custom mimetype ' + long_name);
var result = this.get_output_cell(0, 1);
this.test.assertTrue(result.hasOwnProperty(long_name),
'pyout custom mimetype ' + long_name);
});
});

@ -76,12 +76,13 @@ casper.wait_for_output = function (cell_num) {
};
// return the output of a given cell
casper.get_output_cell = function (cell_num) {
var result = casper.evaluate(function (c) {
casper.get_output_cell = function (cell_num, out_num) {
out_num = out_num || 0;
var result = casper.evaluate(function (c, o) {
var cell = IPython.notebook.get_cell(c);
return cell.output_area.outputs[0];
return cell.output_area.outputs[o];
},
{c : cell_num});
{c : cell_num, o : out_num});
return result;
};

Loading…
Cancel
Save