Commit b80bc8ec authored by Romain Courteaud's avatar Romain Courteaud

[erp5_notebook] Implement a JSMD viewer

This is not a notebook (as it does not provide a "live" editing functionnality).

It is only used to trigger the calculation and view the result.

Except for the JSMD format, this code is not related to iodide.

Python plugins are not supported for now.
parent ac2ca85d
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy"
content="script-src 'unsafe-inline' 'unsafe-eval' *; style-src 'unsafe-inline' *">
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width" />
<title>JSMD Eval Gadget</title>
<script src="rsvp.js" type="text/javascript"></script>
<script src="marked.js" type="text/javascript"></script>
<script src="gadget_jsmd_eval.js" type="text/javascript"></script>
</head>
<body class="pane-content">
</body>
</html>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="File" module="OFS.Image"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>__name__</string> </key>
<value> <string>gadget_jsmd_eval.html</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/html</string> </value>
</item>
<item>
<key> <string>precondition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
/*global window, console, RSVP, document, URL, eval, XMLHttpRequest, marked */
/*jslint nomen: true, indent: 2, maxerr: 3 */
(function (window) {
"use strict";
var IODide = function createIODide() {
return;
},
JSMDCell = function createJSMDCell(type, line_list) {
this._type = type;
this._line_list = line_list;
},
split_line_regex = /[\r\n|\n|\r]/,
cell_type_regexp = /^\%\% (\w+)$/;
window.iodide = new IODide();
IODide.prototype.addOutputHandler = function () {
return;
};
// Copied from jio
function ajax(param) {
var xhr = new XMLHttpRequest();
return new RSVP.Promise(function (resolve, reject) {
var k;
xhr.open(param.type || "GET", param.url, true);
xhr.responseType = param.dataType || "";
if (typeof param.headers === 'object' && param.headers !== null) {
for (k in param.headers) {
if (param.headers.hasOwnProperty(k)) {
xhr.setRequestHeader(k, param.headers[k]);
}
}
}
xhr.addEventListener("load", function (e) {
if (e.target.status >= 400) {
return reject(e);
}
resolve(e);
});
xhr.addEventListener("error", reject);
if (typeof param.xhrFields === 'object' && param.xhrFields !== null) {
for (k in param.xhrFields) {
if (param.xhrFields.hasOwnProperty(k)) {
xhr[k] = param.xhrFields[k];
}
}
}
if (param.timeout !== undefined && param.timeout !== 0) {
xhr.timeout = param.timeout;
xhr.ontimeout = function () {
return reject(new Error("Gateway Timeout"));
};
}
if (typeof param.beforeSend === 'function') {
param.beforeSend(xhr);
}
xhr.send(param.data);
}, function () {
xhr.abort();
});
}
function parseJSMDCellList(jsmd) {
// Split the text into a list of Iodide cells
var line_list = jsmd.split(split_line_regex),
i,
len = line_list.length,
current_line,
current_type,
current_text_list,
next_type,
cell_list = [];
function pushNewCell() {
if (current_type !== undefined) {
cell_list.push(new JSMDCell(current_type[1],
current_text_list));
}
}
for (i = 0; i < len; i += 1) {
current_line = line_list[i];
next_type = current_line.match(cell_type_regexp);
if (next_type) {
// New type detexted
pushNewCell();
current_type = next_type;
current_text_list = [];
} else if (current_text_list !== undefined) {
current_text_list.push(current_line);
}
}
// Push last cell
pushNewCell();
return cell_list;
}
function executeUnknownCellType(cell) {
throw new Error('Unsupported cell: ' + cell._type);
}
function executeJSCell(line_list) {
// console.info('eval', line_list);
var text = line_list.join('\n'),
pre,
br,
code;
try {
return eval.call(window, text);
} catch (e) {
console.error(e);
pre = document.createElement('pre');
pre.textContent = e.message;
br = document.createElement('br');
pre.appendChild(br);
code = document.createElement('code');
code.textContent = text;
pre.appendChild(code);
document.body.appendChild(pre);
throw e;
}
}
function executeCssCell(line_list) {
var style = document.createElement('style');
style.textContent = line_list.join('\n');
document.head.appendChild(style);
}
function loadJSResource(url) {
// Copied from renderJS
return new RSVP.Promise(
function waitForJSLoadEvent(resolve, reject) {
var newScript;
newScript = document.createElement('script');
newScript.async = false;
newScript.type = 'text/javascript';
newScript.onload = function (evt) {
resolve(evt.target.value);
};
newScript.onerror = function (error) {
console.warn(error);
reject(error);
};
newScript.src = url;
document.head.appendChild(newScript);
}
);
}
function deferJSResourceLoading(url) {
return function () {
return loadJSResource(url);
};
}
function loadCSSResource(url) {
// Copied from renderJS
return new RSVP.Promise(
function waitForCSSLoadEvent(resolve, reject) {
var link;
link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = url;
link.onload = resolve;
link.onerror = reject;
document.head.appendChild(link);
}
);
}
function deferCSSResourceLoading(url) {
return function () {
return loadCSSResource(url);
};
}
function loadTextResource(line) {
var line_split = line.split('=', 2),
variable = line_split[0],
url = line_split[1];
console.log(line_split);
return new RSVP.Queue()
.push(function () {
return ajax({url: url});
})
.push(function (evt) {
window[variable] = evt.target.responseText;
});
}
function deferTextResourceLoading(line) {
return function () {
return loadTextResource(line);
};
}
function executeResourceCell(line_list) {
var queue = new RSVP.Queue(),
len = line_list.length,
i;
for (i = 0; i < len; i += 1) {
if (line_list[i]) {
queue.push(deferJSResourceLoading(line_list[i]));
}
}
return queue;
}
function executeFetchCell(line_list) {
var queue = new RSVP.Queue(),
len = line_list.length,
i,
line;
for (i = 0; i < len; i += 1) {
line = line_list[i];
if (line) {
if (line.startsWith('js: ')) {
queue.push(deferJSResourceLoading(line.slice(4)));
} else if (line.startsWith('css: ')) {
queue.push(deferCSSResourceLoading(line.slice(5)));
} else if (line.startsWith('text: ')) {
queue.push(deferTextResourceLoading(line.slice(6)));
} else {
queue.cancel();
throw new Error('Unsupported fetch type: ' + line);
}
}
}
return queue;
}
function executeMarkdownCell(line_list) {
var renderer = new marked.Renderer();
return new RSVP.Promise(function (resolve, reject) {
marked(line_list.join('\n'),
{renderer: renderer},
function (err, content) {
if (err) {
reject(err);
}
var div = document.createElement('div');
div.classList.add('user-markdown');
div.innerHTML = content;
document.body.appendChild(div);
resolve();
});
});
}
function executeCell(cell) {
if (cell._type === 'raw') {
// Do nothing...
return;
}
if (cell._type === 'js') {
return executeJSCell(cell._line_list);
}
if (cell._type === 'resource') {
return executeResourceCell(cell._line_list);
}
if (cell._type === 'fetch') {
return executeFetchCell(cell._line_list);
}
if (cell._type === 'md') {
return executeMarkdownCell(cell._line_list);
}
if (cell._type === 'css') {
return executeCssCell(cell._line_list);
}
return executeUnknownCellType(cell);
}
function deferCellExecution(cell) {
return function () {
return executeCell(cell);
};
}
document.addEventListener('DOMContentLoaded', function () {
var jsmd = document.querySelector('[type="text/x-jsmd"]').textContent,
cell_list = parseJSMDCellList(jsmd),
len = cell_list.length,
i,
queue = new RSVP.Queue();
for (i = 0; i < len; i += 1) {
queue.push(deferCellExecution(cell_list[i]));
}
return queue
.push(function () {
console.info('JSMD executed.');
}, function (error) {
console.error(error);
var pre = document.createElement('pre');
pre.textContent = error;
document.body.appendChild(pre);
});
}, false);
}(window));
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="File" module="OFS.Image"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>__name__</string> </key>
<value> <string>gadget_jsmd_eval.js</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>application/javascript</string> </value>
</item>
<item>
<key> <string>precondition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
html {
height: 100%;
width: 100%;
display: block;
box-sizing: border-box;
margin: 0;
padding: 0;
}
*, *:before, *:after {
box-sizing: inherit;
}
body {
height: 100%;
width: 100%;
display: block;
word-wrap: break-word;
margin: 0;
padding: 0;
}
iframe {
max-width: 100%;
max-height: 100vh;
width: 100%;
height: 25em;
border: none;
margin: 0;
padding: 0;
}
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="File" module="OFS.Image"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>__name__</string> </key>
<value> <string>gadget_jsmd_viewer.css</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/css</string> </value>
</item>
<item>
<key> <string>precondition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width" />
<title>JSMD Editor Gadget</title>
<link rel="stylesheet" type="text/css" href="gadget_jsmd_viewer.css" />
<script src="rsvp.js" type="text/javascript"></script>
<script src="renderjs.js" type="text/javascript"></script>
<script src="jiodev.js" type="text/javascript"></script>
<script src="gadget_global.js" type="text/javascript"></script>
<script src="gadget_jsmd_viewer.js" type="text/javascript"></script>
</head>
<body>
<div></div>
</body>
</html>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="File" module="OFS.Image"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>__name__</string> </key>
<value> <string>gadget_jsmd_viewer.html</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/html</string> </value>
</item>
<item>
<key> <string>precondition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
/*global window, rJS, console, RSVP, jIO, DOMParser, Blob, document,
URL, loopEventListener */
/*jslint nomen: true, indent: 2, maxerr: 3 */
(function (window, rJS, RSVP, jIO, DOMParser, document, URL,
loopEventListener) {
"use strict";
function fetchHTML(url, base_url) {
url = new URL(url, base_url).href;
return new RSVP.Queue()
.push(function () {
return jIO.util.ajax({url: url});
})
.push(function (evt) {
// Insert a "base" element, in order to resolve all relative links
// which could get broken with a data url
var doc = (new DOMParser()).parseFromString(evt.target.responseText,
'text/html'),
base = doc.createElement('base');
base.href = url;
doc.head.insertBefore(base, doc.head.firstChild);
return doc;
});
}
rJS(window)
/////////////////////////////////////////////////////////////////
// declared methods
/////////////////////////////////////////////////////////////////
.declareMethod("render", function (options) {
return this.changeState(options);
})
.onStateChange(function () {
// Reset everything if something change
var gadget = this,
base_url = document.location.toString(),
doc;
return fetchHTML("gadget_jsmd_eval.html", base_url)
.push(function (result) {
doc = result;
// Insert text
doc.body.textContent = '';
// Insert the JSMD value inside the HTML
var script = document.createElement('script'),
iframe = document.createElement("iframe");
script.setAttribute('type', 'text/x-jsmd');
script.setAttribute('id', 'jsmd-source');
script.textContent = gadget.state.value;
doc.head.appendChild(script);
/*
blob = new Blob([doc.documentElement.outerHTML],
{type: "text/html;charset=UTF-8"});
return jIO.util.readBlobAsDataURL(blob);
})
.push(function (evt) {
*/
// XXX Insecure
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin");
// iframe.setAttribute("csp", "default-src *; script-src * 'unsafe-inline';");
// iframe.setAttribute("src", evt.target.result);
iframe.setAttribute("srcdoc", doc.documentElement.outerHTML);
gadget.element.innerHTML = iframe.outerHTML;
gadget.listenResize();
});
})
.declareJob('listenResize', function () {
var gadget = this;
function resize() {
gadget.element.querySelector("iframe").style.height =
(window.innerHeight -
gadget.element.querySelector("iframe").offsetTop) + "px";
}
resize();
return loopEventListener(window, 'resize', false, resize);
});
}(window, rJS, RSVP, jIO, DOMParser, document, URL, loopEventListener));
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="File" module="OFS.Image"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>__name__</string> </key>
<value> <string>gadget_jsmd_viewer.js</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>application/javascript</string> </value>
</item>
<item>
<key> <string>precondition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="File" module="OFS.Image"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>__name__</string> </key>
<value> <string>marked.js</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>application/javascript</string> </value>
</item>
<item>
<key> <string>precondition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
...@@ -13,7 +13,8 @@ lockGadgetInQueue, unlockGadgetInQueue, unlockGadgetInFailedQueue*/ ...@@ -13,7 +13,8 @@ lockGadgetInQueue, unlockGadgetInQueue, unlockGadgetInFailedQueue*/
"minipaint": {"url": "minipaint.gadget.html"}, "minipaint": {"url": "minipaint.gadget.html"},
"jquery-sheets": {"url": "jquery-sheets.gadget.html"}, "jquery-sheets": {"url": "jquery-sheets.gadget.html"},
"pdf": {"url": "pdf_js/pdfjs.gadget.html"}, "pdf": {"url": "pdf_js/pdfjs.gadget.html"},
"notebook_editor": {"url": "gadget_notebook.html"} "notebook_editor": {"url": "gadget_notebook.html"},
"jsmd_editor": {"url": "gadget_jsmd_viewer.html"}
}; };
...@@ -73,15 +74,16 @@ lockGadgetInQueue, unlockGadgetInQueue, unlockGadgetInFailedQueue*/ ...@@ -73,15 +74,16 @@ lockGadgetInQueue, unlockGadgetInQueue, unlockGadgetInFailedQueue*/
if ((modification_dict.hasOwnProperty('editable')) || if ((modification_dict.hasOwnProperty('editable')) ||
(modification_dict.hasOwnProperty('editor')) || (modification_dict.hasOwnProperty('editor')) ||
(gadget.state.editor == 'notebook_editor')) { (gadget.state.editor === 'notebook_editor')) {
// Clear first to DOM, append after to reduce flickering/manip // Clear first to DOM, append after to reduce flickering/manip
while (element.firstChild) { while (element.firstChild) {
element.removeChild(element.firstChild); element.removeChild(element.firstChild);
} }
if (modification_dict.hasOwnProperty('maximize') || if (modification_dict.hasOwnProperty('maximize') ||
(gadget.state.editor == 'notebook_editor')) { (gadget.state.editor === 'notebook_editor')) {
// for fck_editor fields, we want to be able to maximize also in non editable // for fck_editor fields, we want to be able to maximize also in non editable
if ((gadget.state.maximize && gadget.state.editable) || if ((gadget.state.maximize && gadget.state.editable) ||
(gadget.state.maximize && gadget.state.editor === 'jsmd_editor') ||
(gadget.state.maximize && gadget.state.editor === 'fck_editor')) { (gadget.state.maximize && gadget.state.editor === 'fck_editor')) {
element.appendChild(div_max); element.appendChild(div_max);
queue queue
...@@ -108,11 +110,16 @@ lockGadgetInQueue, unlockGadgetInQueue, unlockGadgetInFailedQueue*/ ...@@ -108,11 +110,16 @@ lockGadgetInQueue, unlockGadgetInQueue, unlockGadgetInFailedQueue*/
if ((gadget.state.editable && if ((gadget.state.editable &&
(editor_dict.hasOwnProperty(gadget.state.editor))) || (editor_dict.hasOwnProperty(gadget.state.editor))) ||
(!gadget.state.editable && gadget.state.editor === 'fck_editor') || (!gadget.state.editable && gadget.state.editor === 'fck_editor') ||
(!gadget.state.editable && gadget.state.editor === 'jsmd_editor') ||
(gadget.state.editor === 'pdf')) { (gadget.state.editor === 'pdf')) {
queue queue
.push(function () { .push(function () {
var url = editor_dict[gadget.state.editor].url;
if (gadget.state.editable && (gadget.state.editor === 'jsmd_editor')) {
url = editor_dict.codemirror.url;
}
return gadget.declareGadget( return gadget.declareGadget(
editor_dict[gadget.state.editor].url, url,
{ {
scope: 'editor', scope: 'editor',
sandbox: 'iframe', sandbox: 'iframe',
...@@ -134,6 +141,7 @@ lockGadgetInQueue, unlockGadgetInQueue, unlockGadgetInFailedQueue*/ ...@@ -134,6 +141,7 @@ lockGadgetInQueue, unlockGadgetInQueue, unlockGadgetInFailedQueue*/
if ((gadget.state.editable && if ((gadget.state.editable &&
(editor_dict.hasOwnProperty(gadget.state.editor))) || (editor_dict.hasOwnProperty(gadget.state.editor))) ||
(!gadget.state.editable && gadget.state.editor === 'fck_editor') || (!gadget.state.editable && gadget.state.editor === 'fck_editor') ||
(!gadget.state.editable && gadget.state.editor === 'jsmd_editor') ||
(gadget.state.editor === 'pdf')) { (gadget.state.editor === 'pdf')) {
queue queue
.push(function () { .push(function () {
......
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