Index: Makefile.in
==================================================================
--- Makefile.in
+++ Makefile.in
@@ -72,10 +72,21 @@
USE_SEE = @USE_SEE@
APPNAME = fossil
#
# APPNAME = fossil-fuzz
# may be more appropriate for fuzzing.
+
+#### Emscripten stuff for optionally doing in-tree builds
+# of any WASM components. We store precompiled WASM in the the SCM, so
+# this is only useful for people who actively work on WASM
+# components. EMSDK_ENV refers to the "environment" script which comes
+# with the Emscripten SDK package:
+# https://emscripten.org/docs/getting_started/downloads.html
+EMSDK_HOME = @EMSDK_HOME@
+EMSDK_ENV = @EMSDK_ENV@
+EMCC_OPT = @EMCC_OPT@
+EMCC_WRAPPER = $(SRCDIR_tools)/emcc.sh
.PHONY: all tags
include $(SRCDIR)/main.mk
Index: auto.def
==================================================================
--- auto.def
+++ auto.def
@@ -28,10 +28,11 @@
static=0 => {Link a static executable}
fusefs=1 => {Disable the Fuse Filesystem}
fossil-debug=0 => {Build with fossil debugging enabled}
no-opt=0 => {Build without optimization}
json=0 => {Build with fossil JSON API enabled}
+ with-emsdk:path => {Directory containing the Emscripten SDK}
}
# Update the minimum required SQLite version number here, and also
# in src/main.c near the sqlite3_libversion_number() call. Take care
# that both places agree!
@@ -632,10 +633,37 @@
set tclconfig(TCL_LD_FLAGS) [string map [list -ldl ""] \
$tclconfig(TCL_LD_FLAGS)]
define-append EXTRA_LDFLAGS $tclconfig(TCL_LD_FLAGS)
define FOSSIL_ENABLE_TCL
}
+
+# Emscripten is a purely optional component used only for doing
+# in-tree builds of WASM stuff, as opposed to WASM binaries we import
+# from other places. This is only set up for Unix-style OSes and is
+# untested anywhere but Linux.
+set emsdkHome [opt-val with-emsdk]
+define EMSDK_HOME ""
+define EMSDK_ENV ""
+define EMCC_OPT "-Oz"
+if {$emsdkHome eq "" && [info exists ::env(EMSDK)]} {
+ # Fall back to checking the environment. $EMSDK gets set
+ # by sourcing emsdk_env.sh.
+ set emsdkHome $::env(EMSDK)
+}
+if {$emsdkHome ne ""} {
+ define EMSDK_HOME $emsdkHome
+ set emsdkEnv "$emsdkHome/emsdk_env.sh"
+ if {[file exists $emsdkEnv]} {
+ puts "Using Emscripten SDK environment from $emsdkEnv."
+ define EMSDK_ENV $emsdkEnv
+ if {[info exists ::env(EMCC_OPT)]} {
+ define EMCC_OPT $::env(EMCC_OPT)
+ }
+ } else {
+ puts "emsdk_env.sh not found. Assuming emcc is in the PATH."
+ }
+}
# Network functions require libraries on some systems
cc-check-function-in-lib gethostbyname nsl
if {![cc-check-function-in-lib socket {socket network}]} {
# Last resort, may be Windows
@@ -726,8 +754,16 @@
# Linux can only infer the dependency on pthread from OpenSSL when
# doing dynamic linkage.
define-append LIBS -lpthread
}
+if {[get-define EMSDK_HOME] ne ""} {
+ define EMCC_WRAPPER $::autosetup(dir)/../tools/emcc.sh
+ make-template tools/emcc.sh.in
+ catch {exec chmod u+x tools/emcc.sh}
+} else {
+ define EMCC_WRAPPER ""
+ catch {exec rm -f tools/emcc.sh}
+}
make-template Makefile.in
make-config-header autoconfig.h -auto {USE_* FOSSIL_*}
ADDED extsrc/pikchr-worker.js
Index: extsrc/pikchr-worker.js
==================================================================
--- /dev/null
+++ extsrc/pikchr-worker.js
@@ -0,0 +1,221 @@
+/*
+ 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 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 .result 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('
{
+ var _scriptDir = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : undefined;
+
+ return (
+function(initPikchrModule) {
+ initPikchrModule = initPikchrModule || {};
+
+var Module = typeof initPikchrModule != "undefined" ? initPikchrModule : {};
+
+var readyPromiseResolve, readyPromiseReject;
+
+Module["ready"] = new Promise(function(resolve, reject) {
+ readyPromiseResolve = resolve;
+ readyPromiseReject = reject;
+});
+
+var moduleOverrides = Object.assign({}, Module);
+
+var arguments_ = [];
+
+var thisProgram = "./this.program";
+
+var quit_ = (status, toThrow) => {
+ throw toThrow;
+};
+
+var ENVIRONMENT_IS_WEB = true;
+
+var ENVIRONMENT_IS_WORKER = false;
+
+var scriptDirectory = "";
+
+function locateFile(path) {
+ if (Module["locateFile"]) {
+ return Module["locateFile"](path, scriptDirectory);
+ }
+ return scriptDirectory + path;
+}
+
+var read_, readAsync, readBinary, setWindowTitle;
+
+if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) {
+ if (ENVIRONMENT_IS_WORKER) {
+ scriptDirectory = self.location.href;
+ } else if (typeof document != "undefined" && document.currentScript) {
+ scriptDirectory = document.currentScript.src;
+ }
+ if (_scriptDir) {
+ scriptDirectory = _scriptDir;
+ }
+ if (scriptDirectory.indexOf("blob:") !== 0) {
+ scriptDirectory = scriptDirectory.substr(0, scriptDirectory.replace(/[?#].*/, "").lastIndexOf("/") + 1);
+ } else {
+ scriptDirectory = "";
+ }
+ {
+ read_ = url => {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", url, false);
+ xhr.send(null);
+ return xhr.responseText;
+ };
+ if (ENVIRONMENT_IS_WORKER) {
+ readBinary = url => {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", url, false);
+ xhr.responseType = "arraybuffer";
+ xhr.send(null);
+ return new Uint8Array(xhr.response);
+ };
+ }
+ readAsync = (url, onload, onerror) => {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", url, true);
+ xhr.responseType = "arraybuffer";
+ xhr.onload = () => {
+ if (xhr.status == 200 || xhr.status == 0 && xhr.response) {
+ onload(xhr.response);
+ return;
+ }
+ onerror();
+ };
+ xhr.onerror = onerror;
+ xhr.send(null);
+ };
+ }
+ setWindowTitle = title => document.title = title;
+} else {}
+
+var out = Module["print"] || console.log.bind(console);
+
+var err = Module["printErr"] || console.warn.bind(console);
+
+Object.assign(Module, moduleOverrides);
+
+moduleOverrides = null;
+
+if (Module["arguments"]) arguments_ = Module["arguments"];
+
+if (Module["thisProgram"]) thisProgram = Module["thisProgram"];
+
+if (Module["quit"]) quit_ = Module["quit"];
+
+var wasmBinary;
+
+if (Module["wasmBinary"]) wasmBinary = Module["wasmBinary"];
+
+var noExitRuntime = Module["noExitRuntime"] || true;
+
+if (typeof WebAssembly != "object") {
+ abort("no native wasm support detected");
+}
+
+var wasmMemory;
+
+var ABORT = false;
+
+var EXITSTATUS;
+
+function getCFunc(ident) {
+ var func = Module["_" + ident];
+ return func;
+}
+
+function ccall(ident, returnType, argTypes, args, opts) {
+ var toC = {
+ "string": function(str) {
+ var ret = 0;
+ if (str !== null && str !== undefined && str !== 0) {
+ var len = (str.length << 2) + 1;
+ ret = stackAlloc(len);
+ stringToUTF8(str, ret, len);
+ }
+ return ret;
+ },
+ "array": function(arr) {
+ var ret = stackAlloc(arr.length);
+ writeArrayToMemory(arr, ret);
+ return ret;
+ }
+ };
+ function convertReturnValue(ret) {
+ if (returnType === "string") {
+ return UTF8ToString(ret);
+ }
+ if (returnType === "boolean") return Boolean(ret);
+ return ret;
+ }
+ var func = getCFunc(ident);
+ var cArgs = [];
+ var stack = 0;
+ if (args) {
+ for (var i = 0; i < args.length; i++) {
+ var converter = toC[argTypes[i]];
+ if (converter) {
+ if (stack === 0) stack = stackSave();
+ cArgs[i] = converter(args[i]);
+ } else {
+ cArgs[i] = args[i];
+ }
+ }
+ }
+ var ret = func.apply(null, cArgs);
+ function onDone(ret) {
+ if (stack !== 0) stackRestore(stack);
+ return convertReturnValue(ret);
+ }
+ ret = onDone(ret);
+ return ret;
+}
+
+function cwrap(ident, returnType, argTypes, opts) {
+ argTypes = argTypes || [];
+ var numericArgs = argTypes.every(function(type) {
+ return type === "number";
+ });
+ var numericRet = returnType !== "string";
+ if (numericRet && numericArgs && !opts) {
+ return getCFunc(ident);
+ }
+ return function() {
+ return ccall(ident, returnType, argTypes, arguments, opts);
+ };
+}
+
+var UTF8Decoder = typeof TextDecoder != "undefined" ? new TextDecoder("utf8") : undefined;
+
+function UTF8ArrayToString(heapOrArray, idx, maxBytesToRead) {
+ var endIdx = idx + maxBytesToRead;
+ var endPtr = idx;
+ while (heapOrArray[endPtr] && !(endPtr >= endIdx)) ++endPtr;
+ if (endPtr - idx > 16 && heapOrArray.buffer && UTF8Decoder) {
+ return UTF8Decoder.decode(heapOrArray.subarray(idx, endPtr));
+ } else {
+ var str = "";
+ while (idx < endPtr) {
+ var u0 = heapOrArray[idx++];
+ if (!(u0 & 128)) {
+ str += String.fromCharCode(u0);
+ continue;
+ }
+ var u1 = heapOrArray[idx++] & 63;
+ if ((u0 & 224) == 192) {
+ str += String.fromCharCode((u0 & 31) << 6 | u1);
+ continue;
+ }
+ var u2 = heapOrArray[idx++] & 63;
+ if ((u0 & 240) == 224) {
+ u0 = (u0 & 15) << 12 | u1 << 6 | u2;
+ } else {
+ u0 = (u0 & 7) << 18 | u1 << 12 | u2 << 6 | heapOrArray[idx++] & 63;
+ }
+ if (u0 < 65536) {
+ str += String.fromCharCode(u0);
+ } else {
+ var ch = u0 - 65536;
+ str += String.fromCharCode(55296 | ch >> 10, 56320 | ch & 1023);
+ }
+ }
+ }
+ return str;
+}
+
+function UTF8ToString(ptr, maxBytesToRead) {
+ return ptr ? UTF8ArrayToString(HEAPU8, ptr, maxBytesToRead) : "";
+}
+
+function stringToUTF8Array(str, heap, outIdx, maxBytesToWrite) {
+ if (!(maxBytesToWrite > 0)) return 0;
+ var startIdx = outIdx;
+ var endIdx = outIdx + maxBytesToWrite - 1;
+ for (var i = 0; i < str.length; ++i) {
+ var u = str.charCodeAt(i);
+ if (u >= 55296 && u <= 57343) {
+ var u1 = str.charCodeAt(++i);
+ u = 65536 + ((u & 1023) << 10) | u1 & 1023;
+ }
+ if (u <= 127) {
+ if (outIdx >= endIdx) break;
+ heap[outIdx++] = u;
+ } else if (u <= 2047) {
+ if (outIdx + 1 >= endIdx) break;
+ heap[outIdx++] = 192 | u >> 6;
+ heap[outIdx++] = 128 | u & 63;
+ } else if (u <= 65535) {
+ if (outIdx + 2 >= endIdx) break;
+ heap[outIdx++] = 224 | u >> 12;
+ heap[outIdx++] = 128 | u >> 6 & 63;
+ heap[outIdx++] = 128 | u & 63;
+ } else {
+ if (outIdx + 3 >= endIdx) break;
+ heap[outIdx++] = 240 | u >> 18;
+ heap[outIdx++] = 128 | u >> 12 & 63;
+ heap[outIdx++] = 128 | u >> 6 & 63;
+ heap[outIdx++] = 128 | u & 63;
+ }
+ }
+ heap[outIdx] = 0;
+ return outIdx - startIdx;
+}
+
+function stringToUTF8(str, outPtr, maxBytesToWrite) {
+ return stringToUTF8Array(str, HEAPU8, outPtr, maxBytesToWrite);
+}
+
+function writeArrayToMemory(array, buffer) {
+ HEAP8.set(array, buffer);
+}
+
+var buffer, HEAP8, HEAPU8, HEAP16, HEAPU16, HEAP32, HEAPU32, HEAPF32, HEAPF64;
+
+function updateGlobalBufferAndViews(buf) {
+ buffer = buf;
+ Module["HEAP8"] = HEAP8 = new Int8Array(buf);
+ Module["HEAP16"] = HEAP16 = new Int16Array(buf);
+ Module["HEAP32"] = HEAP32 = new Int32Array(buf);
+ Module["HEAPU8"] = HEAPU8 = new Uint8Array(buf);
+ Module["HEAPU16"] = HEAPU16 = new Uint16Array(buf);
+ Module["HEAPU32"] = HEAPU32 = new Uint32Array(buf);
+ Module["HEAPF32"] = HEAPF32 = new Float32Array(buf);
+ Module["HEAPF64"] = HEAPF64 = new Float64Array(buf);
+}
+
+var INITIAL_MEMORY = Module["INITIAL_MEMORY"] || 16777216;
+
+var wasmTable;
+
+var __ATPRERUN__ = [];
+
+var __ATINIT__ = [];
+
+var __ATPOSTRUN__ = [];
+
+var runtimeInitialized = false;
+
+function keepRuntimeAlive() {
+ return noExitRuntime;
+}
+
+function preRun() {
+ if (Module["preRun"]) {
+ if (typeof Module["preRun"] == "function") Module["preRun"] = [ Module["preRun"] ];
+ while (Module["preRun"].length) {
+ addOnPreRun(Module["preRun"].shift());
+ }
+ }
+ callRuntimeCallbacks(__ATPRERUN__);
+}
+
+function initRuntime() {
+ runtimeInitialized = true;
+ callRuntimeCallbacks(__ATINIT__);
+}
+
+function postRun() {
+ if (Module["postRun"]) {
+ if (typeof Module["postRun"] == "function") Module["postRun"] = [ Module["postRun"] ];
+ while (Module["postRun"].length) {
+ addOnPostRun(Module["postRun"].shift());
+ }
+ }
+ callRuntimeCallbacks(__ATPOSTRUN__);
+}
+
+function addOnPreRun(cb) {
+ __ATPRERUN__.unshift(cb);
+}
+
+function addOnInit(cb) {
+ __ATINIT__.unshift(cb);
+}
+
+function addOnPostRun(cb) {
+ __ATPOSTRUN__.unshift(cb);
+}
+
+var runDependencies = 0;
+
+var runDependencyWatcher = null;
+
+var dependenciesFulfilled = null;
+
+function addRunDependency(id) {
+ runDependencies++;
+ if (Module["monitorRunDependencies"]) {
+ Module["monitorRunDependencies"](runDependencies);
+ }
+}
+
+function removeRunDependency(id) {
+ runDependencies--;
+ if (Module["monitorRunDependencies"]) {
+ Module["monitorRunDependencies"](runDependencies);
+ }
+ if (runDependencies == 0) {
+ if (runDependencyWatcher !== null) {
+ clearInterval(runDependencyWatcher);
+ runDependencyWatcher = null;
+ }
+ if (dependenciesFulfilled) {
+ var callback = dependenciesFulfilled;
+ dependenciesFulfilled = null;
+ callback();
+ }
+ }
+}
+
+function abort(what) {
+ {
+ if (Module["onAbort"]) {
+ Module["onAbort"](what);
+ }
+ }
+ what = "Aborted(" + what + ")";
+ err(what);
+ ABORT = true;
+ EXITSTATUS = 1;
+ what += ". Build with -sASSERTIONS for more info.";
+ var e = new WebAssembly.RuntimeError(what);
+ readyPromiseReject(e);
+ throw e;
+}
+
+var dataURIPrefix = "data:application/octet-stream;base64,";
+
+function isDataURI(filename) {
+ return filename.startsWith(dataURIPrefix);
+}
+
+var wasmBinaryFile;
+
+wasmBinaryFile = "pikchr.wasm";
+
+if (!isDataURI(wasmBinaryFile)) {
+ wasmBinaryFile = locateFile(wasmBinaryFile);
+}
+
+function getBinary(file) {
+ try {
+ if (file == wasmBinaryFile && wasmBinary) {
+ return new Uint8Array(wasmBinary);
+ }
+ if (readBinary) {
+ return readBinary(file);
+ } else {
+ throw "both async and sync fetching of the wasm failed";
+ }
+ } catch (err) {
+ abort(err);
+ }
+}
+
+function getBinaryPromise() {
+ if (!wasmBinary && (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER)) {
+ if (typeof fetch == "function") {
+ return fetch(wasmBinaryFile, {
+ credentials: "same-origin"
+ }).then(function(response) {
+ if (!response["ok"]) {
+ throw "failed to load wasm binary file at '" + wasmBinaryFile + "'";
+ }
+ return response["arrayBuffer"]();
+ }).catch(function() {
+ return getBinary(wasmBinaryFile);
+ });
+ }
+ }
+ return Promise.resolve().then(function() {
+ return getBinary(wasmBinaryFile);
+ });
+}
+
+function createWasm() {
+ var info = {
+ "a": asmLibraryArg
+ };
+ function receiveInstance(instance, module) {
+ var exports = instance.exports;
+ Module["asm"] = exports;
+ wasmMemory = Module["asm"]["d"];
+ updateGlobalBufferAndViews(wasmMemory.buffer);
+ wasmTable = Module["asm"]["g"];
+ addOnInit(Module["asm"]["e"]);
+ removeRunDependency("wasm-instantiate");
+ }
+ addRunDependency("wasm-instantiate");
+ function receiveInstantiationResult(result) {
+ receiveInstance(result["instance"]);
+ }
+ function instantiateArrayBuffer(receiver) {
+ return getBinaryPromise().then(function(binary) {
+ return WebAssembly.instantiate(binary, info);
+ }).then(function(instance) {
+ return instance;
+ }).then(receiver, function(reason) {
+ err("failed to asynchronously prepare wasm: " + reason);
+ abort(reason);
+ });
+ }
+ function instantiateAsync() {
+ if (!wasmBinary && typeof WebAssembly.instantiateStreaming == "function" && !isDataURI(wasmBinaryFile) && typeof fetch == "function") {
+ return fetch(wasmBinaryFile, {
+ credentials: "same-origin"
+ }).then(function(response) {
+ var result = WebAssembly.instantiateStreaming(response, info);
+ return result.then(receiveInstantiationResult, function(reason) {
+ err("wasm streaming compile failed: " + reason);
+ err("falling back to ArrayBuffer instantiation");
+ return instantiateArrayBuffer(receiveInstantiationResult);
+ });
+ });
+ } else {
+ return instantiateArrayBuffer(receiveInstantiationResult);
+ }
+ }
+ if (Module["instantiateWasm"]) {
+ try {
+ var exports = Module["instantiateWasm"](info, receiveInstance);
+ return exports;
+ } catch (e) {
+ err("Module.instantiateWasm callback failed with error: " + e);
+ return false;
+ }
+ }
+ instantiateAsync().catch(readyPromiseReject);
+ return {};
+}
+
+var tempDouble;
+
+var tempI64;
+
+function callRuntimeCallbacks(callbacks) {
+ while (callbacks.length > 0) {
+ var callback = callbacks.shift();
+ if (typeof callback == "function") {
+ callback(Module);
+ continue;
+ }
+ var func = callback.func;
+ if (typeof func == "number") {
+ if (callback.arg === undefined) {
+ getWasmTableEntry(func)();
+ } else {
+ getWasmTableEntry(func)(callback.arg);
+ }
+ } else {
+ func(callback.arg === undefined ? null : callback.arg);
+ }
+ }
+}
+
+function getValue(ptr, type = "i8") {
+ if (type.endsWith("*")) type = "i32";
+ switch (type) {
+ case "i1":
+ return HEAP8[ptr >> 0];
+
+ case "i8":
+ return HEAP8[ptr >> 0];
+
+ case "i16":
+ return HEAP16[ptr >> 1];
+
+ case "i32":
+ return HEAP32[ptr >> 2];
+
+ case "i64":
+ return HEAP32[ptr >> 2];
+
+ case "float":
+ return HEAPF32[ptr >> 2];
+
+ case "double":
+ return Number(HEAPF64[ptr >> 3]);
+
+ default:
+ abort("invalid type for getValue: " + type);
+ }
+ return null;
+}
+
+function getWasmTableEntry(funcPtr) {
+ return wasmTable.get(funcPtr);
+}
+
+function setValue(ptr, value, type = "i8") {
+ if (type.endsWith("*")) type = "i32";
+ switch (type) {
+ case "i1":
+ HEAP8[ptr >> 0] = value;
+ break;
+
+ case "i8":
+ HEAP8[ptr >> 0] = value;
+ break;
+
+ case "i16":
+ HEAP16[ptr >> 1] = value;
+ break;
+
+ case "i32":
+ HEAP32[ptr >> 2] = value;
+ break;
+
+ case "i64":
+ tempI64 = [ value >>> 0, (tempDouble = value, +Math.abs(tempDouble) >= 1 ? tempDouble > 0 ? (Math.min(+Math.floor(tempDouble / 4294967296), 4294967295) | 0) >>> 0 : ~~+Math.ceil((tempDouble - +(~~tempDouble >>> 0)) / 4294967296) >>> 0 : 0) ],
+ HEAP32[ptr >> 2] = tempI64[0], HEAP32[ptr + 4 >> 2] = tempI64[1];
+ break;
+
+ case "float":
+ HEAPF32[ptr >> 2] = value;
+ break;
+
+ case "double":
+ HEAPF64[ptr >> 3] = value;
+ break;
+
+ default:
+ abort("invalid type for setValue: " + type);
+ }
+}
+
+function ___assert_fail(condition, filename, line, func) {
+ abort("Assertion failed: " + UTF8ToString(condition) + ", at: " + [ filename ? UTF8ToString(filename) : "unknown filename", line, func ? UTF8ToString(func) : "unknown function" ]);
+}
+
+function abortOnCannotGrowMemory(requestedSize) {
+ abort("OOM");
+}
+
+function _emscripten_resize_heap(requestedSize) {
+ var oldSize = HEAPU8.length;
+ requestedSize = requestedSize >>> 0;
+ abortOnCannotGrowMemory(requestedSize);
+}
+
+function _exit(status) {
+ exit(status);
+}
+
+var asmLibraryArg = {
+ "a": ___assert_fail,
+ "b": _emscripten_resize_heap,
+ "c": _exit
+};
+
+var asm = createWasm();
+
+var ___wasm_call_ctors = Module["___wasm_call_ctors"] = function() {
+ return (___wasm_call_ctors = Module["___wasm_call_ctors"] = Module["asm"]["e"]).apply(null, arguments);
+};
+
+var _pikchr = Module["_pikchr"] = function() {
+ return (_pikchr = Module["_pikchr"] = Module["asm"]["f"]).apply(null, arguments);
+};
+
+var stackSave = Module["stackSave"] = function() {
+ return (stackSave = Module["stackSave"] = Module["asm"]["h"]).apply(null, arguments);
+};
+
+var stackRestore = Module["stackRestore"] = function() {
+ return (stackRestore = Module["stackRestore"] = Module["asm"]["i"]).apply(null, arguments);
+};
+
+var stackAlloc = Module["stackAlloc"] = function() {
+ return (stackAlloc = Module["stackAlloc"] = Module["asm"]["j"]).apply(null, arguments);
+};
+
+Module["cwrap"] = cwrap;
+
+Module["stackSave"] = stackSave;
+
+Module["stackRestore"] = stackRestore;
+
+Module["setValue"] = setValue;
+
+Module["getValue"] = getValue;
+
+var calledRun;
+
+function ExitStatus(status) {
+ this.name = "ExitStatus";
+ this.message = "Program terminated with exit(" + status + ")";
+ this.status = status;
+}
+
+dependenciesFulfilled = function runCaller() {
+ if (!calledRun) run();
+ if (!calledRun) dependenciesFulfilled = runCaller;
+};
+
+function run(args) {
+ args = args || arguments_;
+ if (runDependencies > 0) {
+ return;
+ }
+ preRun();
+ if (runDependencies > 0) {
+ return;
+ }
+ function doRun() {
+ if (calledRun) return;
+ calledRun = true;
+ Module["calledRun"] = true;
+ if (ABORT) return;
+ initRuntime();
+ readyPromiseResolve(Module);
+ if (Module["onRuntimeInitialized"]) Module["onRuntimeInitialized"]();
+ postRun();
+ }
+ if (Module["setStatus"]) {
+ Module["setStatus"]("Running...");
+ setTimeout(function() {
+ setTimeout(function() {
+ Module["setStatus"]("");
+ }, 1);
+ doRun();
+ }, 1);
+ } else {
+ doRun();
+ }
+}
+
+Module["run"] = run;
+
+function exit(status, implicit) {
+ EXITSTATUS = status;
+ procExit(status);
+}
+
+function procExit(code) {
+ EXITSTATUS = code;
+ if (!keepRuntimeAlive()) {
+ if (Module["onExit"]) Module["onExit"](code);
+ ABORT = true;
+ }
+ quit_(code, new ExitStatus(code));
+}
+
+if (Module["preInit"]) {
+ if (typeof Module["preInit"] == "function") Module["preInit"] = [ Module["preInit"] ];
+ while (Module["preInit"].length > 0) {
+ Module["preInit"].pop()();
+ }
+}
+
+run();
+
+
+ return initPikchrModule.ready
+}
+);
+})();
+if (typeof exports === 'object' && typeof module === 'object')
+ module.exports = initPikchrModule;
+else if (typeof define === 'function' && define['amd'])
+ define([], function() { return initPikchrModule; });
+else if (typeof exports === 'object')
+ exports["initPikchrModule"] = initPikchrModule;
ADDED extsrc/pikchr.wasm
Index: extsrc/pikchr.wasm
==================================================================
--- /dev/null
+++ extsrc/pikchr.wasm
cannot compute difference between binary files
Index: skins/darkmode/css.txt
==================================================================
--- skins/darkmode/css.txt
+++ skins/darkmode/css.txt
@@ -131,13 +131,12 @@
.button:focus,
button:focus,
input[type=button]:focus,
input[type=reset]:focus,
input[type=submit]:focus {
- color: #333;
+ outline: 2px outset #333;
border-color: #888;
- outline: 0
}
/* All page content from the bottom of the menu or submenu down to
** the footer */
div.content {
Index: src/builtin.c
==================================================================
--- src/builtin.c
+++ src/builtin.c
@@ -130,20 +130,19 @@
blob_reset(&x);
}
/*
** Input zList is a list of numeric identifiers for files in
-** aBuiltinFiles[]. Return the concatenation of all of those
-** files using mimetype zType, or as application/javascript if
-** zType is 0.
+** aBuiltinFiles[]. Return the concatenation of all of those files
+** using mimetype zType, or as text/javascript if zType is 0.
*/
static void builtin_deliver_multiple_js_files(
const char *zList, /* List of numeric identifiers */
const char *zType /* Override mimetype */
){
Blob *pOut;
- if( zType==0 ) zType = "application/javascript";
+ if( zType==0 ) zType = "text/javascript";
cgi_set_content_type(zType);
pOut = cgi_output_blob();
while( zList[0] ){
int i = atoi(zList);
if( i>0 && i<=count(aBuiltinFiles) ){
@@ -203,11 +202,11 @@
@ File "%h(zName)" not found
return;
}
if( zType==0 ){
if( sqlite3_strglob("*.js", zName)==0 ){
- zType = "application/javascript";
+ zType = "text/javascript";
}else{
zType = mimetype_from_name(zName);
}
}
cgi_set_content_type(zType);
Index: src/cgi.c
==================================================================
--- src/cgi.c
+++ src/cgi.c
@@ -307,11 +307,11 @@
if(!g.isHTTP) return /* e.g. JSON CLI mode, where g.zTop is not set */;
else if( zPath==0 ){
zPath = g.zTop;
if( zPath[0]==0 ) zPath = "/";
}
- if( g.zBaseURL!=0 && strncmp(g.zBaseURL, "https:", 6)==0 ){
+ if( g.zBaseURL!=0 && fossil_strncmp(g.zBaseURL, "https:", 6)==0 ){
zSecure = " secure;";
}
if( lifetime!=0 ){
blob_appendf(&extraHeader,
"Set-Cookie: %s=%t; Path=%s; max-age=%d; HttpOnly; "
@@ -330,13 +330,37 @@
** Return true if the response should be sent with Content-Encoding: gzip.
*/
static int is_gzippable(void){
if( g.fNoHttpCompress ) return 0;
if( strstr(PD("HTTP_ACCEPT_ENCODING", ""), "gzip")==0 ) return 0;
- return strncmp(zContentType, "text/", 5)==0
- || sqlite3_strglob("application/*xml", zContentType)==0
- || sqlite3_strglob("application/*javascript", zContentType)==0;
+ /* Maintenance note: this oddball structure is intended to make
+ ** adding new mimetypes to this list less of a performance hit than
+ ** doing a strcmp/glob over a growing set of compressible types. */
+ switch(zContentType ? *zContentType : 0){
+ case (int)'a':
+ if(0==fossil_strncmp("application/",zContentType,12)){
+ const char * z = &zContentType[12];
+ switch(*z){
+ case (int)'j':
+ return fossil_strcmp("javascript", z)==0
+ || fossil_strcmp("json", z)==0;
+ case (int)'w': return fossil_strcmp("wasm", z)==0;
+ case (int)'x':
+ return fossil_strcmp("x-tcl", z)==0
+ || fossil_strcmp("x-tar", z)==0;
+ default:
+ return sqlite3_strglob("*xml", z)==0;
+ }
+ }
+ break;
+ case (int)'i':
+ return fossil_strcmp(zContentType, "image/svg+xml")==0
+ || fossil_strcmp(zContentType, "image/vnd.microsoft.icon")==0;
+ case (int)'t':
+ return fossil_strncmp(zContentType, "text/", 5)==0;
+ }
+ return 0;
}
/*
** The following routines read or write content from/to the wire for
@@ -419,10 +443,29 @@
if( !g.httpUseSSL ){
fflush(g.httpOut);
}
}
+/*
+** Given a Content-Type value, returns a string suitable for appending
+** to the Content-Type header for adding (or not) the "; charset=..."
+** part. It returns an empty string for most types or if zContentType
+** is NULL.
+**
+** See forum post f60dece061c364d1 for the discussions which lead to
+** this. Previously we always appended the charset, but WASM loaders
+** are pedantic and refuse to load any responses which have a
+** charset. Also, adding a charset is not strictly appropriate for
+** most types (and not required for many others which may ostensibly
+** benefit from one, as detailed in that forum post).
+*/
+static const char * content_type_charset(const char *zContentType){
+ if(0==fossil_strncmp(zContentType,"text/",5)){
+ return "; charset=utf-8";
+ }
+ return "";
+}
/*
** Generate the reply to a web request. The output might be an
** full HTTP response, or a CGI response, depending on how things have
** be set up.
@@ -493,11 +536,12 @@
/* Content intended for logged in users should only be cached in
** the browser, not some shared location.
*/
if( iReplyStatus!=304 ) {
- blob_appendf(&hdr, "Content-Type: %s; charset=utf-8\r\n", zContentType);
+ blob_appendf(&hdr, "Content-Type: %s%s\r\n", zContentType,
+ content_type_charset(zContentType));
if( fossil_strcmp(zContentType,"application/x-fossil")==0 ){
cgi_combine_header_and_body();
blob_compress(&cgiContent[0], &cgiContent[0]);
}
@@ -569,11 +613,12 @@
int iStat,
const char *zStat
){
char *zLocation;
CGIDEBUG(("redirect to %s\n", zURL));
- if( strncmp(zURL,"http:",5)==0 || strncmp(zURL,"https:",6)==0 ){
+ if( fossil_strncmp(zURL,"http:",5)==0
+ || fossil_strncmp(zURL,"https:",6)==0 ){
zLocation = mprintf("Location: %s\r\n", zURL);
}else if( *zURL=='/' ){
int n1 = (int)strlen(g.zBaseURL);
int n2 = (int)strlen(g.zTop);
if( g.zBaseURL[n1-1]=='/' ) zURL++;
@@ -653,11 +698,11 @@
const char *zMethod = P("REQUEST_METHOD");
if( zMethod==0 ) return 0;
if( strcmp(zMethod,"POST")!=0 ) return 0;
}
nBase = (int)strlen(g.zBaseURL);
- if( strncmp(g.zBaseURL,zRef,nBase)!=0 ) return 0;
+ if( fossil_strncmp(g.zBaseURL,zRef,nBase)!=0 ) return 0;
if( zRef[nBase]!=0 && zRef[nBase]!='/' ) return 0;
return 1;
}
/*
@@ -920,11 +965,12 @@
int len = *pLen;
int i;
int nBoundary = strlen(zBoundary);
*pnContent = len;
for(i=0; i
0 && z[i-1]=='\r' ) i--;
z[i] = 0;
*pnContent = i;
i += nBoundary;
break;
@@ -1348,11 +1394,11 @@
*/
void cgi_decode_post_parameters(void){
int len = blob_size(&g.cgiIn);
if( len==0 ) return;
if( fossil_strcmp(g.zContentType,"application/x-www-form-urlencoded")==0
- || strncmp(g.zContentType,"multipart/form-data",19)==0
+ || fossil_strncmp(g.zContentType,"multipart/form-data",19)==0
){
char *z = blob_str(&g.cgiIn);
cgi_trace(z);
if( g.zContentType[0]=='a' ){
add_param_list(z, '&');
@@ -2326,11 +2372,11 @@
if( zBrowser ){
assert( strstr(zBrowser,"%d")!=0 );
zBrowser = mprintf(zBrowser /*works-like:"%d"*/, iPort);
#if defined(__CYGWIN__)
/* On Cygwin, we can do better than "echo" */
- if( strncmp(zBrowser, "echo ", 5)==0 ){
+ if( fossil_strncmp(zBrowser, "echo ", 5)==0 ){
wchar_t *wUrl = fossil_utf8_to_unicode(zBrowser+5);
wUrl[wcslen(wUrl)-2] = 0; /* Strip terminating " &" */
if( (size_t)ShellExecuteW(0, L"open", wUrl, 0, 0, 1)<33 ){
fossil_warning("cannot start browser\n");
}
@@ -2486,11 +2532,11 @@
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec", 0};
if( 7==sscanf(zDate, "%3[A-Za-z], %d %3[A-Za-z] %d %d:%d:%d", zIgnore,
&mday, zMonth, &year, &hour, &min, &sec)){
if( year > 1900 ) year -= 1900;
for(mon=0; azMonths[mon]; mon++){
- if( !strncmp( azMonths[mon], zMonth, 3 )){
+ if( !fossil_strncmp( azMonths[mon], zMonth, 3 )){
int nDay;
int isLeapYr;
static int priorDays[] =
{ 0, 31, 59, 90,120,151,181,212,243,273,304,334 };
if( mon<0 ){
Index: src/default.css
==================================================================
--- src/default.css
+++ src/default.css
@@ -1007,10 +1007,13 @@
background-color: #ffb;
}
label {
white-space: nowrap;
}
+label[for] {
+ cursor: pointer;
+}
.copy-button {
display: inline-block;
width: 14px;
height: 14px;
/*Note: .24em is slightly smaller than the average width of a normal space.*/
@@ -1075,16 +1078,21 @@
}
.warning {
color: black;
background: yellow;
}
-.hidden {
- /* The framework-wide way of hiding elements is to assign them this
- CSS class. To make them visible again, remove it. The !important
- qualifiers are unfortunate but sometimes necessary when hidden
- element has other classes which specify visibility-related
- options. */
+.hidden, .initially-hidden {
+ /* The framework-wide way of hiding elements is to assign them th
+ .hidden class. To make them visible again, remove it. The
+ !important qualifiers are unfortunate but sometimes necessary
+ when hidden element has other classes which specify
+ visibility-related options. The .initially-hidden class is for
+ pages which need to show, e.g., a progress widget while a large
+ WASM blob loads. Elements aside from that load-time widget can be
+ made .initially-hidden and then have that class removed once the
+ long-running startup process is done. See /pikchrshow for an
+ example. */
position: absolute !important;
opacity: 0 !important;
pointer-events: none !important;
display: none !important;
}
Index: src/doc.c
==================================================================
--- src/doc.c
+++ src/doc.c
@@ -156,11 +156,16 @@
{ "jad", 3, "text/vnd.sun.j2me.app-descriptor" },
{ "jar", 3, "application/java-archive" },
{ "jpe", 3, "image/jpeg" },
{ "jpeg", 4, "image/jpeg" },
{ "jpg", 3, "image/jpeg" },
- { "js", 2, "application/javascript" },
+ { "js", 2, "text/javascript" },
+ /* application/javascript is commonly used for JS, but the
+ ** spec says text/javascript is correct:
+ ** https://html.spec.whatwg.org/multipage/scripting.html
+ ** #scriptingLanguages:javascript-mime-type */
+ { "json", 4, "application/json" },
{ "kar", 3, "audio/midi" },
{ "latex", 5, "application/x-latex" },
{ "lha", 3, "application/octet-stream" },
{ "lsp", 3, "application/x-lisp" },
{ "lzh", 3, "application/octet-stream" },
@@ -173,10 +178,11 @@
{ "mesh", 4, "model/mesh" },
{ "mid", 3, "audio/midi" },
{ "midi", 4, "audio/midi" },
{ "mif", 3, "application/x-mif" },
{ "mime", 4, "www/mime" },
+ { "mjs", 3, "text/javascript" /*EM6 modules*/ },
{ "mkd", 3, "text/x-markdown" },
{ "mov", 3, "video/quicktime" },
{ "movie", 5, "video/x-sgi-movie" },
{ "mp2", 3, "audio/mpeg" },
{ "mp3", 3, "audio/mpeg" },
Index: src/fossil.page.pikchrshow.js
==================================================================
--- src/fossil.page.pikchrshow.js
+++ src/fossil.page.pikchrshow.js
@@ -1,12 +1,20 @@
(function(F/*the fossil object*/){
"use strict";
/**
- Client-side implementation of the /pikchrshow app. Requires that
+ Client-side implementation of the /pikchrshowcs app. Requires that
the fossil JS bootstrapping is complete and that these fossil JS
APIs have been installed: fossil.fetch, fossil.dom,
fossil.copybutton, fossil.popupwidget, fossil.storage
+
+ Maintenance funkiness note: this file is for the legacy
+ /pikchrshowcs app, which was formerly named /pikchrshow. This
+ file and its replacement were not renamed because the replacement
+ impl would end up getting this file's name and cause confusion in
+ the file history. Whether that confusion would be less than this
+ file's name matching the _other_ /pikchrshow impl will cause more
+ or less confusion than that remains to be seen.
*/
const E = (s)=>document.querySelector(s),
D = F.dom,
P = F.page;
ADDED src/fossil.page.pikchrshowasm.js
Index: src/fossil.page.pikchrshowasm.js
==================================================================
--- /dev/null
+++ src/fossil.page.pikchrshowasm.js
@@ -0,0 +1,686 @@
+/*
+ 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 WASM rendition of fossil's
+ /pikchrshow app. It sets up the various UI bits, loads a Worker for
+ the pikchr process, and manages the communication between the UI and
+ worker.
+
+ API dependencies: fossil.dom, fossil.copybutton, fossil.storage
+*/
+(function(F/*fossil object*/){
+ 'use strict';
+
+ /* Recall that the 'self' symbol, except where locally
+ overwritten, refers to the global window or worker object. */
+
+ const D = F.dom;
+ /** Name of the stored copy of this app's config. */
+ const configStorageKey = 'pikchrshow-config';
+
+ /* 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]);
+ };
+
+ /** The main application object. */
+ const PS = {
+ /* Config options. */
+ config: {
+ /* If true, display input/output areas side-by-side, else stack
+ them vertically. */
+ sideBySide: true,
+ /* If true, swap positions of the input/output areas. */
+ swapInOut: false,
+ /* If true, the SVG is allowed to resize to fit the parent
+ content area, else the parent is resized to fit the rendered
+ SVG (as sized by pikchr). */
+ renderAutofit: false,
+ /* If true, automatically render while the user is typing. */
+ renderWhileTyping: false
+ },
+ /* Various DOM elements. */
+ e: {
+ previewCopyButton: E('#preview-copy-button'),
+ previewModeLabel: E('label[for=preview-copy-button]'),
+ zoneInputButtons: E('.zone-wrapper.input > legend > .button-bar'),
+ zoneOutputButtons: E('.zone-wrapper.output > legend > .button-bar'),
+ outText: E('#pikchr-output-text'),
+ pikOutWrapper: E('#pikchr-output-wrapper'),
+ pikOut: E('#pikchr-output'),
+ btnRender: E('#btn-render')
+ },
+ renderModes: ['svg'/*SVG must be at index 0*/,'markdown', 'wiki', 'text'],
+ renderModeLabels: {
+ svg: 'SVG', markdown: 'Markdown', wiki: 'Fossil Wiki', text: 'Text'
+ },
+ _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;
+ }
+ 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){
+ this.worker.postMessage({type, data});
+ return this;
+ },
+ /** Stores this object's config in the browser's storage. */
+ storeConfig: function(){
+ F.storage.setJSON(configStorageKey,this.config);
+ }
+ };
+ PS.renderModes.selectedIndex = 0;
+ PS._config = F.storage.getJSON(configStorageKey);
+ if(PS._config){
+ /* Copy all properties to PS.config which are currently in
+ PS._config. We don't bother copying any other properties: those
+ would be stale/removed config entries. */
+ Object.keys(PS.config).forEach(function(k){
+ if(PS._config.hasOwnProperty(k)){
+ PS.config[k] = PS._config[k];
+ }
+ });
+ delete PS._config;
+ }
+
+ PS.worker = new Worker('builtin/extsrc/pikchr-worker.js');
+ PS.worker.onmessage = (ev)=>PS.runMsgHandlers(ev.data);
+ PS.addMsgHandler('stdout', console.log.bind(console));
+ PS.addMsgHandler('stderr', console.error.bind(console));
+
+ /** Handles status updates from the Module object. */
+ PS.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. */
+ }
+ });
+
+ PS.e.previewModeLabel.innerText =
+ PS.renderModeLabels[PS.renderModes[PS.renderModes.selectedIndex]];
+
+ /**
+ The 'pikchrshow-ready' event is fired (with no payload) when the
+ wasm module has finished loading. */
+ PS.addMsgHandler('pikchrshow-ready', function(){
+ PS.clearMsgHandlers('pikchrshow-ready');
+ F.page.onPikchrshowLoaded();
+ });
+
+ /**
+ Performs all app initialization which must wait until after the
+ worker module is loaded. This function removes itself when it's
+ called.
+ */
+ F.page.onPikchrshowLoaded = function(){
+ delete this.onPikchrshowLoaded;
+ // Unhide all elements which start out hidden
+ EAll('.initially-hidden').forEach((e)=>e.classList.remove('initially-hidden'));
+ const taInput = E('#input');
+ const btnClearIn = E('#btn-clear');
+ btnClearIn.addEventListener('click',function(){
+ taInput.value = '';
+ },false);
+ const getCurrentText = function(){
+ let text;
+ if(taInput.selectionStart', m.pikchr, ''].join('');
+ break;
+ default:
+ body = m.result;
+ }
+ this.e.outText.value = body;
+ this.e.outText.classList.remove('hidden');
+ pikOut.classList.add('hidden');
+ this.e.pikOutWrapper.classList.add('text');
+ break;
+ }
+ case 'svg':
+ this.e.outText.classList.add('hidden');
+ pikOut.classList.remove('hidden');
+ this.e.pikOutWrapper.classList.remove('text');
+ pikOut.innerHTML = m.result;
+ this.e.outText.value = m.result/*for clipboard copy*/;
+ break;
+ default: throw new Error("Unhandled render mode: "+mode);
+ }
+ let vw = null, vh = null;
+ if('svg'===mode){
+ if(m.isError){
+ vw = vh = '100%';
+ }else if(this.config.renderAutofit){
+ /* FIXME: current behavior doesn't work as desired when width>height
+ (e.g. non-side-by-side mode).*/
+ vw = vh = '98%';
+ }else{
+ vw = m.width+1+'px'; vh = m.height+1+'px';
+ /* +1 is b/c the SVG uses floating point sizes but pikchr()
+ returns truncated integers. */
+ }
+ pikOut.style.width = vw;
+ pikOut.style.height = vh;
+ }
+ }.bind(PS))/*'pikchr' msg handler*/;
+
+ E('#btn-render-mode').addEventListener('click',function(){
+ const modes = this.renderModes;
+ modes.selectedIndex = (modes.selectedIndex + 1) % modes.length;
+ this.e.previewModeLabel.innerText = this.renderModeLabels[modes[modes.selectedIndex]];
+ if(this.e.pikOut.dataset.pikchr){
+ this.render(this.e.pikOut.dataset.pikchr);
+ }
+ }.bind(PS));
+ F.copyButton(PS.e.previewCopyButton, {copyFromElement: PS.e.outText});
+ PS.e.previewModeLabel.addEventListener('click', ()=>PS.e.previewCopyButton.click(), false);
+
+ PS.addMsgHandler('working',function f(ev){
+ switch(ev.data){
+ case 'start': /* See notes in preStartWork(). */; return;
+ case 'end':
+ //preStartWork._.pageTitle.innerText = preStartWork._.pageTitleOrig;
+ this.e.btnRender.removeAttribute('disabled');
+ this.e.pikOutWrapper.classList[this.config.renderAutofit ? 'add' : 'remove']('autofit');
+ return;
+ }
+ console.warn("Unhandled 'working' event:",ev.data);
+ }.bind(PS));
+
+ /* 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
+ PS.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 = !!PS.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(){
+ PS.config[this.dataset.config] = this.checked;
+ PS.storeConfig();
+ }, false);
+ });
+ E('#opt-cb-autofit').addEventListener('change',function(){
+ /* PS.config.renderAutofit was set by the data-config
+ event handler. */
+ if(0==PS.renderModes.selectedIndex && PS.e.pikOut.dataset.pikchr){
+ PS.render(PS.e.pikOut.dataset.pikchr);
+ }
+ });
+ /* For each button with data-cmd=X, map a click handler which
+ calls PS.render(X). */
+ const cmdClick = function(){PS.render(this.dataset.cmd);};
+ EAll('button[data-cmd]').forEach(
+ e => e.addEventListener('click', cmdClick, false)
+ );
+
+
+ ////////////////////////////////////////////////////////////
+ // Set up selection list of predefined scripts...
+ if(true){
+ const selectScript = PS.e.selectScript = D.select();
+ D.append(PS.e.zoneInputButtons, selectScript);
+ PS.predefinedPiks.forEach(function(script,ndx){
+ const opt = D.option(script.code ? script.code.trim() :'', script.name);
+ D.append(selectScript, opt);
+ if(!ndx) selectScript.selectedIndex = 0 /*timing/ordering workaround*/;
+ if(ndx && !script.code){
+ /* Treat entries w/ no code as separators EXCEPT for the
+ first one, which we want to keep selectable solely for
+ cosmetic reasons. */
+ D.disable(opt);
+ }
+ });
+ delete PS.predefinedPiks;
+ selectScript.addEventListener('change', function(ev){
+ const val = ev.target.value;
+ if(!val) return;
+ setCurrentText(val);
+ }, false);
+ }/*Examples*/
+
+ /**
+ TODO: Handle load/import of an external pikchr file.
+ */
+ if(0) E('#load-pikchr').addEventListener('change',function(){
+ const f = this.files[0];
+ const r = new FileReader();
+ const status = {loaded: 0, total: 0};
+ this.setAttribute('disabled','disabled');
+ const that = this;
+ r.addEventListener('load', function(){
+ that.removeAttribute('disabled');
+ stdout("Loaded",f.name+". Opening pikchr...");
+ PS.wMsg('open',{
+ filename: f.name,
+ buffer: this.result
+ });
+ });
+ r.addEventListener('error',function(){
+ that.removeAttribute('disabled');
+ stderr("Loading",f.name,"failed for unknown reasons.");
+ });
+ r.addEventListener('abort',function(){
+ that.removeAttribute('disabled');
+ stdout("Cancelled loading of",f.name+".");
+ });
+ r.readAsArrayBuffer(f);
+ });
+
+ EAll('fieldset.collapsible').forEach(function(fs){
+ const btnToggle = E(fs,'legend > #btn-options-toggle'),
+ content = EAll(fs,':scope > div');
+ btnToggle.addEventListener('click', function(){
+ fs.classList.toggle('collapsed');
+ content.forEach((d)=>d.classList.toggle('hidden'));
+ }, false);
+ });
+
+ PS.e.btnRender.click();
+
+ /** Debounce handler for auto-rendering while typing. */
+ const debounceAutoRender = F.debounce(function f(){
+ if(!PS._isDirty) return;
+ const text = getCurrentText();
+ if(f._ === text){
+ PS._isDirty = false;
+ return;
+ }
+ f._ = text;
+ PS._isDirty = false;
+ PS.render(text || '');
+ }, 800, false);
+
+ taInput.addEventListener('keydown',function f(ev){
+ if((ev.ctrlKey || ev.shiftKey) && 13 === ev.keyCode){
+ // Ctrl-enter and shift-enter both run the current input
+ PS._isDirty = false/*prevent a pending debounce from re-rendering*/;
+ ev.preventDefault();
+ ev.stopPropagation();
+ renderCurrentText();
+ return;
+ }
+ if(!PS.config.renderWhileTyping) return;
+ /* Auto-render while typing... */
+ switch(ev.keyCode){
+ case (ev.keyCode<32): /*any ctrl char*/
+ /* ^^^ w/o that, simply tapping ctrl is enough to
+ force a re-render. Similarly, TAB-ing focus away
+ should not re-render. */
+ case 33: case 34: /* page up/down */
+ case 35: case 36: /* home/end */
+ case 37: case 38: case 39: case 40: /* arrows */
+ return;
+ }
+ PS._isDirty = true;
+ debounceAutoRender();
+ }, false);
+
+ 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 > div.header'),
+ E('body > div.mainmenu'),
+ E('body > div.footer')
+ ];
+ const resized = function f(){
+ if(f.$disabled) return;
+ const wh = window.innerHeight;
+ var ht;
+ var extra = 0;
+ elemsToCount.forEach((e)=>e ? extra += F.dom.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', F.debounce(resized, 250), false);
+ return resized;
+ })()/*ForceResizeKludge*/;
+
+ delete ForceResizeKludge.$disabled;
+ ForceResizeKludge();
+ }/*onPikchrshowLoaded()*/;
+
+
+ /**
+ Predefined scripts. Each entry is an object:
+
+ {
+ name: required string,
+ code: optional code string. An entry with a falsy code is treated
+ like a separator in the resulting SELECT element (a
+ disabled OPTION).
+ }
+ */
+ PS.predefinedPiks = [
+ {name: "-- Example Scripts --", code: false},
+/*
+ The following were imported from the pikchr test scripts:
+
+ https://fossil-scm.org/pikchr/dir/examples
+*/
+{name:"Cardinal headings",code:` linerad = 5px
+C: circle "Center" rad 150%
+ circle "N" at 1.0 n of C; arrow from C to last chop ->
+ circle "NE" at 1.0 ne of C; arrow from C to last chop <-
+ circle "E" at 1.0 e of C; arrow from C to last chop <->
+ circle "SE" at 1.0 se of C; arrow from C to last chop ->
+ circle "S" at 1.0 s of C; arrow from C to last chop <-
+ circle "SW" at 1.0 sw of C; arrow from C to last chop <->
+ circle "W" at 1.0 w of C; arrow from C to last chop ->
+ circle "NW" at 1.0 nw of C; arrow from C to last chop <-
+ arrow from 2nd circle to 3rd circle chop
+ arrow from 4th circle to 3rd circle chop
+ arrow from SW to S chop <->
+ circle "ESE" at 2.0 heading 112.5 from Center \
+ thickness 150% fill lightblue radius 75%
+ arrow from Center to ESE thickness 150% <-> chop
+ arrow from ESE up 1.35 then to NE chop
+ line dashed <- from E.e to (ESE.x,E.y)
+ line dotted <-> thickness 50% from N to NW chop
+`},{name:"Core object types",code:`AllObjects: [
+
+# First row of objects
+box "box"
+box rad 10px "box (with" "rounded" "corners)" at 1in right of previous
+circle "circle" at 1in right of previous
+ellipse "ellipse" at 1in right of previous
+
+# second row of objects
+OVAL1: oval "oval" at 1in below first box
+oval "(tall &" "thin)" "oval" width OVAL1.height height OVAL1.width \
+ at 1in right of previous
+cylinder "cylinder" at 1in right of previous
+file "file" at 1in right of previous
+
+# third row shows line-type objects
+dot "dot" above at 1in below first oval
+line right from 1.8cm right of previous "lines" above
+arrow right from 1.8cm right of previous "arrows" above
+spline from 1.8cm right of previous \
+ go right .15 then .3 heading 30 then .5 heading 160 then .4 heading 20 \
+ then right .15
+"splines" at 3rd vertex of previous
+
+# The third vertex of the spline is not actually on the drawn
+# curve. The third vertex is a control point. To see its actual
+# position, uncomment the following line:
+#dot color red at 3rd vertex of previous spline
+
+# Draw various lines below the first line
+line dashed right from 0.3cm below start of previous line
+line dotted right from 0.3cm below start of previous
+line thin right from 0.3cm below start of previous
+line thick right from 0.3cm below start of previous
+
+
+# Draw arrows with different arrowhead configurations below
+# the first arrow
+arrow <- right from 0.4cm below start of previous arrow
+arrow <-> right from 0.4cm below start of previous
+
+# Draw splines with different arrowhead configurations below
+# the first spline
+spline same from .4cm below start of first spline ->
+spline same from .4cm below start of previous <-
+spline same from .4cm below start of previous <->
+
+] # end of AllObjects
+
+# Label the whole diagram
+text "Examples Of Pikchr Objects" big bold at .8cm above north of AllObjects
+`},{name:"Swimlanes",code:` $laneh = 0.75
+
+ # Draw the lanes
+ down
+ box width 3.5in height $laneh fill 0xacc9e3
+ box same fill 0xc5d8ef
+ box same as first box
+ box same as 2nd box
+ line from 1st box.sw+(0.2,0) up until even with 1st box.n \
+ "Alan" above aligned
+ line from 2nd box.sw+(0.2,0) up until even with 2nd box.n \
+ "Betty" above aligned
+ line from 3rd box.sw+(0.2,0) up until even with 3rd box.n \
+ "Charlie" above aligned
+ line from 4th box.sw+(0.2,0) up until even with 4th box.n \
+ "Darlene" above aligned
+
+ # fill in content for the Alice lane
+ right
+A1: circle rad 0.1in at end of first line + (0.2,-0.2) \
+ fill white thickness 1.5px "1"
+ arrow right 50%
+ circle same "2"
+ arrow right until even with first box.e - (0.65,0.0)
+ ellipse "future" fit fill white height 0.2 width 0.5 thickness 1.5px
+A3: circle same at A1+(0.8,-0.3) "3" fill 0xc0c0c0
+ arrow from A1 to last circle chop "fork!" below aligned
+
+ # content for the Betty lane
+B1: circle same as A1 at A1-(0,$laneh) "1"
+ arrow right 50%
+ circle same "2"
+ arrow right until even with first ellipse.w
+ ellipse same "future"
+B3: circle same at A3-(0,$laneh) "3"
+ arrow right 50%
+ circle same as A3 "4"
+ arrow from B1 to 2nd last circle chop
+
+ # content for the Charlie lane
+C1: circle same as A1 at B1-(0,$laneh) "1"
+ arrow 50%
+ circle same "2"
+ arrow right 0.8in "goes" "offline"
+C5: circle same as A3 "5"
+ arrow right until even with first ellipse.w \
+ "back online" above "pushes 5" below "pulls 3 & 4" below
+ ellipse same "future"
+
+ # content for the Darlene lane
+D1: circle same as A1 at C1-(0,$laneh) "1"
+ arrow 50%
+ circle same "2"
+ arrow right until even with C5.w
+ circle same "5"
+ arrow 50%
+ circle same as A3 "6"
+ arrow right until even with first ellipse.w
+ ellipse same "future"
+D3: circle same as B3 at B3-(0,2*$laneh) "3"
+ arrow 50%
+ circle same "4"
+ arrow from D1 to D3 chop
+`},{
+ name: "The Stuff of Dreams",
+ code:`
+O: text "DREAMS" color grey
+circle rad 0.9 at 0.6 above O thick color red
+text "INEXPENSIVE" big bold at 0.9 above O color red
+
+circle rad 0.9 at 0.6 heading 120 from O thick color green
+text "FAST" big bold at 0.9 heading 120 from O color green
+
+circle rad 0.9 at 0.6 heading -120 from O thick color blue
+text "HIGH" big bold "QUALITY" big bold at 0.9 heading -120 from O color blue
+
+text "EXPENSIVE" at 0.55 below O color cyan
+text "SLOW" at 0.55 heading -60 from O color magenta
+text "POOR" "QUALITY" at 0.55 heading 60 from O color gold
+`}
+ ];
+
+
+})(window.fossil);
Index: src/json.c
==================================================================
--- src/json.c
+++ src/json.c
@@ -558,11 +558,11 @@
/*
** Guesses a RESPONSE Content-Type value based (primarily) on the
** HTTP_ACCEPT header.
**
** It will try to figure out if the client can support
-** application/json or application/javascript, and will fall back to
+** application/json, text/javascript, and will fall back to
** text/plain if it cannot figure out anything more specific.
**
** Returned memory is static and immutable, but if the environment
** changes after calling this then subsequent calls to this function
** might return different (also static/immutable) values.
@@ -573,12 +573,12 @@
cset = PD("HTTP_ACCEPT_CHARSET",NULL);
doUtf8 = ((NULL == cset) || (NULL!=strstr("utf-8",cset)))
? 1 : 0;
if( g.json.jsonp ){
return doUtf8
- ? "application/javascript; charset=utf-8"
- : "application/javascript";
+ ? "text/javascript; charset=utf-8"
+ : "text/javascript";
}else{
/*
Content-type
If the browser does not sent an ACCEPT for application/json
@@ -605,18 +605,19 @@
/*
** Given a request CONTENT_TYPE value, this function returns true
** if it is of a type which the JSON API can ostensibly read.
**
- ** It accepts any of application/json, text/plain, or
- ** application/javascript. The former is preferred, but was not
- ** widespread when this API was initially built, so the latter forms
- ** are permitted as fallbacks.
+ ** It accepts any of application/json, text/plain,
+ ** application/javascript, or text/javascript. The former is
+ ** preferred, but was not widespread when this API was initially
+ ** built, so the latter forms are permitted as fallbacks.
*/
int json_can_consume_content_type(const char * zType){
return fossil_strcmp(zType, "application/json")==0
|| fossil_strcmp(zType,"text/plain")==0/*assume this MIGHT be JSON*/
+ || fossil_strcmp(zType,"text/javascript")==0
|| fossil_strcmp(zType,"application/javascript")==0;
}
/*
** Sends pResponse to the output stream as the response object. This
@@ -627,18 +628,19 @@
** In CLI mode pResponse is sent to stdout immediately. In HTTP
** mode pResponse replaces any current CGI content but cgi_reply()
** is not called to flush the output.
**
** If g.json.jsonp is not NULL then the content type is set to
-** application/javascript and the output is wrapped in a jsonp
+** text/javascript and the output is wrapped in a jsonp
** wrapper.
*/
void json_send_response( cson_value const * pResponse ){
assert( NULL != pResponse );
if( g.isHTTP ){
cgi_reset_content();
if( g.json.jsonp ){
+ cgi_set_content_type("text/javascript");
cgi_printf("%s(",g.json.jsonp);
}
cson_output( pResponse, cson_data_dest_cgi, NULL, &g.json.outOpt );
if( g.json.jsonp ){
cgi_append_content(")",1);
Index: src/main.mk
==================================================================
--- src/main.mk
+++ src/main.mk
@@ -163,10 +163,13 @@
$(SRCDIR)/xfer.c \
$(SRCDIR)/xfersetup.c \
$(SRCDIR)/zip.c
EXTRA_FILES = \
+ $(SRCDIR)/../extsrc/pikchr-worker.js \
+ $(SRCDIR)/../extsrc/pikchr.js \
+ $(SRCDIR)/../extsrc/pikchr.wasm \
$(SRCDIR)/../skins/ardoise/css.txt \
$(SRCDIR)/../skins/ardoise/details.txt \
$(SRCDIR)/../skins/ardoise/footer.txt \
$(SRCDIR)/../skins/ardoise/header.txt \
$(SRCDIR)/../skins/black_and_white/css.txt \
@@ -231,10 +234,11 @@
$(SRCDIR)/fossil.page.brlist.js \
$(SRCDIR)/fossil.page.chat.js \
$(SRCDIR)/fossil.page.fileedit.js \
$(SRCDIR)/fossil.page.forumpost.js \
$(SRCDIR)/fossil.page.pikchrshow.js \
+ $(SRCDIR)/fossil.page.pikchrshowasm.js \
$(SRCDIR)/fossil.page.whistory.js \
$(SRCDIR)/fossil.page.wikiedit.js \
$(SRCDIR)/fossil.pikchr.js \
$(SRCDIR)/fossil.popupwidget.js \
$(SRCDIR)/fossil.storage.js \
@@ -266,10 +270,11 @@
$(SRCDIR)/sounds/e.wav \
$(SRCDIR)/sounds/f.wav \
$(SRCDIR)/style.admin_log.css \
$(SRCDIR)/style.chat.css \
$(SRCDIR)/style.fileedit.css \
+ $(SRCDIR)/style.pikchrshow.css \
$(SRCDIR)/style.wikiedit.css \
$(SRCDIR)/tree.js \
$(SRCDIR)/useredit.js \
$(SRCDIR)/wiki.wiki
@@ -2108,13 +2113,18 @@
$(OBJDIR)/pikchr.o: $(SRCDIR_extsrc)/pikchr.c
$(XTCC) $(PIKCHR_OPTIONS) -c $(SRCDIR_extsrc)/pikchr.c -o $@
$(OBJDIR)/cson_amalgamation.o: $(SRCDIR_extsrc)/cson_amalgamation.c
$(XTCC) -c $(SRCDIR_extsrc)/cson_amalgamation.c -o $@
+
+$(SRCDIR_extsrc)/pikchr.js: $(SRCDIR_extsrc)/pikchr.c
+ $(EMCC_WRAPPER) -o $@ $(EMCC_OPT) --no-entry -sEXPORTED_RUNTIME_METHODS=cwrap,setValue,getValue,stackSave,stackRestore -sEXPORTED_FUNCTIONS=_pikchr $(SRCDIR_extsrc)/pikchr.c -sENVIRONMENT=web -sMODULARIZE -sEXPORT_NAME=initPikchrModule --minify 0
+ @chmod -x $(SRCDIR_extsrc)/pikchr.wasm
+wasm: $(SRCDIR_extsrc)/pikchr.js
#
# The list of all the targets that do not correspond to real files. This stops
# 'make' from getting confused when someone makes an error in a rule.
#
.PHONY: all install test clean
Index: src/pikchrshow.c
==================================================================
--- src/pikchrshow.c
+++ src/pikchrshow.c
@@ -230,30 +230,36 @@
blob_reset(&bIn);
return isErr;
}
/*
-** WEBPAGE: pikchrshow
+** Legacy impl of /pikchrshow. pikchrshow_page() will delegate to
+** this one if the "legacy" or "ajax" request arguments are set.
**
** A pikchr code editor and previewer, allowing users to experiment
** with pikchr code or prototype it for use in copy/pasting into forum
-** posts, wiki pages, or embedded docs.
-**
-** It optionally accepts a p=pikchr-script-code URL parameter or POST
-** value to pre-populate the editor with that code.
+** posts, wiki pages, or embedded docs. This version of pikchrshow
+** uses JavaScript to send pikchr code to the server for
+** processing. The newer /pikchrshow applications runs pikchr on the
+** client machine, without the need for back-and-forth network
+** traffic.
*/
-void pikchrshow_page(void){
+void pikchrshowcs_page(void){
const char *zContent = 0;
int isDark; /* true if the current skin is "dark" */
int pikFlags =
PIKCHR_PROCESS_DIV
| PIKCHR_PROCESS_SRC
| PIKCHR_PROCESS_ERR_PRE;
login_check_credentials();
if( !g.perm.RdWiki && !g.perm.Read && !g.perm.RdForum ){
- cgi_redirectf("%R/login?g=pikchrshow");
+ cgi_redirectf("%R/login?g=pikchrshowcs");
+ }
+ if(P("wasm")){
+ pikchrshow_page();
+ return;
}
zContent = PD("content",P("p"));
if(P("ajax")!=0){
/* Called from the JS-side preview updater.
TODO: respond with JSON instead.*/
@@ -279,11 +285,11 @@
"box rad 10px \"Markdown\" \"Formatter\" \"(markdown.c)\" fit\n"
"arrow right 200% \"HTML+SVG\" \"Output\"\n"
"arrow <-> down from last box.s\n"
"box same \"Pikchr\" \"Formatter\" \"(pikchr.c)\" fit\n";
}
- style_header("PikchrShow");
+ style_header("PikchrShow Client/Server");
CX("");
CX("Input pikchr code and tap Preview (or Shift-Enter) to render "
- "it:
");
+ "it. Switch to WASM mode. ");
CX("