summaryrefslogtreecommitdiffstats
path: root/ext/wasm/fiddle
diff options
context:
space:
mode:
Diffstat (limited to 'ext/wasm/fiddle')
-rw-r--r--ext/wasm/fiddle/emscripten.css24
-rw-r--r--ext/wasm/fiddle/fiddle-worker.js384
-rw-r--r--ext/wasm/fiddle/fiddle.js815
-rw-r--r--ext/wasm/fiddle/index.html278
4 files changed, 1501 insertions, 0 deletions
diff --git a/ext/wasm/fiddle/emscripten.css b/ext/wasm/fiddle/emscripten.css
new file mode 100644
index 0000000..7e3dc81
--- /dev/null
+++ b/ext/wasm/fiddle/emscripten.css
@@ -0,0 +1,24 @@
+/* emcscript-related styling, used during the module load/intialization processes... */
+.emscripten { padding-right: 0; margin-left: auto; margin-right: auto; display: block; }
+div.emscripten { text-align: center; }
+div.emscripten_border { border: 1px solid black; }
+#module-spinner { overflow: visible; }
+#module-spinner > * {
+ margin-top: 1em;
+}
+.spinner {
+ height: 50px;
+ width: 50px;
+ margin: 0px auto;
+ animation: rotation 0.8s linear infinite;
+ border-left: 10px solid rgb(0,150,240);
+ border-right: 10px solid rgb(0,150,240);
+ border-bottom: 10px solid rgb(0,150,240);
+ border-top: 10px solid rgb(100,0,200);
+ border-radius: 100%;
+ background-color: rgb(200,100,250);
+}
+@keyframes rotation {
+ from {transform: rotate(0deg);}
+ to {transform: rotate(360deg);}
+}
diff --git a/ext/wasm/fiddle/fiddle-worker.js b/ext/wasm/fiddle/fiddle-worker.js
new file mode 100644
index 0000000..67f6100
--- /dev/null
+++ b/ext/wasm/fiddle/fiddle-worker.js
@@ -0,0 +1,384 @@
+/*
+ 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 the JS Worker file for the sqlite3 fiddle app. It loads the
+ sqlite3 wasm module and offers access to the db via the Worker
+ message-passing interface.
+
+ Forewarning: this API is still very much Under Construction and
+ subject to any number of changes as experience reveals what those
+ need to be.
+
+ 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.
+
+ Workers-to-Main 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.
+
+ - working: data='start'|'end'. Indicates that work is about to be
+ sent to the module or has just completed. This can be used, e.g.,
+ to disable UI elements which should not be activated while work
+ is pending. Example:
+
+ {type:'working', data:'start'}
+
+ Main-to-Worker types:
+
+ - shellExec: data=text to execute as if it had been entered in the
+ sqlite3 CLI shell app (as opposed to sqlite3_exec()). This event
+ causes the worker to emit a 'working' event (data='start') before
+ it starts and a 'working' event (data='end') when it finished. If
+ called while work is currently being executed it emits stderr
+ message instead of doing actual work, as the underlying db cannot
+ handle concurrent tasks. Example:
+
+ {type:'shellExec', data: 'select * from sqlite_master'}
+
+ - More TBD as the higher-level db layer develops.
+*/
+
+/*
+ Apparent browser(s) bug: console messages emitted may be duplicated
+ in the console, even though they're provably only run once. See:
+
+ https://stackoverflow.com/questions/49659464
+
+ Noting that it happens in Firefox as well as Chrome. Harmless but
+ annoying.
+*/
+"use strict";
+(function(){
+ /**
+ Posts a message in the form {type,data}. If passed more than 2
+ args, the 3rd must be an array of "transferable" values to pass
+ as the 2nd argument to postMessage(). */
+ const wMsg =
+ (type,data,transferables)=>{
+ postMessage({type, data}, transferables || []);
+ };
+ const stdout = (...args)=>wMsg('stdout', args);
+ const stderr = (...args)=>wMsg('stderr', args);
+ const toss = (...args)=>{
+ throw new Error(args.join(' '));
+ };
+ const fixmeOPFS = "(FIXME: won't work with OPFS-over-sqlite3_vfs.)";
+ let sqlite3 /* gets assigned when the wasm module is loaded */;
+
+ self.onerror = function(/*message, source, lineno, colno, error*/) {
+ const err = arguments[4];
+ if(err && 'ExitStatus'==err.name){
+ /* This is relevant for the sqlite3 shell binding but not the
+ lower-level binding. */
+ fiddleModule.isDead = true;
+ stderr("FATAL ERROR:", err.message);
+ stderr("Restarting the app requires reloading the page.");
+ wMsg('error', err);
+ }
+ console.error(err);
+ fiddleModule.setStatus('Exception thrown, see JavaScript console: '+err);
+ };
+
+ const Sqlite3Shell = {
+ /** Returns the name of the currently-opened db. */
+ dbFilename: function f(){
+ if(!f._) f._ = sqlite3.wasm.xWrap('fiddle_db_filename', "string", ['string']);
+ return f._(0);
+ },
+ dbHandle: function f(){
+ if(!f._) f._ = sqlite3.wasm.xWrap("fiddle_db_handle", "sqlite3*");
+ return f._();
+ },
+ dbIsOpfs: function f(){
+ return sqlite3.opfs && sqlite3.capi.sqlite3_js_db_uses_vfs(
+ this.dbHandle(), "opfs"
+ );
+ },
+ runMain: function f(){
+ if(f.argv) return 0===f.argv.rc;
+ const dbName = "/fiddle.sqlite3";
+ f.argv = [
+ 'sqlite3-fiddle.wasm',
+ '-bail', '-safe',
+ dbName
+ /* Reminder: because of how we run fiddle, we have to ensure
+ that any argv strings passed to its main() are valid until
+ the wasm environment shuts down. */
+ ];
+ const capi = sqlite3.capi, wasm = sqlite3.wasm;
+ /* We need to call sqlite3_shutdown() in order to avoid numerous
+ legitimate warnings from the shell about it being initialized
+ after sqlite3_initialize() has been called. This means,
+ however, that any initialization done by the JS code may need
+ to be re-done (e.g. re-registration of dynamically-loaded
+ VFSes). We need a more generic approach to running such
+ init-level code. */
+ capi.sqlite3_shutdown();
+ f.argv.pArgv = wasm.allocMainArgv(f.argv);
+ f.argv.rc = wasm.exports.fiddle_main(
+ f.argv.length, f.argv.pArgv
+ );
+ if(f.argv.rc){
+ stderr("Fatal error initializing sqlite3 shell.");
+ fiddleModule.isDead = true;
+ return false;
+ }
+ stdout("SQLite version", capi.sqlite3_libversion(),
+ capi.sqlite3_sourceid().substr(0,19));
+ stdout('Welcome to the "fiddle" shell.');
+ if(sqlite3.opfs){
+ stdout("\nOPFS is available. To open a persistent db, use:\n\n",
+ " .open file:name?vfs=opfs\n\nbut note that some",
+ "features (e.g. upload) do not yet work with OPFS.");
+ sqlite3.opfs.registerVfs();
+ }
+ stdout('\nEnter ".help" for usage hints.');
+ this.exec([ // initialization commands...
+ '.nullvalue NULL',
+ '.headers on'
+ ].join('\n'));
+ return true;
+ },
+ /**
+ Runs the given text through the shell as if it had been typed
+ in by a user. Fires a working/start event before it starts and
+ working/end event when it finishes.
+ */
+ exec: function f(sql){
+ if(!f._){
+ if(!this.runMain()) return;
+ f._ = sqlite3.wasm.xWrap('fiddle_exec', null, ['string']);
+ }
+ if(fiddleModule.isDead){
+ stderr("shell module has exit()ed. Cannot run SQL.");
+ return;
+ }
+ wMsg('working','start');
+ try {
+ if(f._running){
+ stderr('Cannot run multiple commands concurrently.');
+ }else if(sql){
+ if(Array.isArray(sql)) sql = sql.join('');
+ f._running = true;
+ f._(sql);
+ }
+ }finally{
+ delete f._running;
+ wMsg('working','end');
+ }
+ },
+ resetDb: function f(){
+ if(!f._) f._ = sqlite3.wasm.xWrap('fiddle_reset_db', null);
+ stdout("Resetting database.");
+ f._();
+ stdout("Reset",this.dbFilename());
+ },
+ /* Interrupt can't work: this Worker is tied up working, so won't get the
+ interrupt event which would be needed to perform the interrupt. */
+ interrupt: function f(){
+ if(!f._) f._ = sqlite3.wasm.xWrap('fiddle_interrupt', null);
+ stdout("Requesting interrupt.");
+ f._();
+ }
+ };
+
+ self.onmessage = function f(ev){
+ ev = ev.data;
+ if(!f.cache){
+ f.cache = {
+ prevFilename: null
+ };
+ }
+ //console.debug("worker: onmessage.data",ev);
+ switch(ev.type){
+ case 'shellExec': Sqlite3Shell.exec(ev.data); return;
+ case 'db-reset': Sqlite3Shell.resetDb(); return;
+ case 'interrupt': Sqlite3Shell.interrupt(); return;
+ /** Triggers the export of the current db. Fires an
+ event in the form:
+
+ {type:'db-export',
+ data:{
+ filename: name of db,
+ buffer: contents of the db file (Uint8Array),
+ error: on error, a message string and no buffer property.
+ }
+ }
+ */
+ case 'db-export': {
+ const fn = Sqlite3Shell.dbFilename();
+ stdout("Exporting",fn+".");
+ const fn2 = fn ? fn.split(/[/\\]/).pop() : null;
+ try{
+ if(!fn2) toss("DB appears to be closed.");
+ const buffer = sqlite3.capi.sqlite3_js_db_export(
+ Sqlite3Shell.dbHandle()
+ );
+ wMsg('db-export',{filename: fn2, buffer: buffer.buffer}, [buffer.buffer]);
+ }catch(e){
+ console.error("Export failed:",e);
+ /* Post a failure message so that UI elements disabled
+ during the export can be re-enabled. */
+ wMsg('db-export',{
+ filename: fn,
+ error: e.message
+ });
+ }
+ return;
+ }
+ case 'open': {
+ /* Expects: {
+ buffer: ArrayBuffer | Uint8Array,
+ filename: the filename for the db. Any dir part is
+ stripped.
+ }
+ */
+ const opt = ev.data;
+ let buffer = opt.buffer;
+ stderr('open():',fixmeOPFS);
+ if(buffer instanceof ArrayBuffer){
+ buffer = new Uint8Array(buffer);
+ }else if(!(buffer instanceof Uint8Array)){
+ stderr("'open' expects {buffer:Uint8Array} containing an uploaded db.");
+ return;
+ }
+ const fn = (
+ opt.filename
+ ? opt.filename.split(/[/\\]/).pop().replace('"','_')
+ : ("db-"+((Math.random() * 10000000) | 0)+
+ "-"+((Math.random() * 10000000) | 0)+".sqlite3")
+ );
+ try {
+ /* We cannot delete the existing db file until the new one
+ is installed, which means that we risk overflowing our
+ quota (if any) by having both the previous and current
+ db briefly installed in the virtual filesystem. */
+ const fnAbs = '/'+fn;
+ const oldName = Sqlite3Shell.dbFilename();
+ if(oldName && oldName===fnAbs){
+ /* We cannot create the replacement file while the current file
+ is opened, nor does the shell have a .close command, so we
+ must temporarily switch to another db... */
+ Sqlite3Shell.exec('.open :memory:');
+ fiddleModule.FS.unlink(fnAbs);
+ }
+ fiddleModule.FS.createDataFile("/", fn, buffer, true, true);
+ Sqlite3Shell.exec('.open "'+fnAbs+'"');
+ if(oldName && oldName!==fnAbs){
+ try{fiddleModule.fsUnlink(oldName)}
+ catch(e){/*ignored*/}
+ }
+ stdout("Replaced DB with",fn+".");
+ }catch(e){
+ stderr("Error installing db",fn+":",e.message);
+ }
+ return;
+ }
+ };
+ console.warn("Unknown fiddle-worker message type:",ev);
+ };
+
+ /**
+ emscripten module for use with build mode -sMODULARIZE.
+ */
+ const fiddleModule = {
+ print: stdout,
+ 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('fiddle-module.js'+self.location.search);
+ /**
+ initFiddleModule() is installed via fiddle-module.js due to
+ building with:
+
+ emcc ... -sMODULARIZE=1 -sEXPORT_NAME=initFiddleModule
+ */
+ sqlite3InitModule(fiddleModule).then((_sqlite3)=>{
+ sqlite3 = _sqlite3;
+ console.warn("Installing sqlite3 module globally (in Worker)",
+ "for use in the dev console.", sqlite3);
+ globalThis.sqlite3 = sqlite3;
+ const dbVfs = sqlite3.wasm.xWrap('fiddle_db_vfs', "*", ['string']);
+ fiddleModule.fsUnlink = (fn)=>{
+ return sqlite3.wasm.sqlite3_wasm_vfs_unlink(dbVfs(0), fn);
+ };
+ wMsg('fiddle-ready');
+ }).catch(e=>{
+ console.error("Fiddle worker init failed:",e);
+ });
+})();
diff --git a/ext/wasm/fiddle/fiddle.js b/ext/wasm/fiddle/fiddle.js
new file mode 100644
index 0000000..2a3d174
--- /dev/null
+++ b/ext/wasm/fiddle/fiddle.js
@@ -0,0 +1,815 @@
+/*
+ 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 the main entry point for the sqlite3 fiddle app. It sets up the
+ various UI bits, loads a Worker for the db connection, and manages the
+ communication between the UI and worker.
+*/
+(function(){
+ 'use strict';
+ /* Recall that the 'self' symbol, except where locally
+ overwritten, refers to the global window or worker object. */
+
+ const storage = (function(NS/*namespace object in which to store this module*/){
+ /* Pedantic licensing note: this code originated in the Fossil SCM
+ source tree, where it has a different license, but the person who
+ ported it into sqlite is the same one who wrote it for fossil. */
+ 'use strict';
+ NS = NS||{};
+
+ /**
+ This module provides a basic wrapper around localStorage
+ or sessionStorage or a dummy proxy object if neither
+ of those are available.
+ */
+ const tryStorage = function f(obj){
+ if(!f.key) f.key = 'storage.access.check';
+ try{
+ obj.setItem(f.key, 'f');
+ const x = obj.getItem(f.key);
+ obj.removeItem(f.key);
+ if(x!=='f') throw new Error(f.key+" failed")
+ return obj;
+ }catch(e){
+ return undefined;
+ }
+ };
+
+ /** Internal storage impl for this module. */
+ const $storage =
+ tryStorage(window.localStorage)
+ || tryStorage(window.sessionStorage)
+ || tryStorage({
+ // A basic dummy xyzStorage stand-in
+ $$$:{},
+ setItem: function(k,v){this.$$$[k]=v},
+ getItem: function(k){
+ return this.$$$.hasOwnProperty(k) ? this.$$$[k] : undefined;
+ },
+ removeItem: function(k){delete this.$$$[k]},
+ clear: function(){this.$$$={}}
+ });
+
+ /**
+ For the dummy storage we need to differentiate between
+ $storage and its real property storage for hasOwnProperty()
+ to work properly...
+ */
+ const $storageHolder = $storage.hasOwnProperty('$$$') ? $storage.$$$ : $storage;
+
+ /**
+ A prefix which gets internally applied to all storage module
+ property keys so that localStorage and sessionStorage across the
+ same browser profile instance do not "leak" across multiple apps
+ being hosted by the same origin server. Such cross-polination is
+ still there but, with this key prefix applied, it won't be
+ immediately visible via the storage API.
+
+ With this in place we can justify using localStorage instead of
+ sessionStorage.
+
+ One implication of using localStorage and sessionStorage is that
+ their scope (the same "origin" and client application/profile)
+ allows multiple apps on the same origin to use the same
+ storage. Thus /appA/foo could then see changes made via
+ /appB/foo. The data do not cross user- or browser boundaries,
+ though, so it "might" arguably be called a
+ feature. storageKeyPrefix was added so that we can sandbox that
+ state for each separate app which shares an origin.
+
+ See: https://fossil-scm.org/forum/forumpost/4afc4d34de
+
+ Sidebar: it might seem odd to provide a key prefix and stick all
+ properties in the topmost level of the storage object. We do that
+ because adding a layer of object to sandbox each app would mean
+ (de)serializing that whole tree on every storage property change.
+ e.g. instead of storageObject.projectName.foo we have
+ storageObject[storageKeyPrefix+'foo']. That's soley for
+ efficiency's sake (in terms of battery life and
+ environment-internal storage-level effort).
+ */
+ const storageKeyPrefix = (
+ $storageHolder===$storage/*localStorage or sessionStorage*/
+ ? (
+ (NS.config ?
+ (NS.config.projectCode || NS.config.projectName
+ || NS.config.shortProjectName)
+ : false)
+ || window.location.pathname
+ )+'::' : (
+ '' /* transient storage */
+ )
+ );
+
+ /**
+ A proxy for localStorage or sessionStorage or a
+ page-instance-local proxy, if neither one is availble.
+
+ Which exact storage implementation is uses is unspecified, and
+ apps must not rely on it.
+ */
+ NS.storage = {
+ storageKeyPrefix: storageKeyPrefix,
+ /** Sets the storage key k to value v, implicitly converting
+ it to a string. */
+ set: (k,v)=>$storage.setItem(storageKeyPrefix+k,v),
+ /** Sets storage key k to JSON.stringify(v). */
+ setJSON: (k,v)=>$storage.setItem(storageKeyPrefix+k,JSON.stringify(v)),
+ /** Returns the value for the given storage key, or
+ dflt if the key is not found in the storage. */
+ get: (k,dflt)=>$storageHolder.hasOwnProperty(
+ storageKeyPrefix+k
+ ) ? $storage.getItem(storageKeyPrefix+k) : dflt,
+ /** Returns true if the given key has a value of "true". If the
+ key is not found, it returns true if the boolean value of dflt
+ is "true". (Remember that JS persistent storage values are all
+ strings.) */
+ getBool: function(k,dflt){
+ return 'true'===this.get(k,''+(!!dflt));
+ },
+ /** Returns the JSON.parse()'d value of the given
+ storage key's value, or dflt is the key is not
+ found or JSON.parse() fails. */
+ getJSON: function f(k,dflt){
+ try {
+ const x = this.get(k,f);
+ return x===f ? dflt : JSON.parse(x);
+ }
+ catch(e){return dflt}
+ },
+ /** Returns true if the storage contains the given key,
+ else false. */
+ contains: (k)=>$storageHolder.hasOwnProperty(storageKeyPrefix+k),
+ /** Removes the given key from the storage. Returns this. */
+ remove: function(k){
+ $storage.removeItem(storageKeyPrefix+k);
+ return this;
+ },
+ /** Clears ALL keys from the storage. Returns this. */
+ clear: function(){
+ this.keys().forEach((k)=>$storage.removeItem(/*w/o prefix*/k));
+ return this;
+ },
+ /** Returns an array of all keys currently in the storage. */
+ keys: ()=>Object.keys($storageHolder).filter((v)=>(v||'').startsWith(storageKeyPrefix)),
+ /** Returns true if this storage is transient (only available
+ until the page is reloaded), indicating that fileStorage
+ and sessionStorage are unavailable. */
+ isTransient: ()=>$storageHolder!==$storage,
+ /** Returns a symbolic name for the current storage mechanism. */
+ storageImplName: function(){
+ if($storage===window.localStorage) return 'localStorage';
+ else if($storage===window.sessionStorage) return 'sessionStorage';
+ else return 'transient';
+ },
+
+ /**
+ Returns a brief help text string for the currently-selected
+ storage type.
+ */
+ storageHelpDescription: function(){
+ return {
+ localStorage: "Browser-local persistent storage with an "+
+ "unspecified long-term lifetime (survives closing the browser, "+
+ "but maybe not a browser upgrade).",
+ sessionStorage: "Storage local to this browser tab, "+
+ "lost if this tab is closed.",
+ "transient": "Transient storage local to this invocation of this page."
+ }[this.storageImplName()];
+ }
+ };
+ return NS.storage;
+ })({})/*storage API setup*/;
+
+
+ /** Name of the stored copy of SqliteFiddle.config. */
+ const configStorageKey = 'sqlite3-fiddle-config';
+
+ /**
+ The SqliteFiddle object is intended to be the primary
+ app-level object for the main-thread side of the sqlite
+ fiddle application. It uses a worker thread to load the
+ sqlite WASM module and communicate with it.
+ */
+ const SF/*local convenience alias*/
+ = window.SqliteFiddle/*canonical name*/ = {
+ /* Config options. */
+ config: {
+ /* If true, SqliteFiddle.echo() will auto-scroll the
+ output widget to the bottom when it receives output,
+ else it won't. */
+ autoScrollOutput: true,
+ /* If true, the output area will be cleared before each
+ command is run, else it will not. */
+ autoClearOutput: false,
+ /* If true, SqliteFiddle.echo() will echo its output to
+ the console, in addition to its normal output widget.
+ That slows it down but is useful for testing. */
+ echoToConsole: false,
+ /* If true, display input/output areas side-by-side. */
+ sideBySide: true,
+ /* If true, swap positions of the input/output areas. */
+ swapInOut: false
+ },
+ /**
+ Emits the given text, followed by a line break, to the
+ output widget. If given more than one argument, they are
+ join()'d together with a space between each. As a special
+ case, if passed a single array, that array is used in place
+ of the arguments array (this is to facilitate receiving
+ lists of arguments via worker events).
+ */
+ echo: function f(text) {
+ /* Maintenance reminder: we currently require/expect a textarea
+ output element. It might be nice to extend this to behave
+ differently if the output element is a non-textarea element,
+ in which case it would need to append the given text as a TEXT
+ node and add a line break. */
+ if(!f._){
+ f._ = document.getElementById('output');
+ f._.value = ''; // clear browser cache
+ }
+ if(arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
+ else if(1===arguments.length && Array.isArray(text)) text = text.join(' ');
+ // These replacements are necessary if you render to raw HTML
+ //text = text.replace(/&/g, "&");
+ //text = text.replace(/</g, "&lt;");
+ //text = text.replace(/>/g, "&gt;");
+ //text = text.replace('\n', '<br>', 'g');
+ if(null===text){/*special case: clear output*/
+ f._.value = '';
+ return;
+ }else if(this.echo._clearPending){
+ delete this.echo._clearPending;
+ f._.value = '';
+ }
+ if(this.config.echoToConsole) console.log(text);
+ if(this.jqTerm) this.jqTerm.echo(text);
+ f._.value += text + "\n";
+ if(this.config.autoScrollOutput){
+ f._.scrollTop = f._.scrollHeight;
+ }
+ },
+ _msgMap: {},
+ /** Adds a worker message handler for messages of the given
+ type. */
+ addMsgHandler: function f(type,callback){
+ if(Array.isArray(type)){
+ type.forEach((t)=>this.addMsgHandler(t, callback));
+ return this;
+ }
+ (this._msgMap.hasOwnProperty(type)
+ ? this._msgMap[type]
+ : (this._msgMap[type] = [])).push(callback);
+ return this;
+ },
+ /** Given a worker message, runs all handlers for msg.type. */
+ runMsgHandlers: function(msg){
+ const list = (this._msgMap.hasOwnProperty(msg.type)
+ ? this._msgMap[msg.type] : false);
+ if(!list){
+ console.warn("No handlers found for message type:",msg);
+ return false;
+ }
+ //console.debug("runMsgHandlers",msg);
+ list.forEach((f)=>f(msg));
+ return true;
+ },
+ /** Removes all message handlers for the given message type. */
+ clearMsgHandlers: function(type){
+ delete this._msgMap[type];
+ return this;
+ },
+ /* Posts a message in the form {type, data} to the db worker. Returns this. */
+ wMsg: function(type,data,transferables){
+ this.worker.postMessage({type, data}, transferables || []);
+ return this;
+ },
+ /**
+ Prompts for confirmation and, if accepted, deletes
+ all content and tables in the (transient) database.
+ */
+ resetDb: function(){
+ if(window.confirm("Really destroy all content and tables "
+ +"in the (transient) db?")){
+ this.wMsg('db-reset');
+ }
+ return this;
+ },
+ /** Stores this object's config in the browser's storage. */
+ storeConfig: function(){
+ storage.setJSON(configStorageKey,this.config);
+ }
+ };
+
+ if(1){ /* Restore SF.config */
+ const storedConfig = storage.getJSON(configStorageKey);
+ if(storedConfig){
+ /* Copy all properties to SF.config which are currently in
+ storedConfig. We don't bother copying any other
+ properties: those have been removed from the app in the
+ meantime. */
+ Object.keys(SF.config).forEach(function(k){
+ if(storedConfig.hasOwnProperty(k)){
+ SF.config[k] = storedConfig[k];
+ }
+ });
+ }
+ }
+
+ SF.worker = new Worker('fiddle-worker.js'+self.location.search);
+ SF.worker.onmessage = (ev)=>SF.runMsgHandlers(ev.data);
+ SF.addMsgHandler(['stdout', 'stderr'], (ev)=>SF.echo(ev.data));
+
+ /* querySelectorAll() proxy */
+ const EAll = function(/*[element=document,] cssSelector*/){
+ return (arguments.length>1 ? arguments[0] : document)
+ .querySelectorAll(arguments[arguments.length-1]);
+ };
+ /* querySelector() proxy */
+ const E = function(/*[element=document,] cssSelector*/){
+ return (arguments.length>1 ? arguments[0] : document)
+ .querySelector(arguments[arguments.length-1]);
+ };
+
+ /** Handles status updates from the Emscripten Module object. */
+ SF.addMsgHandler('module', function f(ev){
+ ev = ev.data;
+ if('status'!==ev.type){
+ console.warn("Unexpected module-type message:",ev);
+ return;
+ }
+ if(!f.ui){
+ f.ui = {
+ status: E('#module-status'),
+ progress: E('#module-progress'),
+ spinner: E('#module-spinner')
+ };
+ }
+ const msg = ev.data;
+ if(f.ui.progres){
+ progress.value = msg.step;
+ progress.max = msg.step + 1/*we don't know how many steps to expect*/;
+ }
+ if(1==msg.step){
+ f.ui.progress.classList.remove('hidden');
+ f.ui.spinner.classList.remove('hidden');
+ }
+ if(msg.text){
+ f.ui.status.classList.remove('hidden');
+ f.ui.status.innerText = msg.text;
+ }else{
+ if(f.ui.progress){
+ f.ui.progress.remove();
+ f.ui.spinner.remove();
+ delete f.ui.progress;
+ delete f.ui.spinner;
+ }
+ f.ui.status.classList.add('hidden');
+ /* The module can post messages about fatal problems,
+ e.g. an exit() being triggered or assertion failure,
+ after the last "load" message has arrived, so
+ leave f.ui.status and message listener intact. */
+ }
+ });
+
+ /**
+ The 'fiddle-ready' event is fired (with no payload) when the
+ wasm module has finished loading. Interestingly, that happens
+ _before_ the final module:status event */
+ SF.addMsgHandler('fiddle-ready', function(){
+ SF.clearMsgHandlers('fiddle-ready');
+ self.onSFLoaded();
+ });
+
+ /**
+ Performs all app initialization which must wait until after the
+ worker module is loaded. This function removes itself when it's
+ called.
+ */
+ self.onSFLoaded = function(){
+ delete this.onSFLoaded;
+ // Unhide all elements which start out hidden
+ EAll('.initially-hidden').forEach((e)=>e.classList.remove('initially-hidden'));
+ E('#btn-reset').addEventListener('click',()=>SF.resetDb());
+ const taInput = E('#input');
+ const btnClearIn = E('#btn-clear');
+ btnClearIn.addEventListener('click',function(){
+ taInput.value = '';
+ },false);
+ // Ctrl-enter and shift-enter both run the current SQL.
+ taInput.addEventListener('keydown',function(ev){
+ if((ev.ctrlKey || ev.shiftKey) && 13 === ev.keyCode){
+ ev.preventDefault();
+ ev.stopPropagation();
+ btnShellExec.click();
+ }
+ }, false);
+ const taOutput = E('#output');
+ const btnClearOut = E('#btn-clear-output');
+ btnClearOut.addEventListener('click',function(){
+ taOutput.value = '';
+ if(SF.jqTerm) SF.jqTerm.clear();
+ },false);
+ const btnShellExec = E('#btn-shell-exec');
+ btnShellExec.addEventListener('click',function(ev){
+ let sql;
+ ev.preventDefault();
+ if(taInput.selectionStart<taInput.selectionEnd){
+ sql = taInput.value.substring(taInput.selectionStart,taInput.selectionEnd).trim();
+ }else{
+ sql = taInput.value.trim();
+ }
+ if(sql) SF.dbExec(sql);
+ },false);
+
+ const btnInterrupt = E("#btn-interrupt");
+ //btnInterrupt.classList.add('hidden');
+ /** To be called immediately before work is sent to the
+ worker. Updates some UI elements. The 'working'/'end'
+ event will apply the inverse, undoing the bits this
+ function does. This impl is not in the 'working'/'start'
+ event handler because that event is given to us
+ asynchronously _after_ we need to have performed this
+ work.
+ */
+ const preStartWork = function f(){
+ if(!f._){
+ const title = E('title');
+ f._ = {
+ btnLabel: btnShellExec.innerText,
+ pageTitle: title,
+ pageTitleOrig: title.innerText
+ };
+ }
+ f._.pageTitle.innerText = "[working...] "+f._.pageTitleOrig;
+ btnShellExec.setAttribute('disabled','disabled');
+ btnInterrupt.removeAttribute('disabled','disabled');
+ };
+
+ /* Sends the given text to the db module to evaluate as if it
+ had been entered in the sqlite3 CLI shell. If it's null or
+ empty, this is a no-op. */
+ SF.dbExec = function f(sql){
+ if(null!==sql && this.config.autoClearOutput){
+ this.echo._clearPending = true;
+ }
+ preStartWork();
+ this.wMsg('shellExec',sql);
+ };
+
+ SF.addMsgHandler('working',function f(ev){
+ switch(ev.data){
+ case 'start': /* See notes in preStartWork(). */; return;
+ case 'end':
+ preStartWork._.pageTitle.innerText = preStartWork._.pageTitleOrig;
+ btnShellExec.innerText = preStartWork._.btnLabel;
+ btnShellExec.removeAttribute('disabled');
+ btnInterrupt.setAttribute('disabled','disabled');
+ return;
+ }
+ console.warn("Unhandled 'working' event:",ev.data);
+ });
+
+ /* For each checkbox with data-csstgt, set up a handler which
+ toggles the given CSS class on the element matching
+ E(data-csstgt). */
+ EAll('input[type=checkbox][data-csstgt]')
+ .forEach(function(e){
+ const tgt = E(e.dataset.csstgt);
+ const cssClass = e.dataset.cssclass || 'error';
+ e.checked = tgt.classList.contains(cssClass);
+ e.addEventListener('change', function(){
+ tgt.classList[
+ this.checked ? 'add' : 'remove'
+ ](cssClass)
+ }, false);
+ });
+ /* For each checkbox with data-config=X, set up a binding to
+ SF.config[X]. These must be set up AFTER data-csstgt
+ checkboxes so that those two states can be synced properly. */
+ EAll('input[type=checkbox][data-config]')
+ .forEach(function(e){
+ const confVal = !!SF.config[e.dataset.config];
+ if(e.checked !== confVal){
+ /* Ensure that data-csstgt mappings (if any) get
+ synced properly. */
+ e.checked = confVal;
+ e.dispatchEvent(new Event('change'));
+ }
+ e.addEventListener('change', function(){
+ SF.config[this.dataset.config] = this.checked;
+ SF.storeConfig();
+ }, false);
+ });
+ /* For each button with data-cmd=X, map a click handler which
+ calls SF.dbExec(X). */
+ const cmdClick = function(){SF.dbExec(this.dataset.cmd);};
+ EAll('button[data-cmd]').forEach(
+ e => e.addEventListener('click', cmdClick, false)
+ );
+
+ btnInterrupt.addEventListener('click',function(){
+ SF.wMsg('interrupt');
+ });
+
+ /** Initiate a download of the db. */
+ const btnExport = E('#btn-export');
+ const eLoadDb = E('#load-db');
+ const btnLoadDb = E('#btn-load-db');
+ btnLoadDb.addEventListener('click', ()=>eLoadDb.click());
+ /**
+ Enables (if passed true) or disables all UI elements which
+ "might," if timed "just right," interfere with an
+ in-progress db import/export/exec operation.
+ */
+ const enableMutatingElements = function f(enable){
+ if(!f._elems){
+ f._elems = [
+ /* UI elements to disable while import/export are
+ running. Normally the export is fast enough
+ that this won't matter, but we really don't
+ want to be reading (from outside of sqlite) the
+ db when the user taps btnShellExec. */
+ btnShellExec, btnExport, eLoadDb
+ ];
+ }
+ f._elems.forEach( enable
+ ? (e)=>e.removeAttribute('disabled')
+ : (e)=>e.setAttribute('disabled','disabled') );
+ };
+ btnExport.addEventListener('click',function(){
+ enableMutatingElements(false);
+ SF.wMsg('db-export');
+ });
+ SF.addMsgHandler('db-export', function(ev){
+ enableMutatingElements(true);
+ ev = ev.data;
+ if(ev.error){
+ SF.echo("Export failed:",ev.error);
+ return;
+ }
+ const blob = new Blob([ev.buffer],
+ {type:"application/x-sqlite3"});
+ const a = document.createElement('a');
+ document.body.appendChild(a);
+ a.href = window.URL.createObjectURL(blob);
+ a.download = ev.filename;
+ a.addEventListener('click',function(){
+ setTimeout(function(){
+ SF.echo("Exported (possibly auto-downloaded):",ev.filename);
+ window.URL.revokeObjectURL(a.href);
+ a.remove();
+ },500);
+ });
+ a.click();
+ });
+ /**
+ Handle load/import of an external db file.
+ */
+ eLoadDb.addEventListener('change',function(){
+ const f = this.files[0];
+ const r = new FileReader();
+ const status = {loaded: 0, total: 0};
+ enableMutatingElements(false);
+ r.addEventListener('loadstart', function(){
+ SF.echo("Loading",f.name,"...");
+ });
+ r.addEventListener('progress', function(ev){
+ SF.echo("Loading progress:",ev.loaded,"of",ev.total,"bytes.");
+ });
+ const that = this;
+ r.addEventListener('load', function(){
+ enableMutatingElements(true);
+ SF.echo("Loaded",f.name+". Opening db...");
+ SF.wMsg('open',{
+ filename: f.name,
+ buffer: this.result
+ }, [this.result]);
+ });
+ r.addEventListener('error',function(){
+ enableMutatingElements(true);
+ SF.echo("Loading",f.name,"failed for unknown reasons.");
+ });
+ r.addEventListener('abort',function(){
+ enableMutatingElements(true);
+ SF.echo("Cancelled loading of",f.name+".");
+ });
+ r.readAsArrayBuffer(f);
+ });
+
+ EAll('fieldset.collapsible').forEach(function(fs){
+ const btnToggle = E(fs,'legend > .fieldset-toggle'),
+ content = EAll(fs,':scope > div');
+ btnToggle.addEventListener('click', function(){
+ fs.classList.toggle('collapsed');
+ content.forEach((d)=>d.classList.toggle('hidden'));
+ }, false);
+ });
+
+ /**
+ Given a DOM element, this routine measures its "effective
+ height", which is the bounding top/bottom range of this element
+ and all of its children, recursively. For some DOM structure
+ cases, a parent may have a reported height of 0 even though
+ children have non-0 sizes.
+
+ Returns 0 if !e or if the element really has no height.
+ */
+ const effectiveHeight = function f(e){
+ if(!e) return 0;
+ if(!f.measure){
+ f.measure = function callee(e, depth){
+ if(!e) return;
+ const m = e.getBoundingClientRect();
+ if(0===depth){
+ callee.top = m.top;
+ callee.bottom = m.bottom;
+ }else{
+ callee.top = m.top ? Math.min(callee.top, m.top) : callee.top;
+ callee.bottom = Math.max(callee.bottom, m.bottom);
+ }
+ Array.prototype.forEach.call(e.children,(e)=>callee(e,depth+1));
+ if(0===depth){
+ //console.debug("measure() height:",e.className, callee.top, callee.bottom, (callee.bottom - callee.top));
+ f.extra += callee.bottom - callee.top;
+ }
+ return f.extra;
+ };
+ }
+ f.extra = 0;
+ f.measure(e,0);
+ return f.extra;
+ };
+
+ /**
+ Returns a function, that, as long as it continues to be invoked,
+ will not be triggered. The function will be called after it stops
+ being called for N milliseconds. If `immediate` is passed, call
+ the callback immediately and hinder future invocations until at
+ least the given time has passed.
+
+ If passed only 1 argument, or passed a falsy 2nd argument,
+ the default wait time set in this function's $defaultDelay
+ property is used.
+
+ Source: underscore.js, by way of https://davidwalsh.name/javascript-debounce-function
+ */
+ const debounce = function f(func, wait, immediate) {
+ var timeout;
+ if(!wait) wait = f.$defaultDelay;
+ return function() {
+ const context = this, args = Array.prototype.slice.call(arguments);
+ const later = function() {
+ timeout = undefined;
+ if(!immediate) func.apply(context, args);
+ };
+ const callNow = immediate && !timeout;
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ if(callNow) func.apply(context, args);
+ };
+ };
+ debounce.$defaultDelay = 500 /*arbitrary*/;
+
+ const ForceResizeKludge = (function(){
+ /* Workaround for Safari mayhem regarding use of vh CSS
+ units.... We cannot use vh units to set the main view
+ size because Safari chokes on that, so we calculate
+ that height here. Larger than ~95% is too big for
+ Firefox on Android, causing the input area to move
+ off-screen. */
+ const appViews = EAll('.app-view');
+ const elemsToCount = [
+ /* Elements which we need to always count in the
+ visible body size. */
+ E('body > header'),
+ E('body > footer')
+ ];
+ const resized = function f(){
+ if(f.$disabled) return;
+ const wh = window.innerHeight;
+ var ht;
+ var extra = 0;
+ elemsToCount.forEach((e)=>e ? extra += effectiveHeight(e) : false);
+ ht = wh - extra;
+ appViews.forEach(function(e){
+ e.style.height =
+ e.style.maxHeight = [
+ "calc(", (ht>=100 ? ht : 100), "px",
+ " - 2em"/*fudge value*/,")"
+ /* ^^^^ hypothetically not needed, but both
+ Chrome/FF on Linux will force scrollbars on the
+ body if this value is too small. */
+ ].join('');
+ });
+ };
+ resized.$disabled = true/*gets deleted when setup is finished*/;
+ window.addEventListener('resize', debounce(resized, 250), false);
+ return resized;
+ })();
+
+ /** Set up a selection list of examples */
+ (function(){
+ const xElem = E('#select-examples');
+ const examples = [
+ {name: "Help", sql: [
+ "-- ================================================\n",
+ "-- Use ctrl-enter or shift-enter to execute sqlite3\n",
+ "-- shell commands and SQL.\n",
+ "-- If a subset of the text is currently selected,\n",
+ "-- only that part is executed.\n",
+ "-- ================================================\n",
+ ".help\n"
+ ]},
+ //{name: "Timer on", sql: ".timer on"},
+ // ^^^ re-enable if emscripten re-enables getrusage()
+ {name: "Setup table T", sql:[
+ ".nullvalue NULL\n",
+ "CREATE TABLE t(a,b);\n",
+ "INSERT INTO t(a,b) VALUES('abc',123),('def',456),(NULL,789),('ghi',012);\n",
+ "SELECT * FROM t;\n"
+ ]},
+ {name: "Table list", sql: ".tables"},
+ {name: "Box Mode", sql: ".mode box"},
+ {name: "JSON Mode", sql: ".mode json"},
+ {name: "Mandlebrot", sql:[
+ "WITH RECURSIVE",
+ " xaxis(x) AS (VALUES(-2.0) UNION ALL SELECT x+0.05 FROM xaxis WHERE x<1.2),\n",
+ " yaxis(y) AS (VALUES(-1.0) UNION ALL SELECT y+0.1 FROM yaxis WHERE y<1.0),\n",
+ " m(iter, cx, cy, x, y) AS (\n",
+ " SELECT 0, x, y, 0.0, 0.0 FROM xaxis, yaxis\n",
+ " UNION ALL\n",
+ " SELECT iter+1, cx, cy, x*x-y*y + cx, 2.0*x*y + cy FROM m \n",
+ " WHERE (x*x + y*y) < 4.0 AND iter<28\n",
+ " ),\n",
+ " m2(iter, cx, cy) AS (\n",
+ " SELECT max(iter), cx, cy FROM m GROUP BY cx, cy\n",
+ " ),\n",
+ " a(t) AS (\n",
+ " SELECT group_concat( substr(' .+*#', 1+min(iter/7,4), 1), '') \n",
+ " FROM m2 GROUP BY cy\n",
+ " )\n",
+ "SELECT group_concat(rtrim(t),x'0a') as Mandelbrot FROM a;\n",
+ ]}
+ ];
+ const newOpt = function(lbl,val){
+ const o = document.createElement('option');
+ if(Array.isArray(val)) val = val.join('');
+ o.value = val;
+ if(!val) o.setAttribute('disabled',true);
+ o.appendChild(document.createTextNode(lbl));
+ xElem.appendChild(o);
+ };
+ newOpt("Examples (replaces input!)");
+ examples.forEach((o)=>newOpt(o.name, o.sql));
+ //xElem.setAttribute('disabled',true);
+ xElem.selectedIndex = 0;
+ xElem.addEventListener('change', function(){
+ taInput.value = '-- ' +
+ this.selectedOptions[0].innerText +
+ '\n' + this.value;
+ SF.dbExec(this.value);
+ });
+ })()/* example queries */;
+
+ //SF.echo(null/*clear any output generated by the init process*/);
+ if(window.jQuery && window.jQuery.terminal){
+ /* Set up the terminal-style view... */
+ const eTerm = window.jQuery('#view-terminal').empty();
+ SF.jqTerm = eTerm.terminal(SF.dbExec.bind(SF),{
+ prompt: 'sqlite> ',
+ greetings: false /* note that the docs incorrectly call this 'greeting' */
+ });
+ /* Set up a button to toggle the views... */
+ const head = E('header#titlebar');
+ const btnToggleView = document.createElement('button');
+ btnToggleView.appendChild(document.createTextNode("Toggle View"));
+ head.appendChild(btnToggleView);
+ btnToggleView.addEventListener('click',function f(){
+ EAll('.app-view').forEach(e=>e.classList.toggle('hidden'));
+ if(document.body.classList.toggle('terminal-mode')){
+ ForceResizeKludge();
+ }
+ }, false);
+ btnToggleView.click()/*default to terminal view*/;
+ }
+ SF.echo('This experimental app is provided in the hope that it',
+ 'may prove interesting or useful but is not an officially',
+ 'supported deliverable of the sqlite project. It is subject to',
+ 'any number of changes or outright removal at any time.\n');
+ const urlParams = new URL(self.location.href).searchParams;
+ SF.dbExec(urlParams.get('sql') || null);
+ delete ForceResizeKludge.$disabled;
+ ForceResizeKludge();
+ }/*onSFLoaded()*/;
+})();
diff --git a/ext/wasm/fiddle/index.html b/ext/wasm/fiddle/index.html
new file mode 100644
index 0000000..272f1ac
--- /dev/null
+++ b/ext/wasm/fiddle/index.html
@@ -0,0 +1,278 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <title>SQLite3 Fiddle</title>
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <!-- to add a togglable terminal-style view, uncomment the following
+ two lines and ensure that these files are on the web server. -->
+ <!--script src="jqterm/jqterm-bundle.min.js"></script>
+ <link rel="stylesheet" href="jqterm/jquery.terminal.min.css"/-->
+ <link rel="stylesheet" href="emscripten.css"/>
+ <style>
+ /* The following styles are for app-level use. */
+ :root {
+ --sqlite-blue: #044a64;
+ --textarea-color1: #044a64;
+ --textarea-color2: white;
+ }
+ textarea {
+ font-family: monospace;
+ flex: 1 1 auto;
+ background-color: var(--textarea-color1);
+ color: var(--textarea-color2);
+ }
+ textarea#input {
+ color: var(--textarea-color1);
+ background-color: var(--textarea-color2);
+ }
+ header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background-color: var(--sqlite-blue);
+ color: white;
+ font-size: 120%;
+ font-weight: bold;
+ border-radius: 0.25em;
+ padding: 0.2em 0.5em;
+ }
+ header > .powered-by {
+ font-size: 80%;
+ }
+ header a, header a:visited, header a:hover {
+ color: inherit;
+ }
+ #main-wrapper {
+ display: flex;
+ flex-direction: column-reverse;
+ flex: 1 1 auto;
+ margin: 0.5em 0;
+ overflow: hidden;
+ }
+ #main-wrapper.side-by-side {
+ flex-direction: row;
+ }
+ #main-wrapper.side-by-side > fieldset {
+ margin-left: 0.25em;
+ margin-right: 0.25em;
+ }
+ #main-wrapper:not(.side-by-side) > fieldset {
+ margin-bottom: 0.25em;
+ }
+ #main-wrapper.swapio {
+ flex-direction: column;
+ }
+ #main-wrapper.side-by-side.swapio {
+ flex-direction: row-reverse;
+ }
+ .zone-wrapper{
+ display: flex;
+ margin: 0;
+ flex: 1 1 0%;
+ border-radius: 0.5em;
+ min-width: inherit/*important: resolves inability to scroll fieldset child element!*/;
+ padding: 0.35em 0 0 0;
+ }
+ .zone-wrapper textarea {
+ border-radius: 0.5em;
+ flex: 1 1 auto;
+ /*min/max width resolve an inexplicable margin on the RHS. The -1em
+ is for the padding, else we overlap the parent boundaries.*/
+ /*min-width: calc(100% - 1em);
+ max-width: calc(100% - 1em);
+ padding: 0 0.5em;*/
+ }
+
+ .zone-wrapper.input { flex: 10 1 auto; }
+ .zone-wrapper.output { flex: 20 1 auto; }
+ .zone-wrapper > div {
+ display:flex;
+ flex: 1 1 0%;
+ }
+ .zone-wrapper.output {}
+ .button-bar {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ align-content: space-between;
+ justify-content: flex-start;
+ }
+ .button-bar > * {
+ margin: 0.05em 0.5em 0.05em 0;
+ flex: 0 1 auto;
+ align-self: auto;
+ }
+ label[for] {
+ cursor: pointer;
+ }
+ .error {
+ color: red;
+ background-color: yellow;
+ }
+ .hidden, .initially-hidden {
+ position: absolute !important;
+ opacity: 0 !important;
+ pointer-events: none !important;
+ display: none !important;
+ }
+ fieldset {
+ border-radius: 0.5em;
+ border: 1px inset;
+ padding: 0.25em;
+ }
+ fieldset.options {
+ font-size: 80%;
+ margin-top: 0.5em;
+ }
+ fieldset:not(.options) > legend {
+ font-size: 80%;
+ }
+ fieldset.options > div {
+ display: flex;
+ flex-wrap: wrap;
+ }
+ fieldset button {
+ font-size: inherit;
+ }
+ fieldset.collapsible > legend > .fieldset-toggle::after {
+ content: " [hide]";
+ position: relative;
+ }
+ fieldset.collapsible.collapsed > legend > .fieldset-toggle::after {
+ content: " [show]";
+ position: relative;
+ }
+ span.labeled-input {
+ padding: 0.25em;
+ margin: 0.05em 0.25em;
+ border-radius: 0.25em;
+ white-space: nowrap;
+ background: #0002;
+ display: flex;
+ align-items: center;
+ }
+ span.labeled-input > *:nth-child(2) {
+ margin-left: 0.3em;
+ }
+ .center { text-align: center; }
+ body.terminal-mode {
+ max-height: calc(100% - 2em);
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ }
+ #view-terminal {}
+ .app-view {
+ flex: 20 1 auto;
+ }
+ #view-split {
+ display: flex;
+ flex-direction: column-reverse;
+ }
+ </style>
+ </head>
+ <body>
+ <header id='titlebar'>
+ <span>SQLite3 Fiddle</span>
+ <span class='powered-by'>Powered by
+ <a href='https://sqlite.org'>SQLite3</a></span>
+ </header>
+ <!-- emscripten bits -->
+ <figure id="module-spinner">
+ <div class="spinner"></div>
+ <div class='center'><strong>Initializing app...</strong></div>
+ <div class='center'>
+ On a slow internet connection this may take a moment. If this
+ message displays for "a long time", intialization may have
+ failed and the JavaScript console may contain clues as to why.
+ </div>
+ </figure>
+ <div class="emscripten" id="module-status">Downloading...</div>
+ <div class="emscripten">
+ <progress value="0" max="100" id="module-progress" hidden='1'></progress>
+ </div><!-- /emscripten bits -->
+
+ <div id='view-terminal' class='app-view hidden initially-hidden'>
+ This is a placeholder for a terminal-like view which is not in
+ the default build.
+ </div>
+
+ <div id='view-split' class='app-view initially-hidden'>
+ <fieldset class='options collapsible'>
+ <legend><button class='fieldset-toggle'>Options</button></legend>
+ <div class=''>
+ <span class='labeled-input'>
+ <input type='checkbox' id='opt-cb-sbs'
+ data-csstgt='#main-wrapper'
+ data-cssclass='side-by-side'
+ data-config='sideBySide'>
+ <label for='opt-cb-sbs'>Side-by-side</label>
+ </span>
+ <span class='labeled-input'>
+ <input type='checkbox' id='opt-cb-swapio'
+ data-csstgt='#main-wrapper'
+ data-cssclass='swapio'
+ data-config='swapInOut'>
+ <label for='opt-cb-swapio'>Swap in/out</label>
+ </span>
+ <span class='labeled-input'>
+ <input type='checkbox' id='opt-cb-autoscroll'
+ data-config='autoScrollOutput'>
+ <label for='opt-cb-autoscroll'>Auto-scroll output</label>
+ </span>
+ <span class='labeled-input'>
+ <input type='checkbox' id='opt-cb-autoclear'
+ data-config='autoClearOutput'>
+ <label for='opt-cb-autoclear'>Auto-clear output</label>
+ </span>
+ <span class='labeled-input'>
+ <input type='file' id='load-db' class='hidden'/>
+ <button id='btn-load-db'>Load DB...</button>
+ </span>
+ <span class='labeled-input'>
+ <button id='btn-export'>Download DB</button>
+ </span>
+ <span class='labeled-input'>
+ <button id='btn-reset'>Reset DB</button>
+ </span>
+ </div>
+ </fieldset><!-- .options -->
+ <div id='main-wrapper' class=''>
+ <fieldset class='zone-wrapper input'>
+ <legend><div class='button-bar'>
+ <button id='btn-shell-exec'>Run</button>
+ <button id='btn-clear'>Clear Input</button>
+ <!--button data-cmd='.help'>Help</button-->
+ <select id='select-examples'></select>
+ </div></legend>
+ <div><textarea id="input"
+ placeholder="Shell input. Ctrl-enter/shift-enter runs it.">
+-- ==================================================
+-- Use ctrl-enter or shift-enter to execute sqlite3
+-- shell commands and SQL.
+-- If a subset of the text is currently selected,
+-- only that part is executed.
+-- ==================================================
+.nullvalue NULL
+.headers on
+</textarea></div>
+ </fieldset>
+ <fieldset class='zone-wrapper output'>
+ <legend><div class='button-bar'>
+ <button id='btn-clear-output'>Clear Output</button>
+ <button id='btn-interrupt' class='hidden' disabled>Interrupt</button>
+ <!-- interruption cannot work in the current configuration
+ because we cannot send an interrupt message when work
+ is currently underway. At that point the Worker is
+ tied up and will not receive the message. -->
+ </div></legend>
+ <div><textarea id="output" readonly
+ placeholder="Shell output."></textarea></div>
+ </fieldset>
+ </div>
+ </div> <!-- #view-split -->
+ <script src="fiddle.js"></script>
+ </body>
+</html>