Skip to content

Instantly share code, notes, and snippets.

@sebble
Last active August 29, 2015 14:16
Show Gist options
  • Save sebble/8f9ef5f5a84f5ad46726 to your computer and use it in GitHub Desktop.
Save sebble/8f9ef5f5a84f5ad46726 to your computer and use it in GitHub Desktop.
Distraction Free IPython Notebook

Distraction free writing in IPython notebooks

  • Minimal styles to emphasise writing
  • WYWIWYG markdown editing to improve writing flow

Can we outsource HTML->MD editing to https://jejacks0n.github.io/mercury/documentation/#markdown-region?

Note: IPython notebooks use marked internally, a reverse (HTML->MD converter should match supported marked syntax, IPython configurations, and ignore dynamic IPython features (e.g., heading anchors, MathJax)

IPython features to avoid

See /usr/local/lib/python2.7/dist-packages/IPython/html/static/notebook/js/textcell.js line 267 MarkdownCell.prototype.render

  • MathJax
  • Headers

Features (todo)

  • Enter on 'heading' cell creates new cell below in wysiwyg more
  • Shift+Enter on 'heading' cell inserts paragraph below (normal) -- maybe this can be done by setting shiftKey=false; return true;
  • All new cells are markdown by default (use y) to revert to code.
/*
Placeholder for custom user CSS
mainly to be overridden in profile/static/custom/custom.css
This will always be an empty file in IPython
*/
// leave at least 2 line with only a star on it below, or doc generation fails
/**
*
*
* Placeholder for custom user javascript
* mainly to be overridden in profile/static/custom/custom.js
* This will always be an empty file in IPython
*
* User could add any javascript in the `profile/static/custom/custom.js` file
* (and should create it if it does not exist).
* It will be executed by the ipython notebook at load time.
*
* Same thing with `profile/static/custom/custom.css` to inject custom css into the notebook.
*
* Classes and functions are available at load time and may be accessed plainly:
*
* IPython.Cell.options_default.cm_config.extraKeys['Home'] = 'goLineLeft';
* IPython.Cell.options_default.cm_config.extraKeys['End'] = 'goLineRight';
*
* Instances are created later however and must be accessed using events:
* require([
* 'base/js/namespace',
* 'base/js/events'
* ], function(IPython, events) {
* events.on("app_initialized.NotebookApp", function () {
* IPython.keyboard_manager....
* });
* });
*
* __Example 1:__
*
* Create a custom button in toolbar that execute `%qtconsole` in kernel
* and hence open a qtconsole attached to the same kernel as the current notebook
*
* require([
* 'base/js/namespace',
* 'base/js/events'
* ], function(IPython, events) {
* events.on('app_initialized.NotebookApp', function(){
* IPython.toolbar.add_buttons_group([
* {
* 'label' : 'run qtconsole',
* 'icon' : 'icon-terminal', // select your icon from http://fortawesome.github.io/Font-Awesome/icons
* 'callback': function () {
* IPython.notebook.kernel.execute('%qtconsole')
* }
* }
* // add more button here if needed.
* ]);
* });
* });
*
* __Example 2:__
*
* At the completion of the dashboard loading, load an unofficial javascript extension
* that is installed in profile/static/custom/
*
* require([
* 'base/js/events'
* ], function(events) {
* events.on('app_initialized.DashboardApp', function(){
* require(['custom/unofficial_extension.js'])
* });
* });
*
* __Example 3:__
*
* Use `jQuery.getScript(url [, success(script, textStatus, jqXHR)] );`
* to load custom script into the notebook.
*
* // to load the metadata ui extension example.
* $.getScript('/static/notebook/js/celltoolbarpresets/example.js');
* // or
* // to load the metadata ui extension to control slideshow mode / reveal js for nbconvert
* $.getScript('/static/notebook/js/celltoolbarpresets/slideshow.js');
*
*
* @module IPython
* @namespace IPython
* @class customjs
* @static
*/
// events: grep -ohrE "events.trigger\('[^']+" | grep -Eo "[^']+$" | sort -u
require([
'custom/he',
'base/js/namespace',
'base/js/events',
'jquery'
], function(he,IPython,events,$){
events.on("notebook_loaded.Notebook", function () {
$('body').on('keydown.wysiwyg', function(e){
if ($(this).find('.selected.rendered').length && wysiwyg_enabled && e.keyCode === 13) {
if ($(this).find('.selected.rendered .text_cell_render.rendered_html[contenteditable!=true]').length) {
console.log('blocking Ctrl+Entr');
$(this)
.find('.selected .text_cell_render.rendered_html')
.attr('contenteditable', 'true')
.focus();
$(this)
.find('.MathJax,.MathJax_Preview,.MathJax_Display,.anchor-link')
.attr('contenteditable', 'false');
$(this)
.find('div.cell.selected')
.addClass('wysiwyg_edit_mode');
IPython.keyboard_manager.enabled = false;
return false;
}
return true;
} else {
return true;
}
});
$('body').on('click.wysiwyg', '.text_cell_render.rendered_html', function(e){
if (wysiwyg_enabled) {
$(this)
.attr('contenteditable', 'true');
$(this)
.find('.MathJax,.MathJax_Preview,.MathJax_Display,.anchor-link')
.attr('contenteditable', 'false');
$(this)
.closest('div.cell.selected')
.addClass('wysiwyg_edit_mode');
} else {
return true;
}
});
$('body').on('blur.wysiwyg', '.text_cell_render.rendered_html', function(e){
if (wysiwyg_enabled) {
$(this)
.attr('contenteditable', 'false');
$(this)
.closest('div.cell')
.removeClass('wysiwyg_edit_mode');
return false;
} else {
return true;
}
});
var onKeys = function(e){
$edit = $(e.target);
if ($edit.attr('contenteditable') == 'true' && wysiwyg_enabled) {
IPython.keyboard_manager.enabled = false;
// helpfully this is restored to true immediately after editing...
//~ // re-evaluate the raw markdown in this cell
//~ if (e.type === 'keydown' && e.ctrlKey && e.keyCode === 13) {
//~ reformat.apply(this);
//~ IPython.notebook.execute_cell();
//~ }
// edit raw markdown
if (e.type === 'keydown' && e.ctrlKey && e.keyCode === 13) {
console.log('edit mode?');
reformat.apply(this);
$(this)
.attr('contenteditable','false');
$(this)
.closest('div.cell.selected')
.removeClass('wysiwyg_edit_mode');
IPython.notebook.edit_mode();
return false;
}
// evaluate cell and insert new below
else if (e.type === 'keydown' && e.altKey && e.keyCode === 13) {
reformat.apply(this);
IPython.notebook.execute_cell_and_insert_below();
IPython.notebook.to_markdown();
IPython.notebook.execute_cell();
return false;
}
// save notebook
else if (e.type === 'keydown' && e.ctrlKey && e.keyCode === 83) {
IPython.notebook.save_notebook();
return false;
}
// leave edit mode
else if (e.keyCode === 27) {
console.log('leave edit');
$(this)
.attr('contenteditable','false');
$(this)
.closest('div.cell.selected')
.removeClass('wysiwyg_edit_mode');
return false;
}
} else {
IPython.keyboard_manager.enabled = true;
console.log('skip');
return true;
}
};
var wysiwyg_enabled = false;
var distraction_enabled = false;
// add menu items option
$('#view_menu').append('<li id="toggle_distraction" title="Enable/disable distraction-free notebook style"><a href="#">Toggle Distraction-Free</a></li>');
$('#edit_menu').append('<li class="divider"></li>').append('<li id="toggle_wysiwyg" title="Enable/disable WYSIWYG markdown mode"><a href="#">Enable WYSIWYG editing</a></li>');
// bind wysiwyg action
$('#toggle_wysiwyg').click(function(){
if (wysiwyg_enabled) {
wysiwyg_enabled = false;
$(this).find('a').text('Enable WYSIWYG editing');
$('body')
.off('keydown.wysiwyg', '.text_cell_render.rendered_html');
} else {
/** Are you sure? **/
IPython.dialog.modal({
title: "Enable WYSIWYG markdown editing?",
body: $("<p><strong>Warning:</strong> WYWSIWYG markdown editing is unstable and will destroy many advanced elements including <em>MathJax</em>, <em>headings</em>, <em>everything else</em></p><p>Do you wish to continue?</p>"),
buttons: {
'No': {
class: 'btn-primary'
},
'Yes': {
class: 'btn-warning',
click: function(){
wysiwyg_enabled = true;
$(this).find('a').text('Disable WYSIWYG editing');
// make editable, attach keyboard shortcuts
$('body')
.on('keydown.wysiwyg', '.text_cell_render.rendered_html', onKeys);
}
}
},
notebook: IPython.ntoebook,
keyboard_manager: IPython.keyboard_manager
});
}
});
// bind distraction action
$('#toggle_distraction').click(function(){
if (distraction_enabled) {
distraction_enabled = false;
$('#header-container').show();
$('.header-bar').show();
$('div#maintoolbar').show();
IPython.menubar._size_header();
} else {
distraction_enabled = true;
$('#header-container').hide();
$('.header-bar').hide();
$('div#maintoolbar').hide();
IPython.menubar._size_header();
}
});
function getDisplayType (element) {
var cStyle = element.currentStyle || window.getComputedStyle(element, "");
return cStyle.display;
}
var reformat = function(e){
//console.log($(this).parent().find('.CodeMirror')[0].CodeMirror.getDoc());
var CM = $(this).parent().find('.CodeMirror')[0].CodeMirror;
//console.log(e);
//console.log(this);
// replace b,i
$(this).find('b').replaceWith(function(){
return $("<strong />").append($(this).contents());
});
$(this).find('i').replaceWith(function(){
return $("<em />").append($(this).contents());
});
// replace divs
$(this).find('div').replaceWith(function(){
return $('<p>'+$(this).html()+'</p>');
});
// wrap contiguous inline regions
var to_wrap = [];
$(this).contents().each(function(){
if (this.nodeType === 3) {
to_wrap.push(this);
} else if (getDisplayType(this) === 'inline') {
to_wrap.push(this);
} else {
if (to_wrap.length > 0)
$(to_wrap).wrapAll('<p>');
to_wrap = [];
}
});
if (to_wrap.length > 0)
$(to_wrap).wrapAll('<p>');
// delete empty
$(this).contents()
.filter(function(){return $.trim(this.innerHTML) === ""})
.remove();
// delete unstyled
$(this).find('span, div').not('[class]').replaceWith(function(){
return $(this).contents();
});
// update source-views
//$('.source').text($('.document').html());
//$('.markdown').text(toMarkdown($('.document').html()));
//console.log(toMarkdown($(this).html()));
/** IPython notebook fixes **/
// remove heading ids
$(this).find('h1,h2,h3,h4,h5,h6')
.removeAttr('id');
// remove heading anchors
$(this).find('h1,h2,h3,h4,h5,h6').find('.anchor-link')
.remove();
// remove mathjax
$(this).find('.MathJax,.MathJax_Preview,.MathJax_Display')
.remove();
// restore inline maths
$(this).find('script[type="math/tex"]')
.replaceWith(function(){return '$'+$(this).text()+'$';});
// restore display maths
$(this).find('script[type="math/tex; mode=display"]')
.replaceWith(function(){return '$$'+$(this).text()+'$$';});
CM.getDoc().setValue(toMarkdown($(this).html()));
};
$.getScript('/static/custom/to-markdown.js').done(function(d){
$('body').on('input','[contenteditable]',reformat);
events.on("command_mode.Cell", function (e) {
//console.log(e);
});
}).fail(function(a,b,c){
console.log('Failed to load SJIM.');
});
$('head').append('<style>div.cell.selected.wysiwyg_edit_mode{border-color:green;}</style>');
$('#toggle_wysiwyg,#toggle_distraction').click();
// catch all new cells and make markdown as default?
events.on("create.Cell", function (e) {
if (distraction_enabled) {
// problem: create happens before selection moves
// and 'event' doesn't point to the created :-(
// solution: wait until re-selection has happened?
// -- not elegant
setTimeout(function(){
//console.log($('.selected'));
if ($('.selected .rendered_html').length == 0) {
//console.log('to_markdown');
IPython.notebook.to_markdown();
IPython.notebook.execute_cell();
}
if ($('.selected.unrendered').length) {
//console.log('execute_cell');
}
//IPython.notebook.to_markdown();
//IPython.notebook.execute_cell();
},200);
}
});
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment