diff options
Diffstat (limited to 'extsrc/pikchr-worker.js')
| -rw-r--r-- | extsrc/pikchr-worker.js | 222 |
1 files changed, 222 insertions, 0 deletions
diff --git a/extsrc/pikchr-worker.js b/extsrc/pikchr-worker.js new file mode 100644 index 0000000..2df5bb5 --- /dev/null +++ b/extsrc/pikchr-worker.js @@ -0,0 +1,222 @@ +/* + 2022-05-20 + + The author disclaims copyright to this source code. In place of a + legal notice, here is a blessing: + + * May you do good and not evil. + * May you find forgiveness for yourself and forgive others. + * May you share freely, never taking more than you give. + + *********************************************************************** + + This is a JS Worker file for use with the pikchr wasm build. It + loads the pikchr wasm module and offers access to it via the Worker + message-passing interface. + + Because we can have only a single message handler, as opposed to an + arbitrary number of discrete event listeners like with DOM elements, + we have to define a lower-level message API. Messages abstractly + look like: + + { type: string, data: type-specific value } + + Where 'type' is used for dispatching and 'data' is a + 'type'-dependent value. + + The 'type' values expected by each side of the main/worker + connection vary. The types are described below but subject to + change at any time as this experiment evolves. + + Main-to-Worker message types: + + - pikchr: data=pikchr-format text to render or an object: + + { + pikchr: source code for the pikchr, + darkMode: boolean true to adjust colors for a dark color scheme, + cssClass: CSS class name to add to the SVG + } + + Workers-to-Main message types: + + - stdout, stderr: indicate stdout/stderr output from the wasm + layer. The data property is the string of the output, noting + that the emscripten binding emits these one line at a time. Thus, + if a C-side puts() emits multiple lines in a single call, the JS + side will see that as multiple calls. Example: + + {type:'stdout', data: 'Hi, world.'} + + - module: Status text. This is intended to alert the main thread + about module loading status so that, e.g., the main thread can + update a progress widget and DTRT when the module is finished + loading and available for work. Status messages come in the form + + {type:'module', data:{ + type:'status', + data: {text:string|null, step:1-based-integer} + } + + with an incrementing step value for each subsequent message. When + the module loading is complete, a message with a text value of + null is posted. + + - pikchr: + + {type: 'pikchr', + data:{ + pikchr: input text, + result: rendered result (SVG on success, HTML on error), + isError: bool, true if .pikchr holds an error report, + flags: integer: flags used to configure the pikchr rendering, + width: if !isError, width (integer pixels) of the SVG, + height: if !isError, height (integer pixels) of the SVG + } + } + +*/ + +"use strict"; +(function(){ + /** + Posts a message in the form {type,data} unless passed more than + 2 args, in which case it posts {type, data:[arg1...argN]}. + */ + const wMsg = function(type,data){ + postMessage({ + type, + data: arguments.length<3 + ? data + : Array.prototype.slice.call(arguments,1) + }); + }; + + const stderr = function(){wMsg('stderr', Array.prototype.slice.call(arguments));}; + + self.onerror = function(/*message, source, lineno, colno, error*/) { + const err = arguments[4]; + if(err && 'ExitStatus'==err.name){ + /* This "cannot happen" for this wasm binding, but just in + case... */ + pikchrModule.isDead = true; + stderr("FATAL ERROR:", err.message); + stderr("Restarting the app requires reloading the page."); + wMsg('error', err); + } + pikchrModule.setStatus('Exception thrown, see JavaScript console: '+err); + }; + + self.onmessage = function f(ev){ + ev = ev.data; + switch(ev.type){ + /** + Runs the given text through pikchr and emits a 'pikchr' + message result (output format documented above). + + Fires a working/start event before it starts and + working/end event when it finishes. + */ + case 'pikchr': + if(pikchrModule.isDead){ + stderr("wasm module has exit()ed. Cannot pikchr."); + return; + } + if(!f._){ + f._ = pikchrModule.cwrap('pikchr', 'string', [ + 'string'/*script*/, 'string'/*CSS class*/, 'number'/*flags*/, + 'number'/*output: SVG width*/, 'number'/*output: SVG height*/ + ]); + } + wMsg('working','start'); + const stack = pikchrModule.stackSave(); + try { + const pnWidth = pikchrModule.stackAlloc(4), + pnHeight = pikchrModule.stackAlloc(4); + let script = '', flags = 0, cssClass = null; + if('string'===typeof ev.data){ + script = ev.data; + }else if(ev.data && 'object'===typeof ev.data){ + script = ev.data.pikchr; + flags = ev.data.darkMode ? 0x02 : 0; + if(ev.data.cssClass) cssClass = ev.data.cssClass; + } + pikchrModule.setValue(pnWidth, 0, "i32"); + pikchrModule.setValue(pnHeight, 0, "i32"); + const msg = { + pikchr: script, + result: (f._(script, cssClass, flags, pnWidth, pnHeight) || "").trim(), + flags: flags + }; + msg.isError = !!(msg.result && msg.result.startsWith('<div')); + if(msg.isError){ + msg.width = msg.height = null; + }else{ + msg.width = pikchrModule.getValue(pnWidth, "i32"); + msg.height = pikchrModule.getValue(pnHeight, "i32"); + } + wMsg('pikchr', msg); + } finally { + pikchrModule.stackRestore(stack); + wMsg('working','end'); + } + return; + }; + console.warn("Unknown pikchr-worker message type:",ev); + }; + + /** + emscripten module for use with build mode -sMODULARIZE. + */ + const pikchrModule = { + print: function(){wMsg('stdout', Array.prototype.slice.call(arguments));}, + printErr: stderr, + /** + Intercepts status updates from the emscripting module init + and fires worker events with a type of 'status' and a + payload of: + + { + text: string | null, // null at end of load process + step: integer // starts at 1, increments 1 per call + } + + We have no way of knowing in advance how many steps will + be processed/posted, so creating a "percentage done" view is + not really practical. One can be approximated by giving it a + current value of message.step and max value of message.step+1, + though. + + When work is finished, a message with a text value of null is + submitted. + + After a message with text==null is posted, the module may later + post messages about fatal problems, e.g. an exit() being + triggered, so it is recommended that UI elements for posting + status messages not be outright removed from the DOM when + text==null, and that they instead be hidden until/unless + text!=null. + */ + setStatus: function f(text){ + if(!f.last) f.last = { step: 0, text: '' }; + else if(text === f.last.text) return; + f.last.text = text; + wMsg('module',{ + type:'status', + data:{step: ++f.last.step, text: text||null} + }); + } + }; + + importScripts('pikchr-v8806526039.js'); + /** + initPikchrModule() is installed via pikchr.js due to + building with: + + emcc ... -sMODULARIZE=1 -sEXPORT_NAME=initPikchrModule + */ + initPikchrModule(pikchrModule).then(function(thisModule){ + //globalThis.M = pikchrModule; console.warn("pikchrModule=globalThis.M=",globalThis.M); + wMsg('pikchr-ready', pikchrModule.ccall('pikchr_version','string')); + }); +})(); |
