1 module guino;
2 
3 import std.json;
4 import std.string;
5 import std.conv : to;
6 
7 // Window size hints
8 enum WEBVIEW_HINT_NONE=0;  /// Width and height are default size
9 enum WEBVIEW_HINT_MIN=1;   /// Width and height are minimum bounds
10 enum WEBVIEW_HINT_MAX=2;   /// Width and height are maximum bounds
11 enum WEBVIEW_HINT_FIXED=3; /// Window size can not be changed by a user
12 
13 
14 /// The main struct
15 struct WebView {
16 
17    private webview_t handle = null;
18 
19    /++ Create a new WebView.
20    + Params: enableDebug = allow code inspection from webview
21    +/
22    this(bool enableDebug, void * window = null) { create(enableDebug, window); }
23 
24    /// Ditto
25    void create(bool enableDebug = false, void * window = null) in(handle is null, error_already_inited) { handle = webview_create(enableDebug, window);  }
26 
27    /// Start the WebView. Be sure to set size before.
28    void run() in(handle !is null, error_not_inited) { webview_run(handle); }
29 
30    /++ Set webview HTML directly.
31    + ---
32       webview.html = "<html><body>hello, world!</body></html>";
33    + ---
34    +/
35    void html(string data) in(handle !is null, error_not_inited) { webview_set_html(handle, data.toStringz); }
36 
37    /// Navigates webview to the given URL. URL may be a properly encoded data URI.
38    void navigate(string url) in(handle !is null, error_not_inited) { webview_navigate(handle, url.toStringz); }
39 
40    /// Set the window title
41    void title(string title) in(handle !is null, error_not_inited) { webview_set_title(handle, title.toStringz);}
42 
43    /// Set the window size
44    void size(int width, int height, int hints = WEBVIEW_HINT_NONE) in(handle !is null, error_not_inited) { webview_set_size(handle, width, height, hints);}
45 
46    ///
47    void terminate() { if(handle !is null) webview_terminate(handle); handle = null; }
48 
49    /// Returns a handle to the native window
50    void* window() in(handle !is null, error_not_inited) { return webview_get_window(handle); }
51 
52 
53    /++ Eval some js code. When it is done, `func()` is called (optional)
54     + ---
55     + void callback(JSValue v) { writeln("Result from js: ", v); }
56     + webview.eval!callback("1+1");
57     + ---
58     +/
59    void eval(alias func = null)(string js, void* extra = null)
60    in(handle !is null, error_not_inited)
61    {
62       static if (is (typeof(func) == typeof(null))) webview_eval(handle, js.toStringz);
63       else
64       {
65          import std.uuid;
66 
67          struct EvalPayload
68          {
69             void        *extra;
70             webview_t   handle;
71             string      uuid;
72          }
73 
74          auto uuid = "__eval__" ~ randomUUID().toString.replace("-", "_");
75          EvalPayload *payload = new EvalPayload(extra, handle, uuid);
76 
77          bindJs!(
78             (JSONValue[] v, void* extra)
79             {
80                import core.memory : GC;
81 
82                EvalPayload *p = cast(EvalPayload*) extra;
83                webview_unbind(p.handle, p.uuid.toStringz);
84                static if (__traits(compiles, func(v[0], p.extra))) func(v[0], p.extra);
85                else func(v[0]);
86                p.destroy();
87                GC.free(p);
88             }
89          )
90          (uuid, cast(void*)payload);
91 
92          eval(uuid ~ `(eval('` ~ js.escapeJs() ~ `'));`);
93       }
94    }
95 
96    /++ Helper function to convert a file to data uri to embed inside html.
97    + It works at compile-time so you must set source import paths on your project.
98    + See_Also: [toDataUri], [fileAsDataUri], https://dlang.org/spec/expression.html#import_expressions
99    + ---
100    + webview.byId("myimg").src = importAsDataUri!"image.jpg";
101    +/
102    static string importAsDataUri(string file)(string mimeType = "application/octet-stream")
103    {
104       import std.algorithm;
105       import std.encoding;
106       import std.base64;
107 
108       auto mime = mimeType;
109       auto extIdx = file.lastIndexOf('.');
110       if (extIdx >= 0 && file[extIdx..$] in mimeTypes)
111          mime = mimeTypes[file[extIdx..$]];
112 
113       auto bytes = import(file).representation;
114       auto bom = getBOM(bytes);
115       bytes = bytes[bom.sequence.length .. $];
116 
117       return WebView.toDataUri(bytes, mime);
118    }
119 
120    /++ Helper function to convert bytes to data uri to embed inside html
121    + See_Also: [importAsDataUri], [fileAsDataUri]
122    +/
123    static auto toDataUri(const ubyte[] bytes, string mimeType = "application/octet-stream")
124    {
125       import std.base64;
126       return ("data:" ~ mimeType ~ ";base64," ~ Base64.encode(bytes));
127    }
128 
129    /++ Helper function to convert bytes to data uri to embed inside html
130    + See_Also: [importAsDataUri], [toDataUri]
131    +/
132    static string fileAsDataUri(string file, string mimeType = "application/octet-stream")
133    {
134       import std.file;
135       auto mime = mimeType;
136       auto extIdx = file.lastIndexOf('.');
137       if (extIdx >= 0 && file[extIdx..$] in mimeTypes)
138          mime = mimeTypes[file[extIdx..$]];
139 
140       ubyte[] data = cast(ubyte[])std.file.read(file);
141       return toDataUri(data, mime);
142    }
143 
144    /++ Injects JavaScript code at the initialization of the new page. Every time
145      + the webview will open a new page - this initialization code will be
146      + executed. It is guaranteed that code is executed before window.onload.
147      +/
148    void onInit(string js) in(handle !is null, error_not_inited) { webview_init(handle, js.toStringz); }
149 
150    /// Ditto
151    void onInit(alias func)()
152    {
153       import std.uuid;
154       string uuid = "_init_" ~ randomUUID.toString().replace("-", "_");
155       static void proxy(JSONValue[]) { func(); }
156       bindJs!(proxy)(uuid);
157       onInit(uuid ~ "();");
158    }
159 
160 
161    /++ Respond to a binding call from js.
162    + See_Also: [resolve], [reject]
163    +/
164    void respond(string seq, bool resolved, JSONValue v) in(handle !is null, error_not_inited) { webview_return(handle, seq.toStringz, resolved?0:1, v.toString.toStringz); }
165 
166    /++ Resolve a js promise
167    + See_Also: [respond], [reject]
168    +/
169    void resolve(string seq, JSONValue v) in(handle !is null, error_not_inited) { respond(seq, 0, v); }
170 
171    /++ Reject a js promise
172    + See_Also: [respond], [resolve]
173    +/
174    void reject(string seq, JSONValue v) in(handle !is null, error_not_inited) { respond(seq, 1, v); }
175 
176    /++ Removes a D callback that was previously set by `bind()`
177     +  See_Also: [bindJs]
178     ++/
179    void unbindJs(string name) in(handle !is null, error_not_inited) { webview_unbind(handle, name.toStringz); }
180 
181    /++ Create a callback in D for a js function.
182      + See_Also: [response], [resolve], [reject], [unbindJs]
183      + ---
184      + // Simple callback without params
185      + void hello() { ... }
186      +
187      + // Js arguments will be passed as JSON array
188      + void world(JSONValue[] args) { ... }
189      +
190      + // You can use the sequence arg to return a response.
191      + void reply(JSONValue[] args, string sequence) { ... resolve(sequence, JSONValue("Hello Js!")); }
192      +
193      + webview.bind!hello;          // If you call hello() from js, hello() from D will respond
194      + webview.bind!hello("test")   // If tou call test() from js, hello() from D will respond
195      + ---
196      +/
197    void bindJs(alias func)(string jsFunc = __traits(identifier, func), void* extra = null)
198    in(handle !is null, error_not_inited)
199    {
200 
201       static if (__traits(compiles, func(JSONValue[].init, string.init, (void *).init)))
202          extern(C) void callback(const char *seq, const char *request, void *extra)
203          {
204             func(parseJSON(request.to!string).array, seq.to!string, extra);
205          }
206 
207       else static if (__traits(compiles, func(JSONValue[].init, (void*).init)))
208          extern(C) void callback(const char *seq, const char *request, void *extra)
209          {
210             func(parseJSON(request.to!string).array, extra);
211          }
212 
213 
214       else static if (__traits(compiles, func(JSONValue[].init, string.init)))
215          extern(C) void callback(const char *seq, const char *request, void *extra)
216          {
217             func(parseJSON(request.to!string).array, seq.to!string);
218          }
219 
220       else static if (__traits(compiles, func(JSONValue[].init)))
221          extern(C) void callback(const char *seq, const char *request, void *extra)
222          {
223             func(parseJSON(request.to!string).array);
224          }
225 
226       else static if (__traits(compiles, func()))
227          extern(C) void callback(const char *seq, const char *request, void *extra)
228          {
229             func();
230          }
231 
232 
233       static if (__traits(compiles, callback(null, null, null))) webview_bind(handle, jsFunc.toStringz, &callback, extra);
234       else static assert(0, "Can't bind `" ~ typeof(func).stringof ~ "` try with `void func(JSONValue[] args)`, for example.");
235    }
236 
237    /// Ditto
238    void bindJs(alias func)(void* extra) in(handle !is null, error_not_inited) { bindJs!func( __traits(identifier, func), extra); }
239 
240 
241    /++ A helper function to parse args passed as JSONValue[]
242     + See_Also: [bindJs]
243     + ---
244     + void myFunction(JSONValue[] arg)
245     + {
246     +    with(WebView.parseJsArgs!(int, "hello", string, "world")(args))
247     +    {
248     +       // Just like we have named args
249     +       writeln(hello);
250     +       writeln(world);
251     +    }
252     + }
253     + ---
254     +/
255    static auto parseJsArgs(T...)(JSONValue[] v)
256    {
257       import std.typecons : Tuple;
258 
259       alias TUPLE = Tuple!T;
260       TUPLE ret;
261       bool 	fail;
262 
263       static foreach(idx; 0..TUPLE.length)
264       {
265          fail = false;
266          if (idx < v.length)
267          {
268             try { mixin("ret[" ~ idx.to!string ~ "]") = v[idx].get!(typeof(TUPLE[idx])); }
269             catch (Exception e) { fail = true; }
270          }
271          else fail = true;
272 
273          if (fail)
274             mixin("ret[" ~ idx.to!string ~ "]") = typeof(TUPLE[idx]).init;
275       }
276 
277       return ret;
278    }
279 
280 
281    /++ Search for an element in the dom, using a css selector. Returned element can forward calls to js.
282    + See_Also: [byId]
283    +/
284    Element bySelector(string query)
285    in(handle !is null, error_not_inited)
286    {
287       return new Element(this, "document.querySelector('" ~ query.escapeJs() ~ "')");
288    }
289 
290    /++  Search for an element in the dom, using id. Returned element can forward calls to js.
291    + See_Also: [bySelector]
292    + ---
293    + webview.byId("myid").innerText = "Hi!";
294    + webview.byId("myid").setAttribute("href", "https://example.com");
295    + ---
296    +/
297    Element byId(string id)
298    in(handle !is null, error_not_inited)
299    {
300       return new Element(this, "document.getElementById('" ~ id.escapeJs() ~ "')");
301    }
302 
303    class Element
304    {
305       private string js;
306       private WebView wv;
307 
308       private this(WebView wv, string js) { this.wv = wv; this.js = js; }
309 
310 
311       // Try to use js dom from D
312       void opDispatch(string name, T...)(T val) {
313 
314          static if (T.length == 0) zeroParamsOpDispatch!(name)(val);
315          else static if (T.length == 1) singleParamOpDispatch!(name)(val);
316          else static if (T.length > 1)
317          {
318 
319             import std.conv;
320             import std.traits;
321             import std.string;
322 
323             string jsCode = js ~ "." ~ name ~ "(";
324 
325             static foreach(v; val)
326             {
327                static if (isIntegral!(typeof(v)) || isFloatingPoint!(typeof(v))) jsCode ~= v.to!string ~ ",";
328                else static if (isSomeString!(typeof(v))) jsCode ~= `'` ~ v.escapeJs() ~ `',`;
329                else throw new Exception("Can't assign " ~ T.stringof);
330             }
331 
332             if (jsCode.endsWith(",")) jsCode = jsCode[0..$-1];
333 
334             jsCode ~= ")";
335             wv.eval(jsCode);
336          }
337 
338       }
339 
340       private void zeroParamOpDispatch(string name, T)(T val) {
341          import std.conv;
342          import std.traits;
343 
344          string jsCode;
345 
346          string fullName = js ~ "." ~ name;
347 
348          jsCode ~= "if (" ~fullName ~ " != null && (" ~ fullName ~ " instanceof Function)) {\n";
349          jsCode ~= fullName ~ "()";
350          jsCode ~= "\n}";
351 
352          wv.eval(jsCode);
353       }
354 
355       private void singleParamOpDispatch(string name, T)(T val) {
356          import std.conv;
357          import std.traits;
358 
359          string jsCode;
360 
361          string fullName = js ~ "." ~ name;
362 
363          jsCode ~= "if (" ~fullName ~ " != null && (" ~ fullName ~ " instanceof Function)) {\n";
364          jsCode ~= js ~ "." ~ name;
365          static if (isIntegral!T || isFloatingPoint!T) jsCode ~= val.to!string;
366          else static if (isSomeString!T) jsCode ~= `('` ~ val.escapeJs() ~ "'";
367          else throw new Exception("Can't assign " ~ T.stringof);
368          jsCode ~= ");";
369 
370          jsCode ~= "\n} else { \n";
371          static if (isIntegral!T || isFloatingPoint!T) jsCode ~= js ~ "." ~ name ~ `=` ~ val.to!string ~ ";";
372          else static if (isSomeString!T) jsCode ~= js ~ "." ~ name ~ `= '` ~ val.escapeJs() ~ "';";
373          else throw new Exception("Can't assign " ~ T.stringof);
374 
375          jsCode ~= "\n}";
376 
377          wv.eval(jsCode);
378       }
379 
380       void setAttribute(T)(string name, T val) {
381          import std.conv;
382          import std.traits;
383 
384          string jsCode;
385 
386          static if (isIntegral!T || isFloatingPoint!T) jsCode = js ~ ".setAttribute('" ~ name.escapeJs() ~ `', ` ~ val.to!string ~ ");";
387          else static if (isSomeString!T) jsCode = js ~ ".setAttribute('" ~ name.escapeJs() ~ `', '` ~ val.escapeJs() ~ "');";
388          else throw new Exception("Can't assign " ~ T.stringof);
389 
390          wv.eval(jsCode);
391       }
392 
393    }
394 
395    class Elements
396    {
397       private string js;
398       private WebView wv;
399 
400       private this(WebView wv, string js) { this.wv = wv; this.js = js; }
401 
402       Element opIndex(size_t idx)
403       {
404          return new Element(wv, js ~ "[" ~ idx.to!string ~ "]");
405       }
406 
407    }
408 
409 }
410 
411 /// Helper function to escape js strings
412 string escapeJs(string s, char stringDelimeter = '\'')
413 {
414    return s.replace(`\`, `\\`).replace(stringDelimeter, `\` ~ stringDelimeter);
415 }
416 
417 
418 private:
419 
420 private immutable error_not_inited = "WebView is not initialized. Please call .create() method first.";
421 private immutable error_already_inited = "Please call .terminate() first.";
422 
423 
424 enum string[string] mimeTypes =
425 [
426    ".aac" : "audio/aac", ".abw" : "application/x-abiword", ".arc" : "application/x-freearc", ".avif" : "image/avif",
427    ".bin" : "application/octet-stream", ".bmp" : "image/bmp", ".bz" : "application/x-bzip", ".bz2" : "application/x-bzip2",
428    ".cda" : "application/x-cdf", ".csh" : "application/x-csh", ".css" : "text/css", ".csv" : "text/csv",
429    ".doc" : "application/msword", ".docx" : "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
430    ".eot" : "application/vnd.ms-fontobject", ".epub" : "application/epub+zip", ".gz" : "application/gzip",
431    ".gif" : "image/gif", ".htm" : "text/html", ".html" : "text/html", ".ico" : "image/vnd.microsoft.icon",
432    ".ics" : "text/calendar", ".jar" : "application/java-archive", ".jpeg" : "image/jpeg", ".jpg" : "image/jpeg",
433    ".js" : "text/javascript", ".json" : "application/json", ".jsonld" : "application/ld+json", ".mid" : ".midi",
434    ".mjs" : "text/javascript", ".mp3" : "audio/mpeg",".mp4" : "video/mp4", ".mpeg" : "video/mpeg", ".mpkg" : "application/vnd.apple.installer+xml",
435    ".odp" : "application/vnd.oasis.opendocument.presentation", ".ods" : "application/vnd.oasis.opendocument.spreadsheet",
436    ".odt" : "application/vnd.oasis.opendocument.text", ".oga" : "audio/ogg", ".ogv" : "video/ogg", ".ogx" : "application/ogg",
437    ".opus" : "audio/opus", ".otf" : "font/otf", ".png" : "image/png", ".pdf" : "application/pdf", ".php" : "application/x-httpd-php",
438    ".ppt" : "application/vnd.ms-powerpoint", ".pptx" : "application/vnd.openxmlformats-officedocument.presentationml.presentation",
439    ".rar" : "application/vnd.rar", ".rtf" : "application/rtf", ".sh" : "application/x-sh", ".svg" : "image/svg+xml",
440    ".swf" : "application/x-shockwave-flash", ".tar" : "application/x-tar", ".tif" : "image/tiff", ".tiff" : "image/tiff",
441    ".ts" : "video/mp2t", ".ttf" : "font/ttf", ".txt" : "text/plain", ".vsd" : "application/vnd.visio", ".wasm" : "application/wasm",
442    ".wav" : "audio/wav", ".weba" : "audio/webm", ".webm" : "video/webm", ".webp" : "image/webp", ".woff" : "font/woff", ".woff2" : "font/woff2",
443    ".xhtml" : "application/xhtml+xml", ".xls" : "application/vnd.ms-excel", ".xlsx" : "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
444    ".xml" : "application/xml", ".xul" : "application/vnd.mozilla.xul+xml", ".zip" : "application/zip", ".3gp" : "video/3gpp",
445    ".3g2" : "video/3gpp2", ".7z" : "application/x-7z-compressed"
446 ];
447 
448 
449 alias webview_t = void*;
450 
451 // Creates a new webview instance. If debug is non-zero - developer tools will
452 // be enabled (if the platform supports them). The window parameter can be a
453 // pointer to the native window wb.handle. If it's non-null - then child WebView
454 // is embedded into the given parent window. Otherwise a new window is created.
455 // Depending on the platform, a GtkWindow, NSWindow or HWND pointer can be
456 // passed here. Returns null on failure. Creation can fail for various reasons
457 // such as when required runtime dependencies are missing or when window creation
458 // fails.
459 extern(C) webview_t webview_create(int _debug, void *window);
460 
461 // Destroys a webview and closes the native window.
462 extern(C)  void webview_destroy(webview_t w);
463 
464 // Runs the main loop until it's terminated. After this function exits - you
465 // must destroy the webview.
466 extern(C)  void webview_run(webview_t w);
467 
468 // Stops the main loop. It is safe to call this function from another other
469 // background thread.
470 extern(C)  void webview_terminate(webview_t w);
471 
472 // Posts a function to be executed on the main thread. You normally do not need
473 // to call this function, unless you want to tweak the native window.
474 extern(C)  void
475 webview_dispatch(webview_t w, dispatchCallback, void *arg);
476 
477 alias dispatchCallback = extern(C) void function(webview_t w, void* arg);
478 
479 // Returns a native window wb.handle pointer. When using a GTK backend the pointer
480 // is a GtkWindow pointer, when using a Cocoa backend the pointer is a NSWindow
481 // pointer, when using a Win32 backend the pointer is a HWND pointer.
482 extern(C)  void *webview_get_window(webview_t w);
483 
484 // Updates the title of the native window. Must be called from the UI thread.
485 extern(C)  void webview_set_title(webview_t w, const char *title);
486 
487 
488 // Updates the size of the native window. See WEBVIEW_HINT constants.
489 extern(C)  void webview_set_size(webview_t w, int width, int height,
490                                   int hints);
491 
492 // Navigates webview to the given URL. URL may be a properly encoded data URI.
493 // Examples:
494 // webview_navigate(w, "https://github.com/webview/webview");
495 // webview_navigate(w, "data:text/html,%3Ch1%3EHello%3C%2Fh1%3E");
496 // webview_navigate(w, "data:text/html;base64,PGgxPkhlbGxvPC9oMT4=");
497 extern(C)  void webview_navigate(webview_t w, const char *url);
498 
499 // Set webview HTML directly.
500 // Example: webview_set_html(w, "<h1>Hello</h1>");
501 extern(C)  void webview_set_html(webview_t w, const char *html);
502 
503 // Injects JavaScript code at the initialization of the new page. Every time
504 // the webview will open a new page this initialization code will be
505 // executed. It is guaranteed that code is executed before window.onload.
506 extern(C)  void webview_init(webview_t w, const char *js);
507 
508 // Evaluates arbitrary JavaScript code. Evaluation happens asynchronously, also
509 // the result of the expression is ignored. Use RPC bindings if you want to
510 // receive notifications about the results of the evaluation.
511 extern(C)  void webview_eval(webview_t w, const char *js);
512 
513 // Binds a native C callback so that it will appear under the given name as a
514 // global JavaScript function. Internally it uses webview_init(). The callback
515 // receives a sequential request id, a request string and a user-provided
516 // argument pointer. The request string is a JSON array of all the arguments
517 // passed to the JavaScript function.
518 extern(C)  void webview_bind(webview_t w, const char *name,
519                               bindCallback,
520                               void *arg);
521 
522 alias bindCallback = extern(C) void function (const char *seq, const char *req, void *arg);
523 
524 // Removes a native C callback that was previously set by webview_bind.
525 extern(C)  void webview_unbind(webview_t w, const char *name);
526 
527 // Responds to a binding call from the JS side. The ID/sequence number must
528 // match the value passed to the binding wb.handler in order to respond to the
529 // call and complete the promise on the JS side. A status of zero resolves
530 // the promise, and any other value rejects it. The result must either be a
531 // valid JSON value or an empty string for the primitive JS value "undefined".
532 extern(C)  void webview_return(webview_t w, const char *seq, int status, const char *result);