Commit 8827f2ce authored by Sebastian's avatar Sebastian

renderjs-extension: initial. Added core functionality

parents
# Overview: renderJS for jupyter notebook via extension
This repo contains two seperate things:
1. A [jupyter frontend extension](http://jupyter-notebook.readthedocs.io/en/latest/extending/frontend_extensions.html)
also called nbextension containing the javascript and html required to load arbitrary gadgets. See folder
`renderjs_nbextension/`.
2. An [ipython extension](https://ipython.org/ipython-doc/3/config/extensions/index.html) providing necessary python the commands to initiate the renderjs and controll the loaded gadgets. This happens directly from the notebook (cells). See folder `renderjs_ipyextension/`.
To make this work both the nbextension and the ipython-extension need to be installed and enabled.
# Install
TODO
# Use
All of the following python calls are made from the notebook itself.
## Loading the ipython extension:
```python
%load_ext renderjs_extension
```
## Import and initialize the extension:
```python
import renderjs_extension as rjs
rjs.init_renderjs()
```
## Load a renderjs gadget:
```python
myGadget = rjs.load_gadget("https://my.gadget.url/gadget.html")
```
## Destroy a renderjs gadget:
```python
myGadget.destroy()
```
If he cell defining `myGadget` is executed again, the current `myGadget` is automatically destroyed and a new one created.
## Call a `declared_method` of a loaded gadget:
```python
arg1 = "Hello"
arg2 = 'World'
arg3 = 2016
arg4 = { "yes": "no!" }
myGadget.call_declared_method("method_name", arg1, arg2, arg3, arg4)
```
Arguments can be arbitrary as long as it is python-compatible. They get converted to js-arguments via pythons JSON command.
# Development
The extension is split into a ipython extension and a nbextension für jupyter. This is necessary because both JS in the frontend
and python in the backend are required to load a renderjs gadget.
## ipython extension (backend)
Here all the functionality for interacting with the extension via jupyter notebook cells is exposed. Unfortunately, ipython
has no knowledge of the kernel and therefore no knowledge of the jupyter frontend.
Still we want to execute javascipt in the frontend. To solve this issue, ipython is used to inject inline
javascipt into the page via ipython display. This is a rather dirty hack but I could not find a better solution as of now.
The inject JS code get's executed and fires an event to which the frontend-extension listens to.
Gadgets are represented by a python-object and carry a uid to identify them.
## nbextension (frontend)
Nothing happens on-load of the frontend part. Everything is controlled by the backend (mostly via events fired from the ipython).
Exception to event-based communication is the initial and controller gadget (loading_gadget)
which is injected into the page directly and which is initialized by a manual bootstrap of renderjs (since the page is
already loaded; normal bootstrapping happens on-load).
The loading_gadget is responsible for loading other
(actual) gadgets and pass information from the backend to them.
The loading_gadget declares a renderjs service for each possible event which could be fired from the backend
(such as call_declared_method, destroy_gadget, ...).
Multiple gadgets are possible which are all controlled by the same loading_gadget. To differentiate between gadgets
each gadget sends along a uid (generated in the backend and member-variable of the python gadget-object)
and the loading_gadget determines the right gadget (by .getDeclaredGadget(uid)) and passes along the function call.
\ No newline at end of file
This diff is collapsed.
# TODO
\ No newline at end of file
#
# TODO
#
# prevent multiple class of init_renderjs - ie. check if loading_gadget there
# find proper location for static files
#
#
# For each gadget which is to be integrated into the jupyter page one
# RJSGadget object is created. It is used to call call_declared_method
# and destroy.
class RJSGadget:
def __init__(self, gadgetId):
# The UID is used to identify the gadget and gets passed along
# to the events sent to the frontend so that the loading_gadget
# is able to decide to which gadget the fired event belongs
# Python call on object with (uid) ->
# -> Fire event (uid) ->
# -> loading_gadget (uid) ->
# -> pass on the call to gadget with (uid)
self.gadgetId = gadgetId
def __del__(self):
pass
# Fires an event with
# * the uid of the gadget
# * the name of the declared_method
# * the arguments to be passed to the declared_method
# The arguments are packed into a json string and passed to js as such
def call_declared_method(self, method_name, *args):
from IPython.core.display import display, HTML
import json
j_str = json.dumps(args)
script = '''
<script>
var call_event = new CustomEvent("call_gadget",
{ "detail": {
"gadgetId": "''' + self.gadgetId + '''",
"methodName": "''' + method_name + '''",
"methodArgs": ''' + "'" + j_str + "'" + '''
}});
var loadingDiv = document.querySelector(".loading_gadget");
if(loadingDiv != null) {
loadingDiv.dispatchEvent(call_event);
} else {
console.log("~~ call: RenderJS init required first!");
}
</script>
'''
display(HTML(script))
# Fires an event to the destroy this gadget (self)
# Only thing passed is the uid of the gadget
def destroy(self):
from IPython.core.display import display, HTML
script = '''
<script>
var destroy_event = new CustomEvent("destroy_gadget",
{ "detail": { "gadgetId": "''' + self.gadgetId + '''" }});
var loadingDiv = document.querySelector(".loading_gadget");
if(loadingDiv != null) {
loadingDiv.dispatchEvent(destroy_event);
} else {
console.log("~~ destroy: RenderJS init required first!");
}
</script>
'''
display(HTML(script))
# Do nothing on load
def load_ipython_extension(ipython):
pass
# Do nothing on unload
def unload_ipython_extension(ipython):
pass
# Create the original load_gadget with modified rsvp, renderjs
# Because jupyter notebook has already loaded when this can be called
# a manual intialization of the whole renderJS setup is required
#
# First the libs rsvp, renderjs-gadget-global and renderjs (patched)
# are injected into the page. The patch on renderjs itself is to enable
# the following manual bootstrap
# After the scripts are present, a div is appended containing the
# loading_gadget.
# After everything is inplace, rJS.manualBootstrap initializes the
# loading_gadget in exactly the same way as when rJS is normally initialized
# (on-load)
def init_renderjs():
from IPython.core.display import display, HTML
script = '''
<script>
var loadingDiv = document.querySelector(".loading_gadget");
if(loadingDiv == null) {
console.log("~~ Initializing RenderJS!");
$.getScript("/nbextensions/renderjs_nbextension/rsvp-2.0.4.js", function() {
console.log("~~ loading_gadget: rsvp.js loaded");
$.getScript("/nbextensions/renderjs_nbextension/rjs_gadget_global_js.js", function() {
console.log("~~ loading_gadget: renderjs-gadget-global.js loaded");
$.getScript("/nbextensions/renderjs_nbextension/renderjs-latest.js", function() {
console.log("~~ loading_gadget: renderjs.js loaded");
$("#notebook-container").append('<div data-gadget-url="/nbextensions/renderjs_nbextension/loading_gadget.html" data-gadget-scope="public"></div>');
rJS.manualBootstrap();
});
});
});
} else {
console.log("~~ Renderjs seems to be initialized already!");
}
</script>
'''
display(HTML(script))
# Load a gadget given a URL to the HTML file of the gadget
# -> Generates a new uid for the gadget to-be-loaded
# -> Fires an event which loading_gadget listens on and passes on the URL
# -> returns the python gadget-object which has the uid as member-variable
def load_gadget(gadgetUrl):
from IPython.core.display import display, HTML
import uuid
gadgetId = str(uuid.uuid4())
script = '''
<script>
var load_event = new CustomEvent("load_gadget",
{ "detail": { "url": "''' + gadgetUrl + '", "gadgetId": "' + gadgetId + '''" }});
var loadingDiv = document.querySelector(".loading_gadget");
if(loadingDiv != null) {
loadingDiv.dispatchEvent(load_event);
} else {
console.log("~~ load: RenderJS init required first!");
}
</script>
'''
display(HTML(script))
return RJSGadget(gadgetId)
from setuptools import setup
setup(
name='renderjs_ipyextension',
version='0.0.4',
packages=['renderjs_ipyextension'],
# PyPI Data
author='Sebastian Kreisel',
author_email='sebastian.kreisel@nexedi.com',
description='RenderJS gadgets for jupyter (frontend-part)',
keywords='renderjs jupyter nbextension',
license='GPL 2',
url='https://lab.nexedi.com/Kreisel/renderjs_extension'
)
This diff is collapsed.
# Frontend part of the jupyter extension for RenderJS
TODO
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<title>Test Gadget</title>
<script src="loading_gadget.js" type="text/javascript"></script>
<!--
div element to dispatch/receive events on to communicate between
the ipython-extension (backend) and the nbextension (frontend)
-->
<div class="loading_gadget"></div>
</head>
<body>
</body>
</html>
/*global window, rJS, jIO, FormData */
/*jslint indent: 2, maxerr: 3 */
(function (window, rJS) {
"use strict";
rJS(window)
.ready(function (gadget) {
console.log("~~ loading_gadget: is ready");
return gadget;
})
/*
Service listening to load gadget-events (which creates the gadget)
*/
.declareService(function () {
var gadget = this;
function loadGadget(e) {
// The URL of the gadget-to-be-loaded
var url = e.detail.url;
// The uid of the gadget-to-be-loaded
var gadgetId = e.detail.gadgetId;
console.log("~~ gadgetID=" + gadgetId);
console.log("~~ trying to load gadget with url=" + url);
// Find the running/post-active cell to append the
// gadget-to-be-loaded to
var gadgetParent = null;
var notebookcontainer = $("#notebook-container");
// If this gets executed quick the cell might still be "running"
gadgetParent = document.querySelector(".running");
// If no cell is running take the cell before the currently active
// cell as parent
if(gadgetParent != null) {
var activeIndex = -1;
// Find the index of the class=selected cell
for(var i = 0; i < notebookcontainer.children(".cell").length; i++) {
var iele = notebookcontainer.children(".cell")[i];
if((' ' + iele.className + ' ').indexOf(' selected ') > -1) {
activeIndex = i;
break;
}
}
// The cell before the selected cell is what we want
gadgetParent = notebookcontainer.children(".cell")[activeIndex - 1];
}
if(gadgetParent == null) {
console.log("~~ ERROR: gadgetParent is null");
return;
}
// If there is already a gadget associated with the cell, destroy it
// (overwrite it)
if (gadgetParent.querySelector(".external_gadget") != null) {
gadgetParent.removeChild(gadgetParent.querySelector(".external_gadget"));
}
// Create the gadget's div
var gadgetDiv = document.createElement("div");
gadgetDiv.className += "external_gadget";
gadgetParent.appendChild(gadgetDiv);
var options = {
element: gadgetDiv,
sandbox: "iframe",
scope: gadgetId
};
// Create the new gadget in an iframe
return gadget.declareGadget(url, options)
.push(function(external_gadget) {
external_gadget.getElement()
.push(function(element) {
element.querySelector("iframe").style.width="100%";
element.querySelector("iframe").style.height="400px";
//element.querySelector("iframe"). += " " + gadgetId;
return element;
});
return external_gadget;
});
}
// Listener for "load gadget" events
return gadget.getElement()
.push(function(ele) {
return loopEventListener(ele.querySelector(".loading_gadget"),
'load_gadget', false, loadGadget);
});
})
/*
Service listening to destroy-events
*/
.declareService(function () {
var gadget = this;
function destroyGadget(e) {
// The uid of the gadget-to-be-destroyed
var gadgetId = e.detail.gadgetId;
console.log("~~ destroying gadget with id=" + gadgetId);
// Get the dom-element of the gadget and remove it thereby destroying
// the gadget itself
gadget.getDeclaredGadget(gadgetId)
.push(function(subGadget) {
subGadget.getElement()
.push(function(ele) {
ele.parentNode.removeChild(ele);
console.log("~~ destroyed gadget!");
});
});
}
// Listener for "destroy" events
return gadget.getElement()
.push(function(ele) {
return loopEventListener(ele.querySelector(".loading_gadget"),
'destroy_gadget', false, destroyGadget);
});
})
/*
Service listening to callDeclMethod-events
*/
.declareService(function() {
var gadget = this;
function callGadget(e) {
// The id of the gadget-to-be-called
var gadgetId = e.detail.gadgetId;
var methodName = e.detail.methodName;
// Parse the JSON which got created in the backend via python json
var methodArgList = JSON.parse(e.detail.methodArgs);
console.log(methodArgList);
gadget.getDeclaredGadget(gadgetId)
.push(function(subGadget) {
// Called methodName on subGadget and use subGadget as this and
// methodArgList as arguments
subGadget[methodName].apply(subGadget, methodArgList);
});
}
// Listener for "call" events
return gadget.getElement()
.push(function(ele) {
return loopEventListener(ele.querySelector(".loading_gadget"),
'call_gadget', false, callGadget);
});
});
}(window, rJS));
define([
'base/js/namespace'
], function(Jupyter) {
function load_ipython_extension() {
console.log("~~ nbextension (frontend) loaded");
};
return {
load_ipython_extension: load_ipython_extension
};
});
{
"load_extensions": {
"renderjs_nbextension/main": true
}
}
This diff is collapsed.
/*global window, RSVP, FileReader */
/*jslint indent: 2, maxerr: 3, unparam: true */
(function (window, RSVP, FileReader) {
"use strict";
window.loopEventListener = function (target, type, useCapture, callback,
prevent_default) {
//////////////////////////
// Infinite event listener (promise is never resolved)
// eventListener is removed when promise is cancelled/rejected
//////////////////////////
var handle_event_callback,
callback_promise;
if (prevent_default === undefined) {
prevent_default = true;
}
function cancelResolver() {
if ((callback_promise !== undefined) &&
(typeof callback_promise.cancel === "function")) {
callback_promise.cancel();
}
}
function canceller() {
if (handle_event_callback !== undefined) {
target.removeEventListener(type, handle_event_callback, useCapture);
}
cancelResolver();
}
function itsANonResolvableTrap(resolve, reject) {
var result;
handle_event_callback = function (evt) {
if (prevent_default) {
evt.stopPropagation();
evt.preventDefault();
}
cancelResolver();
try {
result = callback(evt);
} catch (e) {
result = RSVP.reject(e);
}
callback_promise = result;
new RSVP.Queue()
.push(function () {
return result;
})
.push(undefined, function (error) {
if (!(error instanceof RSVP.CancellationError)) {
canceller();
reject(error);
}
});
};
target.addEventListener(type, handle_event_callback, useCapture);
}
return new RSVP.Promise(itsANonResolvableTrap, canceller);
};
window.promiseEventListener = function (target, type, useCapture) {
//////////////////////////
// Resolve the promise as soon as the event is triggered
// eventListener is removed when promise is cancelled/resolved/rejected
//////////////////////////
var handle_event_callback;
function canceller() {
target.removeEventListener(type, handle_event_callback, useCapture);
}
function resolver(resolve) {
handle_event_callback = function (evt) {
canceller();
evt.stopPropagation();
evt.preventDefault();
resolve(evt);
return false;
};
target.addEventListener(type, handle_event_callback, useCapture);
}
return new RSVP.Promise(resolver, canceller);
};
window.promiseReadAsText = function (file) {
return new RSVP.Promise(function (resolve, reject) {
var reader = new FileReader();
reader.onload = function (evt) {
resolve(evt.target.result);
};
reader.onerror = function (evt) {
reject(evt);
};
reader.readAsText(file);
});
};
window.promiseDoWhile = function (loopFunction, input) {
// calls loopFunction(input) until it returns a non positive value
// this queue is to protect the inner loop queue from the
// `promiseDoWhile` caller, avoiding it to enqueue the inner
// loop queue.
return new RSVP.Queue()
.push(function () {
// here is the inner loop queue
var loop_queue = new RSVP.Queue();
function iterate(previous_iteration_result) {
if (!previous_iteration_result) {
return input;
}
loop_queue.push(iterate);
return loopFunction(input);
}
return loop_queue
.push(function () {
return loopFunction(input);
})
.push(iterate);
});
};
}(window, RSVP, FileReader));
\ No newline at end of file
This diff is collapsed.
# -*- coding: utf-8 -*-
from setuptools import setup
setup(
name='renderjs_nbextension',
version='0.0.4',
packages=['renderjs_nbextension'],
package_data = {
'': ['*.js', '*.html', '*.json']
},
# PyPI Data
author='Sebastian Kreisel',
author_email='sebastian.kreisel@nexedi.com',
description='RenderJS gadgets for jupyter (frontend-part)',
keywords='renderjs jupyter nbextension',
license='GPL 2',
url='https://lab.nexedi.com/Kreisel/renderjs_extension'
)
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment