Fossil SCM
pikchrshow now supports stashing a single pikchr to/from browser-local storage to enable switching back and forth between example scripts while editing one's own script. Experimentally re-indented the C-side implementation to make its generated HTML hierarchy clearer. Over-the-wire size is now 4.8kb.
Commit
49a04c97b5a78055f2da0f7920ba4c9a2a444bb22336a1296e91ebe9c9f7b590
Parent
56a744638ac7dae…
2 files changed
+46
-10
+95
-87
+46
-10
| --- src/fossil.page.pikchrshow.js | ||
| +++ src/fossil.page.pikchrshow.js | ||
| @@ -2,11 +2,11 @@ | ||
| 2 | 2 | "use strict"; |
| 3 | 3 | /** |
| 4 | 4 | Client-side implementation of the /pikchrshow app. Requires that |
| 5 | 5 | the fossil JS bootstrapping is complete and that these fossil JS |
| 6 | 6 | APIs have been installed: fossil.fetch, fossil.dom, |
| 7 | - fossil.copybutton, fossil.popupwidget | |
| 7 | + fossil.copybutton, fossil.popupwidget, fossil.storage | |
| 8 | 8 | */ |
| 9 | 9 | const E = (s)=>document.querySelector(s), |
| 10 | 10 | D = F.dom, |
| 11 | 11 | P = F.page; |
| 12 | 12 | |
| @@ -26,10 +26,13 @@ | ||
| 26 | 26 | D.addClass(D.span(),'copy-button'), |
| 27 | 27 | 'id','preview-copy-button' |
| 28 | 28 | ), |
| 29 | 29 | previewModeLabel: D.label('preview-copy-button'), |
| 30 | 30 | btnSubmit: E('#pikchr-submit-preview'), |
| 31 | + btnStash: E('#pikchr-stash'), | |
| 32 | + btnUnstash: E('#pikchr-unstash'), | |
| 33 | + btnClearStash: E('#pikchr-clear-stash'), | |
| 31 | 34 | cbDarkMode: E('#flipcolors-wrapper > input[type=checkbox]'), |
| 32 | 35 | taContent: E('#content'), |
| 33 | 36 | taPreviewText: D.textarea(20,0,true), |
| 34 | 37 | uiControls: E('#pikchrshow-controls'), |
| 35 | 38 | previewModeToggle: D.button("Preview mode"), |
| @@ -228,20 +231,55 @@ | ||
| 228 | 231 | ].forEach(function(e){ |
| 229 | 232 | Object.keys(dropEvents).forEach( |
| 230 | 233 | (k)=>e.addEventListener(k, dropEvents[k], true) |
| 231 | 234 | ); |
| 232 | 235 | }); |
| 236 | + | |
| 237 | + //////////////////////////////////////////////////////////// | |
| 238 | + // Setup stash/unstash | |
| 239 | + const stashKey = 'pikchrshow-stash'; | |
| 240 | + P.e.btnStash.addEventListener('click', function(){ | |
| 241 | + const val = P.e.taContent.value; | |
| 242 | + if(val){ | |
| 243 | + F.storage.set(stashKey, val); | |
| 244 | + D.enable(P.e.btnUnstash); | |
| 245 | + F.toast.message("Stashed pikchr."); | |
| 246 | + } | |
| 247 | + }, false); | |
| 248 | + P.e.btnUnstash.addEventListener('click', function(){ | |
| 249 | + const val = F.storage.get(stashKey); | |
| 250 | + P.e.taContent.value = val || ''; | |
| 251 | + }, false); | |
| 252 | + P.e.btnClearStash.addEventListener('click', function(){ | |
| 253 | + F.storage.remove(stashKey); | |
| 254 | + D.disable(P.e.btnUnstash); | |
| 255 | + F.toast.message("Cleared pikchr stash."); | |
| 256 | + }, false); | |
| 257 | + F.helpButtonlets.create(P.e.btnClearStash.nextElementSibling); | |
| 258 | + // If we have stashed contents, enable Unstash, else disable it: | |
| 259 | + if(F.storage.contains(stashKey)) D.enable(P.e.btnUnstash); | |
| 260 | + else D.disable(P.e.btnUnstash); | |
| 233 | 261 | |
| 234 | 262 | //////////////////////////////////////////////////////////// |
| 235 | 263 | // If we start with content, get it in sync with the state |
| 236 | - // generated by P.preview(). | |
| 237 | - if(P.e.taContent.value/*was pre-filled server-side*/){ | |
| 264 | + // generated by P.preview(). Normally the server pre-populates it | |
| 265 | + // with an example. | |
| 266 | + let needsPreview; | |
| 267 | + if(!P.e.taContent.value){ | |
| 268 | + P.e.taContent.value = F.storage.get(stashKey,''); | |
| 269 | + needsPreview = true; | |
| 270 | + } | |
| 271 | + if(P.e.taContent.value){ | |
| 238 | 272 | /* Fill our "response" state so that renderPreview() can work */ |
| 239 | 273 | P.response.inputText = P.e.taContent.value; |
| 240 | 274 | P.response.raw = P.e.previewTarget.innerHTML; |
| 241 | - P.renderPreview()/*it's already rendered, but this gets all | |
| 242 | - labels/headers in sync.*/; | |
| 275 | + if(needsPreview) P.preview(); | |
| 276 | + else{ | |
| 277 | + /*If it's from the server, it's already rendered, but this | |
| 278 | + gets all labels/headers in sync.*/ | |
| 279 | + P.renderPreview(); | |
| 280 | + } | |
| 243 | 281 | } |
| 244 | 282 | }/*F.onPageLoad()*/); |
| 245 | 283 | |
| 246 | 284 | /** |
| 247 | 285 | Updates the preview view based on the current preview mode and |
| @@ -324,11 +362,12 @@ | ||
| 324 | 362 | P.preview = function fp(){ |
| 325 | 363 | if(!fp.hasOwnProperty('toDisable')){ |
| 326 | 364 | fp.toDisable = [ |
| 327 | 365 | /* input elements to disable during ajax operations */ |
| 328 | 366 | this.e.btnSubmit, this.e.taContent, |
| 329 | - this.e.cbAutoPreview, this.e.selectScript | |
| 367 | + this.e.cbAutoPreview, this.e.selectScript, | |
| 368 | + this.e.btnStash, this.e.btnClearStash | |
| 330 | 369 | /* handled separately: previewModeToggle, previewCopyButton, |
| 331 | 370 | markupAlignRadios */ |
| 332 | 371 | ]; |
| 333 | 372 | fp.target = this.e.previewTarget; |
| 334 | 373 | fp.updateView = function(c,isError){ |
| @@ -357,22 +396,19 @@ | ||
| 357 | 396 | } |
| 358 | 397 | const self = this; |
| 359 | 398 | const fd = new FormData(); |
| 360 | 399 | fd.append('ajax', true); |
| 361 | 400 | fd.append('content',content); |
| 362 | - F.message( | |
| 363 | - "Fetching preview..." | |
| 364 | - ).fetch('pikchrshow',{ | |
| 401 | + F.fetch('pikchrshow',{ | |
| 365 | 402 | payload: fd, |
| 366 | 403 | responseHeaders: 'x-pikchrshow-is-error', |
| 367 | 404 | onload: (r,isErrHeader)=>{ |
| 368 | 405 | const isErr = +isErrHeader ? true : false; |
| 369 | 406 | if(!isErr && sampleScript){ |
| 370 | 407 | sampleScript.cached = r; |
| 371 | 408 | } |
| 372 | 409 | fp.updateView(r,isErr); |
| 373 | - F.message('Updated preview.'); | |
| 374 | 410 | }, |
| 375 | 411 | onerror: (e)=>{ |
| 376 | 412 | F.fetch.onerror(e); |
| 377 | 413 | fp.updateView("Error fetching preview: "+e, true); |
| 378 | 414 | } |
| 379 | 415 |
| --- src/fossil.page.pikchrshow.js | |
| +++ src/fossil.page.pikchrshow.js | |
| @@ -2,11 +2,11 @@ | |
| 2 | "use strict"; |
| 3 | /** |
| 4 | Client-side implementation of the /pikchrshow app. Requires that |
| 5 | the fossil JS bootstrapping is complete and that these fossil JS |
| 6 | APIs have been installed: fossil.fetch, fossil.dom, |
| 7 | fossil.copybutton, fossil.popupwidget |
| 8 | */ |
| 9 | const E = (s)=>document.querySelector(s), |
| 10 | D = F.dom, |
| 11 | P = F.page; |
| 12 | |
| @@ -26,10 +26,13 @@ | |
| 26 | D.addClass(D.span(),'copy-button'), |
| 27 | 'id','preview-copy-button' |
| 28 | ), |
| 29 | previewModeLabel: D.label('preview-copy-button'), |
| 30 | btnSubmit: E('#pikchr-submit-preview'), |
| 31 | cbDarkMode: E('#flipcolors-wrapper > input[type=checkbox]'), |
| 32 | taContent: E('#content'), |
| 33 | taPreviewText: D.textarea(20,0,true), |
| 34 | uiControls: E('#pikchrshow-controls'), |
| 35 | previewModeToggle: D.button("Preview mode"), |
| @@ -228,20 +231,55 @@ | |
| 228 | ].forEach(function(e){ |
| 229 | Object.keys(dropEvents).forEach( |
| 230 | (k)=>e.addEventListener(k, dropEvents[k], true) |
| 231 | ); |
| 232 | }); |
| 233 | |
| 234 | //////////////////////////////////////////////////////////// |
| 235 | // If we start with content, get it in sync with the state |
| 236 | // generated by P.preview(). |
| 237 | if(P.e.taContent.value/*was pre-filled server-side*/){ |
| 238 | /* Fill our "response" state so that renderPreview() can work */ |
| 239 | P.response.inputText = P.e.taContent.value; |
| 240 | P.response.raw = P.e.previewTarget.innerHTML; |
| 241 | P.renderPreview()/*it's already rendered, but this gets all |
| 242 | labels/headers in sync.*/; |
| 243 | } |
| 244 | }/*F.onPageLoad()*/); |
| 245 | |
| 246 | /** |
| 247 | Updates the preview view based on the current preview mode and |
| @@ -324,11 +362,12 @@ | |
| 324 | P.preview = function fp(){ |
| 325 | if(!fp.hasOwnProperty('toDisable')){ |
| 326 | fp.toDisable = [ |
| 327 | /* input elements to disable during ajax operations */ |
| 328 | this.e.btnSubmit, this.e.taContent, |
| 329 | this.e.cbAutoPreview, this.e.selectScript |
| 330 | /* handled separately: previewModeToggle, previewCopyButton, |
| 331 | markupAlignRadios */ |
| 332 | ]; |
| 333 | fp.target = this.e.previewTarget; |
| 334 | fp.updateView = function(c,isError){ |
| @@ -357,22 +396,19 @@ | |
| 357 | } |
| 358 | const self = this; |
| 359 | const fd = new FormData(); |
| 360 | fd.append('ajax', true); |
| 361 | fd.append('content',content); |
| 362 | F.message( |
| 363 | "Fetching preview..." |
| 364 | ).fetch('pikchrshow',{ |
| 365 | payload: fd, |
| 366 | responseHeaders: 'x-pikchrshow-is-error', |
| 367 | onload: (r,isErrHeader)=>{ |
| 368 | const isErr = +isErrHeader ? true : false; |
| 369 | if(!isErr && sampleScript){ |
| 370 | sampleScript.cached = r; |
| 371 | } |
| 372 | fp.updateView(r,isErr); |
| 373 | F.message('Updated preview.'); |
| 374 | }, |
| 375 | onerror: (e)=>{ |
| 376 | F.fetch.onerror(e); |
| 377 | fp.updateView("Error fetching preview: "+e, true); |
| 378 | } |
| 379 |
| --- src/fossil.page.pikchrshow.js | |
| +++ src/fossil.page.pikchrshow.js | |
| @@ -2,11 +2,11 @@ | |
| 2 | "use strict"; |
| 3 | /** |
| 4 | Client-side implementation of the /pikchrshow app. Requires that |
| 5 | the fossil JS bootstrapping is complete and that these fossil JS |
| 6 | APIs have been installed: fossil.fetch, fossil.dom, |
| 7 | fossil.copybutton, fossil.popupwidget, fossil.storage |
| 8 | */ |
| 9 | const E = (s)=>document.querySelector(s), |
| 10 | D = F.dom, |
| 11 | P = F.page; |
| 12 | |
| @@ -26,10 +26,13 @@ | |
| 26 | D.addClass(D.span(),'copy-button'), |
| 27 | 'id','preview-copy-button' |
| 28 | ), |
| 29 | previewModeLabel: D.label('preview-copy-button'), |
| 30 | btnSubmit: E('#pikchr-submit-preview'), |
| 31 | btnStash: E('#pikchr-stash'), |
| 32 | btnUnstash: E('#pikchr-unstash'), |
| 33 | btnClearStash: E('#pikchr-clear-stash'), |
| 34 | cbDarkMode: E('#flipcolors-wrapper > input[type=checkbox]'), |
| 35 | taContent: E('#content'), |
| 36 | taPreviewText: D.textarea(20,0,true), |
| 37 | uiControls: E('#pikchrshow-controls'), |
| 38 | previewModeToggle: D.button("Preview mode"), |
| @@ -228,20 +231,55 @@ | |
| 231 | ].forEach(function(e){ |
| 232 | Object.keys(dropEvents).forEach( |
| 233 | (k)=>e.addEventListener(k, dropEvents[k], true) |
| 234 | ); |
| 235 | }); |
| 236 | |
| 237 | //////////////////////////////////////////////////////////// |
| 238 | // Setup stash/unstash |
| 239 | const stashKey = 'pikchrshow-stash'; |
| 240 | P.e.btnStash.addEventListener('click', function(){ |
| 241 | const val = P.e.taContent.value; |
| 242 | if(val){ |
| 243 | F.storage.set(stashKey, val); |
| 244 | D.enable(P.e.btnUnstash); |
| 245 | F.toast.message("Stashed pikchr."); |
| 246 | } |
| 247 | }, false); |
| 248 | P.e.btnUnstash.addEventListener('click', function(){ |
| 249 | const val = F.storage.get(stashKey); |
| 250 | P.e.taContent.value = val || ''; |
| 251 | }, false); |
| 252 | P.e.btnClearStash.addEventListener('click', function(){ |
| 253 | F.storage.remove(stashKey); |
| 254 | D.disable(P.e.btnUnstash); |
| 255 | F.toast.message("Cleared pikchr stash."); |
| 256 | }, false); |
| 257 | F.helpButtonlets.create(P.e.btnClearStash.nextElementSibling); |
| 258 | // If we have stashed contents, enable Unstash, else disable it: |
| 259 | if(F.storage.contains(stashKey)) D.enable(P.e.btnUnstash); |
| 260 | else D.disable(P.e.btnUnstash); |
| 261 | |
| 262 | //////////////////////////////////////////////////////////// |
| 263 | // If we start with content, get it in sync with the state |
| 264 | // generated by P.preview(). Normally the server pre-populates it |
| 265 | // with an example. |
| 266 | let needsPreview; |
| 267 | if(!P.e.taContent.value){ |
| 268 | P.e.taContent.value = F.storage.get(stashKey,''); |
| 269 | needsPreview = true; |
| 270 | } |
| 271 | if(P.e.taContent.value){ |
| 272 | /* Fill our "response" state so that renderPreview() can work */ |
| 273 | P.response.inputText = P.e.taContent.value; |
| 274 | P.response.raw = P.e.previewTarget.innerHTML; |
| 275 | if(needsPreview) P.preview(); |
| 276 | else{ |
| 277 | /*If it's from the server, it's already rendered, but this |
| 278 | gets all labels/headers in sync.*/ |
| 279 | P.renderPreview(); |
| 280 | } |
| 281 | } |
| 282 | }/*F.onPageLoad()*/); |
| 283 | |
| 284 | /** |
| 285 | Updates the preview view based on the current preview mode and |
| @@ -324,11 +362,12 @@ | |
| 362 | P.preview = function fp(){ |
| 363 | if(!fp.hasOwnProperty('toDisable')){ |
| 364 | fp.toDisable = [ |
| 365 | /* input elements to disable during ajax operations */ |
| 366 | this.e.btnSubmit, this.e.taContent, |
| 367 | this.e.cbAutoPreview, this.e.selectScript, |
| 368 | this.e.btnStash, this.e.btnClearStash |
| 369 | /* handled separately: previewModeToggle, previewCopyButton, |
| 370 | markupAlignRadios */ |
| 371 | ]; |
| 372 | fp.target = this.e.previewTarget; |
| 373 | fp.updateView = function(c,isError){ |
| @@ -357,22 +396,19 @@ | |
| 396 | } |
| 397 | const self = this; |
| 398 | const fd = new FormData(); |
| 399 | fd.append('ajax', true); |
| 400 | fd.append('content',content); |
| 401 | F.fetch('pikchrshow',{ |
| 402 | payload: fd, |
| 403 | responseHeaders: 'x-pikchrshow-is-error', |
| 404 | onload: (r,isErrHeader)=>{ |
| 405 | const isErr = +isErrHeader ? true : false; |
| 406 | if(!isErr && sampleScript){ |
| 407 | sampleScript.cached = r; |
| 408 | } |
| 409 | fp.updateView(r,isErr); |
| 410 | }, |
| 411 | onerror: (e)=>{ |
| 412 | F.fetch.onerror(e); |
| 413 | fp.updateView("Error fetching preview: "+e, true); |
| 414 | } |
| 415 |
+95
-87
| --- src/pikchrshow.c | ||
| +++ src/pikchrshow.c | ||
| @@ -70,100 +70,108 @@ | ||
| 70 | 70 | "arrow right 200% \"HTML+SVG\" \"Output\"\n" |
| 71 | 71 | "arrow <-> down from last box.s\n" |
| 72 | 72 | "box same \"Pikchr\" \"Formatter\" \"(pikchr.c)\" fit\n"; |
| 73 | 73 | } |
| 74 | 74 | style_header("PikchrShow"); |
| 75 | - CX("<style>"); | |
| 76 | - CX("div.content { padding-top: 0.5em }\n"); | |
| 77 | - CX("#sbs-wrapper {" | |
| 78 | - "display: flex; flex-direction: column;" | |
| 79 | - "}\n"); | |
| 80 | - CX("#sbs-wrapper > * {" | |
| 81 | - "margin: 0 0.25em 0.5em 0; flex: 1 10 auto;" | |
| 82 | - "align-self: stretch;" | |
| 83 | - "}\n"); | |
| 84 | - CX("#sbs-wrapper textarea {" | |
| 85 | - "max-width: initial; flex: 1 1 auto;" | |
| 86 | - "}\n"); | |
| 87 | - CX("#pikchrshow-output, #pikchrshow-form" | |
| 88 | - "{display: flex; flex-direction: column; align-items: stretch;}"); | |
| 89 | - CX("#pikchrshow-form > * {margin: 0.25em 0}\n"); | |
| 90 | - CX("#pikchrshow-output {flex: 5 1 auto; padding: 0}\n"); | |
| 91 | - CX("#pikchrshow-output > pre, " | |
| 92 | - "#pikchrshow-output > pre > div, " | |
| 93 | - "#pikchrshow-output > pre > div > pre " | |
| 94 | - "{margin: 0; padding: 0}\n"); | |
| 95 | - CX("#pikchrshow-output.error > pre " | |
| 96 | - /* Server-side error report */ | |
| 97 | - "{padding: 0.5em}\n"); | |
| 98 | - CX("#pikchrshow-controls {" /* where the buttons live */ | |
| 99 | - "display: flex; flex-direction: row; " | |
| 100 | - "align-items: center; flex-wrap: wrap;" | |
| 101 | - "}\n"); | |
| 102 | - CX("#pikchrshow-controls > * {" | |
| 103 | - "display: inline; margin: 0 0.25em 0.5em 0;" | |
| 104 | - "}\n"); | |
| 105 | - CX("#pikchrshow-output-wrapper label {" | |
| 106 | - "cursor: pointer;" | |
| 107 | - "}\n"); | |
| 108 | - CX("body.pikchrshow .input-with-label > * {" | |
| 109 | - "margin: 0 0.2em;" | |
| 110 | - "}\n"); | |
| 111 | - CX("body.pikchrshow .input-with-label > label {" | |
| 112 | - "cursor: pointer;" | |
| 113 | - "}\n"); | |
| 114 | - CX("#pikchrshow-output.dark-mode svg {" | |
| 115 | - /* Flip the colors to approximate a dark theme look */ | |
| 116 | - "filter: invert(1) hue-rotate(180deg);" | |
| 117 | - "}\n"); | |
| 118 | - CX("#pikchrshow-output-wrapper {" | |
| 119 | - "padding: 0.25em 0.5em; border-radius: 0.25em;" | |
| 120 | - "border-width: 1px;"/*some skins disable fieldset borders*/ | |
| 121 | - "}\n"); | |
| 122 | - CX("#pikchrshow-output-wrapper > legend > *:not(.copy-button){" | |
| 123 | - "margin-right: 0.5em; vertical-align: middle;" | |
| 124 | - "}\n"); | |
| 125 | - CX("body.pikchrshow .v-align-middle{" | |
| 126 | - "vertical-align: middle" | |
| 127 | - "}\n"); | |
| 128 | - CX(".dragover {border: 3px dotted rgba(0,255,0,0.6)}\n"); | |
| 129 | - CX("</style>"); | |
| 75 | + CX("<style>"); { | |
| 76 | + CX("div.content { padding-top: 0.5em }\n"); | |
| 77 | + CX("#sbs-wrapper {" | |
| 78 | + "display: flex; flex-direction: column;" | |
| 79 | + "}\n"); | |
| 80 | + CX("#sbs-wrapper > * {" | |
| 81 | + "margin: 0 0.25em 0.5em 0; flex: 1 10 auto;" | |
| 82 | + "align-self: stretch;" | |
| 83 | + "}\n"); | |
| 84 | + CX("#sbs-wrapper textarea {" | |
| 85 | + "max-width: initial; flex: 1 1 auto;" | |
| 86 | + "}\n"); | |
| 87 | + CX("#pikchrshow-output, #pikchrshow-form" | |
| 88 | + "{display: flex; flex-direction: column; align-items: stretch;}"); | |
| 89 | + CX("#pikchrshow-form > * {margin: 0.25em 0}\n"); | |
| 90 | + CX("#pikchrshow-output {flex: 5 1 auto; padding: 0}\n"); | |
| 91 | + CX("#pikchrshow-output > pre, " | |
| 92 | + "#pikchrshow-output > pre > div, " | |
| 93 | + "#pikchrshow-output > pre > div > pre " | |
| 94 | + "{margin: 0; padding: 0}\n"); | |
| 95 | + CX("#pikchrshow-output.error > pre " | |
| 96 | + /* Server-side error report */ | |
| 97 | + "{padding: 0.5em}\n"); | |
| 98 | + CX("#pikchrshow-controls {" /* where the buttons live */ | |
| 99 | + "display: flex; flex-direction: row; " | |
| 100 | + "align-items: center; flex-wrap: wrap;" | |
| 101 | + "}\n"); | |
| 102 | + CX("#pikchrshow-controls > * {" | |
| 103 | + "display: inline; margin: 0 0.25em 0.5em 0;" | |
| 104 | + "}\n"); | |
| 105 | + CX("#pikchrshow-output-wrapper label {" | |
| 106 | + "cursor: pointer;" | |
| 107 | + "}\n"); | |
| 108 | + CX("body.pikchrshow .input-with-label > * {" | |
| 109 | + "margin: 0 0.2em;" | |
| 110 | + "}\n"); | |
| 111 | + CX("body.pikchrshow .input-with-label > label {" | |
| 112 | + "cursor: pointer;" | |
| 113 | + "}\n"); | |
| 114 | + CX("#pikchrshow-output.dark-mode svg {" | |
| 115 | + /* Flip the colors to approximate a dark theme look */ | |
| 116 | + "filter: invert(1) hue-rotate(180deg);" | |
| 117 | + "}\n"); | |
| 118 | + CX("#pikchrshow-output-wrapper {" | |
| 119 | + "padding: 0.25em 0.5em; border-radius: 0.25em;" | |
| 120 | + "border-width: 1px;"/*some skins disable fieldset borders*/ | |
| 121 | + "}\n"); | |
| 122 | + CX("#pikchrshow-output-wrapper > legend > *:not(.copy-button){" | |
| 123 | + "margin-right: 0.5em; vertical-align: middle;" | |
| 124 | + "}\n"); | |
| 125 | + CX("body.pikchrshow .v-align-middle{" | |
| 126 | + "vertical-align: middle" | |
| 127 | + "}\n"); | |
| 128 | + CX(".dragover {border: 3px dotted rgba(0,255,0,0.6)}\n"); | |
| 129 | + } CX("</style>"); | |
| 130 | 130 | CX("<div>Input pikchr code and tap Preview to render it:</div>"); |
| 131 | - CX("<div id='sbs-wrapper'>"); | |
| 132 | - CX("<div id='pikchrshow-form'>"); | |
| 133 | - CX("<textarea id='content' name='content' rows='15'>%s</textarea>", | |
| 134 | - zContent/*safe-for-%s*/); | |
| 135 | - CX("<div id='pikchrshow-controls'>"); | |
| 136 | - CX("<button id='pikchr-submit-preview'>Preview</button>"); | |
| 137 | - style_labeled_checkbox("flipcolors-wrapper", "flipcolors", | |
| 138 | - "Dark mode?", | |
| 139 | - "1", isDark, 0); | |
| 140 | - CX("</div>"/*#pikchrshow-controls*/); | |
| 141 | - CX("</div>"/*#pikchrshow-form*/); | |
| 142 | - CX("<fieldset id='pikchrshow-output-wrapper'>"); | |
| 143 | - CX("<legend></legend>" | |
| 144 | - /* Reminder: Firefox does not properly flexbox a LEGEND element, | |
| 145 | - always flowing it in column mode. */); | |
| 146 | - CX("<div id='pikchrshow-output'>"); | |
| 147 | - if(*zContent){ | |
| 148 | - int w = 0, h = 0; | |
| 149 | - char *zOut = pikchr(zContent, "pikchr", 0, &w, &h); | |
| 150 | - if( w>0 && h>0 ){ | |
| 151 | - const char *zNonce = safe_html_nonce(1); | |
| 152 | - CX("%s<div style='max-width:%dpx;'>\n%s</div>%s", | |
| 153 | - zNonce, w, zOut, zNonce); | |
| 154 | - }else{ | |
| 155 | - CX("<pre>\n%s\n</pre>\n", zOut); | |
| 156 | - } | |
| 157 | - fossil_free(zOut); | |
| 158 | - } | |
| 159 | - CX("</div>"/*#pikchrshow-output*/); | |
| 160 | - CX("</fieldset>"/*#pikchrshow-output-wrapper*/); | |
| 161 | - CX("</div>"/*sbs-wrapper*/); | |
| 131 | + CX("<div id='sbs-wrapper'>"); { | |
| 132 | + CX("<div id='pikchrshow-form'>"); { | |
| 133 | + CX("<textarea id='content' name='content' rows='15'>" | |
| 134 | + "%s</textarea>",zContent/*safe-for-%s*/); | |
| 135 | + CX("<div id='pikchrshow-controls'>"); { | |
| 136 | + CX("<button id='pikchr-submit-preview'>Preview</button>"); | |
| 137 | + CX("<div class='input-with-label'>"); { | |
| 138 | + CX("<button id='pikchr-stash'>Stash</button>"); | |
| 139 | + CX("<button id='pikchr-unstash'>Unstash</button>"); | |
| 140 | + CX("<button id='pikchr-clear-stash'>Clear stash</button>"); | |
| 141 | + CX("<span>Stores/restores a single pikchr script to/from " | |
| 142 | + "browser-local storage from/to the editor.</span>" | |
| 143 | + /* gets turned into a help-buttonlet */); | |
| 144 | + } CX("</div>"/*stash controls*/); | |
| 145 | + style_labeled_checkbox("flipcolors-wrapper", "flipcolors", | |
| 146 | + "Dark mode?", | |
| 147 | + "1", isDark, 0); | |
| 148 | + } CX("</div>"/*#pikchrshow-controls*/); | |
| 149 | + } | |
| 150 | + CX("</div>"/*#pikchrshow-form*/); | |
| 151 | + CX("<fieldset id='pikchrshow-output-wrapper'>"); { | |
| 152 | + CX("<legend></legend>" | |
| 153 | + /* Reminder: Firefox does not properly flexbox a LEGEND | |
| 154 | + element, always flowing it in column mode. */); | |
| 155 | + CX("<div id='pikchrshow-output'>"); | |
| 156 | + if(*zContent){ | |
| 157 | + int w = 0, h = 0; | |
| 158 | + char *zOut = pikchr(zContent, "pikchr", 0, &w, &h); | |
| 159 | + if( w>0 && h>0 ){ | |
| 160 | + const char *zNonce = safe_html_nonce(1); | |
| 161 | + CX("%s<div style='max-width:%dpx;'>\n%s</div>%s", | |
| 162 | + zNonce, w, zOut, zNonce); | |
| 163 | + }else{ | |
| 164 | + CX("<pre>\n%s\n</pre>\n", zOut); | |
| 165 | + } | |
| 166 | + fossil_free(zOut); | |
| 167 | + } CX("</div>"/*#pikchrshow-output*/); | |
| 168 | + } CX("</fieldset>"/*#pikchrshow-output-wrapper*/); | |
| 169 | + } CX("</div>"/*sbs-wrapper*/); | |
| 162 | 170 | if(!builtin_bundle_all_fossil_js_apis()){ |
| 163 | 171 | builtin_emit_fossil_js_apis("dom", "fetch", "copybutton", |
| 164 | - "popupwidget", 0); | |
| 172 | + "popupwidget", "storage", 0); | |
| 165 | 173 | } |
| 166 | 174 | builtin_emit_fossil_js_apis("page.pikchrshow", 0); |
| 167 | 175 | builtin_fulfill_js_requests(); |
| 168 | 176 | style_footer(); |
| 169 | 177 | } |
| 170 | 178 |
| --- src/pikchrshow.c | |
| +++ src/pikchrshow.c | |
| @@ -70,100 +70,108 @@ | |
| 70 | "arrow right 200% \"HTML+SVG\" \"Output\"\n" |
| 71 | "arrow <-> down from last box.s\n" |
| 72 | "box same \"Pikchr\" \"Formatter\" \"(pikchr.c)\" fit\n"; |
| 73 | } |
| 74 | style_header("PikchrShow"); |
| 75 | CX("<style>"); |
| 76 | CX("div.content { padding-top: 0.5em }\n"); |
| 77 | CX("#sbs-wrapper {" |
| 78 | "display: flex; flex-direction: column;" |
| 79 | "}\n"); |
| 80 | CX("#sbs-wrapper > * {" |
| 81 | "margin: 0 0.25em 0.5em 0; flex: 1 10 auto;" |
| 82 | "align-self: stretch;" |
| 83 | "}\n"); |
| 84 | CX("#sbs-wrapper textarea {" |
| 85 | "max-width: initial; flex: 1 1 auto;" |
| 86 | "}\n"); |
| 87 | CX("#pikchrshow-output, #pikchrshow-form" |
| 88 | "{display: flex; flex-direction: column; align-items: stretch;}"); |
| 89 | CX("#pikchrshow-form > * {margin: 0.25em 0}\n"); |
| 90 | CX("#pikchrshow-output {flex: 5 1 auto; padding: 0}\n"); |
| 91 | CX("#pikchrshow-output > pre, " |
| 92 | "#pikchrshow-output > pre > div, " |
| 93 | "#pikchrshow-output > pre > div > pre " |
| 94 | "{margin: 0; padding: 0}\n"); |
| 95 | CX("#pikchrshow-output.error > pre " |
| 96 | /* Server-side error report */ |
| 97 | "{padding: 0.5em}\n"); |
| 98 | CX("#pikchrshow-controls {" /* where the buttons live */ |
| 99 | "display: flex; flex-direction: row; " |
| 100 | "align-items: center; flex-wrap: wrap;" |
| 101 | "}\n"); |
| 102 | CX("#pikchrshow-controls > * {" |
| 103 | "display: inline; margin: 0 0.25em 0.5em 0;" |
| 104 | "}\n"); |
| 105 | CX("#pikchrshow-output-wrapper label {" |
| 106 | "cursor: pointer;" |
| 107 | "}\n"); |
| 108 | CX("body.pikchrshow .input-with-label > * {" |
| 109 | "margin: 0 0.2em;" |
| 110 | "}\n"); |
| 111 | CX("body.pikchrshow .input-with-label > label {" |
| 112 | "cursor: pointer;" |
| 113 | "}\n"); |
| 114 | CX("#pikchrshow-output.dark-mode svg {" |
| 115 | /* Flip the colors to approximate a dark theme look */ |
| 116 | "filter: invert(1) hue-rotate(180deg);" |
| 117 | "}\n"); |
| 118 | CX("#pikchrshow-output-wrapper {" |
| 119 | "padding: 0.25em 0.5em; border-radius: 0.25em;" |
| 120 | "border-width: 1px;"/*some skins disable fieldset borders*/ |
| 121 | "}\n"); |
| 122 | CX("#pikchrshow-output-wrapper > legend > *:not(.copy-button){" |
| 123 | "margin-right: 0.5em; vertical-align: middle;" |
| 124 | "}\n"); |
| 125 | CX("body.pikchrshow .v-align-middle{" |
| 126 | "vertical-align: middle" |
| 127 | "}\n"); |
| 128 | CX(".dragover {border: 3px dotted rgba(0,255,0,0.6)}\n"); |
| 129 | CX("</style>"); |
| 130 | CX("<div>Input pikchr code and tap Preview to render it:</div>"); |
| 131 | CX("<div id='sbs-wrapper'>"); |
| 132 | CX("<div id='pikchrshow-form'>"); |
| 133 | CX("<textarea id='content' name='content' rows='15'>%s</textarea>", |
| 134 | zContent/*safe-for-%s*/); |
| 135 | CX("<div id='pikchrshow-controls'>"); |
| 136 | CX("<button id='pikchr-submit-preview'>Preview</button>"); |
| 137 | style_labeled_checkbox("flipcolors-wrapper", "flipcolors", |
| 138 | "Dark mode?", |
| 139 | "1", isDark, 0); |
| 140 | CX("</div>"/*#pikchrshow-controls*/); |
| 141 | CX("</div>"/*#pikchrshow-form*/); |
| 142 | CX("<fieldset id='pikchrshow-output-wrapper'>"); |
| 143 | CX("<legend></legend>" |
| 144 | /* Reminder: Firefox does not properly flexbox a LEGEND element, |
| 145 | always flowing it in column mode. */); |
| 146 | CX("<div id='pikchrshow-output'>"); |
| 147 | if(*zContent){ |
| 148 | int w = 0, h = 0; |
| 149 | char *zOut = pikchr(zContent, "pikchr", 0, &w, &h); |
| 150 | if( w>0 && h>0 ){ |
| 151 | const char *zNonce = safe_html_nonce(1); |
| 152 | CX("%s<div style='max-width:%dpx;'>\n%s</div>%s", |
| 153 | zNonce, w, zOut, zNonce); |
| 154 | }else{ |
| 155 | CX("<pre>\n%s\n</pre>\n", zOut); |
| 156 | } |
| 157 | fossil_free(zOut); |
| 158 | } |
| 159 | CX("</div>"/*#pikchrshow-output*/); |
| 160 | CX("</fieldset>"/*#pikchrshow-output-wrapper*/); |
| 161 | CX("</div>"/*sbs-wrapper*/); |
| 162 | if(!builtin_bundle_all_fossil_js_apis()){ |
| 163 | builtin_emit_fossil_js_apis("dom", "fetch", "copybutton", |
| 164 | "popupwidget", 0); |
| 165 | } |
| 166 | builtin_emit_fossil_js_apis("page.pikchrshow", 0); |
| 167 | builtin_fulfill_js_requests(); |
| 168 | style_footer(); |
| 169 | } |
| 170 |
| --- src/pikchrshow.c | |
| +++ src/pikchrshow.c | |
| @@ -70,100 +70,108 @@ | |
| 70 | "arrow right 200% \"HTML+SVG\" \"Output\"\n" |
| 71 | "arrow <-> down from last box.s\n" |
| 72 | "box same \"Pikchr\" \"Formatter\" \"(pikchr.c)\" fit\n"; |
| 73 | } |
| 74 | style_header("PikchrShow"); |
| 75 | CX("<style>"); { |
| 76 | CX("div.content { padding-top: 0.5em }\n"); |
| 77 | CX("#sbs-wrapper {" |
| 78 | "display: flex; flex-direction: column;" |
| 79 | "}\n"); |
| 80 | CX("#sbs-wrapper > * {" |
| 81 | "margin: 0 0.25em 0.5em 0; flex: 1 10 auto;" |
| 82 | "align-self: stretch;" |
| 83 | "}\n"); |
| 84 | CX("#sbs-wrapper textarea {" |
| 85 | "max-width: initial; flex: 1 1 auto;" |
| 86 | "}\n"); |
| 87 | CX("#pikchrshow-output, #pikchrshow-form" |
| 88 | "{display: flex; flex-direction: column; align-items: stretch;}"); |
| 89 | CX("#pikchrshow-form > * {margin: 0.25em 0}\n"); |
| 90 | CX("#pikchrshow-output {flex: 5 1 auto; padding: 0}\n"); |
| 91 | CX("#pikchrshow-output > pre, " |
| 92 | "#pikchrshow-output > pre > div, " |
| 93 | "#pikchrshow-output > pre > div > pre " |
| 94 | "{margin: 0; padding: 0}\n"); |
| 95 | CX("#pikchrshow-output.error > pre " |
| 96 | /* Server-side error report */ |
| 97 | "{padding: 0.5em}\n"); |
| 98 | CX("#pikchrshow-controls {" /* where the buttons live */ |
| 99 | "display: flex; flex-direction: row; " |
| 100 | "align-items: center; flex-wrap: wrap;" |
| 101 | "}\n"); |
| 102 | CX("#pikchrshow-controls > * {" |
| 103 | "display: inline; margin: 0 0.25em 0.5em 0;" |
| 104 | "}\n"); |
| 105 | CX("#pikchrshow-output-wrapper label {" |
| 106 | "cursor: pointer;" |
| 107 | "}\n"); |
| 108 | CX("body.pikchrshow .input-with-label > * {" |
| 109 | "margin: 0 0.2em;" |
| 110 | "}\n"); |
| 111 | CX("body.pikchrshow .input-with-label > label {" |
| 112 | "cursor: pointer;" |
| 113 | "}\n"); |
| 114 | CX("#pikchrshow-output.dark-mode svg {" |
| 115 | /* Flip the colors to approximate a dark theme look */ |
| 116 | "filter: invert(1) hue-rotate(180deg);" |
| 117 | "}\n"); |
| 118 | CX("#pikchrshow-output-wrapper {" |
| 119 | "padding: 0.25em 0.5em; border-radius: 0.25em;" |
| 120 | "border-width: 1px;"/*some skins disable fieldset borders*/ |
| 121 | "}\n"); |
| 122 | CX("#pikchrshow-output-wrapper > legend > *:not(.copy-button){" |
| 123 | "margin-right: 0.5em; vertical-align: middle;" |
| 124 | "}\n"); |
| 125 | CX("body.pikchrshow .v-align-middle{" |
| 126 | "vertical-align: middle" |
| 127 | "}\n"); |
| 128 | CX(".dragover {border: 3px dotted rgba(0,255,0,0.6)}\n"); |
| 129 | } CX("</style>"); |
| 130 | CX("<div>Input pikchr code and tap Preview to render it:</div>"); |
| 131 | CX("<div id='sbs-wrapper'>"); { |
| 132 | CX("<div id='pikchrshow-form'>"); { |
| 133 | CX("<textarea id='content' name='content' rows='15'>" |
| 134 | "%s</textarea>",zContent/*safe-for-%s*/); |
| 135 | CX("<div id='pikchrshow-controls'>"); { |
| 136 | CX("<button id='pikchr-submit-preview'>Preview</button>"); |
| 137 | CX("<div class='input-with-label'>"); { |
| 138 | CX("<button id='pikchr-stash'>Stash</button>"); |
| 139 | CX("<button id='pikchr-unstash'>Unstash</button>"); |
| 140 | CX("<button id='pikchr-clear-stash'>Clear stash</button>"); |
| 141 | CX("<span>Stores/restores a single pikchr script to/from " |
| 142 | "browser-local storage from/to the editor.</span>" |
| 143 | /* gets turned into a help-buttonlet */); |
| 144 | } CX("</div>"/*stash controls*/); |
| 145 | style_labeled_checkbox("flipcolors-wrapper", "flipcolors", |
| 146 | "Dark mode?", |
| 147 | "1", isDark, 0); |
| 148 | } CX("</div>"/*#pikchrshow-controls*/); |
| 149 | } |
| 150 | CX("</div>"/*#pikchrshow-form*/); |
| 151 | CX("<fieldset id='pikchrshow-output-wrapper'>"); { |
| 152 | CX("<legend></legend>" |
| 153 | /* Reminder: Firefox does not properly flexbox a LEGEND |
| 154 | element, always flowing it in column mode. */); |
| 155 | CX("<div id='pikchrshow-output'>"); |
| 156 | if(*zContent){ |
| 157 | int w = 0, h = 0; |
| 158 | char *zOut = pikchr(zContent, "pikchr", 0, &w, &h); |
| 159 | if( w>0 && h>0 ){ |
| 160 | const char *zNonce = safe_html_nonce(1); |
| 161 | CX("%s<div style='max-width:%dpx;'>\n%s</div>%s", |
| 162 | zNonce, w, zOut, zNonce); |
| 163 | }else{ |
| 164 | CX("<pre>\n%s\n</pre>\n", zOut); |
| 165 | } |
| 166 | fossil_free(zOut); |
| 167 | } CX("</div>"/*#pikchrshow-output*/); |
| 168 | } CX("</fieldset>"/*#pikchrshow-output-wrapper*/); |
| 169 | } CX("</div>"/*sbs-wrapper*/); |
| 170 | if(!builtin_bundle_all_fossil_js_apis()){ |
| 171 | builtin_emit_fossil_js_apis("dom", "fetch", "copybutton", |
| 172 | "popupwidget", "storage", 0); |
| 173 | } |
| 174 | builtin_emit_fossil_js_apis("page.pikchrshow", 0); |
| 175 | builtin_fulfill_js_requests(); |
| 176 | style_footer(); |
| 177 | } |
| 178 |