Fossil

Check-in [d330c091]
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:pikchrshow: support 4 different preview modes, clipboard copy of previewed content, and markup alignment option (left/center).
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: d330c09135556fc98bbb709d54ae876ae1b172ed21c6f7b7da25ca8137e69e30
User & Date: stephan 2020-09-10 22:31:13.197
Context
2020-09-10
22:56
Update Pikchr to the latest code from the Pikchr website. ... (check-in: a5c685fa user: drh tags: trunk)
22:31
pikchrshow: support 4 different preview modes, clipboard copy of previewed content, and markup alignment option (left/center). ... (check-in: d330c091 user: stephan tags: trunk)
22:15
Fix typo in the 'admin_sql' page. ... (check-in: e654b300 user: mistachkin tags: trunk)
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/fossil.dom.js.
95
96
97
98
99
100
101

102
103
104
105
106
107
108
  dom.br = dom.createElemFactory('br');
  dom.text = (t)=>document.createTextNode(t||'');
  dom.button = function(label){
    const b = this.create('button');
    if(label) b.appendChild(this.text(label));
    return b;
  };

  dom.select = dom.createElemFactory('select');
  /**
     Returns an OPTION element with the given value and label
     text (which defaults to the value).

     May be called as (value), (selectElement), (selectElement,
     value), (value, label) or (selectElement, value,







>







95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
  dom.br = dom.createElemFactory('br');
  dom.text = (t)=>document.createTextNode(t||'');
  dom.button = function(label){
    const b = this.create('button');
    if(label) b.appendChild(this.text(label));
    return b;
  };
  dom.textarea = dom.createElemFactory('textarea');
  dom.select = dom.createElemFactory('select');
  /**
     Returns an OPTION element with the given value and label
     text (which defaults to the value).

     May be called as (value), (selectElement), (selectElement,
     value), (value, label) or (selectElement, value,
Changes to src/fossil.page.pikchrshow.js.
1
2
3
4
5
6

7
8
9
10








11
12
13
14


15
16
17





18

19














20
21
22
23
24
25

26


27

28














29
30






















































31
32
33
34

35
36
37



38
39
40
41
42
43
44

45


46
47
48
49
50
51
52
(function(F/*the fossil object*/){
  "use strict";
  /**
     Client-side implementation of the /pikchrshow app. Requires that
     the fossil JS bootstrapping is complete and that these fossil
     JS APIs have been installed: fossil.fetch, fossil.dom

  */
  const E = (s)=>document.querySelector(s),
        D = F.dom,
        P = F.page;








  F.onPageLoad(function() {
    document.body.classList.add('pikchrshow');
    P.e = { /* various DOM elements we work with... */
      previewTarget: E('#pikchrshow-output'),


      btnSubmit: E('#pikchr-submit-preview'),
      cbDarkMode: E('#flipcolors-wrapper > input[type=checkbox]'),
      taContent: E('#content')





    };
















    P.e.cbDarkMode.addEventListener('change', function(ev){
      if(ev.target.checked) D.addClass(P.e.previewTarget, 'dark-mode');
      else D.removeClass(P.e.previewTarget, 'dark-mode');
    }, false);
    if(P.e.cbDarkMode.checked) D.addClass(P.e.previewTarget, 'dark-mode');


    P.e.btnSubmit.addEventListener('click', function(){


      P.preview();

    }, false);














  }/*F.onPageLoad()*/);























































  P.preview = function fp(){
    if(!fp.hasOwnProperty('toDisable')){
      fp.toDisable = [
        P.e.btnSubmit, P.e.taContent

      ];
      fp.target = P.e.previewTarget;
      fp.updateView = function(c,isError){



        D.enable(fp.toDisable);
        fp.target.innerHTML = c || '';
        if(isError) D.addClass(fp.target, 'error');
        else D.removeClass(fp.target, 'error');
      };
    }
    D.disable(fp.toDisable);

    const content = this.e.taContent.value;


    if(!content){
      fp.updateView("No pikchr content!",true);
      return this;
    }
    const self = this;
    const fd = new FormData();
    fd.append('ajax', true);




|
|
>




>
>
>
>
>
>
>
>




>
>


|
>
>
>
>
>

>

>
>
>
>
>
>
>
>
>
>
>
>
>
>






>
|
>
>
|
>

>
>
>
>
>
>
>
>
>
>
>
>
>
>


>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>


|
|
>

|

>
>
>

<
<
|



>
|
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145


146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
(function(F/*the fossil object*/){
  "use strict";
  /**
     Client-side implementation of the /pikchrshow app. Requires that
     the fossil JS bootstrapping is complete and that these fossil JS
     APIs have been installed: fossil.fetch, fossil.dom,
     fossil.copybutton
  */
  const E = (s)=>document.querySelector(s),
        D = F.dom,
        P = F.page;

  P.previewMode = 0 /*0==rendered SVG, 1==pikchr text markdown,
                      2==pikchr text fossil, 3==raw SVG. */
  P.response = {/*stashed state for the server's preview response*/
    isError: false,
    inputText: undefined /* value of the editor field at render-time */,
    raw: undefined /* raw response text/HTML from server */
  };
  F.onPageLoad(function() {
    document.body.classList.add('pikchrshow');
    P.e = { /* various DOM elements we work with... */
      previewTarget: E('#pikchrshow-output'),
      previewModeLabel: E('#pikchrshow-output-wrapper > legend'),
      btnCopy: E('#pikchrshow-output-wrapper > legend > .copy-button'),
      btnSubmit: E('#pikchr-submit-preview'),
      cbDarkMode: E('#flipcolors-wrapper > input[type=checkbox]'),
      taContent: E('#content'),
      taPreviewText: D.attr(D.textarea(), 'rows', 20, 'cols', 60,
                            'readonly', true),
      divControls: E('#pikchrshow-controls'),
      btnTogglePreviewMode: D.button("Preview mode"),
      selectMarkupAlignment: D.select()
    };
    D.append(P.e.divControls, P.e.btnTogglePreviewMode);

    // Setup markup alignment selection...
    D.append(P.e.divControls, P.e.selectMarkupAlignment);
    D.disable(D.option(P.e.selectMarkupAlignment, '', 'Markup Alignment'));
    ['left', 'center'].forEach(function(val,ndx){
      D.option(P.e.selectMarkupAlignment, ndx ? val : '', val);
    });

    // Setup clipboard-copy of markup/SVG...
    F.copyButton(P.e.btnCopy, {copyFromElement: P.e.taPreviewText});
    P.e.btnCopy.addEventListener('text-copied',function(ev){
       D.flashOnce(ev.target);
    },false);

    // Set up dark mode simulator...
    P.e.cbDarkMode.addEventListener('change', function(ev){
      if(ev.target.checked) D.addClass(P.e.previewTarget, 'dark-mode');
      else D.removeClass(P.e.previewTarget, 'dark-mode');
    }, false);
    if(P.e.cbDarkMode.checked) D.addClass(P.e.previewTarget, 'dark-mode');

    // Set up preview update and preview mode toggle...
    P.e.btnSubmit.addEventListener('click', ()=>P.preview(), false);
    P.e.btnTogglePreviewMode.addEventListener('click', function(){
      /* Rotate through the 4 available preview modes */
      P.previewMode = ++P.previewMode % 4;
      P.renderPreview();
    }, false);
    P.e.selectMarkupAlignment.addEventListener('change', function(ev){
      /* Update markdown/fossil wiki preview if it's active */
      if(P.previewMode==1 || P.previewMode==2){
        P.renderPreview();
      }
    }, false);

    if(P.e.taContent.value/*was pre-filled server-side*/){
      /* Fill our "response" state so that renderPreview() can work */
      P.response.inputText = P.e.taContent.value;
      P.response.raw = P.e.previewTarget.innerHTML;
      P.renderPreview()/*not strictly necessary, but gets all
                         labels/headers in alignment.*/;
    }    
  }/*F.onPageLoad()*/);

  /**
     Updates the preview view based on the current preview mode and
     error state.
  */
  P.renderPreview = function f(){
    if(!f.hasOwnProperty('rxNonce')){
      f.rxNonce = /<!--.+-->\r?\n?/g /*nonce comments*/;
    }
    const preTgt = this.e.previewTarget;
    if(this.response.isError){
      preTgt.innerHTML = this.response.raw;
      D.addClass(preTgt, 'error');
      this.e.previewModeLabel.innerText = "Error";
      return;
    }
    D.removeClass(preTgt, 'error');
    D.removeClass(this.e.btnTogglePreviewMode, 'hidden');
    let label;
    switch(this.previewMode){
    case 0:
      label = "Rendered SVG";
      preTgt.innerHTML = this.response.raw;
      this.e.taPreviewText.value = this.response.raw.replace(f.rxNonce, '')/*for copy button*/;
      break;
    case 1:
      label = "Markdown";
      this.e.taPreviewText.value = [
        '```pikchr'+(this.e.selectMarkupAlignment.value
                     ? ' '+this.e.selectMarkupAlignment.value : ''),
        this.response.inputText, '```'
      ].join('\n');
      D.append(D.clearElement(preTgt), this.e.taPreviewText);
      break;
    case 2:
      label = "Fossil wiki";
      this.e.taPreviewText.value = [
        '<verbatim type="pikchr',
        this.e.selectMarkupAlignment.value ? ' '+this.e.selectMarkupAlignment.value : '',
        '">', this.response.inputText, '</verbatim>'
      ].join('');
      D.append(D.clearElement(preTgt), this.e.taPreviewText);
      break;
    case 3:
      label = "Raw SVG";
      this.e.taPreviewText.value = this.response.raw.replace(f.rxNonce, '');
      D.append(D.clearElement(preTgt), this.e.taPreviewText);
      break;
    }
    D.append(D.clearElement(this.e.previewModeLabel),
             label, this.e.btnCopy);
  };

  /** Fetches the preview from the server and updates the preview to
      the rendered SVG content or error report. */
  P.preview = function fp(){
    if(!fp.hasOwnProperty('toDisable')){
      fp.toDisable = [ /* elements to disable during ajax operations */
        this.e.btnSubmit, this.e.taContent,
        this.e.btnTogglePreviewMode, this.e.selectMarkupAlignment,
      ];
      fp.target = this.e.previewTarget;
      fp.updateView = function(c,isError){
        P.previewMode = 0;
        P.response.raw = c;
        P.response.isError = isError;
        D.enable(fp.toDisable);


        P.renderPreview();
      };
    }
    D.disable(fp.toDisable);
    D.addClass(this.e.btnTogglePreviewMode, 'hidden');
    const content = this.e.taContent.value.trim();
    this.response.raw = undefined;
    this.response.inputText = content;
    if(!content){
      fp.updateView("No pikchr content!",true);
      return this;
    }
    const self = this;
    const fd = new FormData();
    fd.append('ajax', true);
Changes to src/pikchrshow.c.
68
69
70
71
72
73
74
75



76
77
78
79
80
81
82
83
84
85
86
87
88
89

90
91
92
93
94
95
96
97
98
99
100




101
102
103
104
105
106
107
108
109
110
111
112


113


114
115
116
117
118
119
120
121
122
123
124
125
126

127
128
129
130
131
132
133
134
135
  }
  style_header("PikchrShow");
  CX("<style>");
  CX("div.content { padding-top: 0.5em }");
  CX("#sbs-wrapper {"
     "display: flex; flex-direction: row; flex-wrap: wrap;"
     "}");
  CX("#sbs-wrapper > * {margin: 0 0 1em 0}");



  CX("#pikchrshow-output, #pikchrshow-form"
     "{display: flex; flex-direction: column}");
  CX("#pikchrshow-form {flex: 2 1 auto}");
  CX("#pikchrshow-form > * {margin: 0.25em 0}");
  CX("#pikchrshow-output {"
     "flex: 1 1 auto; border-width: 1px; border-style: solid;"
     "border-radius: 0.25em; padding: 0.5em;"
     "}");
  CX("#pikchrshow-output > pre, "
     "#pikchrshow-output > pre > div, "
     "#pikchrshow-output > pre > div > pre "
     "{margin: 0; padding: 0}");
  CX("#pikchrshow-controls {"
     "display: flex; flex-direction: row; align-items: center;"

     "}");
  CX("#pikchrshow-controls > * {"
     "display: inline; margin-left: 0.5em;"
     "}");
  CX("#pikchrshow-controls > .input-with-label > * {"
     "cursor: pointer;"
     "}");
  CX("#pikchrshow-output.dark-mode > svg {"
     /* Flip the colors to approximate a dark theme look */
     "filter: invert(1) hue-rotate(180deg);"
     "}");




  CX("</style>");
  CX("<div>Input pikchr code and tap Preview to render it:</div>");
  CX("<div id='sbs-wrapper'>");
  CX("<div id='pikchrshow-form'>");
  CX("<textarea id='content' name='content' rows='15'>%s</textarea>",
     zContent/*safe-for-%s*/);
  CX("<div id='pikchrshow-controls'>");
  CX("<button id='pikchr-submit-preview'>Preview</button>");
  style_labeled_checkbox("flipcolors-wrapper", "flipcolors",
                         "Simulate dark color theme?",
                         "1", flipColors, 0);
  CX("</div>"/*#pikchrshow-controls*/);


  CX("</div>"/*#pikchrshow-form*/);


  CX("<div id='pikchrshow-output'>");
  if(*zContent){
    int w = 0, h = 0;
    char *zOut = pikchr(zContent, "pikchr", 0, &w, &h);
    if( w>0 && h>0 ){
      const char *zNonce = safe_html_nonce(1);
      CX("%s\n%s%s", zNonce, zOut, zNonce);
    }else{
      CX("<pre>\n%s\n</pre>\n", zOut);
    }
    fossil_free(zOut);
  }
  CX("</div>"/*#pikchrshow-output*/);

  CX("</div>"/*sbs-wrapper*/);
  if(!builtin_bundle_all_fossil_js_apis()){
    builtin_emit_fossil_js_apis("dom", "fetch", 0);
  }
  builtin_emit_fossil_js_apis("page.pikchrshow", 0);
  builtin_fulfill_js_requests();
  style_footer();
}








|
>
>
>

|
<

|
<
<
<




|
|
>


|








>
>
>
>




<
<



|


>
>

>
>













>


|






68
69
70
71
72
73
74
75
76
77
78
79
80

81
82



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108


109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
  }
  style_header("PikchrShow");
  CX("<style>");
  CX("div.content { padding-top: 0.5em }");
  CX("#sbs-wrapper {"
     "display: flex; flex-direction: row; flex-wrap: wrap;"
     "}");
  CX("#sbs-wrapper > * {"
     "margin: 0 0.25em 0.5em 0; flex: 1 10 auto;"
     "}");
  CX("#sbs-wrapper textarea {max-width: initial}");
  CX("#pikchrshow-output, #pikchrshow-form"
     "{display: flex; flex-direction: column; align-items: stretch;}");

  CX("#pikchrshow-form > * {margin: 0.25em 0}");
  CX("#pikchrshow-output {flex: 5 1 auto; padding: 0}");



  CX("#pikchrshow-output > pre, "
     "#pikchrshow-output > pre > div, "
     "#pikchrshow-output > pre > div > pre "
     "{margin: 0; padding: 0}");
  CX("#pikchrshow-controls {" /* where the buttons live */
     "display: flex; flex-direction: row; "
     "align-items: center; flex-wrap: wrap;"
     "}");
  CX("#pikchrshow-controls > * {"
     "display: inline; margin: 0 0.25em 0.5em 0;"
     "}");
  CX("#pikchrshow-controls > .input-with-label > * {"
     "cursor: pointer;"
     "}");
  CX("#pikchrshow-output.dark-mode > svg {"
     /* Flip the colors to approximate a dark theme look */
     "filter: invert(1) hue-rotate(180deg);"
     "}");
  CX("#sbs-wrapper > fieldset {"
     "padding: 0.25em 0.5em; border-radius: 0.25em;"
     "}");
  CX("fieldset > legend > .copy-button {margin-left: 0.25em}");
  CX("</style>");
  CX("<div>Input pikchr code and tap Preview to render it:</div>");
  CX("<div id='sbs-wrapper'>");
  CX("<div id='pikchrshow-form'>");


  CX("<div id='pikchrshow-controls'>");
  CX("<button id='pikchr-submit-preview'>Preview</button>");
  style_labeled_checkbox("flipcolors-wrapper", "flipcolors",
                         "Simulate dark theme?",
                         "1", flipColors, 0);
  CX("</div>"/*#pikchrshow-controls*/);
  CX("<textarea id='content' name='content' rows='15'>%s</textarea>",
     zContent/*safe-for-%s*/);
  CX("</div>"/*#pikchrshow-form*/);
  CX("<fieldset id='pikchrshow-output-wrapper'>");
  CX("<legend>Preview <span class='copy-button'></span></legend>");
  CX("<div id='pikchrshow-output'>");
  if(*zContent){
    int w = 0, h = 0;
    char *zOut = pikchr(zContent, "pikchr", 0, &w, &h);
    if( w>0 && h>0 ){
      const char *zNonce = safe_html_nonce(1);
      CX("%s\n%s%s", zNonce, zOut, zNonce);
    }else{
      CX("<pre>\n%s\n</pre>\n", zOut);
    }
    fossil_free(zOut);
  }
  CX("</div>"/*#pikchrshow-output*/);
  CX("</fieldset>");
  CX("</div>"/*sbs-wrapper*/);
  if(!builtin_bundle_all_fossil_js_apis()){
    builtin_emit_fossil_js_apis("dom", "fetch", "copybutton", 0);
  }
  builtin_emit_fossil_js_apis("page.pikchrshow", 0);
  builtin_fulfill_js_requests();
  style_footer();
}