From 3462b34fcac510eb1c5aab093584dda84cb4ca85 Mon Sep 17 00:00:00 2001 From: Julien Rebetez Date: Mon, 19 Oct 2015 23:12:57 +0200 Subject: [PATCH 01/24] =?UTF-8?q?Implement=20markdown=20cell=20attachments?= =?UTF-8?q?.=20Allow=20drag=E2=80=99n=E2=80=99drop=20of=20images=20into=20?= =?UTF-8?q?markdown=20cells.=20See=20#613?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- notebook/static/base/js/security.js | 4 ++ notebook/static/notebook/js/textcell.js | 55 +++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/notebook/static/base/js/security.js b/notebook/static/base/js/security.js index 6e2b34345..f0d1b125a 100644 --- a/notebook/static/base/js/security.js +++ b/notebook/static/base/js/security.js @@ -30,6 +30,10 @@ define([ } } } + // TODO(julienr): This is a ugly hack to work around the fact that + // by default caja doesn't seem to like base64 src for inline markdown + // images. Not sure if we should do that here + ATTRIBS['img::src'] = 0; return caja.sanitizeAttribs(tagName, attribs, opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger); }; diff --git a/notebook/static/notebook/js/textcell.js b/notebook/static/notebook/js/textcell.js index ddab2bdc1..d8b8191ae 100644 --- a/notebook/static/notebook/js/textcell.js +++ b/notebook/static/notebook/js/textcell.js @@ -256,6 +256,50 @@ define([ } }; + /** @method bind_events **/ + MarkdownCell.prototype.bind_events = function () { + TextCell.prototype.bind_events.apply(this); + var that = this; + + // Inline images insertion. When a user drops an image in a markdown + // cell, we do the following : + // - We insert the base64-encoded image into the cell metadata + // attachments directory, keyed by the filename. + // - We insert an img tag with a 'nbdata' src that refers to the + // attachments entry. + // + // Prevent the default code_mirror 'drop' event handler (which inserts + // the file content) if this is a recognized media file + this.code_mirror.on("drop", function(cm, evt) { + var pos = that.code_mirror.getCursor(); + var files = evt.dataTransfer.files; + for (var i = 0; i < files.length; ++i) { + var file = files[i]; + var key = file.name; + // TODO: Do some wildcard mime matching (image/*) + if (file.type == "image/png") { + evt.stopPropagation(); + evt.preventDefault(); + + var reader = new FileReader; + reader.onloadend = function() { + var img_md = ''; + if (that.metadata.attachments === undefined) { + that.metadata.attachments = {}; + } + that.metadata.attachments[key] = { + 'data': reader.result, + 'mime': file.type + } + //var img_md = ''; + that.code_mirror.replaceRange(img_md, pos); + } + reader.readAsDataURL(file); + } + } + }); + }; + /** * @method render */ @@ -290,6 +334,17 @@ define([ }); // links in markdown cells should open in new tabs html.find("a[href]").not('[href^="#"]').attr("target", "_blank"); + // replace nbdata: by the corresponding entry in metadata + // attachments + html.find('img[src^="nbdata:"]').each(function (i, h) { + h = $(h); + var key = h.attr('src').replace(/^nbdata:/, ''); + if (that.metadata.attachments !== undefined && + key in that.metadata.attachments) { + var att = that.metadata.attachments[key]; + h.attr('src', att['data']); + } + }); that.set_rendered(html); that.typeset(); that.events.trigger("rendered.MarkdownCell", {cell: that}); From 398c90b0aa5fa518e34b0ddd410f0fd306e9a028 Mon Sep 17 00:00:00 2001 From: Julien Rebetez Date: Tue, 20 Oct 2015 11:15:07 +0200 Subject: [PATCH 02/24] =?UTF-8?q?Use=20the=20cell=20=E2=80=98attachments?= =?UTF-8?q?=E2=80=99=20property=20instead=20of=20=E2=80=98metadata.attachm?= =?UTF-8?q?ents=E2=80=99.=20Use=20the=20mime-bundle=20storage=20format=20t?= =?UTF-8?q?o=20store=20the=20attachments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- notebook/static/base/js/utils.js | 18 ++++++++++++++ notebook/static/notebook/js/cell.js | 7 ++++++ notebook/static/notebook/js/textcell.js | 31 +++++++++++++++---------- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/notebook/static/base/js/utils.js b/notebook/static/base/js/utils.js index f4514180c..2c6a699a6 100644 --- a/notebook/static/base/js/utils.js +++ b/notebook/static/base/js/utils.js @@ -788,6 +788,23 @@ define([ return MathJax.Hub.Queue(["Typeset", MathJax.Hub, this]); }); }; + + var parse_b64_data_uri = function(uri) { + /** + * Parses a base64 encoded data-uri to extract mimetype and the + * base64 string. + * + * For example, given 'data:image/png;base64,iVBORw', it will return + * ["image/png", "iVBORw"] + * + * Parameters + */ + var regex = /^data:(.+\/.+);base64,(.*)$/; + var matches = uri.match(regex); + var mime = matches[1]; + var b64_data = matches[2]; + return [mime, b64_data]; + }; var time = {}; time.milliseconds = {}; @@ -877,6 +894,7 @@ define([ resolve_promises_dict: resolve_promises_dict, reject: reject, typeset: typeset, + parse_b64_data_uri: parse_b64_data_uri, time: time, format_datetime: format_datetime, datetime_sort_helper: datetime_sort_helper, diff --git a/notebook/static/notebook/js/cell.js b/notebook/static/notebook/js/cell.js index 2a792cae9..21918b94d 100644 --- a/notebook/static/notebook/js/cell.js +++ b/notebook/static/notebook/js/cell.js @@ -70,6 +70,8 @@ define([ } }); + this.attachments = {}; + // backward compat. Object.defineProperty(this, 'cm_config', { get: function() { @@ -472,6 +474,8 @@ define([ var data = {}; // deepcopy the metadata so copied cells don't share the same object data.metadata = JSON.parse(JSON.stringify(this.metadata)); + // same for attachments + data.attachments = JSON.parse(JSON.stringify(this.attachments)); data.cell_type = this.cell_type; return data; }; @@ -484,6 +488,9 @@ define([ if (data.metadata !== undefined) { this.metadata = data.metadata; } + if (data.attachments !== undefined) { + this.attachments = data.attachments; + } }; diff --git a/notebook/static/notebook/js/textcell.js b/notebook/static/notebook/js/textcell.js index d8b8191ae..a6db69c5d 100644 --- a/notebook/static/notebook/js/textcell.js +++ b/notebook/static/notebook/js/textcell.js @@ -263,8 +263,8 @@ define([ // Inline images insertion. When a user drops an image in a markdown // cell, we do the following : - // - We insert the base64-encoded image into the cell metadata - // attachments directory, keyed by the filename. + // - We insert the base64-encoded image into the cell attachments + // directory, keyed by the filename. // - We insert an img tag with a 'nbdata' src that refers to the // attachments entry. // @@ -284,14 +284,19 @@ define([ var reader = new FileReader; reader.onloadend = function() { var img_md = ''; - if (that.metadata.attachments === undefined) { - that.metadata.attachments = {}; + if (that.attachments === undefined) { + that.attachments = {}; } - that.metadata.attachments[key] = { - 'data': reader.result, - 'mime': file.type + that.attachments[key] = {}; + // Strip the "data:image/png;base64," prefix from the data-url + // to turn it into a base64 encoded string + var d = utils.parse_b64_data_uri(reader.result); + if (file.type != d[0]) { + // TODO(julienr): Not sure what we should do in this case + console.log('File type (' + file.type + ') != data-uri ' + + 'type (' + d[0] + ')'); } - //var img_md = ''; + that.attachments[key][file.type] = [d[1]]; that.code_mirror.replaceRange(img_md, pos); } reader.readAsDataURL(file); @@ -339,10 +344,12 @@ define([ html.find('img[src^="nbdata:"]').each(function (i, h) { h = $(h); var key = h.attr('src').replace(/^nbdata:/, ''); - if (that.metadata.attachments !== undefined && - key in that.metadata.attachments) { - var att = that.metadata.attachments[key]; - h.attr('src', att['data']); + + if (that.attachments !== undefined && + key in that.attachments) { + var att = that.attachments[key]; + var mime = Object.keys(att)[0]; + h.attr('src', 'data:' + mime + ';base64,' + att[mime][0]); } }); that.set_rendered(html); From 3414b8385538e809abf4348fa46f0b7da42ba87c Mon Sep 17 00:00:00 2001 From: Julien Rebetez Date: Tue, 20 Oct 2015 11:28:24 +0200 Subject: [PATCH 03/24] Insert markdown markup for attachments images instead of HTML --- notebook/static/notebook/js/textcell.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/notebook/static/notebook/js/textcell.js b/notebook/static/notebook/js/textcell.js index a6db69c5d..552d910a2 100644 --- a/notebook/static/notebook/js/textcell.js +++ b/notebook/static/notebook/js/textcell.js @@ -283,7 +283,8 @@ define([ var reader = new FileReader; reader.onloadend = function() { - var img_md = ''; + var img_md = '![' + key + '](attachment:' + key + ')'; + if (that.attachments === undefined) { that.attachments = {}; } @@ -339,11 +340,11 @@ define([ }); // links in markdown cells should open in new tabs html.find("a[href]").not('[href^="#"]').attr("target", "_blank"); - // replace nbdata: by the corresponding entry in metadata - // attachments - html.find('img[src^="nbdata:"]').each(function (i, h) { + // replace attachment: by the corresponding entry + // in the cell's attachments + html.find('img[src^="attachment:"]').each(function (i, h) { h = $(h); - var key = h.attr('src').replace(/^nbdata:/, ''); + var key = h.attr('src').replace(/^attachment:/, ''); if (that.attachments !== undefined && key in that.attachments) { From 1c6f589ff65b24c9a5b62b117e0ff7230d5d0209 Mon Sep 17 00:00:00 2001 From: Julien Rebetez Date: Tue, 20 Oct 2015 16:52:45 +0200 Subject: [PATCH 04/24] Add attachments cell toolbar option which opens a metadata-like JSON editor. --- notebook/static/base/js/dialog.js | 63 +++++++++++++++++++ .../js/celltoolbarpresets/attachments.js | 48 ++++++++++++++ notebook/static/notebook/js/notebook.js | 2 + 3 files changed, 113 insertions(+) create mode 100644 notebook/static/notebook/js/celltoolbarpresets/attachments.js diff --git a/notebook/static/base/js/dialog.js b/notebook/static/base/js/dialog.js index ac9885e84..02c113597 100644 --- a/notebook/static/base/js/dialog.js +++ b/notebook/static/base/js/dialog.js @@ -207,11 +207,74 @@ define(function(require) { modal_obj.on('shown.bs.modal', function(){ editor.refresh(); }); }; + + var edit_attachments = function (options) { + options.name = options.name || "Cell"; + var error_div = $('
').css('color', 'red'); + var message = + "Manually edit the JSON below to manipulate attachments for this " + options.name + "."; + + var textarea = $('