Major update of "democode"

master
Hauke D 7 years ago
parent 310bb92b2c
commit 2f4eff19a1

@ -4,14 +4,58 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>WebPerl Code Demo</title>
<iframe name="perlrunner" sandbox="allow-scripts"
src="perlrunner.html" style="display: none;"></iframe>
<!-- ##### WebPerl - http://webperl.zero-g.net #####
Copyright (c) 2018 Hauke Daempfling (haukex@zero-g.net)
at the Leibniz Institute of Freshwater Ecology and Inland Fisheries (IGB),
Berlin, Germany, http://www.igb-berlin.de
This program is free software; you can redistribute it and/or modify
it under the same terms as Perl 5 itself: either the GNU General Public
License as published by the Free Software Foundation (either version 1,
or, at your option, any later version), or the "Artistic License" which
comes with Perl 5.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the licenses for details.
<iframe sandbox="allow-scripts" src="perleditor.html#i1f=Foo%0ABar%0AQuz&i1n=in1.txt&script=use warnings;%0Ause strict;%0A%0Awhile (<>) {%0A s/[aeiou]/_/gi;%0A}&cmdline=perl script.pl in1.txt"
style="border:1px solid black;width:100%;height:20em;"></iframe>
You should have received a copy of the licenses along with this program.
If not, see http://perldoc.perl.org/index-licence.html
##### -->
<!-- TODO: Document
Notes:
- "perl -CSD" is recommended because files are currently always UTF-8
- STDIN is currently not supported, workaround is to supply files on command line
-->
<iframe name="perlrunner" sandbox="allow-scripts"
src="perlrunner.html" style="display:none;"></iframe>
<iframe sandbox="allow-scripts" src="perleditor.html"
style="border:1px solid black;width:100%;height:20em;"></iframe>
<iframe id="perl1" sandbox="allow-scripts"
style="border:1px solid black;width:100%;height:8em;"></iframe>
<iframe id="perl2" sandbox="allow-scripts"
style="border:1px solid black;width:100%;height:40em;"></iframe>
<script>
document.getElementById('perl1').src =
"perleditor.html#" + encodeURIComponent(JSON.stringify( {
cmdline: "perl -e 'print \"Hello, World!\"'",
} ));
document.getElementById('perl2').src =
"perleditor.html#" + encodeURIComponent(JSON.stringify( {
cmdline: "perl devoweler.pl mytext.txt other.txt",
script: "use warnings;\nuse strict;\n\nopen my $vfh, '>', 'vowels.txt' or die $!;\n"
+"while (<>) {\n\tprint $vfh $1 while s/([aeiou])/_/i;\n\tprint;\n}\nclose $vfh;",
script_fn: "devoweler.pl",
inputs: [
{ fn: "mytext.txt", text: "Foo\nBar\nQuz\n" },
{ fn: "other.txt", text: "Hello, World!" },
],
outputs : [ "vowels.txt" ],
} ));
</script>
</head>
<body>

@ -0,0 +1,33 @@
body {
margin: 0.4em;
}
.text {
font-family: Calibri, Ubuntu, "Droid Sans", Tahoma, Arial, Helvetica, sans-serif;
}
pre,textarea,code,.code,.filename,.CodeMirror {
font-family: Consolas, "Ubuntu Mono", "Droid Sans Mono", "Lucida Console", "Courier New", Courier, monospace;
}
pre {
margin: 0;
}
.CodeMirror {
border: 1px solid lightgrey;
height: auto;
}
.CodeMirror-scroll {
max-height: 12em;
}
#runnerstate {
margin-top: 0.1em;
margin-bottom: 0.3em;
font-size: 0.8em;
}
#runnererrors {
background-color: rgba(255,200,200,255);
margin-top: 0.3em;
margin-bottom: 0.3em;
padding: 0.1em 0.2em;
}

