|
1
|
/* |
|
2
|
** Copyright (c) 2020 D. Richard Hipp |
|
3
|
** |
|
4
|
** This program is free software; you can redistribute it and/or |
|
5
|
** modify it under the terms of the Simplified BSD License (also |
|
6
|
** known as the "2-Clause License" or "FreeBSD License".) |
|
7
|
** |
|
8
|
** This program is distributed in the hope that it will be useful, |
|
9
|
** but without any warranty; without even the implied warranty of |
|
10
|
** merchantability or fitness for a particular purpose. |
|
11
|
** |
|
12
|
** Author contact information: |
|
13
|
** [email protected] |
|
14
|
** http://www.hwaci.com/drh/ |
|
15
|
** |
|
16
|
******************************************************************************* |
|
17
|
** |
|
18
|
** This file contains fossil-specific code related to pikchr. |
|
19
|
*/ |
|
20
|
#include "config.h" |
|
21
|
#include <assert.h> |
|
22
|
#include <ctype.h> |
|
23
|
#include "pikchrshow.h" |
|
24
|
|
|
25
|
#if INTERFACE |
|
26
|
/* These are described in pikchr_process()'s docs. */ |
|
27
|
/* The first two must match the values from pikchr.c */ |
|
28
|
#define PIKCHR_PROCESS_PLAINTEXT_ERRORS 0x0001 |
|
29
|
#define PIKCHR_PROCESS_DARK_MODE 0x0002 |
|
30
|
/* end of flags supported directly by pikchr() */ |
|
31
|
#define PIKCHR_PROCESS_PASSTHROUGH 0x0003 /* Pass through these flags */ |
|
32
|
#define PIKCHR_PROCESS_NONCE 0x0010 |
|
33
|
#define PIKCHR_PROCESS_ERR_PRE 0x0020 |
|
34
|
#define PIKCHR_PROCESS_SRC 0x0040 |
|
35
|
#define PIKCHR_PROCESS_DIV 0x0080 |
|
36
|
#define PIKCHR_PROCESS_DIV_INDENT 0x0100 |
|
37
|
#define PIKCHR_PROCESS_DIV_CENTER 0x0200 |
|
38
|
#define PIKCHR_PROCESS_DIV_FLOAT_LEFT 0x0400 |
|
39
|
#define PIKCHR_PROCESS_DIV_FLOAT_RIGHT 0x0800 |
|
40
|
#define PIKCHR_PROCESS_DIV_TOGGLE 0x1000 |
|
41
|
#define PIKCHR_PROCESS_DIV_SOURCE 0x2000 |
|
42
|
#define PIKCHR_PROCESS_DIV_SOURCE_INLINE 0x4000 |
|
43
|
#endif |
|
44
|
|
|
45
|
/* |
|
46
|
** Processes a pikchr script. zIn is the NUL-terminated input |
|
47
|
** script. pikFlags may be a bitmask of any of the PIKCHR_PROCESS_xxx |
|
48
|
** flags documented below. Output is sent to pOut, |
|
49
|
** |
|
50
|
** Returns 0 on success, or non-zero if pikchr processing failed. |
|
51
|
** In either case, the error message (if any) from pikchr will be |
|
52
|
** appended to pOut. |
|
53
|
** |
|
54
|
** pikFlags flag descriptions: |
|
55
|
** |
|
56
|
** - PIKCHR_PROCESS_DIV: if set, the SVG result is wrapped in a DIV |
|
57
|
** element which specifies a max-width style value based on the SVG's |
|
58
|
** calculated size. This flag has multiple mutually exclusive forms: |
|
59
|
** |
|
60
|
** - PIKCHR_PROCESS_DIV uses default element alignment. |
|
61
|
** - PIKCHR_PROCESS_DIV_INDENT indents the div. |
|
62
|
** - PIKCHR_PROCESS_DIV_CENTER centers the div. |
|
63
|
** - PIKCHR_PROCESS_DIV_FLOAT_LEFT floats the div left. |
|
64
|
** - PIKCHR_PROCESS_DIV_FLOAT_RIGHT floats the div right. |
|
65
|
** |
|
66
|
** If more than one is specified, which one is used is undefined. Those |
|
67
|
** flags may be OR'd with one or both of the following: |
|
68
|
** |
|
69
|
** - PIKCHR_PROCESS_DIV_TOGGLE: adds the 'toggle' CSS class to the |
|
70
|
** outer DIV so that event-handler code can install different |
|
71
|
** toggling behaviour than the default. Default is ctrl-click, but |
|
72
|
** this flag enables single-click toggling for the element. |
|
73
|
** |
|
74
|
** - PIKCHR_PROCESS_DIV_SOURCE: adds the 'source' CSS class to the |
|
75
|
** outer DIV, which is a hint to the client-side renderer (see |
|
76
|
** fossil.pikchr.js) that the pikchr should initially be rendered |
|
77
|
** in source code form mode (the default is to hide the source and |
|
78
|
** show the SVG). |
|
79
|
** |
|
80
|
** - PIKCHR_PROCESS_DIV_SOURCE_INLINE: adds the 'source-inline' CSS |
|
81
|
** class to the outer wrapper. This modifier changes how the |
|
82
|
** 'source' CSS class gets applied: with this flag, the source view |
|
83
|
** should be rendered "inline" (same position as the graphic), else |
|
84
|
** it is to be left-aligned. |
|
85
|
** |
|
86
|
** - PIKCHR_PROCESS_NONCE: if set, the resulting SVG/DIV are wrapped |
|
87
|
** in "safe nonce" comments, which are a fossil-internal mechanism |
|
88
|
** which prevents the wiki/markdown processors from re-processing this |
|
89
|
** output. This is necessary when calling this routine in the context |
|
90
|
** of wiki/embedded doc processing, but not (e.g.) when fetching |
|
91
|
** an image for /pikchrpage. |
|
92
|
** |
|
93
|
** - PIKCHR_PROCESS_SRC: if set, a new PRE.pikchr-src element is |
|
94
|
** injected adjacent to the SVG element which contains the |
|
95
|
** HTML-escaped content of the input script. If |
|
96
|
** PIKCHR_PROCESS_DIV_SOURCE or PIKCHR_PROCESS_DIV_SOURCE_INLINE is |
|
97
|
** set, this flag is automatically implied. |
|
98
|
** |
|
99
|
** - PIKCHR_PROCESS_ERR_PRE: if set and pikchr() fails, the resulting |
|
100
|
** error report is wrapped in a PRE element, else it is retained |
|
101
|
** as-is (intended only for console output). |
|
102
|
*/ |
|
103
|
int pikchr_process(const char *zIn, int pikFlags, Blob * pOut){ |
|
104
|
int isErr = 0; |
|
105
|
int w = 0, h = 0; |
|
106
|
char *zOut; |
|
107
|
const char *zNonce = (PIKCHR_PROCESS_NONCE & pikFlags) |
|
108
|
? safe_html_nonce(1) : 0; |
|
109
|
|
|
110
|
if(!(PIKCHR_PROCESS_DIV & pikFlags) |
|
111
|
/* If any DIV_xxx flags are set, set DIV */ |
|
112
|
&& (PIKCHR_PROCESS_DIV_INDENT |
|
113
|
| PIKCHR_PROCESS_DIV_CENTER |
|
114
|
| PIKCHR_PROCESS_DIV_FLOAT_RIGHT |
|
115
|
| PIKCHR_PROCESS_DIV_FLOAT_LEFT |
|
116
|
| PIKCHR_PROCESS_DIV_SOURCE |
|
117
|
| PIKCHR_PROCESS_DIV_SOURCE_INLINE |
|
118
|
| PIKCHR_PROCESS_DIV_TOGGLE |
|
119
|
) & pikFlags){ |
|
120
|
pikFlags |= PIKCHR_PROCESS_DIV; |
|
121
|
} |
|
122
|
if(zNonce){ |
|
123
|
blob_appendf(pOut, "%s\n", zNonce); |
|
124
|
} |
|
125
|
zOut = pikchr(zIn, "pikchr", |
|
126
|
0x01 | (pikFlags&PIKCHR_PROCESS_PASSTHROUGH), |
|
127
|
&w, &h); |
|
128
|
if( w>0 && h>0 ){ |
|
129
|
const char * zClassToggle = ""; |
|
130
|
const char * zClassSource = ""; |
|
131
|
const char * zWrapperClass = ""; |
|
132
|
if(PIKCHR_PROCESS_DIV & pikFlags){ |
|
133
|
if(PIKCHR_PROCESS_DIV_CENTER & pikFlags){ |
|
134
|
zWrapperClass = " center"; |
|
135
|
}else if(PIKCHR_PROCESS_DIV_INDENT & pikFlags){ |
|
136
|
zWrapperClass = " indent"; |
|
137
|
}else if(PIKCHR_PROCESS_DIV_FLOAT_LEFT & pikFlags){ |
|
138
|
zWrapperClass = " float-left"; |
|
139
|
}else if(PIKCHR_PROCESS_DIV_FLOAT_RIGHT & pikFlags){ |
|
140
|
zWrapperClass = " float-right"; |
|
141
|
} |
|
142
|
if(PIKCHR_PROCESS_DIV_TOGGLE & pikFlags){ |
|
143
|
zClassToggle = " toggle"; |
|
144
|
} |
|
145
|
if(PIKCHR_PROCESS_DIV_SOURCE_INLINE & pikFlags){ |
|
146
|
if(PIKCHR_PROCESS_DIV_SOURCE & pikFlags){ |
|
147
|
zClassSource = " source source-inline"; |
|
148
|
}else{ |
|
149
|
zClassSource = " source-inline"; |
|
150
|
} |
|
151
|
pikFlags |= PIKCHR_PROCESS_SRC; |
|
152
|
}else if(PIKCHR_PROCESS_DIV_SOURCE & pikFlags){ |
|
153
|
zClassSource = " source"; |
|
154
|
pikFlags |= PIKCHR_PROCESS_SRC; |
|
155
|
} |
|
156
|
blob_appendf(pOut,"<div class='pikchr-wrapper" |
|
157
|
"%s%s%s'>" |
|
158
|
"<div class=\"pikchr-svg\" " |
|
159
|
"style=\"max-width:%dpx\">\n", |
|
160
|
zWrapperClass/*safe-for-%s*/, |
|
161
|
zClassToggle/*safe-for-%s*/, |
|
162
|
zClassSource/*safe-for-%s*/, w); |
|
163
|
} |
|
164
|
blob_append(pOut, zOut, -1); |
|
165
|
if(PIKCHR_PROCESS_DIV & pikFlags){ |
|
166
|
blob_append(pOut, "</div>\n", 7); |
|
167
|
} |
|
168
|
if(PIKCHR_PROCESS_SRC & pikFlags){ |
|
169
|
static int counter = 0; |
|
170
|
++counter; |
|
171
|
blob_appendf(pOut, "<div class='pikchr-src'>" |
|
172
|
"<pre id='pikchr-src-%d'>%h</pre>" |
|
173
|
"<span class='hidden'>" |
|
174
|
"<a href='%R/pikchrshow?fromSession' " |
|
175
|
"class='pikchr-src-pikchrshow' target='_new-%d' " |
|
176
|
"data-pikchrid='pikchr-src-%d' " |
|
177
|
"title='Open this pikchr in /pikchrshow'" |
|
178
|
">→ /pikchrshow</a></span>" |
|
179
|
"</div>\n", |
|
180
|
counter, zIn, counter, counter); |
|
181
|
} |
|
182
|
if(PIKCHR_PROCESS_DIV & pikFlags){ |
|
183
|
blob_append(pOut, "</div>\n", 7); |
|
184
|
} |
|
185
|
}else{ |
|
186
|
isErr = 2; |
|
187
|
if(PIKCHR_PROCESS_ERR_PRE & pikFlags){ |
|
188
|
blob_append(pOut, "<pre class='error'>\n", 20); |
|
189
|
} |
|
190
|
blob_appendf(pOut, "%h", zOut); |
|
191
|
if(PIKCHR_PROCESS_ERR_PRE & pikFlags){ |
|
192
|
blob_append(pOut, "\n</pre>\n", 8); |
|
193
|
} |
|
194
|
} |
|
195
|
fossil_free(zOut); |
|
196
|
if(zNonce){ |
|
197
|
blob_appendf(pOut, "%s\n", zNonce); |
|
198
|
} |
|
199
|
return isErr; |
|
200
|
} |
|
201
|
|
|
202
|
/* |
|
203
|
** Legacy impl of /pikchrshow. pikchrshow_page() will delegate to |
|
204
|
** this one if the "legacy" or "ajax" request arguments are set. |
|
205
|
** |
|
206
|
** A pikchr code editor and previewer, allowing users to experiment |
|
207
|
** with pikchr code or prototype it for use in copy/pasting into forum |
|
208
|
** posts, wiki pages, or embedded docs. This version of pikchrshow |
|
209
|
** uses JavaScript to send pikchr code to the server for |
|
210
|
** processing. The newer /pikchrshow applications runs pikchr on the |
|
211
|
** client machine, without the need for back-and-forth network |
|
212
|
** traffic. |
|
213
|
*/ |
|
214
|
void pikchrshowcs_page(void){ |
|
215
|
const char *zContent = 0; |
|
216
|
int isDark; /* true if the current skin is "dark" */ |
|
217
|
int pikFlags = |
|
218
|
PIKCHR_PROCESS_DIV |
|
219
|
| PIKCHR_PROCESS_SRC |
|
220
|
| PIKCHR_PROCESS_ERR_PRE; |
|
221
|
|
|
222
|
login_check_credentials(); |
|
223
|
if( !g.perm.RdWiki && !g.perm.Read && !g.perm.RdForum ){ |
|
224
|
cgi_redirectf("%R/login?g=pikchrshowcs"); |
|
225
|
} |
|
226
|
if(P("wasm")){ |
|
227
|
pikchrshow_page(); |
|
228
|
return; |
|
229
|
} |
|
230
|
zContent = PD("content",P("p")); |
|
231
|
if(P("ajax")!=0){ |
|
232
|
/* Called from the JS-side preview updater. |
|
233
|
TODO: respond with JSON instead.*/ |
|
234
|
cgi_set_content_type("text/html"); |
|
235
|
if(zContent && *zContent){ |
|
236
|
Blob out = empty_blob; |
|
237
|
const int isErr = |
|
238
|
pikchr_process(zContent, pikFlags, &out); |
|
239
|
if(isErr){ |
|
240
|
cgi_printf_header("x-pikchrshow-is-error: %d\r\n", isErr); |
|
241
|
} |
|
242
|
CX("%b", &out); |
|
243
|
blob_reset(&out); |
|
244
|
}else{ |
|
245
|
CX("<pre>No content! Nothing to render</pre>"); |
|
246
|
} |
|
247
|
return; |
|
248
|
}/*ajax response*/ |
|
249
|
style_emit_noscript_for_js_page(); |
|
250
|
isDark = skin_detail_boolean("white-foreground"); |
|
251
|
if(!zContent){ |
|
252
|
zContent = "arrow right 200% \"Markdown\" \"Source\"\n" |
|
253
|
"box rad 10px \"Markdown\" \"Formatter\" \"(markdown.c)\" fit\n" |
|
254
|
"arrow right 200% \"HTML+SVG\" \"Output\"\n" |
|
255
|
"arrow <-> down from last box.s\n" |
|
256
|
"box same \"Pikchr\" \"Formatter\" \"(pikchr.c)\" fit\n"; |
|
257
|
} |
|
258
|
style_header("PikchrShow Client/Server"); |
|
259
|
CX("<style>"); { |
|
260
|
CX("div.content { padding-top: 0.5em }\n"); |
|
261
|
CX("#sbs-wrapper {" |
|
262
|
"display: flex; flex-direction: column;" |
|
263
|
"}\n"); |
|
264
|
CX("#sbs-wrapper > * {" |
|
265
|
"margin: 0 0.25em 0.5em 0; flex: 1 10 auto;" |
|
266
|
"align-self: stretch;" |
|
267
|
"}\n"); |
|
268
|
CX("#sbs-wrapper textarea {" |
|
269
|
"max-width: initial; flex: 1 1 auto;" |
|
270
|
"}\n"); |
|
271
|
CX("#pikchrshow-output, #pikchrshow-form" |
|
272
|
"{display: flex; flex-direction: column; align-items: stretch;}"); |
|
273
|
CX("#pikchrshow-form > * {margin: 0.25em 0}\n"); |
|
274
|
CX("#pikchrshow-output {flex: 5 1 auto; padding: 0}\n"); |
|
275
|
CX("#pikchrshow-output > pre, " |
|
276
|
"#pikchrshow-output > pre > div, " |
|
277
|
"#pikchrshow-output > pre > div > pre " |
|
278
|
"{margin: 0; padding: 0}\n"); |
|
279
|
CX("#pikchrshow-output.error > pre " |
|
280
|
/* Server-side error report */ |
|
281
|
"{padding: 0.5em}\n"); |
|
282
|
CX("#pikchrshow-controls {" /* where the buttons live */ |
|
283
|
"display: flex; flex-direction: row; " |
|
284
|
"align-items: center; flex-wrap: wrap;" |
|
285
|
"}\n"); |
|
286
|
CX("#pikchrshow-controls > * {" |
|
287
|
"display: inline; margin: 0 0.25em 0.5em 0;" |
|
288
|
"}\n"); |
|
289
|
CX("#pikchrshow-output-wrapper label {" |
|
290
|
"cursor: pointer;" |
|
291
|
"}\n"); |
|
292
|
CX("body.pikchrshow .input-with-label > * {" |
|
293
|
"margin: 0 0.2em;" |
|
294
|
"}\n"); |
|
295
|
CX("body.pikchrshow .input-with-label > label {" |
|
296
|
"cursor: pointer;" |
|
297
|
"}\n"); |
|
298
|
CX("#pikchrshow-output.dark-mode svg {" |
|
299
|
/* Flip the colors to approximate a dark theme look */ |
|
300
|
"filter: invert(1) hue-rotate(180deg);" |
|
301
|
"}\n"); |
|
302
|
CX("#pikchrshow-output-wrapper {" |
|
303
|
"padding: 0.25em 0.5em; border-radius: 0.25em;" |
|
304
|
"border-width: 1px;"/*some skins disable fieldset borders*/ |
|
305
|
"}\n"); |
|
306
|
CX("#pikchrshow-output-wrapper > legend > *:not(.copy-button){" |
|
307
|
"margin-right: 0.5em; vertical-align: middle;" |
|
308
|
"}\n"); |
|
309
|
CX("body.pikchrshow .v-align-middle{" |
|
310
|
"vertical-align: middle" |
|
311
|
"}\n"); |
|
312
|
CX(".dragover {border: 3px dotted rgba(0,255,0,0.6)}\n"); |
|
313
|
} CX("</style>"); |
|
314
|
CX("<div>Input pikchr code and tap Preview (or Shift-Enter) to render " |
|
315
|
"it. <a href='?wasm'>Switch to WASM mode</a>.</div>"); |
|
316
|
CX("<div id='sbs-wrapper'>"); { |
|
317
|
CX("<div id='pikchrshow-form'>"); { |
|
318
|
CX("<textarea id='content' name='content' rows='15'>" |
|
319
|
"%s</textarea>",zContent/*safe-for-%s*/); |
|
320
|
CX("<div id='pikchrshow-controls'>"); { |
|
321
|
CX("<button id='pikchr-submit-preview'>Preview</button>"); |
|
322
|
CX("<div class='input-with-label'>"); { |
|
323
|
CX("<button id='pikchr-stash'>Stash</button>"); |
|
324
|
CX("<button id='pikchr-unstash'>Unstash</button>"); |
|
325
|
CX("<button id='pikchr-clear-stash'>Clear stash</button>"); |
|
326
|
CX("<span>Stores/restores a single pikchr script to/from " |
|
327
|
"browser-local storage from/to the editor.</span>" |
|
328
|
/* gets turned into a help-buttonlet */); |
|
329
|
} CX("</div>"/*stash controls*/); |
|
330
|
style_labeled_checkbox("flipcolors-wrapper", "flipcolors", |
|
331
|
"Dark mode?", |
|
332
|
"1", isDark, 0); |
|
333
|
} CX("</div>"/*#pikchrshow-controls*/); |
|
334
|
} |
|
335
|
CX("</div>"/*#pikchrshow-form*/); |
|
336
|
CX("<fieldset id='pikchrshow-output-wrapper'>"); { |
|
337
|
CX("<legend></legend>" |
|
338
|
/* Reminder: Firefox does not properly flexbox a LEGEND |
|
339
|
element, always flowing it in column mode. */); |
|
340
|
CX("<div id='pikchrshow-output'>"); |
|
341
|
if(*zContent){ |
|
342
|
Blob out = empty_blob; |
|
343
|
pikchr_process(zContent, pikFlags, &out); |
|
344
|
CX("%b", &out); |
|
345
|
blob_reset(&out); |
|
346
|
} CX("</div>"/*#pikchrshow-output*/); |
|
347
|
} CX("</fieldset>"/*#pikchrshow-output-wrapper*/); |
|
348
|
} CX("</div>"/*sbs-wrapper*/); |
|
349
|
builtin_fossil_js_bundle_or("fetch", "copybutton", "popupwidget", |
|
350
|
"storage", "pikchr", NULL); |
|
351
|
builtin_request_js("fossil.page.pikchrshow.js"); |
|
352
|
builtin_fulfill_js_requests(); |
|
353
|
style_finish_page(); |
|
354
|
} |
|
355
|
|
|
356
|
/* |
|
357
|
** WEBPAGE: pikchrshow |
|
358
|
** |
|
359
|
** A pikchr code editor and previewer, allowing users to experiment |
|
360
|
** with pikchr code or prototype it for use in copy/pasting into forum |
|
361
|
** posts, wiki pages, or embedded docs. This version of pikchrshow |
|
362
|
** uses WebAssembly to run entirely in the client browser, without a |
|
363
|
** need for back-and-forth client/server traffic to perform the |
|
364
|
** rendering. The "legacy" version of this application, which sends |
|
365
|
** all input to the server for rendering, can be accessed by adding |
|
366
|
** the "legacy" URL argument. |
|
367
|
** |
|
368
|
** It optionally accepts a p=pikchr-script-code URL parameter or POST |
|
369
|
** value to pre-populate the editor with that code. |
|
370
|
*/ |
|
371
|
void pikchrshow_page(void){ |
|
372
|
const char *zContent = 0; |
|
373
|
|
|
374
|
if(P("legacy") || P("ajax")){ |
|
375
|
pikchrshowcs_page(); |
|
376
|
return; |
|
377
|
} |
|
378
|
login_check_credentials(); |
|
379
|
if( !g.perm.RdWiki && !g.perm.Read && !g.perm.RdForum ){ |
|
380
|
cgi_redirectf("%R/login?g=pikchrshow"); |
|
381
|
} |
|
382
|
style_emit_noscript_for_js_page(); |
|
383
|
style_header("PikchrShow"); |
|
384
|
zContent = PD("content",P("p")); |
|
385
|
if(!zContent){ |
|
386
|
zContent = "arrow right 200% \"Markdown\" \"Source\"\n" |
|
387
|
"box rad 10px \"Markdown\" \"Formatter\" \"(markdown.c)\" fit\n" |
|
388
|
"arrow right 200% \"HTML+SVG\" \"Output\"\n" |
|
389
|
"arrow <-> down from last box.s\n" |
|
390
|
"box same \"Pikchr\" \"Formatter\" \"(pikchr.c)\" fit\n"; |
|
391
|
} |
|
392
|
/* Wasm load/init progress widget... */ |
|
393
|
CX("<div class='emscripten'>"); { |
|
394
|
CX("<figure id='module-spinner'>"); |
|
395
|
CX("<div class='spinner'></div>"); |
|
396
|
CX("<div class='center'><strong>Initializing app...</strong></div>"); |
|
397
|
CX("<div class='center'>"); |
|
398
|
CX("On a slow internet connection this may take a moment. If this "); |
|
399
|
CX("message displays for \"a long time\", initialization may have "); |
|
400
|
CX("failed and the JavaScript console may contain clues as to why. "); |
|
401
|
CX("</div>"); |
|
402
|
CX("<div><a href='?legacy'>Switch to legacy mode</a></div>"); |
|
403
|
CX("</figure>"); |
|
404
|
CX("<div class='emscripten' id='module-status'>Downloading...</div>"); |
|
405
|
CX("<progress value='0' max='100' id='module-progress' hidden='1'>" |
|
406
|
"</progress>"); |
|
407
|
} CX("</div><!-- .emscripten -->"); |
|
408
|
/* Main view... */ |
|
409
|
CX("<div id='view-split' class='app-view initially-hidden'>"); { |
|
410
|
CX("<fieldset class='options collapsible'>"); { |
|
411
|
CX("<legend><button class='fieldset-toggle'>Options</button></legend>"); |
|
412
|
CX("<div>"); |
|
413
|
CX("<span class='labeled-input'>"); |
|
414
|
CX("<input type='checkbox' id='opt-cb-sbs' "); |
|
415
|
CX("data-csstgt='#main-wrapper' "); |
|
416
|
CX("data-cssclass='side-by-side' "); |
|
417
|
CX("data-config='sideBySide'>"); |
|
418
|
CX("<label for='opt-cb-sbs'>Side-by-side</label>"); |
|
419
|
CX("</span>"); |
|
420
|
CX("<span class='labeled-input'>"); |
|
421
|
CX("<input type='checkbox' id='opt-cb-swapio' "); |
|
422
|
CX("data-csstgt='#main-wrapper' "); |
|
423
|
CX("data-cssclass='swapio' "); |
|
424
|
CX("data-config='swapInOut'>"); |
|
425
|
CX("<label for='opt-cb-swapio'>Swap in/out</label>"); |
|
426
|
CX("</span>"); |
|
427
|
CX("<span class='labeled-input'>"); |
|
428
|
CX("<input type='checkbox' id='opt-cb-autofit' "); |
|
429
|
CX("data-config='renderAutofit'>"); |
|
430
|
CX("<label for='opt-cb-autofit' " |
|
431
|
"title='Attempt to scale SVG to fit viewport. " |
|
432
|
"Whether it will work depends in part on the size " |
|
433
|
"and shape of the image and the viewport.'" |
|
434
|
">Auto-fit SVG</label>"); |
|
435
|
CX("</span>"); |
|
436
|
CX("<span class='labeled-input'>"); |
|
437
|
CX("<input type='checkbox' id='opt-cb-autorender' "); |
|
438
|
CX("data-csstgt='#main-wrapper' "); |
|
439
|
CX("data-cssclass='auto-render' "); |
|
440
|
CX("data-config='renderWhileTyping'>"); |
|
441
|
CX("<label for='opt-cb-autorender'>Render while typing</label>"); |
|
442
|
CX("</span>"); |
|
443
|
CX("<span class='labeled-input'>"); |
|
444
|
CX("<a href='?legacy'>Legacy mode</a>"); |
|
445
|
CX("</span>"); |
|
446
|
CX("</div><!-- options wrapper -->"); |
|
447
|
} CX("</fieldset>"); |
|
448
|
CX("<div id='main-wrapper' class=''>"); { |
|
449
|
CX("<fieldset class='zone-wrapper input'>"); { |
|
450
|
CX("<legend><div class='button-bar'>"); |
|
451
|
CX("<button id='btn-render' " |
|
452
|
"title='Ctrl-Enter/Shift-Enter'>Render</button>"); |
|
453
|
CX("<button id='btn-clear'>Clear Input</button>"); |
|
454
|
CX("</div></legend>"); |
|
455
|
CX("<div><textarea id='input'"); |
|
456
|
CX("placeholder='Pikchr input. Ctrl-enter/shift-enter runs it.'>"); |
|
457
|
CX("/**\n"); |
|
458
|
CX(" Use ctrl-enter or shift-enter to execute\n"); |
|
459
|
CX(" pikchr code. If only a subset is currently\n"); |
|
460
|
CX(" selected, only that part is evaluated.\n*/\n"); |
|
461
|
CX("%s</textarea></div>",zContent/*safe-for-%s*/); |
|
462
|
} CX("</fieldset><!-- .zone-wrapper.input -->"); |
|
463
|
CX("<fieldset class='zone-wrapper output'>"); { |
|
464
|
CX("<legend><div class='button-bar'>"); |
|
465
|
CX("<button id='btn-render-mode'>Render Mode</button> "); |
|
466
|
CX("<span style='white-space:nowrap'>" |
|
467
|
"<button id='preview-copy-button' " |
|
468
|
"title='Tap to copy to clipboard.'><span></span></button>" |
|
469
|
"<label for='preview-copy-button' " |
|
470
|
"title='Tap to copy to clipboard.'></label>" |
|
471
|
"</span>"); |
|
472
|
CX("</div></legend>"); |
|
473
|
CX("<div id='pikchr-output-wrapper'>"); |
|
474
|
CX("<div id='pikchr-output'></div>"); |
|
475
|
CX("<textarea class='hidden' id='pikchr-output-text'></textarea>"); |
|
476
|
CX("</div>"); |
|
477
|
} CX("</fieldset> <!-- .zone-wrapper.output -->"); |
|
478
|
} CX("</div><!-- #main-wrapper -->"); |
|
479
|
} CX("</div><!-- #view-split -->"); |
|
480
|
builtin_fossil_js_bundle_or("dom", "storage", "copybutton", NULL); |
|
481
|
builtin_request_js("fossil.page.pikchrshowasm.js"); |
|
482
|
builtin_fulfill_js_requests(); |
|
483
|
style_finish_page(); |
|
484
|
} |
|
485
|
|
|
486
|
|
|
487
|
/* |
|
488
|
** COMMAND: pikchr* |
|
489
|
** |
|
490
|
** Usage: %fossil pikchr [options] ?INFILE? ?OUTFILE? |
|
491
|
** |
|
492
|
** Accepts a pikchr script as input and outputs the rendered script as |
|
493
|
** an SVG graphic. The INFILE and OUTFILE options default to stdin |
|
494
|
** resp. stdout, and the names "-" can be used as aliases for those |
|
495
|
** streams. |
|
496
|
** |
|
497
|
** Options: |
|
498
|
** -div On success, add a DIV wrapper around the |
|
499
|
** resulting SVG output which limits its max-width to |
|
500
|
** its computed maximum ideal size |
|
501
|
** |
|
502
|
** -div-indent Like -div but indent the div |
|
503
|
** |
|
504
|
** -div-center Like -div but center the div |
|
505
|
** |
|
506
|
** -div-left Like -div but float the div left |
|
507
|
** |
|
508
|
** -div-right Like -div but float the div right |
|
509
|
** |
|
510
|
** -div-toggle Set the 'toggle' CSS class on the div (used by the |
|
511
|
** JavaScript-side post-processor) |
|
512
|
** |
|
513
|
** -div-source Set the 'source' CSS class on the div, which tells |
|
514
|
** CSS to hide the SVG and reveal the source by default. |
|
515
|
** |
|
516
|
** -src Store the input pikchr's source code in the output as |
|
517
|
** a separate element adjacent to the SVG one. Implied |
|
518
|
** by -div-source. |
|
519
|
** |
|
520
|
** -dark Change pikchr colors to assume a dark-mode theme. |
|
521
|
** |
|
522
|
** |
|
523
|
** The -div-indent/center/left/right flags may not be combined. |
|
524
|
*/ |
|
525
|
void pikchr_cmd(void){ |
|
526
|
Blob bIn = empty_blob; |
|
527
|
Blob bOut = empty_blob; |
|
528
|
const char * zInfile = "-"; |
|
529
|
const char * zOutfile = "-"; |
|
530
|
int isErr = 0; |
|
531
|
int pikFlags = find_option("src",0,0)!=0 |
|
532
|
? PIKCHR_PROCESS_SRC : 0; |
|
533
|
|
|
534
|
if(find_option("div",0,0)!=0){ |
|
535
|
pikFlags |= PIKCHR_PROCESS_DIV; |
|
536
|
}else if(find_option("div-indent",0,0)!=0){ |
|
537
|
pikFlags |= PIKCHR_PROCESS_DIV_INDENT; |
|
538
|
}else if(find_option("div-center",0,0)!=0){ |
|
539
|
pikFlags |= PIKCHR_PROCESS_DIV_CENTER; |
|
540
|
}else if(find_option("div-float-left",0,0)!=0){ |
|
541
|
pikFlags |= PIKCHR_PROCESS_DIV_FLOAT_LEFT; |
|
542
|
}else if(find_option("div-float-right",0,0)!=0){ |
|
543
|
pikFlags |= PIKCHR_PROCESS_DIV_FLOAT_RIGHT; |
|
544
|
} |
|
545
|
if(find_option("div-toggle",0,0)!=0){ |
|
546
|
pikFlags |= PIKCHR_PROCESS_DIV_TOGGLE; |
|
547
|
} |
|
548
|
if(find_option("div-source",0,0)!=0){ |
|
549
|
pikFlags |= PIKCHR_PROCESS_DIV_SOURCE | PIKCHR_PROCESS_SRC; |
|
550
|
} |
|
551
|
if(find_option("dark",0,0)!=0){ |
|
552
|
pikFlags |= PIKCHR_PROCESS_DARK_MODE; |
|
553
|
} |
|
554
|
|
|
555
|
verify_all_options(); |
|
556
|
if(g.argc>4){ |
|
557
|
usage("?INFILE? ?OUTFILE?"); |
|
558
|
} |
|
559
|
if(g.argc>2){ |
|
560
|
zInfile = g.argv[2]; |
|
561
|
} |
|
562
|
if(g.argc>3){ |
|
563
|
zOutfile = g.argv[3]; |
|
564
|
} |
|
565
|
blob_read_from_file(&bIn, zInfile, ExtFILE); |
|
566
|
isErr = pikchr_process(blob_str(&bIn), pikFlags, &bOut); |
|
567
|
if(isErr){ |
|
568
|
fossil_fatal("pikchr ERROR: %b", &bOut); |
|
569
|
}else{ |
|
570
|
blob_write_to_file(&bOut, zOutfile); |
|
571
|
} |
|
572
|
blob_reset(&bIn); |
|
573
|
blob_reset(&bOut); |
|
574
|
} |
|
575
|
|