/* ** 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_TH1 0x0004 #define PIKCHR_PROCESS_TH1_NOSVG 0x0008 #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, optionally with embedded TH1, and ** produces HTML code for it. zIn is the NUL-terminated input ** script. pikFlags may be a bitmask of any of the PIKCHR_PROCESS_xxx ** flags documented below. thFlags may be a bitmask of any of the ** TH_INIT_xxx and/or TH_R2B_xxx flags. Output is sent to pOut, ** appending to it without modifying any prior contents. ** ** Returns 0 on success, 1 if TH1 processing failed, or 2 if pikchr ** processing failed. In either case, the error message (if any) from ** TH1 or pikchr will be appended to pOut. ** ** pikFlags flag descriptions: ** ** - PIKCHR_PROCESS_TH1 means to run zIn through TH1, using the TH1 ** init flags specified in the 3rd argument. If thFlags is non-0 then ** this flag is assumed even if it is not specified. ** ** - PIKCHR_PROCESS_TH1_NOSVG means that processing stops after the ** TH1 eval step, thus the output will be (presumably) a ** TH1-generated/processed pikchr script (or whatever else the TH1 ** outputs). If this flag is set, PIKCHR_PROCESS_TH1 is assumed even ** if it is not specified. ** ** All of the remaining flags listed below are ignored if ** PIKCHR_PROCESS_TH1_NOSVG is specified! ** ** - 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, int thFlags, Blob * pOut){ Blob bIn = empty_blob; int isErr = 0; 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(!(PIKCHR_PROCESS_TH1 & pikFlags) /* If any TH1_xxx flags are set, set TH1 */ && (PIKCHR_PROCESS_TH1_NOSVG & pikFlags || thFlags!=0)){ pikFlags |= PIKCHR_PROCESS_TH1; } if(zNonce){ blob_appendf(pOut, "%s\n", zNonce); } if(PIKCHR_PROCESS_TH1 & pikFlags){ Blob out = empty_blob; isErr = Th_RenderToBlob(zIn, &out, thFlags) ? 1 : 0; if(isErr){ blob_append(pOut, blob_str(&out), blob_size(&out)); blob_reset(&out); }else{ bIn = out; } }else{ blob_init(&bIn, zIn, -1); } if(!isErr){ if(PIKCHR_PROCESS_TH1_NOSVG & pikFlags){ blob_append(pOut, blob_str(&bIn), blob_size(&bIn)); }else{ int w = 0, h = 0; const char * zContent = blob_str(&bIn); char *zOut; zOut = pikchr(zContent, "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){ blob_appendf(pOut, "
%h
\n", blob_str(&bIn)); } 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); } blob_reset(&bIn); 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, 0, &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, 0, &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. ** ** ** -th Process the input using TH1 before passing it to pikchr ** ** -th-novar Disable $var and $ TH1 processing. Use this if the ** pikchr script uses '$' for its own purposes and that ** causes issues. This only affects parsing of '$' outside ** of TH1 script blocks. Code in such blocks is unaffected. ** ** -th-nosvg When using -th, output the post-TH1'd script ** instead of the pikchr-rendered output ** ** -th-trace Trace TH1 execution (for debugging purposes) ** ** -dark Change pikchr colors to assume a dark-mode theme. ** ** ** The -div-indent/center/left/right flags may not be combined. ** ** TH1-related Notes and Caveats: ** ** If the -th flag is used, this command must open a fossil database ** for certain functionality to work (via a check-out or the -R REPO ** flag). If opening a db fails, execution will continue but any TH1 ** commands which require a db will trigger a fatal error. ** ** In Fossil skins, TH1 variables in the form $varName are expanded ** as-is and those in the form $ are htmlized in the ** resulting output. This processor disables the htmlizing step, so $x ** and $ are equivalent unless the TH1-processed pikchr script ** invokes the TH1 command [enable_htmlify 1] to enable it. Normally ** that option will interfere with pikchr output, however, e.g. by ** HTML-encoding double-quotes. ** ** Many of the fossil-installed TH1 functions simply do not make any ** sense for pikchr scripts. */ void pikchr_cmd(void){ Blob bIn = empty_blob; Blob bOut = empty_blob; const char * zInfile = "-"; const char * zOutfile = "-"; const int fTh1 = find_option("th",0,0)!=0; const int fNosvg = find_option("th-nosvg",0,0)!=0; int isErr = 0; int pikFlags = find_option("src",0,0)!=0 ? PIKCHR_PROCESS_SRC : 0; u32 fThFlags = TH_INIT_NO_ENCODE | (find_option("th-novar",0,0)!=0 ? TH_R2B_NO_VARS : 0); Th_InitTraceLog()/*processes -th-trace flag*/; 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); if(fTh1){ db_find_and_open_repository(OPEN_ANY_SCHEMA | OPEN_OK_NOT_FOUND, 0) /* ^^^ needed for certain TH1 functions to work */; pikFlags |= PIKCHR_PROCESS_TH1; if(fNosvg) pikFlags |= PIKCHR_PROCESS_TH1_NOSVG; } isErr = pikchr_process(blob_str(&bIn), pikFlags, fTh1 ? fThFlags : 0, &bOut); if(isErr){ fossil_fatal("%s ERROR:%c%b", 1==isErr ? "TH1" : "pikchr", 1==isErr ? ' ' : '\n', &bOut); }else{ blob_write_to_file(&bOut, zOutfile); } Th_PrintTraceLog(); blob_reset(&bIn); blob_reset(&bOut); }