@ -23,31 +23,11 @@ See the licenses for details.
You should have received a copy of the licenses along with this program.
If not, see http://perldoc.perl.org/index-licence.html
-->
##### -->
<!--cacheable--><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.0/normalize.min.css" integrity="sha256-oSrCnRYXvHG31SBifqP2PM1uje7SJUyX0nTwO2RJV54=" crossorigin="anonymous" />
<!--cacheable--><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.39.2/codemirror.min.css" integrity="sha256-I8NyGs4wjbMuBSUE40o55W6k6P7tu/7G28/JGUUYCIs=" crossorigin="anonymous" />
<style>
body {
margin: 0.4em;
}
.text {
font-family: Calibri, Ubuntu, "Droid Sans", Tahoma, Arial, Helvetica, sans-serif;
}
pre,textarea,code,.code,.filename,.CodeMirror {
font-family: Consolas, "Ubuntu Mono", "Droid Sans Mono", "Lucida Console", "Courier New", Courier, monospace;
}
pre {
margin: 0;
}
.CodeMirror {
border: 1px solid grey;
height: auto;
}
.CodeMirror-scroll {
max-height: 10em;
}
</style>
<link rel="stylesheet" href="perleditor.css" />
<!--cacheable--><script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.39.2/codemirror.min.js" integrity="sha256-uRIJ6Wfou5cTtmcCvQNA9lvsYl8sUbZXxnfG+P79ssY=" crossorigin="anonymous"></script>
<!--cacheable--><script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.39.2/mode/perl/perl.min.js" integrity="sha256-Uu9QBfi8gU6J/MzQunal8ewmY+i/BbCkBrcAXA5bcCM=" crossorigin="anonymous"></script>
@ -55,6 +35,11 @@ pre {
<script>
"use strict";
var mergeStdOutErr = 0; //TODO Later: make an options hash instead of individual variable(s)?
var perlRunner; // the Perl runner iframe found by findPerlRunner()
var buttonBlockers = {}; // for updateButtonState()
var lastExitStatus; // for runnerState()
function makeCM (textarea,plain,ro) {
return CodeMirror.fromTextArea( textarea[0], {
viewportMargin: Infinity, // so browser's search works, not good for long documents though
@ -65,67 +50,94 @@ function makeCM (textarea,plain,ro) {
} );
}
var mergeStdOutErr = 0;
function runnerState (text) {
$('#runnerstate').text( text
+ (lastExitStatus ? ' (last exit status was '+lastExitStatus+')'
: '') );
}
function updateButtonState () {
$('#runperl').prop("disabled",
Object.keys(buttonBlockers).length>0 );
}
var cm_std = { 1:null, 2:null }; // 1=stdout, 2=stderr
function stdOutput (which, data) { // 1=stdout, 2=stderr
function stdOutput (which, data) { // which: 1=stdout, 2=stderr
if (mergeStdOutErr) which = 1;
if (!cm_std[which]) {
var div = $(which==1?'#stdout':'#stderr');
div.show();
cm_std[which] = makeCM($('textarea',div),1,1);
div.data('CodeMirrorInstance', cm_std[which]);
var div = $(which==1?'#stdout':'#stderr');
div.show();
var cm = div.data('CodeMirrorInstance');
if (!cm) {
cm = makeCM($('textarea',div),1,1);
div.data('CodeMirrorInstance', cm);
}
if (data && data.length)
cm_std[which].setValue( cm_std[which].getValue() + data );
cm.setValue( cm.getValue() + data );
}
function clearStdOutput () {
if (cm_std["1"]) cm_std["1"].setValue("");
if (cm_std["2"]) cm_std["2"].setValue("");
$('#stdout,#stderr').each(function (i) {
var div = $(this);
var cm = div.data('CodeMirrorInstance');
if (cm) cm.setValue('');
div.hide();
});
}
var perlRunner;
function findPerlRunner () {
// assume calling this function means the runner isn't available
buttonBlockers.runnerState = 1;
updateButtonState();
// poll for perlRunner, which gets set by the message listener
var pollUntil = Date.now() + 10*1000; // milliseconds
var pollId;
pollId = window.setInterval( function () {
if (perlRunner)
window.clearInterval(pollId);
else if ( Date.now()>pollUntil ) {
if (window.confirm("Perl does not appear to have loaded yet, keep waiting?\n"
+"(If you are on a slow connection, click OK to keep waiting.)"))
pollUntil = Date.now() + 10*1000;
else
window.clearInterval(pollId);
}
else
if (window.parent && window.parent.frames["perlrunner"])
window.parent.frames["perlrunner"].postMessage(
{perlRunnerDiscovery:1}, '*');
}, 100);
}
window.addEventListener('message', function (event) {
var data = event.data;
//console.log("editor got message", data); //DB
if (data["perlRunnerState"]) {
var disableBtn = true;
if ( data.perlRunnerState=="Ready" ) {
perlRunner = event.source;
disableBtn = false;
delete buttonBlockers.runnerState;
updateButtonState();
}
else if ( data.perlRunnerState=="Ended" ) {
if ('exitStatus' in data)
lastExitStatus = ''+data.exitStatus;
// we know the runner will reload itself now
perlRunner = null;
findPerlRunner();
}
$('#runperl').prop("disabled",disableBtn);
runnerState("Perl is "+data.perlRunnerState);
}
else if (data["perlOutput"]) {
else if (data["perlOutput"])
stdOutput(data.perlOutput.chan, data.perlOutput.data);
else if (data["perlOutputFiles"]) {
data.perlOutputFiles.forEach(function (outp) {
setupOutputFile(outp.fn, outp.text);
});
}
else if (data["perlRunnerError"]) {
$('#runnererrors').show();
$('#runnererrors>pre').append(data.perlRunnerError+"\n");
}
else console.warn("Perl Editor ignoring unknown message:",data);
});
function findPerlRunner () {
var pollId;
var pollUntil = Date.now() + 10*1000; // milliseconds
pollId = window.setInterval( function () {
if (perlRunner) {
window.clearInterval(pollId);
}
else if ( Date.now()>pollUntil ) {
window.clearInterval(pollId);
console.error("Perl Editor could not contact Perl Runner"); //TODO: report error to user via UI (same for other console.error calls in this script)
}
else
if (window.parent && window.parent.frames["perlrunner"])
window.parent.frames["perlrunner"].postMessage(
{perlRunnerRegisterClient:1}, '*');
}, 100);
}
function parseCmdLine(str) {
function parseCmdLine(str) { //TODO: there's a better way to do this
var re = /"((?:\\"|\\\\|[^"])*)"|'((?:\\'|\\\\|[^'])*)'|(\S+)/g;
var argv = [];
var match;
@ -137,57 +149,83 @@ function parseCmdLine(str) {
return argv;
}
function parseParams(str) { // Thanks to https://stackoverflow.com/a/26849194
if (!str || !str.length) return {};
return str.split('&').reduce(function (params, param) {
var paramSplit = param.split('=').map(function (value) {
return decodeURIComponent(value.replace(/\+/g, ' '));
function fetchUrl(url,cm) { // fetch the contents of a URL into a CodeMirror instance
cm.setValue("Fetching URL\n"+url+"\nPlease wait...");
buttonBlockers["fetchUrls"]++;
updateButtonState();
$.get(url, function (data) {
cm.setValue(data);
},'text').fail(function (jqXHR,textStatus,errorThrown) {
cm.setValue("Fetching URL\n"+url+"\nFailed!\n"+textStatus+"\n"+errorThrown);
}).always(function () {
buttonBlockers.fetchUrls--;
if (!buttonBlockers.fetchUrls)
delete buttonBlockers.fetchUrls;
updateButtonState();
});
params[paramSplit[0]] = paramSplit[1];
return params;
}, {});
}
function initialize (hash) {
console.log("hash", hash); //DB
function setupOutputFile (fn, text) {
var found = $('div.outputs>.filename')
.filter(function(){ return $(this).text() == fn });
var cm;
if (found.length) {
var div = found.parent();
cm = div.data('CodeMirrorInstance');
}
else {
var div = $('<div/>',{class:"codewithfn outputs"});
div.append( $('<div/>',{class:"filename",text:fn}) );
var ta = $('<textarea/>').appendTo(div);
$('#outputhere').before(div);
cm = makeCM(ta,1,1);
div.data('CodeMirrorInstance', cm);
}
cm.setValue( text ? text : '' );
}
$(function () {
var hashdata = window.location.hash.substr(1);
var hash = hashdata.length>0 ? JSON.parse(decodeURIComponent(hashdata)) : {};
// script
if (hash["script"] || hash["scripturl"]) {
var script = $('#script');
script.show();
var cm = makeCM($('#perlcode'));
script.data('CodeMirrorInstance', cm);
var scriptdiv = $('#script');
if ( hash["script"] || hash["script_url"] ) {
scriptdiv.show();
var cm = makeCM($('#perlcode'),0,0);
scriptdiv.data('CodeMirrorInstance', cm);
if (hash["script"])
cm.setValue(hash.script);
else if (hash["scripturl"]) {
//TODO: fetch URL
}
else if (hash["script_url"])
fetchUrl(hash.script_url,cm);
else throw "internal error: this shouldn't happen";
if (hash["scriptfn"])
$('.filename',script).text(hash.scriptfn);
if (hash["script_fn"])
$('>.filename',scriptdiv).text(hash.script_fn);
}
if (hash["cmdline"]) {
// command line
if (hash["cmdline"])
$('#argv').val(hash.cmdline);
}
// input files
$('.inputs').remove();
for (var i=1;;i++) {
if (!( hash["i"+i+"f"] || hash["i"+i+"u"] )) break;
var fn = hash["i"+i+"n"] || "input"+i+".txt";
if ( hash["inputs"] ) hash.inputs.forEach(function(inp,i) {
var fn = inp["fn"] || "input"+(i+1)+".txt";
var div = $('<div/>',{class:"codewithfn inputs"});
div.append( $('<div/>',{class:"filename",text:fn}) );
var ta = $('<textarea/>').appendTo(div);
$('#inputhere').before(div);
var cm = makeCM(ta,1,0);
div.data('CodeMirrorInstance', cm);
if (hash["i"+i+"f"]) {
cm.setValue(hash["i"+i+"f"]);
}
else if (hash["i"+i+"u"]) {
//TODO: fetch URLs
}
}
if (inp["text"])
cm.setValue(inp.text);
else if (inp["url"])
fetchUrl(inp.url,cm);
});
// stdout/stderr
if (hash["mergestdouterr"]) {
if (hash["mergeStdOutErr"]) {
mergeStdOutErr = 1;
$('#stdout>.filename').text("STDOUT+STDERR");
}
@ -196,44 +234,53 @@ function initialize (hash) {
$('#stdout>.filename').text("STDOUT");
}
clearStdOutput();
// output files
for (var i=1;;i++) {
if (hash["o"+i+"n"]) {
var div = $('<div/>',{class:"codewithfn outputs"});
div.append( $('<div/>',{class:"filename",text:hash["o"+i+"n"]}) );
var ta = $('<textarea/>').appendTo(div);
$('#outputhere').before(div);
var cm = makeCM(ta,1,1);
div.data('CodeMirrorInstance', cm);
}
else break;
}
}
$(function () {
initialize(parseParams(window.location.hash.substr(1)));
// output files
$('.outputs').remove();
if ( hash["outputs"] ) hash.outputs.forEach(function(outp) {
setupOutputFile(outp);
});
var btn_runperl = $('#runperl');
btn_runperl.click( function () {
$('#runperl').click( function () {
clearStdOutput();
var rp_data = {};
// command-line args
var argv = parseCmdLine($('#argv').val());
if (argv.length<1 || argv[0]!="perl") {
console.error("invalid command line");
return }
else argv.shift();
var runperl = {
//TODO: input files
argv: argv,
//TODO: script: perl_cm.getValue(),
};
btn_runperl.prop("disabled",true);
perlRunner.postMessage({ runPerl: runperl },'*');
$('#runnererrors>pre').text('Invalid command line, command must be "perl"');
$('#runnererrors').show();
return;
} // else
argv.shift();
rp_data.argv = argv;
$('#runnererrors>pre').text('');
$('#runnererrors').hide();
// script
if (scriptdiv.is(':visible')) {
rp_data.script = scriptdiv.data('CodeMirrorInstance').getValue();
rp_data.script_fn = scriptdiv.find('>.filename').text();
}
// inputs
$('.inputs').each(function () {
var div = $(this);
var fn = $('>.filename',div).text();
var text = div.data('CodeMirrorInstance').getValue();
if (!rp_data["inputs"]) rp_data.inputs = [];
rp_data.inputs.push([fn, text]);
});
// outputs
$('.outputs').each(function () {
var fn = $(this).find('>.filename').text();
if (!rp_data["outputs"]) rp_data.outputs = [];
rp_data.outputs.push(fn);
});
// send message to runner
buttonBlockers.runnerState = 1;
updateButtonState();
lastExitStatus = null;
runnerState("Requesting Perl Run...");
perlRunner.postMessage({ runPerl: rp_data }, '*');
});
btn_runperl.prop("disabled",true);
findPerlRunner();
});
@ -255,6 +302,14 @@ $(function () {
<input type="text" id="argv" class="code" size="60" value='perl' />
</div>
<div id="runnerstate" class="text">
Loading...
</div>
<div id="runnererrors" style="display:none;">
<pre></pre>
</div>
<div id="stdout" class="codewithfn" style="display:none;">
<div class="filename">STDOUT</div>
<textarea></textarea>

@ -23,7 +23,7 @@ See the licenses for details.
You should have received a copy of the licenses along with this program.
If not, see http://perldoc.perl.org/index-licence.html
-->
##### -->
<!-- Possible To-Do for Later: This whole thing could probably also be
accomplished with a Web Worker, but that would probably require a
@ -44,83 +44,124 @@ Perl.endAfterMain=true; // act like command-line perl
var knownClients = [];
var currentClient; // which client we're running Perl for, also keeps state
var curOutputFiles;
var stdbuf = [null,'',''];
function reportErr (err) {
if (currentClient)
currentClient.postMessage({ perlRunnerError: err },'*');
else
console.error(err);
}
Perl.addStateChangeListener(function (from,to) {
if (from==to) return; // won't be needed as of v0.09-beta
for(var i=0; i<knownClients.length; i++)
knownClients[i].postMessage({ perlRunnerState: Perl.state },'*');
if (to=="Ended") {
if (currentClient) {
for (var c=1;c<=2;c++) // flush buffers
if (stdbuf[c].length)
currentClient.postMessage({ perlOutput: { chan:c, data:stdbuf[c] } },'*');
//TODO: how to handle nonzero exit codes and communicate them back to the client
//TODO: post output files
if (to=="Ended" && currentClient) {
for (var chan=1;chan<=2;chan++) // flush buffers
if (stdbuf[chan].length) {
currentClient.postMessage({ perlOutput: { chan:chan, data:stdbuf[chan] } },'*');
stdbuf[chan] = '';
}
currentClient.postMessage({ perlRunnerState: Perl.state,
exitStatus: Perl.exitStatus },'*');
for(var i=0; i<knownClients.length; i++)
if (knownClients[i]!=currentClient)
knownClients[i].postMessage({ perlRunnerState: Perl.state },'*');
if (curOutputFiles) {
var ofs = curOutputFiles.map(function (file) {
//TODO Later: Support binary files as well?
// {encoding:"binary"} => readFile returns Uint8Array
// Should then also provide the same support on FS.writeFile() as well
var of = { fn: file };
try {
of.text = FS.readFile(file, {encoding:"utf8"});
}
catch (e) {
reportErr("couldn't read "+file+" because "+e);
}
return of;
});
currentClient.postMessage({ perlOutputFiles: ofs },'*');
}
else console.error("state change to Ended with no current client?");
}
else {
for(var i=0; i<knownClients.length; i++)
knownClients[i].postMessage({ perlRunnerState: Perl.state },'*');
}
if (to=="Ended") {
if (!currentClient)
console.error("Internal Error: Perl state change to Ended with no client");
window.location.reload(false);
}
});
var stdbuf = [null,'',''];
Perl.output = function (str,chan) {
stdbuf[chan] += str;
var pos = stdbuf[chan].lastIndexOf("\n");
if (pos>-1) {
var line = stdbuf[chan].slice(0,pos);
if (currentClient)
currentClient.postMessage({ perlOutput: { chan:chan, data:line+"\n" } },'*');
else
console.warn("output on",chan==1?"STDOUT":"STDERR",
"with no client?",line);
stdbuf[chan] = stdbuf[chan].slice(pos+1);
}
if (pos<0) return;
var lines = stdbuf[chan].slice(0,pos+1);
if (currentClient)
currentClient.postMessage({ perlOutput: { chan:chan, data:lines } },'*');
else
console.error("Internal Error: Output on",chan==1?"STDOUT":"STDERR","with no client:",lines);
stdbuf[chan] = stdbuf[chan].slice(pos+1);
};
function saveFile (fn, data) {
if (fn.substring(0,1)!='/') // if relative, make absolute
fn = FS.joinPath([FS.cwd(), fn]);
try {
FS.writeFile(fn, data);
}
catch (e) {
reportErr("couldn't write "+file+" because "+e);
}
}
window.addEventListener('message', function (event) {
if (event.data["perlRunnerDiscovery"]) {
if (!knownClients.includes(event.source))
knownClients.push(event.source);
event.source.postMessage({ perlRunnerState: Perl.state },'*');
}
else if (event.data["runPerl"]) {
if (!knownClients.includes(event.source))
knownClients.push(event.source);
// check state
if (currentClient && currentClient !== event.source) {
console.error("Attempt to run Perl from",event.source,
"but am already running Perl for",currentClient);
reportErr("Attempt to run Perl (from "+event.origin
+") but am already running Perl for someone else (see JavaScript console)");
return;
} // else
currentClient = event.source;
if (Perl.state!="Ready") {
reportErr("Attempt to run Perl in state "+Perl.state);
return;
} // else
// set up files and run perl
var rp = event.data.runPerl;
if (rp["script"]) {
var script_fn = 'script.pl';
if (rp["script_fn"])
script_fn = rp.script_fn;
saveFile(script_fn, rp.script);
}
//TODO Later: can we support STDIN? (probably need to look at webperl.js)
if (rp["inputs"])
rp.inputs.forEach(function (inp) {
saveFile(inp[0], inp[1]);
});
curOutputFiles = rp["outputs"];
Perl.start( rp["argv"] ? rp.argv : [] );
}
else console.warn("Perl Runner ignoring unknown message:", event.data);
});
Perl.init(function () {
Module['thisProgram'] = 'perl';
FS.currentPath = ENV.HOME; // NOTE: https://github.com/kripken/emscripten/issues/5873
window.addEventListener('message', function (event) {
var data = event.data;
var result;
if (data["perlRunnerRegisterClient"]) {
result = { perlRunnerState: Perl.state };
if (!knownClients.includes(event.source))
knownClients.push(event.source);
}
else if (data["runPerl"]) {
if (!knownClients.includes(event.source))
knownClients.push(event.source);
if (currentClient) {
//TODO Later: a way to communicate these errors back to client?
console.error("Attempt to run Perl from",event.source,
"but am already running Perl for",currentClient);
return;
}
if (Perl.state!="Ready") {
console.error("Attempt to run Perl in state",Perl.state,
"from",event.source);
return;
}
currentClient = event.source;
var scriptFn = data.runPerl["scriptName"]
? data.runPerl.scriptName : 'script.pl';
Module['thisProgram'] = './'+scriptFn;
FS.writeFile( FS.joinPath([FS.cwd(), scriptFn]),
data.runPerl.script );
//TODO: input files
var argv = data.runPerl["argv"] ? data.runPerl.argv : [scriptFn];
Perl.start(argv);
}
else console.warn("Perl Runner ignoring unknown message:", data);
//console.log("runner got message", data, "result", result); //DB
if (result)
event.source.postMessage(result,
event.origin==null || event.origin=="null" ? '*' : event.origin);
});
});
</script>

Loading…
Cancel
Save