/* ** 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("
Switch to legacy mode
"); 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); }