This repository has been archived by the owner on Dec 10, 2020. It is now read-only.
forked from hakimel/reveal.js
-
Notifications
You must be signed in to change notification settings - Fork 0
/
webperl.js
388 lines (368 loc) · 13.5 KB
/
webperl.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
"use strict";
/** ***** WebPerl - http://webperl.zero-g.net *****
*
* Copyright (c) 2018 Hauke Daempfling ([email protected])
* 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.
*
* You should have received a copy of the licenses along with this program.
* If not, see http://perldoc.perl.org/index-licence.html
**/
/* -- Please see the documentation at http://webperl.zero-g.net/using.html -- */
var Module;
var Perl = {
trace: false, // user may enable this
endAfterMain: false, // user may enable this (before Perl.init)
noMountIdbfs: false, // user may enable this (before Perl.start)
WebPerlVersion: 'v0.09-beta', // user may read (only!) this
state: "Uninitialized", // user may read (only!) this
exitStatus: undefined, // user may read (only!) this
Util: {},
// internal variables:
initStepsLeft: 2, // Must match number of Perl.initStepFinished() calls!
readyCallback: null,
stdout_buf: "", stderr_buf: "", // for our default Perl.output implementation
dispatch: function (perl) {
Perl._call_code_args = Array.prototype.slice.call(arguments, 1);
Perl.eval(perl);
if (Perl._call_code_error) {
var err = Perl._call_code_error;
delete Perl._call_code_error;
throw err;
}
else {
var rv = Perl._call_code_rv;
delete Perl._call_code_rv;
return rv;
}
},
};
/* TODO: Embedded script should be able to influence the running of Perl,
* the cleanest would probably be to set properties on the Perl object,
* such as Perl.autorun = false or Perl.argv = [...]. It should be possible
* for the user to do this for embedded scripts also! Will probably need
* to change the initialization of Perl so that the user can set its properties
* *before* loading webperl.js. */
window.addEventListener("load", function () {
// Note: to get the content of script tags with jQuery: $('script[type="text/perl"]').html()
var scripts = [];
var script_src;
document.querySelectorAll("script[type='text/perl']")
.forEach(function (el) {
if (el.src) {
if (script_src || scripts.length)
console.error('Only a single Perl script may be loaded via "script src=", ignoring others');
else
script_src = el.src;
}
else {
if (script_src)
console.error('Only a single Perl script may be loaded via "script src=", ignoring others');
else
scripts.push(el.innerHTML);
}
});
if (script_src) {
console.debug("Perl: Found a script with src, fetching and running...", script_src);
var xhr = new XMLHttpRequest();
xhr.addEventListener("load", function () {
//TODO Later: Might be nice to name the script in the virtual FS after the URL instead of a generic name
Perl._saveAndRun( this.responseText );
});
xhr.open("GET", script_src);
xhr.send();
}
else if (scripts.length) {
console.debug("Perl: Found",scripts.length,"embedded script(s), autorunning...");
var code = scripts.join(";\n");
// get the first five lines of code
var n = 5 + 1; // the contents of the <script> tag will usually begin with a newline
var i = -1;
while (n-- && i++ < code.length) {
i = code.indexOf("\n", i);
if (i < 0) break;
}
var head = i<0 ? code : code.substring(0,i);
// look for a "use WebPerl"
const regex = /^\s*use\s+WebPerl(\s|;)/m;
if (!regex.exec(head)) { // load WebPerl unless the user loaded it
console.debug("Perl: Autoloading WebPerl");
code = "use WebPerl 'js';\n" + code;
}
Perl._saveAndRun(code);
}
else console.debug("Perl: No embedded scripts");
});
Perl._saveAndRun = function (script) {
Perl.init(function () {
var file = "/tmp/scripts.pl";
try {
FS.writeFile( file, script );
console.debug("Perl: Saved script(s) to ",file,", now running");
window.addEventListener("beforeunload", function () {
// not really needed because we're unloading anyway, but for good measure, end Perl...
console.debug("Perl: beforeunload, ending...");
Perl.end();
});
// run Perl async so that the window has a chance to refresh
window.setTimeout(function () { Perl.start( [ file ] ); }, 1);
}
catch (err) { console.error("Perl:",err); alert("Save to "+file+" failed: "+err); }
});
};
Perl.changeState = function (newState) {
if (Perl.state==newState) return;
var oldState = Perl.state;
Perl.state = newState;
if (Perl.stateChanged) {
console.info("Perl.stateChanged is deprecated, please use Perl.addStateChangeListener instead");
Perl.stateChanged(oldState,newState);
}
for( var i=0 ; i<Perl.stateChangeListeners.length ; i++ )
Perl.stateChangeListeners[i](oldState,newState);
};
Perl.stateChangeListeners = [ function (from,to) {
console.debug("Perl: state changed from "+from+" to "+to);
} ];
Perl.addStateChangeListener = function (handler) {
Perl.stateChangeListeners.push(handler);
};
// chan: 1=STDOUT, 2=STDERR
// implementations are free to ignore the "chan" argument if they want to merge the two streams
Perl.output = function (str,chan) { // can be overridden by the user
var buf = chan==2 ? 'stderr_buf' : 'stdout_buf';
Perl[buf] += str;
var pos = Perl[buf].indexOf("\n");
while (pos>-1) {
console.log( chan==2?"STDERR":"STDOUT", Perl[buf].slice(0,pos) );
Perl[buf] = Perl[buf].slice(pos+1);
pos = Perl[buf].indexOf("\n");
}
};
Perl.outputLine = function (chan,text) { // internal function
if (arguments.length > 2) text = Array.prototype.slice.call(arguments,1).join(' ');
Perl.output(text,chan);
Perl.output("\n",chan);
};
Perl.outputChar = function (chan,c) { // internal function
Perl.output(String.fromCharCode(c),chan);
};
Perl.makeOutputTextarea = function (id) {
var ta = document.createElement('textarea');
if (id) ta.id = id;
ta.rows = 24; ta.cols = 80;
ta.setAttribute("readonly",true);
Perl.output = function (str) {
ta.value = ta.value + str;
ta.scrollTop = ta.scrollHeight;
};
return ta;
};
var getScriptURL = (function() { // with thanks to https://stackoverflow.com/a/2976714
var scripts = document.getElementsByTagName('script');
var index = scripts.length - 1;
var myScript = scripts[index];
return function() { return myScript.src; };
})();
Perl.Util.baseurl = function (urlstr) {
var url = new URL(urlstr);
if (url.protocol=='file:')
return url.href.substring(0, url.href.lastIndexOf('/'));
else
return url.origin + url.pathname.substring(0, url.pathname.lastIndexOf('/'));
};
Perl.init = function (readyCallback) {
if (Perl.state != "Uninitialized")
throw "Perl: can't call init in state "+Perl.state;
Perl.changeState("Initializing");
// Note that a lot of things still won't work for file:// URLs because of the Same-Origin Policy.
// see e.g. https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSRequestNotHttp
var baseurl = Perl.Util.baseurl(getScriptURL());
Perl.readyCallback = readyCallback;
Module = {
noInitialRun: true,
noExitRuntime: true,
print: Perl.outputLine.bind(null,1), printErr: Perl.outputLine.bind(null,2),
stdout: Perl.outputChar.bind(null,1), stderr: Perl.outputChar.bind(null,2),
stdin: function () { return null },
arguments: ['--version'],
onAbort: function () {
console.error("Perl: Aborted (state",Perl.state,")");
alert("Perl aborted in state "+Perl.state);
Perl.exitStatus = -1;
Perl.changeState("Ended");
},
onExit: function (status) { // note this may be called multiple times
if (status==0)
console.debug( "Perl: Exit status",status,"(state",Perl.state,")");
else {
console.error("Perl: Exit status",status,"(state",Perl.state,")");
alert("Perl exited with exit status "+status+" in state "+Perl.state);
}
Perl.exitStatus = status;
Perl.changeState("Ended");
},
onRuntimeInitialized: function () {
console.debug("Perl: Module.onRuntimeInitialized");
Perl.initStepFinished();
},
preRun: [
function () {
if (Perl.noMountIdbfs) {
console.debug("Perl: doing preRun, skipping IndexDB filesystem");
Perl.initStepFinished();
return;
}
console.debug("Perl: doing preRun, fetching IndexDB filesystem...");
try { FS.mkdir('/mnt'); } catch(e) {}
try { FS.mkdir('/mnt/idb'); } catch(e) {}
FS.mount(IDBFS, {} ,'/mnt/idb');
FS.syncfs(true, function (err) {
if (err) { console.error("Perl:",err); alert("Perl: Loading IDBFS failed: "+err); return; }
console.debug("Perl: IndexDB filesystem ready");
Perl.initStepFinished();
});
}
],
locateFile: function (file) {
var wasmRe = /\.(wast|wasm|asm\.js|data)$/;
if (wasmRe.exec(file))
return baseurl+"/"+file;
return file;
},
};
if (Perl.endAfterMain) {
Module.preRun.push(function () {
// patch _main so that afterwards we call emperl_end_perl
var origMain = Module._main;
Module._main = function() {
origMain.apply(this, arguments);
console.debug("Perl: main() has ended, ending perl...");
return Perl.end();
};
});
}
console.debug("Perl: Fetching Emscripten/Perl...");
var script = document.createElement('script');
script.async = true; script.defer = true;
script.src = baseurl+"/emperl.js";
document.getElementsByTagName('head')[0].appendChild(script);
};
Perl.initStepFinished = function () {
if (Perl.state!="Initializing" || Perl.initStepsLeft<1)
throw "Perl: internal error: can't call initStepFinished in state "+Perl.state+" ("+Perl.initStepsLeft+")";
if (--Perl.initStepsLeft) {
console.debug("Perl: One init step done, but "+Perl.initStepsLeft+" steps left, waiting...");
return;
} else console.debug("Perl: All init steps done, doing final initialization...");
/* NOTE: Apparently, when NO_EXIT_RUNTIME is set, and exit() is called from the main program
* (from Module.callMain), Emscripten doesn't report this back to us via an ExitStatus exception
* like it does from ccall - it doesn't even call the addOnExit/ATEXIT or addOnPostRun/ATPOSTRUN
* handlers! So at the moment, the only reliable way I've found to catch the program exit()ing
* is to patch into Emscripten's (undocumented!) Module.quit... */
var origQuit = Module.quit;
Module.quit = function (status, exception) {
console.debug("Perl: quit with",exception);
Module.onExit(status);
origQuit(status,exception);
}
Perl.changeState("Ready");
if (Perl.readyCallback) Perl.readyCallback();
Perl.readyCallback = null;
};
Perl.start = function (argv) {
if (Perl.state!="Ready")
throw "Perl: can't call start in state "+Perl.state;
Perl.changeState("Running");
try {
// Note: currently callMain doesn't seem to throw ExitStatus exceptions, see discussion in Perl.initStepFinished
Module.callMain(argv ? argv : Module.arguments);
}
catch (e) {
if (e instanceof ExitStatus) {
console.debug("Perl: start:",e);
Module.onExit(e.status);
} else throw e;
}
};
Perl.eval = function (code) {
if (Perl.state!="Running")
throw "Perl: can't call eval in state "+Perl.state;
if (Perl.trace) console.debug('Perl: ccall webperl_eval_perl',code);
try {
return ccall("webperl_eval_perl","string",["string"],[code]);
}
catch (e) {
if (e instanceof ExitStatus) {
// the code caused perl to (try to) exit - now we need to call
// Perl's global destruction via our emperl_end_perl() function
Perl.end(); //TODO: Perl.end has already been called at this point (how?)
} else throw e;
}
};
/* Note that Emscripten apparently doesn't support re-running the program once it exits (?).
* So at the moment, once we end Perl, that's it. The only useful effect of ending Perl is
* that global destruction is executed and END blocks are called. But since a user may leave
* a webpage at any moment without warning, WebPerl scripts should not rely on normal termination! */
Perl.end = function () {
if (Perl.state!="Running") {
if (Perl.state=="Ended") {
console.debug("Perl: end called when already Ended");
return;
}
else throw "Perl: can't call end in state "+Perl.state;
}
var status;
try {
status = ccall("emperl_end_perl","number",[],[]);
// we know that emperl_end_perl only calls exit() on a nonzero exit code,
// which means no ExitStatus exception gets thrown on a zero exit code,
// so we *should* reach this point only with status==0
if (status!=0) console.warn("emperl_end_perl returned with status",status);
Module.onExit(status); // does Perl.changeState() for us
}
catch (e) {
if (e instanceof ExitStatus) {
console.debug("Perl: end: ",e);
status = e.status;
Module.onExit(e.status); // does Perl.changeState() for us
} else throw e;
}
return status;
};
Perl.next_glue_id = 0;
Perl.GlueTable = {};
Perl._glue_free_ids = {};
Perl.glue = function (ref) {
var id;
var free_ids = Object.keys(Perl._glue_free_ids);
if (free_ids.length>0) {
id = free_ids[0];
delete Perl._glue_free_ids[id];
if (Perl.trace) console.debug('Perl: Glue reused id',id,'to',ref);
}
else {
if (Perl.next_glue_id>=Number.MAX_SAFE_INTEGER)
throw "Perl.GlueTable is overflowing!"
id = ''+(Perl.next_glue_id++);
if (Perl.trace) console.debug('Perl: Glue id',id,'to',ref);
}
Perl.GlueTable[id] = ref;
return id;
}
Perl.unglue = function (id) {
if (Perl.trace) console.debug('Perl: Unglue id',id,'from',Perl.GlueTable[id]);
delete Perl.GlueTable[id];
Perl._glue_free_ids[id]=1;
}