summaryrefslogtreecommitdiff
path: root/extsrc/pikchr-worker.js
blob: 2df5bb55da7776f0066b96893eb11d211bee1edb (plain) (blame)
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
/*
  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'));
  });
})();