From 4f49b16079b27d7ce633619e4cddfdf2bffbc952 Mon Sep 17 00:00:00 2001 From: cashmere Date: Fri, 4 Sep 2026 09:05:24 +0200 Subject: initial commit --- src/pikchrshow.c | 574 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 574 insertions(+) create mode 100644 src/pikchrshow.c (limited to 'src/pikchrshow.c') diff --git a/src/pikchrshow.c b/src/pikchrshow.c new file mode 100644 index 0000000..b95182c --- /dev/null +++ b/src/pikchrshow.c @@ -0,0 +1,574 @@ +/* +** Copyright (c) 2020 D. Richard Hipp +** +** This program is free software; you can redistribute it and/or +** modify it under the terms of the Simplified BSD License (also +** known as the "2-Clause License" or "FreeBSD License".) +** +** This program is distributed in the hope that it will be useful, +** but without any warranty; without even the implied warranty of +** merchantability or fitness for a particular purpose. +** +** Author contact information: +** drh@hwaci.com +** http://www.hwaci.com/drh/ +** +******************************************************************************* +** +** This file contains fossil-specific code related to pikchr. +*/ +#include "config.h" +#include +#include +#include "pikchrshow.h" + +#if INTERFACE +/* These are described in pikchr_process()'s docs. */ +/* The first two must match the values from pikchr.c */ +#define PIKCHR_PROCESS_PLAINTEXT_ERRORS 0x0001 +#define PIKCHR_PROCESS_DARK_MODE 0x0002 +/* end of flags supported directly by pikchr() */ +#define PIKCHR_PROCESS_PASSTHROUGH 0x0003 /* Pass through these flags */ +#define PIKCHR_PROCESS_NONCE 0x0010 +#define PIKCHR_PROCESS_ERR_PRE 0x0020 +#define PIKCHR_PROCESS_SRC 0x0040 +#define PIKCHR_PROCESS_DIV 0x0080 +#define PIKCHR_PROCESS_DIV_INDENT 0x0100 +#define PIKCHR_PROCESS_DIV_CENTER 0x0200 +#define PIKCHR_PROCESS_DIV_FLOAT_LEFT 0x0400 +#define PIKCHR_PROCESS_DIV_FLOAT_RIGHT 0x0800 +#define PIKCHR_PROCESS_DIV_TOGGLE 0x1000 +#define PIKCHR_PROCESS_DIV_SOURCE 0x2000 +#define PIKCHR_PROCESS_DIV_SOURCE_INLINE 0x4000 +#endif + +/* +** Processes a pikchr script. zIn is the NUL-terminated input +** script. pikFlags may be a bitmask of any of the PIKCHR_PROCESS_xxx +** flags documented below. Output is sent to pOut, +** +** Returns 0 on success, or non-zero if pikchr processing failed. +** In either case, the error message (if any) from pikchr will be +** appended to pOut. +** +** pikFlags flag descriptions: +** +** - PIKCHR_PROCESS_DIV: if set, the SVG result is wrapped in a DIV +** element which specifies a max-width style value based on the SVG's +** calculated size. This flag has multiple mutually exclusive forms: +** +** - PIKCHR_PROCESS_DIV uses default element alignment. +** - PIKCHR_PROCESS_DIV_INDENT indents the div. +** - PIKCHR_PROCESS_DIV_CENTER centers the div. +** - PIKCHR_PROCESS_DIV_FLOAT_LEFT floats the div left. +** - PIKCHR_PROCESS_DIV_FLOAT_RIGHT floats the div right. +** +** If more than one is specified, which one is used is undefined. Those +** flags may be OR'd with one or both of the following: +** +** - PIKCHR_PROCESS_DIV_TOGGLE: adds the 'toggle' CSS class to the +** outer DIV so that event-handler code can install different +** toggling behaviour than the default. Default is ctrl-click, but +** this flag enables single-click toggling for the element. +** +** - PIKCHR_PROCESS_DIV_SOURCE: adds the 'source' CSS class to the +** outer DIV, which is a hint to the client-side renderer (see +** fossil.pikchr.js) that the pikchr should initially be rendered +** in source code form mode (the default is to hide the source and +** show the SVG). +** +** - PIKCHR_PROCESS_DIV_SOURCE_INLINE: adds the 'source-inline' CSS +** class to the outer wrapper. This modifier changes how the +** 'source' CSS class gets applied: with this flag, the source view +** should be rendered "inline" (same position as the graphic), else +** it is to be left-aligned. +** +** - PIKCHR_PROCESS_NONCE: if set, the resulting SVG/DIV are wrapped +** in "safe nonce" comments, which are a fossil-internal mechanism +** which prevents the wiki/markdown processors from re-processing this +** output. This is necessary when calling this routine in the context +** of wiki/embedded doc processing, but not (e.g.) when fetching +** an image for /pikchrpage. +** +** - PIKCHR_PROCESS_SRC: if set, a new PRE.pikchr-src element is +** injected adjacent to the SVG element which contains the +** HTML-escaped content of the input script. If +** PIKCHR_PROCESS_DIV_SOURCE or PIKCHR_PROCESS_DIV_SOURCE_INLINE is +** set, this flag is automatically implied. +** +** - PIKCHR_PROCESS_ERR_PRE: if set and pikchr() fails, the resulting +** error report is wrapped in a PRE element, else it is retained +** as-is (intended only for console output). +*/ +int pikchr_process(const char *zIn, int pikFlags, Blob * pOut){ + int isErr = 0; + int w = 0, h = 0; + char *zOut; + const char *zNonce = (PIKCHR_PROCESS_NONCE & pikFlags) + ? safe_html_nonce(1) : 0; + + if(!(PIKCHR_PROCESS_DIV & pikFlags) + /* If any DIV_xxx flags are set, set DIV */ + && (PIKCHR_PROCESS_DIV_INDENT + | PIKCHR_PROCESS_DIV_CENTER + | PIKCHR_PROCESS_DIV_FLOAT_RIGHT + | PIKCHR_PROCESS_DIV_FLOAT_LEFT + | PIKCHR_PROCESS_DIV_SOURCE + | PIKCHR_PROCESS_DIV_SOURCE_INLINE + | PIKCHR_PROCESS_DIV_TOGGLE + ) & pikFlags){ + pikFlags |= PIKCHR_PROCESS_DIV; + } + if(zNonce){ + blob_appendf(pOut, "%s\n", zNonce); + } + zOut = pikchr(zIn, "pikchr", + 0x01 | (pikFlags&PIKCHR_PROCESS_PASSTHROUGH), + &w, &h); + if( w>0 && h>0 ){ + const char * zClassToggle = ""; + const char * zClassSource = ""; + const char * zWrapperClass = ""; + if(PIKCHR_PROCESS_DIV & pikFlags){ + if(PIKCHR_PROCESS_DIV_CENTER & pikFlags){ + zWrapperClass = " center"; + }else if(PIKCHR_PROCESS_DIV_INDENT & pikFlags){ + zWrapperClass = " indent"; + }else if(PIKCHR_PROCESS_DIV_FLOAT_LEFT & pikFlags){ + zWrapperClass = " float-left"; + }else if(PIKCHR_PROCESS_DIV_FLOAT_RIGHT & pikFlags){ + zWrapperClass = " float-right"; + } + if(PIKCHR_PROCESS_DIV_TOGGLE & pikFlags){ + zClassToggle = " toggle"; + } + if(PIKCHR_PROCESS_DIV_SOURCE_INLINE & pikFlags){ + if(PIKCHR_PROCESS_DIV_SOURCE & pikFlags){ + zClassSource = " source source-inline"; + }else{ + zClassSource = " source-inline"; + } + pikFlags |= PIKCHR_PROCESS_SRC; + }else if(PIKCHR_PROCESS_DIV_SOURCE & pikFlags){ + zClassSource = " source"; + pikFlags |= PIKCHR_PROCESS_SRC; + } + blob_appendf(pOut,"
" + "
\n", + zWrapperClass/*safe-for-%s*/, + zClassToggle/*safe-for-%s*/, + zClassSource/*safe-for-%s*/, w); + } + blob_append(pOut, zOut, -1); + if(PIKCHR_PROCESS_DIV & pikFlags){ + blob_append(pOut, "
\n", 7); + } + if(PIKCHR_PROCESS_SRC & pikFlags){ + static int counter = 0; + ++counter; + blob_appendf(pOut, "
" + "
%h
" + "" + "
\n", + counter, zIn, counter, counter); + } + if(PIKCHR_PROCESS_DIV & pikFlags){ + blob_append(pOut, "
\n", 7); + } + }else{ + isErr = 2; + if(PIKCHR_PROCESS_ERR_PRE & pikFlags){ + blob_append(pOut, "
\n", 20);
+    }
+    blob_appendf(pOut, "%h", zOut);
+    if(PIKCHR_PROCESS_ERR_PRE & pikFlags){
+      blob_append(pOut, "\n
\n", 8); + } + } + fossil_free(zOut); + if(zNonce){ + blob_appendf(pOut, "%s\n", zNonce); + } + return isErr; +} + +/* +** 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. 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 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=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.*/ + cgi_set_content_type("text/html"); + if(zContent && *zContent){ + Blob out = empty_blob; + const int isErr = + pikchr_process(zContent, pikFlags, &out); + if(isErr){ + cgi_printf_header("x-pikchrshow-is-error: %d\r\n", isErr); + } + CX("%b", &out); + blob_reset(&out); + }else{ + CX("
No content! Nothing to render
"); + } + return; + }/*ajax response*/ + style_emit_noscript_for_js_page(); + isDark = skin_detail_boolean("white-foreground"); + if(!zContent){ + zContent = "arrow right 200% \"Markdown\" \"Source\"\n" + "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 Client/Server"); + CX(""); + CX("
Input pikchr code and tap Preview (or Shift-Enter) to render " + "it. Switch to WASM mode.
"); + CX("
"); { + CX("
"); { + CX("",zContent/*safe-for-%s*/); + CX("
"); { + CX(""); + CX("
"); { + CX(""); + CX(""); + CX(""); + CX("Stores/restores a single pikchr script to/from " + "browser-local storage from/to the editor." + /* gets turned into a help-buttonlet */); + } CX("
"/*stash controls*/); + style_labeled_checkbox("flipcolors-wrapper", "flipcolors", + "Dark mode?", + "1", isDark, 0); + } CX("
"/*#pikchrshow-controls*/); + } + CX("
"/*#pikchrshow-form*/); + CX("
"); { + CX("" + /* Reminder: Firefox does not properly flexbox a LEGEND + element, always flowing it in column mode. */); + CX("
"); + if(*zContent){ + Blob out = empty_blob; + pikchr_process(zContent, pikFlags, &out); + CX("%b", &out); + blob_reset(&out); + } CX("
"/*#pikchrshow-output*/); + } CX("
"/*#pikchrshow-output-wrapper*/); + } CX("
"/*sbs-wrapper*/); + builtin_fossil_js_bundle_or("fetch", "copybutton", "popupwidget", + "storage", "pikchr", NULL); + builtin_request_js("fossil.page.pikchrshow.js"); + builtin_fulfill_js_requests(); + style_finish_page(); +} + +/* +** WEBPAGE: pikchrshow +** +** 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. This version of pikchrshow +** uses WebAssembly to run entirely in the client browser, without a +** need for back-and-forth client/server traffic to perform the +** rendering. The "legacy" version of this application, which sends +** all input to the server for rendering, can be accessed by adding +** the "legacy" URL argument. +** +** It optionally accepts a p=pikchr-script-code URL parameter or POST +** value to pre-populate the editor with that code. +*/ +void pikchrshow_page(void){ + const char *zContent = 0; + + if(P("legacy") || P("ajax")){ + pikchrshowcs_page(); + return; + } + login_check_credentials(); + if( !g.perm.RdWiki && !g.perm.Read && !g.perm.RdForum ){ + cgi_redirectf("%R/login?g=pikchrshow"); + } + style_emit_noscript_for_js_page(); + style_header("PikchrShow"); + zContent = PD("content",P("p")); + if(!zContent){ + zContent = "arrow right 200% \"Markdown\" \"Source\"\n" + "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"; + } + /* Wasm load/init progress widget... */ + CX("
"); { + CX("
"); + CX("
"); + CX("
Initializing app...
"); + CX("
"); + CX("On a slow internet connection this may take a moment. If this "); + CX("message displays for \"a long time\", initialization may have "); + CX("failed and the JavaScript console may contain clues as to why. "); + CX("
"); + CX(""); + CX("
"); + CX("
Downloading...
"); + CX(""); + } CX("
"); + /* Main view... */ + CX("
"); { + CX("
"); { + CX(""); + CX("
"); + CX(""); + CX(""); + CX(""); + CX(""); + CX(""); + CX(""); + CX(""); + CX(""); + CX(""); + CX(""); + CX(""); + CX(""); + CX(""); + CX(""); + CX(""); + CX(""); + CX(""); + CX("Legacy mode"); + CX(""); + CX("
"); + } CX("
"); + CX("
"); { + CX("
"); { + CX("
"); + CX(""); + CX(""); + CX("
"); + CX("
",zContent/*safe-for-%s*/); + } CX("
"); + CX("
"); { + CX("
"); + CX(" "); + CX("" + "" + "" + ""); + CX("
"); + CX("
"); + CX("
"); + CX(""); + CX("
"); + } CX("
"); + } CX("
"); + } CX("
"); + builtin_fossil_js_bundle_or("dom", "storage", "copybutton", NULL); + builtin_request_js("fossil.page.pikchrshowasm.js"); + builtin_fulfill_js_requests(); + style_finish_page(); +} + + +/* +** COMMAND: pikchr* +** +** Usage: %fossil pikchr [options] ?INFILE? ?OUTFILE? +** +** Accepts a pikchr script as input and outputs the rendered script as +** an SVG graphic. The INFILE and OUTFILE options default to stdin +** resp. stdout, and the names "-" can be used as aliases for those +** streams. +** +** Options: +** -div On success, add a DIV wrapper around the +** resulting SVG output which limits its max-width to +** its computed maximum ideal size +** +** -div-indent Like -div but indent the div +** +** -div-center Like -div but center the div +** +** -div-left Like -div but float the div left +** +** -div-right Like -div but float the div right +** +** -div-toggle Set the 'toggle' CSS class on the div (used by the +** JavaScript-side post-processor) +** +** -div-source Set the 'source' CSS class on the div, which tells +** CSS to hide the SVG and reveal the source by default. +** +** -src Store the input pikchr's source code in the output as +** a separate element adjacent to the SVG one. Implied +** by -div-source. +** +** -dark Change pikchr colors to assume a dark-mode theme. +** +** +** The -div-indent/center/left/right flags may not be combined. +*/ +void pikchr_cmd(void){ + Blob bIn = empty_blob; + Blob bOut = empty_blob; + const char * zInfile = "-"; + const char * zOutfile = "-"; + int isErr = 0; + int pikFlags = find_option("src",0,0)!=0 + ? PIKCHR_PROCESS_SRC : 0; + + if(find_option("div",0,0)!=0){ + pikFlags |= PIKCHR_PROCESS_DIV; + }else if(find_option("div-indent",0,0)!=0){ + pikFlags |= PIKCHR_PROCESS_DIV_INDENT; + }else if(find_option("div-center",0,0)!=0){ + pikFlags |= PIKCHR_PROCESS_DIV_CENTER; + }else if(find_option("div-float-left",0,0)!=0){ + pikFlags |= PIKCHR_PROCESS_DIV_FLOAT_LEFT; + }else if(find_option("div-float-right",0,0)!=0){ + pikFlags |= PIKCHR_PROCESS_DIV_FLOAT_RIGHT; + } + if(find_option("div-toggle",0,0)!=0){ + pikFlags |= PIKCHR_PROCESS_DIV_TOGGLE; + } + if(find_option("div-source",0,0)!=0){ + pikFlags |= PIKCHR_PROCESS_DIV_SOURCE | PIKCHR_PROCESS_SRC; + } + if(find_option("dark",0,0)!=0){ + pikFlags |= PIKCHR_PROCESS_DARK_MODE; + } + + verify_all_options(); + if(g.argc>4){ + usage("?INFILE? ?OUTFILE?"); + } + if(g.argc>2){ + zInfile = g.argv[2]; + } + if(g.argc>3){ + zOutfile = g.argv[3]; + } + blob_read_from_file(&bIn, zInfile, ExtFILE); + isErr = pikchr_process(blob_str(&bIn), pikFlags, &bOut); + if(isErr){ + fossil_fatal("pikchr ERROR: %b", &bOut); + }else{ + blob_write_to_file(&bOut, zOutfile); + } + blob_reset(&bIn); + blob_reset(&bOut); +} -- cgit v1.3.1