diff --git a/IPython/html/static/notebook/js/cell.js b/IPython/html/static/notebook/js/cell.js index 9e2aac1..b54ade3 100644 --- a/IPython/html/static/notebook/js/cell.js +++ b/IPython/html/static/notebook/js/cell.js @@ -53,7 +53,9 @@ define([ get: function() { return that._metadata; }, set: function(value) { that._metadata = value; - that.celltoolbar.rebuild(); + if (that.celltoolbar) { + that.celltoolbar.rebuild(); + } } }); @@ -194,11 +196,11 @@ define([ if((cur.line !== 0 || cur.ch !==0) && event.keyCode === 38){ event._ipkmIgnore = true; } - var nLastLine = editor.lastLine() - if( ( event.keyCode === 40) - && (( cur.line !== nLastLine) - || ( cur.ch !== editor.getLineHandle(nLastLine).text.length)) - ){ + var nLastLine = editor.lastLine(); + if ((event.keyCode === 40) && + ((cur.line !== nLastLine) || + (cur.ch !== editor.getLineHandle(nLastLine).text.length)) + ) { event._ipkmIgnore = true; } // if this is an edit_shortcuts shortcut, the global keyboard/shortcut @@ -255,6 +257,14 @@ define([ }; /** + * should be overritten by subclass + * @method execute + */ + Cell.prototype.execute = function () { + return; + }; + + /** * handle cell level logic when a cell is rendered * @method render * @return is the action being taken @@ -386,7 +396,9 @@ define([ * @method refresh */ Cell.prototype.refresh = function () { - this.code_mirror.refresh(); + if (this.code_mirror) { + this.code_mirror.refresh(); + } }; /** @@ -590,8 +602,74 @@ define([ this.code_mirror.setOption('mode', default_mode); }; + var UnrecognizedCell = function (options) { + /** Constructor for unrecognized cells */ + Cell.apply(this, arguments); + this.cell_type = 'unrecognized'; + this.celltoolbar = null; + this.data = {}; + + Object.seal(this); + }; + + UnrecognizedCell.prototype = Object.create(Cell.prototype); + + + // cannot merge or split unrecognized cells + UnrecognizedCell.prototype.is_mergeable = function () { + return false; + }; + + UnrecognizedCell.prototype.is_splittable = function () { + return false; + }; + + UnrecognizedCell.prototype.toJSON = function () { + // deepcopy the metadata so copied cells don't share the same object + return JSON.parse(JSON.stringify(this.data)); + }; + + UnrecognizedCell.prototype.fromJSON = function (data) { + this.data = data; + if (data.metadata !== undefined) { + this.metadata = data.metadata; + } else { + data.metadata = this.metadata; + } + this.element.find('.inner_cell').find("a").text("Unrecognized cell type: " + data.cell_type); + }; + + UnrecognizedCell.prototype.create_element = function () { + Cell.prototype.create_element.apply(this, arguments); + var cell = this.element = $("
").addClass('cell unrecognized_cell'); + cell.attr('tabindex','2'); + + var prompt = $('
').addClass('prompt input_prompt'); + cell.append(prompt); + var inner_cell = $('
').addClass('inner_cell'); + inner_cell.append( + $("") + .attr("href", "#") + .text("Unrecognized cell type") + ); + cell.append(inner_cell); + this.element = cell; + }; + + UnrecognizedCell.prototype.bind_events = function () { + Cell.prototype.bind_events.apply(this, arguments); + var cell = this; + + this.element.find('.inner_cell').find("a").click(function () { + cell.events.trigger('unrecognized_cell.Cell', {cell: cell}) + }); + }; + // Backwards compatibility. IPython.Cell = Cell; - return {'Cell': Cell}; + return { + Cell: Cell, + UnrecognizedCell: UnrecognizedCell + }; }); diff --git a/IPython/html/static/notebook/js/notebook.js b/IPython/html/static/notebook/js/notebook.js index 4eecc19..f3f1c64 100644 --- a/IPython/html/static/notebook/js/notebook.js +++ b/IPython/html/static/notebook/js/notebook.js @@ -6,6 +6,7 @@ define([ 'jquery', 'base/js/utils', 'base/js/dialog', + 'notebook/js/cell', 'notebook/js/textcell', 'notebook/js/codecell', 'services/sessions/session', @@ -22,13 +23,14 @@ define([ 'notebook/js/scrollmanager' ], function ( IPython, - $, - utils, - dialog, - textcell, - codecell, + $, + utils, + dialog, + cellmod, + textcell, + codecell, session, - celltoolbar, + celltoolbar, marked, CodeMirror, runMode, @@ -147,7 +149,7 @@ define([ this.minimum_autosave_interval = 120000; this.notebook_name_blacklist_re = /[\/\\:]/; this.nbformat = 4; // Increment this when changing the nbformat - this.nbformat_minor = 0; // Increment this when changing the nbformat + this.nbformat_minor = this.current_nbformat_minor = 0; // Increment this when changing the nbformat this.codemirror_mode = 'ipython'; this.create_elements(); this.bind_events(); @@ -211,6 +213,14 @@ define([ that.dirty = true; }); + this.events.on('unrecognized_cell.Cell', function () { + that.warn_nbformat_minor(); + }); + + this.events.on('unrecognized_output.OutputArea', function () { + that.warn_nbformat_minor(); + }); + this.events.on('set_dirty.Notebook', function (event, data) { that.dirty = data.value; }); @@ -304,6 +314,28 @@ define([ return null; }; }; + + Notebook.prototype.warn_nbformat_minor = function (event) { + // trigger a warning dialog about missing functionality from newer minor versions + var v = 'v' + this.nbformat + '.'; + var orig_vs = v + this.nbformat_minor; + var this_vs = v + this.current_nbformat_minor; + var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " + + this_vs + ". You can still work with this notebook, but cell and output types " + + "introduced in later notebook versions will not be available."; + + dialog.modal({ + notebook: this, + keyboard_manager: this.keyboard_manager, + title : "Newer Notebook", + body : msg, + buttons : { + OK : { + "class" : "btn-danger" + } + } + }); + } /** * Set the dirty flag, and trigger the set_dirty.Notebook event @@ -900,7 +932,8 @@ define([ cell = new textcell.RawCell(cell_options); break; default: - console.log("invalid cell type: ", type); + console.log("Unrecognized cell type: ", type, cellmod); + cell = new cellmod.UnrecognizedCell(cell_options); } if(this._insert_element_at_index(cell.element,index)) { @@ -2222,26 +2255,8 @@ define([ } } }); - } else if (orig_nbformat_minor !== undefined && nbmodel.nbformat_minor < orig_nbformat_minor) { - var that = this; - var orig_vs = 'v' + nbmodel.nbformat + '.' + orig_nbformat_minor; - var this_vs = 'v' + nbmodel.nbformat + '.' + this.nbformat_minor; - msg = "This notebook is version " + orig_vs + ", but we only fully support up to " + - this_vs + ". You can still work with this notebook, but some features " + - "introduced in later notebook versions may not be available."; - - dialog.modal({ - notebook: this, - keyboard_manager: this.keyboard_manager, - title : "Newer Notebook", - body : msg, - buttons : { - OK : { - class : "btn-danger" - } - } - }); - + } else if (this.nbformat_minor < nbmodel.nbformat_minor) { + this.nbformat_minor = nbmodel.nbformat_minor; } // Create the session after the notebook is completely loaded to prevent diff --git a/IPython/html/static/notebook/js/outputarea.js b/IPython/html/static/notebook/js/outputarea.js index 283c491..ae77260 100644 --- a/IPython/html/static/notebook/js/outputarea.js +++ b/IPython/html/static/notebook/js/outputarea.js @@ -245,7 +245,7 @@ define([ 'text/plain' ]; - OutputArea.prototype.validate_output = function (json) { + OutputArea.prototype.validate_mimebundle = function (json) { // scrub invalid outputs var data = json.data; $.map(OutputArea.output_types, function(key){ @@ -263,11 +263,6 @@ define([ OutputArea.prototype.append_output = function (json) { this.expand(); - // validate output data types - if (json.data) { - json = this.validate_output(json); - } - // Clear the output if clear is queued. var needs_height_reset = false; if (this.clear_queued) { @@ -276,14 +271,25 @@ define([ } var record_output = true; - - if (json.output_type === 'execute_result') { - this.append_execute_result(json); - } else if (json.output_type === 'error') { - this.append_error(json); - } else if (json.output_type === 'stream') { - // append_stream might have merged the output with earlier stream output - record_output = this.append_stream(json); + switch(json.output_type) { + case 'execute_result': + json = this.validate_mimebundle(json); + this.append_execute_result(json); + break; + case 'stream': + // append_stream might have merged the output with earlier stream output + record_output = this.append_stream(json); + break; + case 'error': + this.append_error(json); + break; + case 'display_data': + // append handled below + json = this.validate_mimebundle(json); + break; + default: + console.log("unrecognized output type: " + json.output_type); + this.append_unrecognized(json); } // We must release the animation fixed height in a callback since Gecko @@ -485,6 +491,23 @@ define([ }; + OutputArea.prototype.append_unrecognized = function (json) { + var that = this; + var toinsert = this.create_output_area(); + var subarea = $('
').addClass('output_subarea output_unrecognized'); + toinsert.append(subarea); + subarea.append( + $("") + .attr("href", "#") + .text("Unrecognized output: " + json.output_type) + .click(function () { + that.events.trigger('unrecognized_output.OutputArea', {output: json}) + }) + ); + this._safe_append(toinsert); + }; + + OutputArea.prototype.append_display_data = function (json, handle_inserted) { var toinsert = this.create_output_area(); if (this.append_mime_type(json, toinsert, handle_inserted)) { diff --git a/IPython/html/static/notebook/js/textcell.js b/IPython/html/static/notebook/js/textcell.js index 3cb76ab..012e632 100644 --- a/IPython/html/static/notebook/js/textcell.js +++ b/IPython/html/static/notebook/js/textcell.js @@ -338,7 +338,7 @@ define([ var textcell = { TextCell: TextCell, MarkdownCell: MarkdownCell, - RawCell: RawCell, + RawCell: RawCell }; return textcell; }); diff --git a/IPython/html/static/notebook/less/cell.less b/IPython/html/static/notebook/less/cell.less index 8bf3f85..ed4ad4a 100644 --- a/IPython/html/static/notebook/less/cell.less +++ b/IPython/html/static/notebook/less/cell.less @@ -61,3 +61,34 @@ div.prompt:empty { padding-top: 0; padding-bottom: 0; } + +div.unrecognized_cell { + // from text_cell + padding: 5px 5px 5px 0px; + .hbox(); + + .inner_cell { + .border-radius(@border-radius-base); + padding: 5px; + font-weight: bold; + color: red; + border: 1px solid @light_border_color; + background: darken(@cell_background, 5%); + // remove decoration from link + a { + color: inherit; + text-decoration: none; + + &:hover { + color: inherit; + text-decoration: none; + } + } + } +} +@media (max-width: 480px) { + // remove prompt indentation on small screens + div.unrecognized_cell > div.prompt { + display: none; + } +} diff --git a/IPython/html/static/notebook/less/outputarea.less b/IPython/html/static/notebook/less/outputarea.less index 033359b..07f8276 100644 --- a/IPython/html/static/notebook/less/outputarea.less +++ b/IPython/html/static/notebook/less/outputarea.less @@ -172,3 +172,19 @@ input.raw_input:focus { p.p-space { margin-bottom: 10px; } + +div.output_unrecognized { + padding: 5px; + font-weight: bold; + color: red; + // remove decoration from link + a { + color: inherit; + text-decoration: none; + + &:hover { + color: inherit; + text-decoration: none; + } + } +} \ No newline at end of file diff --git a/IPython/html/static/style/ipython.min.css b/IPython/html/static/style/ipython.min.css index 09e2df2..5b5c4e3 100644 --- a/IPython/html/static/style/ipython.min.css +++ b/IPython/html/static/style/ipython.min.css @@ -419,6 +419,44 @@ div.prompt:empty { padding-top: 0; padding-bottom: 0; } +div.unrecognized_cell { + padding: 5px 5px 5px 0px; + /* Old browsers */ + display: -webkit-box; + -webkit-box-orient: horizontal; + -webkit-box-align: stretch; + display: -moz-box; + -moz-box-orient: horizontal; + -moz-box-align: stretch; + display: box; + box-orient: horizontal; + box-align: stretch; + /* Modern browsers */ + display: flex; + flex-direction: row; + align-items: stretch; +} +div.unrecognized_cell .inner_cell { + border-radius: 4px; + padding: 5px; + font-weight: bold; + color: red; + border: 1px solid #cfcfcf; + background: #eaeaea; +} +div.unrecognized_cell .inner_cell a { + color: inherit; + text-decoration: none; +} +div.unrecognized_cell .inner_cell a:hover { + color: inherit; + text-decoration: none; +} +@media (max-width: 480px) { + div.unrecognized_cell > div.prompt { + display: none; + } +} /* any special styling for code cells that are currently running goes here */ div.input { page-break-inside: avoid; @@ -888,6 +926,19 @@ input.raw_input:focus { p.p-space { margin-bottom: 10px; } +div.output_unrecognized { + padding: 5px; + font-weight: bold; + color: red; +} +div.output_unrecognized a { + color: inherit; + text-decoration: none; +} +div.output_unrecognized a:hover { + color: inherit; + text-decoration: none; +} .rendered_html { color: #000000; /* any extras will just be numbers: */ diff --git a/IPython/html/static/style/style.min.css b/IPython/html/static/style/style.min.css index 8a9a28b..568261d 100644 --- a/IPython/html/static/style/style.min.css +++ b/IPython/html/static/style/style.min.css @@ -8291,6 +8291,44 @@ div.prompt:empty { padding-top: 0; padding-bottom: 0; } +div.unrecognized_cell { + padding: 5px 5px 5px 0px; + /* Old browsers */ + display: -webkit-box; + -webkit-box-orient: horizontal; + -webkit-box-align: stretch; + display: -moz-box; + -moz-box-orient: horizontal; + -moz-box-align: stretch; + display: box; + box-orient: horizontal; + box-align: stretch; + /* Modern browsers */ + display: flex; + flex-direction: row; + align-items: stretch; +} +div.unrecognized_cell .inner_cell { + border-radius: 4px; + padding: 5px; + font-weight: bold; + color: red; + border: 1px solid #cfcfcf; + background: #eaeaea; +} +div.unrecognized_cell .inner_cell a { + color: inherit; + text-decoration: none; +} +div.unrecognized_cell .inner_cell a:hover { + color: inherit; + text-decoration: none; +} +@media (max-width: 480px) { + div.unrecognized_cell > div.prompt { + display: none; + } +} /* any special styling for code cells that are currently running goes here */ div.input { page-break-inside: avoid; @@ -8760,6 +8798,19 @@ input.raw_input:focus { p.p-space { margin-bottom: 10px; } +div.output_unrecognized { + padding: 5px; + font-weight: bold; + color: red; +} +div.output_unrecognized a { + color: inherit; + text-decoration: none; +} +div.output_unrecognized a:hover { + color: inherit; + text-decoration: none; +} .rendered_html { color: #000000; /* any extras will just be numbers: */ diff --git a/IPython/nbformat/tests/test4plus.ipynb b/IPython/nbformat/tests/test4plus.ipynb index 5ee0da4..38859b5 100644 --- a/IPython/nbformat/tests/test4plus.ipynb +++ b/IPython/nbformat/tests/test4plus.ipynb @@ -306,6 +306,41 @@ "from IPython.display import Image\n", "Image(\"http://ipython.org/_static/IPy_header.png\")" ] + }, + { + "cell_type": "future cell", + "metadata": {}, + "key": "value" + }, + { + "cell_type": "code", + "execution_count": 99, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "hello\n" + ] + }, + { + "output_type": "future output", + "some key": [ + "some data" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "hello again\n" + ] + } + ], + "source": [ + "future_output()" + ] } ], "metadata": {}, diff --git a/IPython/nbformat/v4/nbformat.v4.schema.json b/IPython/nbformat/v4/nbformat.v4.schema.json index 988b6ac..317a80b 100644 --- a/IPython/nbformat/v4/nbformat.v4.schema.json +++ b/IPython/nbformat/v4/nbformat.v4.schema.json @@ -54,18 +54,19 @@ "cells": { "description": "Array of cells of the current notebook.", "type": "array", - "items": { - "type": "object", - "oneOf": [ - {"$ref": "#/definitions/raw_cell"}, - {"$ref": "#/definitions/markdown_cell"}, - {"$ref": "#/definitions/code_cell"} - ] - } + "items": {"$ref": "#/definitions/cell"} } }, "definitions": { + "cell": { + "type": "object", + "oneOf": [ + {"$ref": "#/definitions/raw_cell"}, + {"$ref": "#/definitions/markdown_cell"}, + {"$ref": "#/definitions/code_cell"} + ] + }, "raw_cell": { "description": "Notebook raw nbconvert cell.", @@ -157,6 +158,31 @@ } } }, + + "unrecognized_cell": { + "description": "Unrecognized cell from a future minor-revision to the notebook format.", + "type": "object", + "additionalProperties": true, + "required": ["cell_type", "metadata"], + "properties": { + "cell_type": { + "description": "String identifying the type of cell.", + "not" : { + "enum": ["markdown", "code", "raw"] + } + }, + "metadata": { + "description": "Cell-level metadata.", + "type": "object", + "properties": { + "name": {"$ref": "#/definitions/misc/metadata_name"}, + "tags": {"$ref": "#/definitions/misc/metadata_tags"} + }, + "additionalProperties": true + } + } + }, + "output": { "type": "object", "oneOf": [ @@ -249,6 +275,21 @@ } }, + "unrecognized_output": { + "description": "Unrecognized output from a future minor-revision to the notebook format.", + "type": "object", + "additionalProperties": true, + "required": ["output_type"], + "properties": { + "output_type": { + "description": "Type of cell output.", + "not": { + "enum": ["execute_result", "display_data", "stream", "error"] + } + } + } + }, + "misc": { "metadata_name": { "description": "The cell's name. If present, must be a non-empty string.", diff --git a/IPython/nbformat/validator.py b/IPython/nbformat/validator.py index a4fcd86..c3f00fc 100644 --- a/IPython/nbformat/validator.py +++ b/IPython/nbformat/validator.py @@ -30,7 +30,6 @@ def _relax_additional_properties(obj): if isinstance(obj, dict): for key, value in obj.items(): if key == 'additionalProperties': - print(obj) value = True else: value = _relax_additional_properties(value) @@ -40,6 +39,15 @@ def _relax_additional_properties(obj): obj[i] = _relax_additional_properties(value) return obj +def _allow_undefined(schema): + schema['definitions']['cell']['oneOf'].append( + {"$ref": "#/definitions/unrecognized_cell"} + ) + schema['definitions']['output']['oneOf'].append( + {"$ref": "#/definitions/unrecognized_output"} + ) + return schema + def get_validator(version=None, version_minor=None): """Load the JSON schema into a Validator""" if version is None: @@ -66,6 +74,8 @@ def get_validator(version=None, version_minor=None): if current_minor < version_minor: # notebook from the future, relax all `additionalProperties: False` requirements schema_json = _relax_additional_properties(schema_json) + # and allow undefined cell types and outputs + schema_json = _allow_undefined(schema_json) validators[version_tuple] = Validator(schema_json) return validators[version_tuple]