Index: src/default.css ================================================================== --- src/default.css +++ src/default.css @@ -1388,7 +1388,9 @@ padding: 1em; font-size: 150%; } .pikchr-src { /* source code view for a pikchr (see fossil.pikchr.js) */ box-sizing: border-box/*reduces UI shift*/; + border-width: 1px; + border-style: dotted; overflow: auto; } Index: src/fossil.dom.js ================================================================== --- src/fossil.dom.js +++ src/fossil.dom.js @@ -713,12 +713,12 @@ /** Parses a string as HTML. Usages: - Array (htmlString) - DOMElement (DOMElement target, htmlString) + (htmlString) + (DOMElement target, htmlString) The first form parses the string as HTML and returns an Array of all elements parsed from it. If string is falsy then it returns an empty array. Index: src/fossil.page.pikchrshow.js ================================================================== --- src/fossil.page.pikchrshow.js +++ src/fossil.page.pikchrshow.js @@ -322,14 +322,13 @@ switch(this.previewMode){ case 0: label = "SVG"; f.showMarkupAlignment(false); D.parseHtml(D.clearElement(preTgt), P.response.raw); - svg = preTgt.querySelector('svg.pikchr'); + svg = f.getSvgNode(this.response.raw); if(svg){ /*for copy button*/ this.e.taPreviewText.value = svg.outerHTML; - F.pikchr.addSrcView(svg); } break; case 1: label = "Markdown"; f.showMarkupAlignment(true); Index: src/fossil.pikchr.js ================================================================== --- src/fossil.pikchr.js +++ src/fossil.pikchr.js @@ -1,87 +1,240 @@ (function(F/*window.fossil object*/){ "use strict"; - const D = F.dom, P = F.pikchr = {}; + + const D = F.dom; + + const P = F.pikchr = { + }; + + //////////////////////////////////////////////////////////////////////// + // Install an app-specific stylesheet, just for development, after which + // it will be moved into default.css + (function(){ + const head = document.head || document.querySelector('head'), + styleTag = document.createElement('style'), + wh = '1cm' /* fixed width/height of buttons */, + styleCSS = ` +.pikchr-button-bar { + position: absolute; + position: absolute; + top: 0; + left: 0; + display: inline-flex; + flex-direction: column; +} +.pikchr-src-button { + min-height: ${wh}; max-height: ${wh}; + min-width: ${wh}; max-width: ${wh}; + font-size: ${wh}; + border: 1px solid black; + background-color: rgba(255,255,0,0.7); + border-radius: 0.25cm; + z-index: 50; + cursor: pointer; + text-align: center; + display: inline-flex; + align-items: center; + justify-content: center; + transform-origin: center; + transition: transform 250ms linear; + padding: 0; margin: 0; +/* MIT-licensed SVG from: https://github.com/leungwensen/svg-icon/blob/master/dist/svg/ant/code.svg */ + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg viewBox='0 0 1195 1195' \ +xmlns='http:/\x2fwww.w3.org/2000/svg'%3e%3cpath d='M321.333 440q-9-10-22.5-10t-22.5 \ +10l-182 181q-9 9-9 22.5t9 22.5l182 182q9 10 22.5 10t22.5-10q10-9 10-22.5t-10-22.5l-159-159 \ +159-159q10-10 10-23t-10-22zm552 0q9-10 22.5-10t22.5 10l182 181q9 9 9 22.5t-9 \ +22.5l-182 182q-9 10-22.5 10t-22.5-10q-10-9-10-22.5t10-22.5l159-159-159-159q-10-10-10-23t10-22zm-97-180q12 \ +6 16 19t-2 24l-371 704q-7 12-19.5 16t-24.5-2q-11-7-15-19.5t2-24.5l371-703q6-12 \ +18.5-16t24.5 2z'/%3e%3c/svg%3e"); + background-size: contain; +} +.pikchr-src-button.src-active { + transform: scaleX(-1); +/* MIT-licensed SVG from: https://github.com/leungwensen/svg-icon/blob/master/dist/svg/ant/picture.svg */ + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg viewBox='0 0 1195 1195' \ +xmlns='http:/\x2fwww.w3.org/2000/svg'%3e%3cpath d='M1045.333 192h-896q-26 0-45 \ +19t-19 45v768q0 26 19 45t45 19h896q26 0 45-19t19-45V256q0-26-19-45t-45-19zm-896 \ +64h896v714l-236-348q-7-12-21-14t-25 7l-154 125-174-243q-10-14-28-13t-26 17l-232 \ +448V256zm855 768h-822l231-447 184 255 179-145zm-182-642q13 0 22.5 9.5t9.5 22.5-9.5 \ +22.5-22.5 9.5-22.5-9.5-9.5-22.5 9.5-22.5 22.5-9.5zm0-64q-40 0-68 28t-28 68 28 68 68 \ +28 68-28 28-68-28-68-68-28z'/%3e%3c/svg%3e"); +} +textarea.pikchr-src-text { + box-sizing: border-box/*reduces UI shift*/; +} +.pikchr-copy-button { + min-width: ${wh}; max-width: ${wh}; + min-height: ${wh}; max-height: ${wh}; + display: inline-block; +} +.pikchr-button-bar > .pikchr-src-button, +.pikchr-button-bar > .pikchr-copy-button { + margin-bottom: 0.3em; +} +`; + head.appendChild(styleTag); + /* Adapted from https://stackoverflow.com/a/524721 */ + styleTag.type = 'text/css'; + D.append(styleTag, styleCSS); + })(); /** - Initializes pikchr-rendered elements with the ability to - toggle between their SVG and source code. + Sets up a "view source" button on one or more pikchr-created SVG + image elements. The first argument may be any of: - - A single SVG.pikchr element. + - A single SVG element. - A collection (with a forEach method) of such elements. - A CSS selector string for one or more such elements. - An array of such strings. Passing no value is equivalent to passing 'svg.pikchr'. - For each SVG in the resulting set, this function sets up event - handlers which allow the user to toggle the SVG between image and - source code modes. The image will switch modes in response to - cltr-click and, if its *parent* element has the "toggle" CSS - class, it will also switch modes in response to single-click. - - If the parent element has the "source" CSS class, the image - starts off with its source code visible and the image hidden, - instead of the default of the other way around. + For each SVG in the resulting set, this function does the + following: + + - It sets the "position" value of the element's *parent* node to + "relative", as that is necessary for what follows. + + - It creates a small pseudo-button, adding it to the SVG + element's parent node, styled to hover in one of the element's + corners. + + - That button, when tapped, toggles the SVG on and off + while revealing or hiding a readonly textarea element + which contains the source code for that pikchr SVG + (which pikchr has helpfully embedded in the SVG's + metadata). Returns this object. + + The 2nd argument is intended to be a plain options object, but it + is currently unused, as it's not yet clear what we can/should + make configurable. Each element will only be processed once by this routine, even if it is passed to this function multiple times. Each processed element gets a "data" attribute set to it to indicate that it was already dealt with. - - This code expects the following structure around the SVGs, and - will not process any which don't match this: - -
*/ - P.addSrcView = function f(svg){ - if(!f.hasOwnProperty('parentClick')){ - f.parentClick = function(ev){ - if(ev.ctrlKey || this.classList.contains('toggle')){ - this._childs.forEach((e)=>e.classList.toggle('hidden')); - } - /* For the sake of small pics, we have to eliminate the - parent element's max-width... */ - const src = this._childs[1]; - if(src.classList.contains('hidden')){ - this.style.maxWidth = this.dataset.origMaxWidth; - }else{ - this.style.maxWidth = "unset"; - } - }; - }; + P.addSrcView = function f(svg,opt){ + if(!f.hasOwnProperty('bodyClick')){ + f.bodyClick = function(){ + D.addClass(document.querySelectorAll('.pikchr-button-bar'), 'hidden'); + }; + document.body.addEventListener('click', f.bodyClick, false); + } if(!svg) svg = 'svg.pikchr'; if('string' === typeof svg){ - document.querySelectorAll(svg).forEach((e)=>f.call(this, e)); + document.querySelectorAll(svg).forEach( + (e)=>f.call(this, e, opt) + ); return this; }else if(svg.forEach){ - svg.forEach((e)=>f.call(this, e)); + svg.forEach((e)=>f.call(this, e, opt)); return this; } if(svg.dataset.pikchrProcessed){ return this; } svg.dataset.pikchrProcessed = 1; const parent = svg.parentNode; - const srcView = svg.nextElementSibling; - if(!srcView || !srcView.classList.contains('pikchr-src')){ - /* Without this element, there's nothing for us to do here. */ + parent.style.position = 'relative' /* REQUIRED for btn placement */; + const srcView = parent.querySelector('.pikchr-src'); + if(!srcView){ + console.warn("No pikchr source node found in",parent); return this; } - parent.dataset.origMaxWidth = parent.style.maxWidth; - parent._childs = [svg, srcView]; - D.addClass(srcView, 'hidden'); - D.removeClass(svg, 'hidden'); - parent.addEventListener('click', f.parentClick, false); - if(parent.classList.contains('source')){ - /* Start off in source-view mode via a very fake click event */ - f.parentClick.call(parent, {ctrlKey:true}); - } + const buttonBar = D.addClass(D.span(), 'pikchr-button-bar'); + const btnFlip = D.append( + D.addClass(D.span(), 'pikchr-src-button'), + ); + const btnCopy = F.copyButton(D.span(), { + cssClass: ['copy-button', 'pikchr-copy-button'], + extractText: ()=>(srcView.classList.contains('hidden') ? svg.outerHTML : srcView.value), + oncopy: ()=>D.flashOnce(btnCopy, ()=>btnFlip.click()) + // ^^^ after copying and flashing, flip back to SVG mode. */ + }); + D.append(buttonBar, btnFlip, btnCopy); + // not yet sure which options we can/should support: + // opt = F.mergeLastWins({},opt); + D.addClass(srcView, 'hidden')/*should already be so, but just in case*/; + D.append(parent, D.addClass(buttonBar, 'hidden')); + + parent.addEventListener('click', function f(ev){ + ev.preventDefault(); + ev.stopPropagation(); + D.toggleClass(buttonBar, 'hidden'); + }, false); + + /** Show the mode-switch buttons only in source view, and switch to + source view if the SVG is tapped. This allows easy switching to + source view while also keeping the buttons out of the way in + SVG mode and giving the user the option of select/copy in the + source mode via normal text-selection approaches. */ + svg.addEventListener('click', function(ev){ + D.removeClass(buttonBar, 'hidden'); + btnFlip.click(); + }, false); + + /** Toggle the source/SVG view on click. */ + btnFlip.addEventListener('click', function f(ev){ + ev.preventDefault(); + ev.stopPropagation(); + if(!f.hasOwnProperty('origMaxWidth')){ + f.origMaxWidth = parent.style.maxWidth; + } + const svgStyle = window.getComputedStyle(svg); + srcView.style.minWidth = svgStyle.width; + srcView.style.minHeight = svgStyle.height; + /* ^^^ The SVG wrapper/parent element has a max-width, so the + textarea will be too small on tiny images and won't be + enlargable. */ + if(0){ + /* We seem to have a fundamental incompatibility with how we + really want to position srcView at the same pos/size as the + svg and how that interacts with centered items. + Until/unless this can be solved, we have to decide between + the lesser of two evils: + + 1) This option. Small images have uselessly tiny source + view which cannot be enlarged because the parent element + has a width and/or max-width. width/max-width are important + for center alignment via the margin:auto trick. + + 2) Center-aligned images shift all the way to the left when + the source view is visible, then back to the center when + source view is hidden. Source views are resizable and may + even grow a bit automatically for tiny images. + */ + if(srcView.classList.contains('hidden')){/*initial state*/ + parent.style.width = f.origMaxWidth; + parent.style.maxWidth = 'unset'; + }else{/*srcView is active*/ + parent.style.maxWidth = f.origMaxWidth; + parent.style.width = 'unset'; + } + }else if(1){ + /* Option #2: gives us good results for non-centered items but + not for centered. We apparently have no(?) reliable way of + distinguishing centered from left/indented pikchrs here + unless we add a CSS class to mark them as such in the + pikchr-to-wiki-image code. */ + if(srcView.classList.contains('hidden')){/*initial state*/ + parent.style.width = 'unset'; + parent.style.maxWidth = 'unset'; + }else{/*srcView is active*/ + parent.style.maxWidth = f.origMaxWidth; + parent.style.width = 'unset'; + } + } + btnFlip.classList.toggle('src-active'); + D.toggleClass([svg, srcView, buttonBar], 'hidden'); + }, false); }; + })(window.fossil); Index: src/markdown_html.c ================================================================== --- src/markdown_html.c +++ src/markdown_html.c @@ -342,40 +342,64 @@ void pikchr_to_html( Blob *ob, /* Write the generated SVG here */ const char *zSrc, int nSrc, /* The Pikchr source text */ const char *zArg, int nArg /* Addition arguments */ ){ - int pikFlags = PIKCHR_PROCESS_NONCE - | PIKCHR_PROCESS_DIV - | PIKCHR_PROCESS_SRC_HIDDEN; - Blob bSrc = empty_blob; - - while( nArg>0 ){ - int i; - for(i=0; i0 && h>0 ){ + static int nSvg = 0; + const char *zSafeNonce = safe_html_nonce(1); + const char *zCss = ""; + const char *zClickOk = "e.ctrlKey"; + blob_append(ob, zSafeNonce, -1); + blob_append_char(ob, '\n'); + while( nArg>0 ){ + int i; + for(i=0; i\n", ++nSvg); + blob_appendf(ob, "
\n", w, zCss); + blob_append(ob, zOut, -1); + blob_appendf(ob, "
\n"); + blob_appendf(ob, "\n", zIn); + blob_appendf(ob, "\n"); + blob_appendf(ob, + "\n", + style_nonce(), nSvg, zClickOk); + blob_appendf(ob, "%s\n", zSafeNonce); + }else{ + blob_appendf(ob, "
\n%s\n
\n", zOut); + } + fossil_free(zIn); + free(zOut); +} + + /* Invoked for `...` blocks where there are nSep grave accents in a ** row that serve as the delimiter. According to CommonMark: ** ** * https://spec.commonmark.org/0.29/#fenced-code-blocks Index: src/pikchrshow.c ================================================================== --- src/pikchrshow.c +++ src/pikchrshow.c @@ -33,21 +33,18 @@ #define PIKCHR_PROCESS_DIV 0x40 #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 #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. +** Processes a pikchr script, optionally with embedded TH1. zIn is the +** input script. pikFlags may be a bitmask of any of the +** PIKCHR_PROCESS_xxx flags (see 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. ** @@ -56,17 +53,17 @@ ** - 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 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! +** 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: ** @@ -74,58 +71,40 @@ ** - 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). +** If more than one is specified, which one is used is undefined. ** ** - 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. +** output. ** -** - 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. +** - PIKCHR_PROCESS_SRC: if set, a new TEXTAREA.pikchr-src element is injected +** adjacet to the SVG element which contains the HTML-escaped content of +** the input script. ** ** - PIKCHR_PROCESS_SRC_HIDDEN: exactly like PIKCHR_PROCESS_SRC but ** the .pikchr-src tag also gets the CSS class 'hidden' (which, in -** fossil's default CSS, will hide that element). This is almost -** always what client code will want to do if it includes the source -** at all. +** fossil's default CSS, will hide that element). ** ** - 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). +** as-is (intended for console output). */ int pikchr_process(const char * zIn, int pikFlags, int thFlags, - Blob * pOut){ + Blob * pOut){ Blob bIn = empty_blob; int isErr = 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_TOGGLE ) & pikFlags){ pikFlags |= PIKCHR_PROCESS_DIV; } if(!(PIKCHR_PROCESS_TH1 & pikFlags) /* If any TH1_xxx flags are set, set TH1 */ @@ -153,12 +132,10 @@ const char * zContent = blob_str(&bIn); char *zOut; zOut = pikchr(zContent, "pikchr", 0, &w, &h); if( w>0 && h>0 ){ - const char * zClassToggle = ""; - const char * zClassSource = ""; const char *zNonce = (PIKCHR_PROCESS_NONCE & pikFlags) ? safe_html_nonce(1) : 0; if(zNonce){ blob_append(pOut, zNonce, -1); } @@ -172,27 +149,19 @@ }else if(PIKCHR_PROCESS_DIV_FLOAT_LEFT & pikFlags){ blob_append(&css, "float:left;padding=4em;", -1); }else if(PIKCHR_PROCESS_DIV_FLOAT_RIGHT & pikFlags){ blob_append(&css, "float:right;padding=4em;", -1); } - if(PIKCHR_PROCESS_DIV_TOGGLE & pikFlags){ - zClassToggle = " toggle"; - } - if(PIKCHR_PROCESS_DIV_SOURCE & pikFlags){ - zClassSource = " source"; - } - blob_appendf(pOut,"
\n", - zClassToggle/*safe-for-%s*/, - zClassSource/*safe-for-%s*/, &css); + blob_appendf(pOut,"
\n", &css); blob_reset(&css); } blob_append(pOut, zOut, -1); if((PIKCHR_PROCESS_SRC & pikFlags) || (PIKCHR_PROCESS_SRC_HIDDEN & pikFlags)){ - blob_appendf(pOut, "
"
-                       "%h
\n", + blob_appendf(pOut, "\n", (PIKCHR_PROCESS_SRC_HIDDEN & pikFlags) ? " hidden" : "", blob_str(&bIn)); } if(PIKCHR_PROCESS_DIV & pikFlags){ @@ -388,11 +357,11 @@ ** ** -div-left Like -div but floats the div left. ** ** -div-right Like -div but floats the div right. ** -** -src Stores the input pikchr's source code in the output as +** -svg-src Stores the input pikchr's source code in the output as ** a separate element adjacent to the SVG one. The ** source element initially has the "hidden" CSS class. ** ** -th Process the input using TH1 before passing it to pikchr. ** @@ -430,11 +399,11 @@ 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 + int pikFlags = find_option("svg-src",0,0)!=0 ? PIKCHR_PROCESS_SRC_HIDDEN : 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*/; @@ -448,16 +417,10 @@ }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; - } verify_all_options(); if(g.argc>4){ usage("?INFILE? ?OUTFILE?"); }