|
|
//----------------------------------------------------------------------------
|
|
|
// Copyright (C) 2008-2012 The IPython Development Team
|
|
|
//
|
|
|
// Distributed under the terms of the BSD License. The full license is in
|
|
|
// the file COPYING, distributed as part of this software.
|
|
|
//----------------------------------------------------------------------------
|
|
|
|
|
|
//============================================================================
|
|
|
// TextCell
|
|
|
//============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
A module that allow to create different type of Text Cell
|
|
|
@module IPython
|
|
|
@namespace IPython
|
|
|
*/
|
|
|
var IPython = (function (IPython) {
|
|
|
"use strict";
|
|
|
|
|
|
// TextCell base class
|
|
|
var keycodes = IPython.keyboard.keycodes;
|
|
|
var security = IPython.security;
|
|
|
|
|
|
/**
|
|
|
* Construct a new TextCell, codemirror mode is by default 'htmlmixed', and cell type is 'text'
|
|
|
* cell start as not redered.
|
|
|
*
|
|
|
* @class TextCell
|
|
|
* @constructor TextCell
|
|
|
* @extend IPython.Cell
|
|
|
* @param {object|undefined} [options]
|
|
|
* @param [options.cm_config] {object} config to pass to CodeMirror, will extend/overwrite default config
|
|
|
* @param [options.placeholder] {string} default string to use when souce in empty for rendering (only use in some TextCell subclass)
|
|
|
*/
|
|
|
var TextCell = function (options) {
|
|
|
// in all TextCell/Cell subclasses
|
|
|
// do not assign most of members here, just pass it down
|
|
|
// in the options dict potentially overwriting what you wish.
|
|
|
// they will be assigned in the base class.
|
|
|
|
|
|
// we cannot put this as a class key as it has handle to "this".
|
|
|
var cm_overwrite_options = {
|
|
|
onKeyEvent: $.proxy(this.handle_keyevent,this)
|
|
|
};
|
|
|
|
|
|
options = this.mergeopt(TextCell,options,{cm_config:cm_overwrite_options});
|
|
|
|
|
|
this.cell_type = this.cell_type || 'text';
|
|
|
|
|
|
IPython.Cell.apply(this, [options]);
|
|
|
|
|
|
this.rendered = false;
|
|
|
};
|
|
|
|
|
|
TextCell.prototype = new IPython.Cell();
|
|
|
|
|
|
TextCell.options_default = {
|
|
|
cm_config : {
|
|
|
extraKeys: {"Tab": "indentMore","Shift-Tab" : "indentLess"},
|
|
|
mode: 'htmlmixed',
|
|
|
lineWrapping : true,
|
|
|
}
|
|
|
};
|
|
|
|
|
|
|
|
|
/**
|
|
|
* Create the DOM element of the TextCell
|
|
|
* @method create_element
|
|
|
* @private
|
|
|
*/
|
|
|
TextCell.prototype.create_element = function () {
|
|
|
IPython.Cell.prototype.create_element.apply(this, arguments);
|
|
|
|
|
|
var cell = $("<div>").addClass('cell text_cell border-box-sizing');
|
|
|
cell.attr('tabindex','2');
|
|
|
|
|
|
var prompt = $('<div/>').addClass('prompt input_prompt');
|
|
|
cell.append(prompt);
|
|
|
var inner_cell = $('<div/>').addClass('inner_cell');
|
|
|
this.celltoolbar = new IPython.CellToolbar(this);
|
|
|
inner_cell.append(this.celltoolbar.element);
|
|
|
var input_area = $('<div/>').addClass('input_area');
|
|
|
this.code_mirror = new CodeMirror(input_area.get(0), this.cm_config);
|
|
|
// The tabindex=-1 makes this div focusable.
|
|
|
var render_area = $('<div/>').addClass('text_cell_render border-box-sizing').
|
|
|
addClass('rendered_html').attr('tabindex','-1');
|
|
|
inner_cell.append(input_area).append(render_area);
|
|
|
cell.append(inner_cell);
|
|
|
this.element = cell;
|
|
|
};
|
|
|
|
|
|
|
|
|
/**
|
|
|
* Bind the DOM evet to cell actions
|
|
|
* Need to be called after TextCell.create_element
|
|
|
* @private
|
|
|
* @method bind_event
|
|
|
*/
|
|
|
TextCell.prototype.bind_events = function () {
|
|
|
IPython.Cell.prototype.bind_events.apply(this);
|
|
|
var that = this;
|
|
|
|
|
|
this.element.dblclick(function () {
|
|
|
if (that.selected === false) {
|
|
|
$([IPython.events]).trigger('select.Cell', {'cell':that});
|
|
|
}
|
|
|
var cont = that.unrender();
|
|
|
if (cont) {
|
|
|
that.focus_editor();
|
|
|
}
|
|
|
});
|
|
|
};
|
|
|
|
|
|
TextCell.prototype.handle_keyevent = function (editor, event) {
|
|
|
|
|
|
// console.log('CM', this.mode, event.which, event.type)
|
|
|
|
|
|
if (this.mode === 'command') {
|
|
|
return true;
|
|
|
} else if (this.mode === 'edit') {
|
|
|
return this.handle_codemirror_keyevent(editor, event);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* This method gets called in CodeMirror's onKeyDown/onKeyPress
|
|
|
* handlers and is used to provide custom key handling.
|
|
|
*
|
|
|
* Subclass should override this method to have custom handeling
|
|
|
*
|
|
|
* @method handle_codemirror_keyevent
|
|
|
* @param {CodeMirror} editor - The codemirror instance bound to the cell
|
|
|
* @param {event} event -
|
|
|
* @return {Boolean} `true` if CodeMirror should ignore the event, `false` Otherwise
|
|
|
*/
|
|
|
TextCell.prototype.handle_codemirror_keyevent = function (editor, event) {
|
|
|
var that = this;
|
|
|
|
|
|
if (event.keyCode === 13 && (event.shiftKey || event.ctrlKey || event.altKey)) {
|
|
|
// Always ignore shift-enter in CodeMirror as we handle it.
|
|
|
return true;
|
|
|
} else if (event.which === keycodes.up && event.type === 'keydown') {
|
|
|
// If we are not at the top, let CM handle the up arrow and
|
|
|
// prevent the global keydown handler from handling it.
|
|
|
if (!that.at_top()) {
|
|
|
event.stop();
|
|
|
return false;
|
|
|
} else {
|
|
|
return true;
|
|
|
};
|
|
|
} else if (event.which === keycodes.down && event.type === 'keydown') {
|
|
|
// If we are not at the bottom, let CM handle the down arrow and
|
|
|
// prevent the global keydown handler from handling it.
|
|
|
if (!that.at_bottom()) {
|
|
|
event.stop();
|
|
|
return false;
|
|
|
} else {
|
|
|
return true;
|
|
|
};
|
|
|
} else if (event.which === keycodes.esc && event.type === 'keydown') {
|
|
|
if (that.code_mirror.options.keyMap === "vim-insert") {
|
|
|
// vim keyMap is active and in insert mode. In this case we leave vim
|
|
|
// insert mode, but remain in notebook edit mode.
|
|
|
// Let' CM handle this event and prevent global handling.
|
|
|
event.stop();
|
|
|
return false;
|
|
|
} else {
|
|
|
// vim keyMap is not active. Leave notebook edit mode.
|
|
|
// Don't let CM handle the event, defer to global handling.
|
|
|
return true;
|
|
|
}
|
|
|
}
|
|
|
return false;
|
|
|
};
|
|
|
|
|
|
// Cell level actions
|
|
|
|
|
|
TextCell.prototype.select = function () {
|
|
|
var cont = IPython.Cell.prototype.select.apply(this);
|
|
|
if (cont) {
|
|
|
if (this.mode === 'edit') {
|
|
|
this.code_mirror.refresh();
|
|
|
}
|
|
|
}
|
|
|
return cont;
|
|
|
};
|
|
|
|
|
|
TextCell.prototype.unrender = function () {
|
|
|
if (this.read_only) return;
|
|
|
var cont = IPython.Cell.prototype.unrender.apply(this);
|
|
|
if (cont) {
|
|
|
var text_cell = this.element;
|
|
|
var output = text_cell.find("div.text_cell_render");
|
|
|
output.hide();
|
|
|
text_cell.find('div.input_area').show();
|
|
|
if (this.get_text() === this.placeholder) {
|
|
|
this.set_text('');
|
|
|
}
|
|
|
this.refresh();
|
|
|
}
|
|
|
return cont;
|
|
|
};
|
|
|
|
|
|
TextCell.prototype.execute = function () {
|
|
|
this.render();
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* setter: {{#crossLink "TextCell/set_text"}}{{/crossLink}}
|
|
|
* @method get_text
|
|
|
* @retrun {string} CodeMirror current text value
|
|
|
*/
|
|
|
TextCell.prototype.get_text = function() {
|
|
|
return this.code_mirror.getValue();
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* @param {string} text - Codemiror text value
|
|
|
* @see TextCell#get_text
|
|
|
* @method set_text
|
|
|
* */
|
|
|
TextCell.prototype.set_text = function(text) {
|
|
|
this.code_mirror.setValue(text);
|
|
|
this.code_mirror.refresh();
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* setter :{{#crossLink "TextCell/set_rendered"}}{{/crossLink}}
|
|
|
* @method get_rendered
|
|
|
* @return {html} html of rendered element
|
|
|
* */
|
|
|
TextCell.prototype.get_rendered = function() {
|
|
|
return this.element.find('div.text_cell_render').html();
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* @method set_rendered
|
|
|
*/
|
|
|
TextCell.prototype.set_rendered = function(text) {
|
|
|
this.element.find('div.text_cell_render').html(text);
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* @method at_top
|
|
|
* @return {Boolean}
|
|
|
*/
|
|
|
TextCell.prototype.at_top = function () {
|
|
|
if (this.rendered) {
|
|
|
return true;
|
|
|
} else {
|
|
|
var cursor = this.code_mirror.getCursor();
|
|
|
if (cursor.line === 0 && cursor.ch === 0) {
|
|
|
return true;
|
|
|
} else {
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* @method at_bottom
|
|
|
* @return {Boolean}
|
|
|
* */
|
|
|
TextCell.prototype.at_bottom = function () {
|
|
|
if (this.rendered) {
|
|
|
return true;
|
|
|
} else {
|
|
|
var cursor = this.code_mirror.getCursor();
|
|
|
if (cursor.line === (this.code_mirror.lineCount()-1) && cursor.ch === this.code_mirror.getLine(cursor.line).length) {
|
|
|
return true;
|
|
|
} else {
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Create Text cell from JSON
|
|
|
* @param {json} data - JSON serialized text-cell
|
|
|
* @method fromJSON
|
|
|
*/
|
|
|
TextCell.prototype.fromJSON = function (data) {
|
|
|
IPython.Cell.prototype.fromJSON.apply(this, arguments);
|
|
|
if (data.cell_type === this.cell_type) {
|
|
|
if (data.source !== undefined) {
|
|
|
this.set_text(data.source);
|
|
|
// make this value the starting point, so that we can only undo
|
|
|
// to this state, instead of a blank cell
|
|
|
this.code_mirror.clearHistory();
|
|
|
// TODO: This HTML needs to be treated as potentially dangerous
|
|
|
// user input and should be handled before set_rendered.
|
|
|
this.set_rendered(data.rendered || '');
|
|
|
this.rendered = false;
|
|
|
this.render();
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
|
|
|
/** Generate JSON from cell
|
|
|
* @return {object} cell data serialised to json
|
|
|
*/
|
|
|
TextCell.prototype.toJSON = function () {
|
|
|
var data = IPython.Cell.prototype.toJSON.apply(this);
|
|
|
data.source = this.get_text();
|
|
|
if (data.source == this.placeholder) {
|
|
|
data.source = "";
|
|
|
}
|
|
|
return data;
|
|
|
};
|
|
|
|
|
|
|
|
|
/**
|
|
|
* @class MarkdownCell
|
|
|
* @constructor MarkdownCell
|
|
|
* @extends IPython.HTMLCell
|
|
|
*/
|
|
|
var MarkdownCell = function (options) {
|
|
|
options = this.mergeopt(MarkdownCell, options);
|
|
|
|
|
|
this.cell_type = 'markdown';
|
|
|
TextCell.apply(this, [options]);
|
|
|
};
|
|
|
|
|
|
MarkdownCell.options_default = {
|
|
|
cm_config: {
|
|
|
mode: 'gfm'
|
|
|
},
|
|
|
placeholder: "Type *Markdown* and LaTeX: $\\alpha^2$"
|
|
|
};
|
|
|
|
|
|
MarkdownCell.prototype = new TextCell();
|
|
|
|
|
|
/**
|
|
|
* @method render
|
|
|
*/
|
|
|
MarkdownCell.prototype.render = function () {
|
|
|
var cont = IPython.TextCell.prototype.render.apply(this);
|
|
|
if (cont) {
|
|
|
var text = this.get_text();
|
|
|
var math = null;
|
|
|
if (text === "") { text = this.placeholder; }
|
|
|
var text_and_math = IPython.mathjaxutils.remove_math(text);
|
|
|
text = text_and_math[0];
|
|
|
math = text_and_math[1];
|
|
|
var html = marked.parser(marked.lexer(text));
|
|
|
html = IPython.mathjaxutils.replace_math(html, math);
|
|
|
html = security.sanitize_html(html);
|
|
|
html = $(html);
|
|
|
// links in markdown cells should open in new tabs
|
|
|
html.find("a[href]").not('[href^="#"]').attr("target", "_blank");
|
|
|
this.set_rendered(html);
|
|
|
this.element.find('div.input_area').hide();
|
|
|
this.element.find("div.text_cell_render").show();
|
|
|
this.typeset();
|
|
|
}
|
|
|
return cont;
|
|
|
};
|
|
|
|
|
|
|
|
|
// RawCell
|
|
|
|
|
|
/**
|
|
|
* @class RawCell
|
|
|
* @constructor RawCell
|
|
|
* @extends IPython.TextCell
|
|
|
*/
|
|
|
var RawCell = function (options) {
|
|
|
|
|
|
options = this.mergeopt(RawCell,options);
|
|
|
TextCell.apply(this, [options]);
|
|
|
this.cell_type = 'raw';
|
|
|
// RawCell should always hide its rendered div
|
|
|
this.element.find('div.text_cell_render').hide();
|
|
|
};
|
|
|
|
|
|
RawCell.options_default = {
|
|
|
placeholder : "Write raw LaTeX or other formats here, for use with nbconvert.\n" +
|
|
|
"It will not be rendered in the notebook.\n" +
|
|
|
"When passing through nbconvert, a Raw Cell's content is added to the output unmodified."
|
|
|
};
|
|
|
|
|
|
RawCell.prototype = new TextCell();
|
|
|
|
|
|
/** @method bind_events **/
|
|
|
RawCell.prototype.bind_events = function () {
|
|
|
TextCell.prototype.bind_events.apply(this);
|
|
|
var that = this;
|
|
|
this.element.focusout(function() {
|
|
|
that.auto_highlight();
|
|
|
});
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Trigger autodetection of highlight scheme for current cell
|
|
|
* @method auto_highlight
|
|
|
*/
|
|
|
RawCell.prototype.auto_highlight = function () {
|
|
|
this._auto_highlight(IPython.config.raw_cell_highlight);
|
|
|
};
|
|
|
|
|
|
/** @method render **/
|
|
|
RawCell.prototype.render = function () {
|
|
|
// Make sure that this cell type can never be rendered
|
|
|
if (this.rendered) {
|
|
|
this.unrender();
|
|
|
}
|
|
|
var text = this.get_text();
|
|
|
if (text === "") { text = this.placeholder; }
|
|
|
this.set_text(text);
|
|
|
};
|
|
|
|
|
|
|
|
|
/**
|
|
|
* @class HeadingCell
|
|
|
* @extends IPython.TextCell
|
|
|
*/
|
|
|
|
|
|
/**
|
|
|
* @constructor HeadingCell
|
|
|
* @extends IPython.TextCell
|
|
|
*/
|
|
|
var HeadingCell = function (options) {
|
|
|
options = this.mergeopt(HeadingCell, options);
|
|
|
|
|
|
this.level = 1;
|
|
|
this.cell_type = 'heading';
|
|
|
TextCell.apply(this, [options]);
|
|
|
|
|
|
/**
|
|
|
* heading level of the cell, use getter and setter to access
|
|
|
* @property level
|
|
|
*/
|
|
|
};
|
|
|
|
|
|
HeadingCell.options_default = {
|
|
|
placeholder: "Type Heading Here"
|
|
|
};
|
|
|
|
|
|
HeadingCell.prototype = new TextCell();
|
|
|
|
|
|
/** @method fromJSON */
|
|
|
HeadingCell.prototype.fromJSON = function (data) {
|
|
|
if (data.level !== undefined){
|
|
|
this.level = data.level;
|
|
|
}
|
|
|
TextCell.prototype.fromJSON.apply(this, arguments);
|
|
|
};
|
|
|
|
|
|
|
|
|
/** @method toJSON */
|
|
|
HeadingCell.prototype.toJSON = function () {
|
|
|
var data = TextCell.prototype.toJSON.apply(this);
|
|
|
data.level = this.get_level();
|
|
|
return data;
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* can the cell be split into two cells
|
|
|
* @method is_splittable
|
|
|
**/
|
|
|
HeadingCell.prototype.is_splittable = function () {
|
|
|
return false;
|
|
|
};
|
|
|
|
|
|
|
|
|
/**
|
|
|
* can the cell be merged with other cells
|
|
|
* @method is_mergeable
|
|
|
**/
|
|
|
HeadingCell.prototype.is_mergeable = function () {
|
|
|
return false;
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Change heading level of cell, and re-render
|
|
|
* @method set_level
|
|
|
*/
|
|
|
HeadingCell.prototype.set_level = function (level) {
|
|
|
this.level = level;
|
|
|
if (this.rendered) {
|
|
|
this.rendered = false;
|
|
|
this.render();
|
|
|
}
|
|
|
};
|
|
|
|
|
|
/** The depth of header cell, based on html (h1 to h6)
|
|
|
* @method get_level
|
|
|
* @return {integer} level - for 1 to 6
|
|
|
*/
|
|
|
HeadingCell.prototype.get_level = function () {
|
|
|
return this.level;
|
|
|
};
|
|
|
|
|
|
|
|
|
HeadingCell.prototype.set_rendered = function (html) {
|
|
|
this.element.find("div.text_cell_render").html(html);
|
|
|
};
|
|
|
|
|
|
|
|
|
HeadingCell.prototype.get_rendered = function () {
|
|
|
var r = this.element.find("div.text_cell_render");
|
|
|
return r.children().first().html();
|
|
|
};
|
|
|
|
|
|
|
|
|
HeadingCell.prototype.render = function () {
|
|
|
var cont = IPython.TextCell.prototype.render.apply(this);
|
|
|
if (cont) {
|
|
|
var text = this.get_text();
|
|
|
var math = null;
|
|
|
// Markdown headings must be a single line
|
|
|
text = text.replace(/\n/g, ' ');
|
|
|
if (text === "") { text = this.placeholder; }
|
|
|
text = Array(this.level + 1).join("#") + " " + text;
|
|
|
var text_and_math = IPython.mathjaxutils.remove_math(text);
|
|
|
text = text_and_math[0];
|
|
|
math = text_and_math[1];
|
|
|
var html = marked.parser(marked.lexer(text));
|
|
|
html = IPython.mathjaxutils.replace_math(html, math);
|
|
|
html = security.sanitize_html(html);
|
|
|
var h = $(html);
|
|
|
// add id and linkback anchor
|
|
|
var hash = h.text().replace(/ /g, '-');
|
|
|
h.attr('id', hash);
|
|
|
h.append(
|
|
|
$('<a/>')
|
|
|
.addClass('anchor-link')
|
|
|
.attr('href', '#' + hash)
|
|
|
.text('ΒΆ')
|
|
|
);
|
|
|
this.set_rendered(h);
|
|
|
this.element.find('div.input_area').hide();
|
|
|
this.element.find("div.text_cell_render").show();
|
|
|
this.typeset();
|
|
|
}
|
|
|
return cont;
|
|
|
};
|
|
|
|
|
|
IPython.TextCell = TextCell;
|
|
|
IPython.MarkdownCell = MarkdownCell;
|
|
|
IPython.RawCell = RawCell;
|
|
|
IPython.HeadingCell = HeadingCell;
|
|
|
|
|
|
|
|
|
return IPython;
|
|
|
|
|
|
}(IPython));
|
|
|
|
|
|
|