Fossil SCM

fossil-scm / src / markdown_html.c
Blame History Raw 1047 lines
1
/*
2
** Copyright (c) 2012 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 callbacks for the markdown parser that generate
19
** XHTML output.
20
*/
21
22
#include "config.h"
23
#include "markdown_html.h"
24
25
#if INTERFACE
26
27
void markdown_to_html(
28
struct Blob *input_markdown,
29
struct Blob *output_title,
30
struct Blob *output_body);
31
32
#endif /* INTERFACE */
33
34
/*
35
** Markdown-internal helper for generating unique link reference IDs.
36
** Fields provide typed interpretation of the underline memory buffer.
37
*/
38
typedef union bitfield64_t bitfield64_t;
39
union bitfield64_t{
40
char c[8]; /* interpret as the array of signed characters */
41
unsigned char b[8]; /* interpret as the array of unsigned characters */
42
};
43
44
/*
45
** An instance of the following structure is passed through the
46
** "opaque" pointer.
47
*/
48
typedef struct MarkdownToHtml MarkdownToHtml;
49
struct MarkdownToHtml {
50
Blob *output_title; /* Store the title here */
51
bitfield64_t unique; /* Enables construction of unique #id elements */
52
53
#ifndef FOOTNOTES_WITHOUT_URI
54
Blob reqURI; /* REQUEST_URI with escaped quotes */
55
#endif
56
};
57
58
59
/* INTER_BLOCK -- skip a line between block level elements */
60
#define INTER_BLOCK(ob) \
61
do { if( blob_size(ob)>0 ) blob_append_char(ob, '\n'); } while (0)
62
63
/*
64
** FOOTNOTES_WITHOUT_URI macro was introduced by [2c1f8f3592ef00e0]
65
** to enable flexibility in rendering of footnote-specific hyperlinks.
66
** It may be defined for a particular build in order to omit
67
** full REQUEST_URIs within footnote-specific (and page-local) hyperlinks.
68
** This *is* used for the builds that incorporate 'base-href-fix' branch
69
** (which in turn fixes footnotes on the preview tab of /wikiedit page).
70
*/
71
#ifndef FOOTNOTES_WITHOUT_URI
72
#define BLOB_APPEND_URI(dest,ctx) blob_appendb(dest,&((ctx)->reqURI))
73
#else
74
#define BLOB_APPEND_URI(dest,ctx)
75
#endif
76
77
/* Converts an integer to a textual base26 representation
78
** with proper null-termination.
79
* Return empty string if that integer is negative. */
80
static bitfield64_t to_base26(int i, int uppercase){
81
bitfield64_t x;
82
int j;
83
memset( &x, 0, sizeof(x) );
84
if( i >= 0 ){
85
for(j=7; j >= 0; j--){
86
x.b[j] = (unsigned char)(uppercase?'A':'a') + i%26;
87
if( (i /= 26) == 0 ) break;
88
}
89
assert( j > 0 ); /* because 2^32 < 26^7 */
90
for(i=0; i<8-j; i++) x.b[i] = x.b[i+j];
91
for( ; i<8 ; i++) x.b[i] = 0;
92
}
93
assert( x.c[7] == 0 );
94
return x;
95
}
96
97
/* HTML escapes
98
**
99
** html_escape() converts < to &lt;, > to &gt;, and & to &amp;.
100
** html_quote() goes further and converts " into &quot; and ' in &#39;.
101
*/
102
static void html_quote(struct Blob *ob, const char *data, size_t size){
103
size_t beg = 0, i = 0;
104
while( i<size ){
105
beg = i;
106
while( i<size
107
&& data[i]!='<'
108
&& data[i]!='>'
109
&& data[i]!='"'
110
&& data[i]!='&'
111
&& data[i]!='\''
112
){
113
i++;
114
}
115
blob_append(ob, data+beg, i-beg);
116
while( i<size ){
117
if( data[i]=='<' ){
118
blob_append_literal(ob, "&lt;");
119
}else if( data[i]=='>' ){
120
blob_append_literal(ob, "&gt;");
121
}else if( data[i]=='&' ){
122
blob_append_literal(ob, "&amp;");
123
}else if( data[i]=='"' ){
124
blob_append_literal(ob, "&quot;");
125
}else if( data[i]=='\'' ){
126
blob_append_literal(ob, "&#39;");
127
}else{
128
break;
129
}
130
i++;
131
}
132
}
133
}
134
static void html_escape(struct Blob *ob, const char *data, size_t size){
135
size_t beg = 0, i = 0;
136
while( i<size ){
137
beg = i;
138
while( i<size
139
&& data[i]!='<'
140
&& data[i]!='>'
141
&& data[i]!='&'
142
){
143
i++;
144
}
145
blob_append(ob, data+beg, i-beg);
146
while( i<size ){
147
if( data[i]=='<' ){
148
blob_append_literal(ob, "&lt;");
149
}else if( data[i]=='>' ){
150
blob_append_literal(ob, "&gt;");
151
}else if( data[i]=='&' ){
152
blob_append_literal(ob, "&amp;");
153
}else{
154
break;
155
}
156
i++;
157
}
158
}
159
}
160
161
162
/* HTML block tags */
163
164
/* Size of the prolog: "<div class='markdown'>\n" */
165
#define PROLOG_SIZE 23
166
167
static void html_prolog(struct Blob *ob, void *opaque){
168
INTER_BLOCK(ob);
169
blob_append_literal(ob, "<div class=\"markdown\">\n");
170
assert( blob_size(ob)==PROLOG_SIZE );
171
}
172
173
static void html_epilog(struct Blob *ob, void *opaque){
174
INTER_BLOCK(ob);
175
blob_append_literal(ob, "</div>\n");
176
}
177
178
static void html_blockhtml(struct Blob *ob, struct Blob *text, void *opaque){
179
char *data = blob_buffer(text);
180
size_t size = blob_size(text);
181
Blob *title = ((MarkdownToHtml*)opaque)->output_title;
182
while( size>0 && fossil_isspace(data[0]) ){ data++; size--; }
183
while( size>0 && fossil_isspace(data[size-1]) ){ size--; }
184
/* If the first raw block is an <h1> element, then use it as the title. */
185
if( blob_size(ob)<=PROLOG_SIZE
186
&& size>9
187
&& title!=0
188
&& sqlite3_strnicmp("<h1",data,3)==0
189
&& sqlite3_strnicmp("</h1>", &data[size-5],5)==0
190
){
191
int nTag = html_tag_length(data);
192
blob_append(title, data+nTag, size - nTag - 5);
193
return;
194
}
195
INTER_BLOCK(ob);
196
blob_append(ob, data, size);
197
blob_append_literal(ob, "\n");
198
}
199
200
static void html_blockcode(struct Blob *ob, struct Blob *text, void *opaque){
201
INTER_BLOCK(ob);
202
blob_append_literal(ob, "<pre><code>");
203
html_escape(ob, blob_buffer(text), blob_size(text));
204
blob_append_literal(ob, "</code></pre>\n");
205
}
206
207
static void html_blockquote(struct Blob *ob, struct Blob *text, void *opaque){
208
INTER_BLOCK(ob);
209
blob_append_literal(ob, "<blockquote>\n");
210
blob_appendb(ob, text);
211
blob_append_literal(ob, "</blockquote>\n");
212
}
213
214
static void html_header(
215
struct Blob *ob,
216
struct Blob *text,
217
int level,
218
void *opaque
219
){
220
struct Blob *title = ((MarkdownToHtml*)opaque)->output_title;
221
char *z = 0;
222
int i,j;
223
/* The first header at the beginning of a text is considered as
224
* a title and not output. */
225
if( blob_size(ob)<=PROLOG_SIZE && title!=0 && blob_size(title)==0 ){
226
blob_appendb(title, text);
227
return;
228
}
229
INTER_BLOCK(ob);
230
z = fossil_strdup(blob_buffer(text));
231
if( z==0 ){
232
j = 0;
233
}else{
234
/*
235
** The GitHub "slugify" algorithm converts the text of a markdown header
236
** into a ID for that header. The algorithm is:
237
**
238
** 1. ASCII alphanumerics -> convert to lower case
239
** 2. Spaces, hyphens, underscores -> convert to '-'
240
** 3. Non-ASCII -> preserve as-is
241
** 4. Other punctuation -> remove
242
** 5. Multiple consecutive dashes -> collapse to one
243
** 6. Leading and trailing dashes -> remove
244
** 7. Markup <...> and &...; -> remove
245
**
246
** This implementation does the conversion in-place.
247
*/
248
for(i=j=0; z[i]; i++){
249
if( fossil_isalnum(z[i]) ){
250
z[j++] = fossil_tolower(z[i]);
251
}else if( fossil_isspace(z[i]) || z[i]=='-' || z[i]=='_' ){
252
if( j>0 && z[j-1]!='-' ) z[j++] = '-';
253
}else if( z[i]=='<' ){
254
do{ i++; }while( z[i]!=0 && z[i]!='>' );
255
}else if( z[i]=='&' ){
256
do{ i++; }while( z[i]!=0 && z[i]!=';' );
257
}else if( (z[i]&0x80)!=0 ){
258
z[j++] = z[i];
259
}
260
}
261
if( j>0 && z[j-1]=='-' ) j--;
262
z[j] = 0;
263
}
264
if( j>0 ){
265
blob_appendf(ob, "<h%d id=\"%s\">", level, z);
266
}else{
267
blob_appendf(ob, "<h%d>", level);
268
}
269
blob_appendb(ob, text);
270
blob_appendf(ob, "</h%d>", level);
271
fossil_free(z);
272
}
273
274
static void html_hrule(struct Blob *ob, void *opaque){
275
INTER_BLOCK(ob);
276
blob_append_literal(ob, "<hr>\n");
277
}
278
279
280
static void html_list(
281
struct Blob *ob,
282
struct Blob *text,
283
int flags,
284
void *opaque
285
){
286
char ol[] = "ol";
287
char ul[] = "ul";
288
char *tag = (flags & MKD_LIST_ORDERED) ? ol : ul;
289
INTER_BLOCK(ob);
290
blob_appendf(ob, "<%s>\n", tag);
291
blob_appendb(ob, text);
292
blob_appendf(ob, "</%s>\n", tag);
293
}
294
295
static void html_list_item(
296
struct Blob *ob,
297
struct Blob *text,
298
int flags,
299
void *opaque
300
){
301
char *text_data = blob_buffer(text);
302
size_t text_size = blob_size(text);
303
while( text_size>0 && text_data[text_size-1]=='\n' ) text_size--;
304
blob_append_literal(ob, "<li>");
305
blob_append(ob, text_data, text_size);
306
blob_append_literal(ob, "</li>\n");
307
}
308
309
static void html_paragraph(struct Blob *ob, struct Blob *text, void *opaque){
310
INTER_BLOCK(ob);
311
blob_append_literal(ob, "<p>");
312
blob_appendb(ob, text);
313
blob_append_literal(ob, "</p>\n");
314
}
315
316
317
static void html_table(
318
struct Blob *ob,
319
struct Blob *head_row,
320
struct Blob *rows,
321
void *opaque
322
){
323
INTER_BLOCK(ob);
324
blob_append_literal(ob, "<table class='md-table'>\n");
325
if( head_row && blob_size(head_row)>0 ){
326
blob_append_literal(ob, "<thead>\n");
327
blob_appendb(ob, head_row);
328
blob_append_literal(ob, "</thead>\n<tbody>\n");
329
}
330
if( rows ){
331
blob_appendb(ob, rows);
332
}
333
if( head_row && blob_size(head_row)>0 ){
334
blob_append_literal(ob, "</tbody>\n");
335
}
336
blob_append_literal(ob, "</table>\n");
337
}
338
339
static void html_table_cell(
340
struct Blob *ob,
341
struct Blob *text,
342
int flags,
343
void *opaque
344
){
345
if( flags & MKD_CELL_HEAD ){
346
blob_append_literal(ob, " <th");
347
}else{
348
blob_append_literal(ob, " <td");
349
}
350
switch( flags & MKD_CELL_ALIGN_MASK ){
351
case MKD_CELL_ALIGN_LEFT: {
352
blob_append_literal(ob, " style=\"text-align:left\"");
353
break;
354
}
355
case MKD_CELL_ALIGN_RIGHT: {
356
blob_append_literal(ob, " style=\"text-align:right\"");
357
break;
358
}
359
case MKD_CELL_ALIGN_CENTER: {
360
blob_append_literal(ob, " style=\"text-align:center\"");
361
break;
362
}
363
}
364
blob_append_literal(ob, ">");
365
blob_appendb(ob, text);
366
if( flags & MKD_CELL_HEAD ){
367
blob_append_literal(ob, "</th>\n");
368
}else{
369
blob_append_literal(ob, "</td>\n");
370
}
371
}
372
373
static void html_table_row(
374
struct Blob *ob,
375
struct Blob *cells,
376
int flags,
377
void *opaque
378
){
379
blob_append_literal(ob, " <tr>\n");
380
blob_appendb(ob, cells);
381
blob_append_literal(ob, " </tr>\n");
382
}
383
384
/*
385
** Render a token of user provided classes.
386
** If bHTML is true then render HTML for (presumably) visible text,
387
** otherwise just a space-separated list of the derived classes.
388
*/
389
static void append_footnote_upc(
390
struct Blob *ob,
391
const struct Blob *upc, /* token of user-provided classes */
392
int bHTML
393
){
394
const char *z = blob_buffer(upc);
395
int i, n = blob_size(upc);
396
397
if( n<3 ) return;
398
assert( z[0]=='.' && z[n-1] == ':' );
399
if( bHTML ){
400
blob_append_literal(ob, "<span class='fn-upc'>"
401
"<span class='fn-upcDot'>.</span>");
402
}
403
n = 0;
404
do{
405
z++;
406
if( *z!='.' && *z!=':' ){
407
assert( fossil_isalnum(*z) || *z=='-' );
408
n++;
409
continue;
410
}
411
assert( n );
412
if( bHTML ) blob_append_literal(ob, "<span class='");
413
blob_append_literal(ob, "fn-upc-");
414
415
for(i=-n; i<0; i++){
416
blob_append_char(ob, fossil_tolower(z[i]) );
417
}
418
if( bHTML ){
419
blob_append_literal(ob, "'>");
420
blob_append(ob, z-n, n);
421
blob_append_literal(ob, "</span>");
422
}else{
423
blob_append_char(ob, ' ');
424
}
425
n = 0;
426
if( bHTML ){
427
if( *z==':' ){
428
blob_append_literal(ob,"<span class='fn-upcColon'>:</span>");
429
}else{
430
blob_append_literal(ob,"<span class='fn-upcDot'>.</span>");
431
}
432
}
433
}while( *z != ':' );
434
if( bHTML ) blob_append_literal(ob,"</span>\n");
435
}
436
437
static int html_footnote_ref(
438
struct Blob *ob, const struct Blob *span, const struct Blob *upc,
439
int iMark, int locus, void *opaque
440
){
441
const struct MarkdownToHtml* ctx = (struct MarkdownToHtml*)opaque;
442
const bitfield64_t l = to_base26(locus-1,0);
443
char pos[32];
444
memset(pos,0,32);
445
assert( locus > 0 );
446
/* expect BUGs if the following yields compiler warnings */
447
if( iMark > 0 ){ /* a regular reference to a footnote */
448
sqlite3_snprintf(sizeof(pos), pos, "%s-%d-%s", ctx->unique.c, iMark, l.c);
449
if(span && blob_size(span)) {
450
blob_append_literal(ob,"<span class='");
451
append_footnote_upc(ob, upc, 0);
452
blob_append_literal(ob,"notescope' id='noteref");
453
blob_appendf(ob,"%s'>",pos);
454
blob_appendb(ob, span);
455
blob_trim(ob);
456
blob_append_literal(ob,"<sup class='noteref'><a href='");
457
BLOB_APPEND_URI(ob, ctx);
458
blob_appendf(ob,"#footnote%s'>%d</a></sup></span>", pos, iMark);
459
}else{
460
blob_trim(ob);
461
blob_append_literal(ob,"<sup class='");
462
append_footnote_upc(ob, upc, 0);
463
blob_append_literal(ob,"noteref'><a href='");
464
BLOB_APPEND_URI(ob, ctx);
465
blob_appendf(ob,"#footnote%s' id='noteref%s'>%d</a></sup>",
466
pos, pos, iMark);
467
}
468
}else{ /* misreference */
469
assert( iMark == -1 );
470
471
sqlite3_snprintf(sizeof(pos), pos, "%s-%s", ctx->unique.c, l.c);
472
if(span && blob_size(span)) {
473
blob_appendf(ob, "<span class='notescope' id='misref%s'>", pos);
474
blob_appendb(ob, span);
475
blob_trim(ob);
476
blob_append_literal(ob, "<sup class='noteref misref'><a href='");
477
BLOB_APPEND_URI(ob, ctx);
478
blob_appendf(ob, "#misreference%s'>misref</a></sup></span>", pos);
479
}else{
480
blob_trim(ob);
481
blob_append_literal(ob, "<sup class='noteref misref'><a href='");
482
BLOB_APPEND_URI(ob, ctx);
483
blob_appendf(ob, "#misreference%s' id='misref%s'>", pos, pos);
484
blob_append_literal(ob, "misref</a></sup>");
485
}
486
}
487
return 1;
488
}
489
490
/* Render a single item of the footnotes list.
491
* Each backref gets a unique id to enable dynamic styling. */
492
static void html_footnote_item(
493
struct Blob *ob, const struct Blob *text, int iMark, int nUsed, void *opaque
494
){
495
const struct MarkdownToHtml* ctx = (struct MarkdownToHtml*)opaque;
496
const char * const unique = ctx->unique.c;
497
assert( nUsed >= 0 );
498
/* expect BUGs if the following yields compiler warnings */
499
500
if( iMark < 0 ){ /* misreferences */
501
assert( iMark == -1 );
502
assert( nUsed );
503
blob_append_literal(ob,"<li class='fn-misreference'>"
504
"<sup class='fn-backrefs'>");
505
if( nUsed == 1 ){
506
blob_appendf(ob,"<a id='misreference%s-a' href='", unique);
507
BLOB_APPEND_URI(ob, ctx);
508
blob_appendf(ob,"#misref%s-a'>^</a>", unique);
509
}else{
510
int i;
511
blob_append_char(ob, '^');
512
for(i=0; i<nUsed && i<26; i++){
513
const int c = i + (unsigned)'a';
514
blob_appendf(ob," <a id='misreference%s-%c' href='", unique,c);
515
BLOB_APPEND_URI(ob, ctx);
516
blob_appendf(ob,"#misref%s-%c'>%c</a>", unique,c, c);
517
}
518
if( i < nUsed ) blob_append_literal(ob," &hellip;");
519
}
520
blob_append_literal(ob,"</sup>\n<span>Misreference</span>");
521
}else if( iMark > 0 ){ /* regular, joined and overnested footnotes */
522
char pos[24];
523
int bJoin = 0;
524
#define _joined_footnote_indicator "<ul class='fn-joined'>"
525
#define _jfi_sz (sizeof(_joined_footnote_indicator)-1)
526
/* make.footnote_item() invocations should pass args accordingly */
527
const struct Blob *upc = text+1;
528
assert( text );
529
/* allow blob_size(text)==0 for constructs like [...](^ [] ()) */
530
memset(pos,0,24);
531
sqlite3_snprintf(sizeof(pos), pos, "%s-%d", unique, iMark);
532
blob_appendf(ob, "<li id='footnote%s' class='", pos);
533
if( nUsed ){
534
if( blob_size(text)>=_jfi_sz &&
535
!memcmp(blob_buffer(text),_joined_footnote_indicator,_jfi_sz)){
536
bJoin = 1;
537
blob_append_literal(ob, "fn-joined ");
538
}
539
append_footnote_upc(ob, upc, 0);
540
}else{
541
blob_append_literal(ob, "fn-toodeep ");
542
}
543
if( nUsed <= 1 ){
544
blob_append_literal(ob, "fn-monoref'><sup class='fn-backrefs'>");
545
blob_appendf(ob,"<a id='footnote%s-a' href='", pos);
546
BLOB_APPEND_URI(ob, ctx);
547
blob_appendf(ob,"#noteref%s-a'>^</a>", pos);
548
}else{
549
int i;
550
blob_append_literal(ob, "fn-polyref'><sup class='fn-backrefs'>^");
551
for(i=0; i<nUsed && i<26; i++){
552
const int c = i + (unsigned)'a';
553
blob_appendf(ob," <a id='footnote%s-%c' href='", pos,c);
554
BLOB_APPEND_URI(ob, ctx);
555
blob_appendf(ob,"#noteref%s-%c'>%c</a>", pos,c, c);
556
}
557
/* It's unlikely that so many backrefs will be usefull */
558
/* but maybe for some machine generated documents... */
559
for(; i<nUsed && i<676; i++){
560
const bitfield64_t l = to_base26(i,0);
561
blob_appendf(ob," <a id='footnote%s-%s' href='", pos, l.c);
562
BLOB_APPEND_URI(ob, ctx);
563
blob_appendf(ob,"#noteref%s-%s'>%s</a>", pos,l.c, l.c);
564
}
565
if( i < nUsed ) blob_append_literal(ob," &hellip;");
566
}
567
blob_append_literal(ob,"</sup>\n");
568
if( bJoin ){
569
blob_append_literal(ob,"<sup class='fn-joined'></sup><ul>");
570
blob_append(ob,blob_buffer(text)+_jfi_sz,blob_size(text)-_jfi_sz);
571
}else if( nUsed ){
572
append_footnote_upc(ob, upc, 1);
573
blob_appendb(ob, text);
574
}else{
575
blob_append_literal(ob,"<i></i>\n"
576
"<pre><code class='language-markdown'>");
577
if( blob_size(upc) ){
578
blob_appendb(ob, upc);
579
}
580
html_escape(ob, blob_buffer(text), blob_size(text));
581
blob_append_literal(ob,"</code></pre>");
582
}
583
#undef _joined_footnote_indicator
584
#undef _jfi_sz
585
}else{ /* a footnote was defined but wasn't referenced */
586
/* make.footnote_item() invocations should pass args accordingly */
587
const struct Blob *id = text-1, *upc = text+1;
588
assert( !nUsed );
589
assert( text );
590
assert( blob_size(text) );
591
assert( blob_size(id) );
592
blob_append_literal(ob,"<li class='fn-unreferenced'>\n[^&nbsp;<code>");
593
html_escape(ob, blob_buffer(id), blob_size(id));
594
blob_append_literal(ob, "</code>&nbsp;]<i></i>\n"
595
"<pre><code class='language-markdown'>");
596
if( blob_size(upc) ){
597
blob_appendb(ob, upc);
598
}
599
html_escape(ob, blob_buffer(text), blob_size(text));
600
blob_append_literal(ob,"</code></pre>");
601
}
602
blob_append_literal(ob, "\n</li>\n");
603
}
604
605
static void html_footnotes(
606
struct Blob *ob, const struct Blob *items, void *opaque
607
){
608
if( items && blob_size(items) ){
609
blob_append_literal(ob,
610
"\n<hr class='footnotes-separator'/>\n<ol class='footnotes'>\n");
611
blob_appendb(ob, items);
612
blob_append_literal(ob, "</ol>\n");
613
}
614
}
615
616
/* HTML span tags */
617
618
static int html_raw_html_tag(struct Blob *ob, struct Blob *text, void *opaque){
619
blob_append(ob, blob_buffer(text), blob_size(text));
620
return 1;
621
}
622
623
static int html_autolink(
624
struct Blob *ob,
625
struct Blob *link,
626
enum mkd_autolink type,
627
void *opaque
628
){
629
if( !link || blob_size(link)<=0 ) return 0;
630
blob_append_literal(ob, "<a href=\"");
631
if( type==MKDA_IMPLICIT_EMAIL ) blob_append_literal(ob, "mailto:");
632
html_quote(ob, blob_buffer(link), blob_size(link));
633
blob_append_literal(ob, "\">");
634
if( type==MKDA_EXPLICIT_EMAIL && blob_size(link)>7 ){
635
/* remove "mailto:" from displayed text */
636
html_escape(ob, blob_buffer(link)+7, blob_size(link)-7);
637
}else{
638
html_escape(ob, blob_buffer(link), blob_size(link));
639
}
640
blob_append_literal(ob, "</a>");
641
return 1;
642
}
643
644
/*
645
** Flags for use with/via pikchr_to_html_add_flags().
646
*/
647
static int pikchrToHtmlFlags = 0;
648
/*
649
** Sets additional pikchr_process() flags to use for all future calls
650
** to pikch_to_html(). This is intended to be used by commands such as
651
** test-wiki-render and test-markdown-render to set the
652
** PIKCHR_PROCESS_DARK_MODE flag for all embedded pikchr elements.
653
**
654
** Not all PIKCHR_PROCESS flags are legal, as pikchr_to_html()
655
** hard-codes a subset of flags and passing arbitrary flags here may
656
** interfere with that.
657
**
658
** The only tested/intended use of this function is to pass it either
659
** 0 or PIKCHR_PROCESS_DARK_MODE.
660
**
661
** Design note: this is not implemented as an additional argument to
662
** pikchr_to_html() because the commands for which dark-mode rendering
663
** are now supported (test-wiki-render and test-markdown-render) are
664
** far removed from their corresponding pikchr_to_html() calls and
665
** there is no direct path from those commands to those calls. A
666
** cleaner, but much more invasive, approach would be to add a flag to
667
** markdown_to_html(), extend the WIKI_... flags with
668
** WIKI_DARK_PIKCHR, and extend both wiki.c:Renderer and
669
** markdown_html.c:MarkdownToHtml to contain and pass on that flag.
670
*/
671
void pikchr_to_html_add_flags( int f ){
672
pikchrToHtmlFlags = f;
673
}
674
675
/*
676
** The nSrc bytes at zSrc[] are Pikchr input text (allegedly). Process that
677
** text and insert the result in place of the original.
678
*/
679
void pikchr_to_html(
680
Blob *ob, /* Write the generated SVG here */
681
const char *zSrc, int nSrc, /* The Pikchr source text */
682
const char *zArg, int nArg /* Addition arguments */
683
){
684
int pikFlags = PIKCHR_PROCESS_NONCE
685
| PIKCHR_PROCESS_DIV
686
| PIKCHR_PROCESS_SRC
687
| PIKCHR_PROCESS_ERR_PRE
688
| pikchrToHtmlFlags;
689
Blob bSrc = empty_blob;
690
const char *zPikVar;
691
double rPikVar;
692
693
while( nArg>0 ){
694
int i;
695
for(i=0; i<nArg && !fossil_isspace(zArg[i]); i++){}
696
if( i==6 && strncmp(zArg, "center", 6)==0 ){
697
pikFlags |= PIKCHR_PROCESS_DIV_CENTER;
698
}else if( i==6 && strncmp(zArg, "indent", 6)==0 ){
699
pikFlags |= PIKCHR_PROCESS_DIV_INDENT;
700
}else if( i==10 && strncmp(zArg, "float-left", 10)==0 ){
701
pikFlags |= PIKCHR_PROCESS_DIV_FLOAT_LEFT;
702
}else if( i==11 && strncmp(zArg, "float-right", 11)==0 ){
703
pikFlags |= PIKCHR_PROCESS_DIV_FLOAT_RIGHT;
704
}else if( i==6 && strncmp(zArg, "toggle", 6)==0 ){
705
pikFlags |= PIKCHR_PROCESS_DIV_TOGGLE;
706
}else if( i==6 && strncmp(zArg, "source", 6)==0 ){
707
pikFlags |= PIKCHR_PROCESS_DIV_SOURCE;
708
}else if( i==13 && strncmp(zArg, "source-inline", 13)==0 ){
709
pikFlags |= PIKCHR_PROCESS_DIV_SOURCE_INLINE;
710
}
711
while( i<nArg && fossil_isspace(zArg[i]) ){ i++; }
712
zArg += i;
713
nArg -= i;
714
}
715
if( skin_detail_boolean("white-foreground") ){
716
pikFlags |= 0x02; /* PIKCHR_DARK_MODE */
717
}
718
zPikVar = skin_detail("pikchr-foreground");
719
if( zPikVar && zPikVar[0] ){
720
blob_appendf(&bSrc, "fgcolor = %s\n", zPikVar);
721
}
722
zPikVar = skin_detail("pikchr-background");
723
if( zPikVar && zPikVar[0] ){
724
blob_appendf(&bSrc, "bgcolor = %s\n", zPikVar);
725
}
726
zPikVar = skin_detail("pikchr-scale");
727
if( zPikVar
728
&& (rPikVar = atof(zPikVar))>=0.1
729
&& rPikVar<10.0
730
){
731
blob_appendf(&bSrc, "scale = %.13g\n", rPikVar);
732
}
733
zPikVar = skin_detail("pikchr-fontscale");
734
if( zPikVar
735
&& (rPikVar = atof(zPikVar))>=0.1
736
&& rPikVar<10.0
737
){
738
blob_appendf(&bSrc, "fontscale = %.13g\n", rPikVar);
739
}
740
blob_append(&bSrc, zSrc, nSrc)
741
/*have to dup input to ensure a NUL-terminated source string */;
742
pikchr_process(blob_str(&bSrc), pikFlags, ob);
743
blob_reset(&bSrc);
744
}
745
746
/* Invoked for `...` blocks where there are nSep grave accents in a
747
** row that serve as the delimiter. According to CommonMark:
748
**
749
** * https://spec.commonmark.org/0.29/#fenced-code-blocks
750
** * https://spec.commonmark.org/0.29/#code-spans
751
**
752
** If nSep is 1 or 2, then this is a code-span which is inline.
753
** If nSep is 3 or more, then this is a fenced code block
754
*/
755
static int html_codespan(
756
struct Blob *ob, /* Write the output here */
757
struct Blob *text, /* The stuff in between the code span marks */
758
int nSep, /* Number of grave accents marks as delimiters */
759
void *opaque
760
){
761
if( text==0 ){
762
/* no-op */
763
}else if( nSep<=2 ){
764
/* One or two graves: an in-line code span */
765
blob_append_literal(ob, "<code>");
766
html_escape(ob, blob_buffer(text), blob_size(text));
767
blob_append_literal(ob, "</code>");
768
}else{
769
/* Three or more graves: a fenced code block */
770
int n = blob_size(text);
771
const char *z = blob_buffer(text);
772
int i;
773
for(i=0; i<n && z[i]!='\n'; i++){}
774
if( i>=n ){
775
blob_appendf(ob, "<pre><code>%#h</code></pre>", n, z);
776
}else{
777
int k, j;
778
i++;
779
for(k=0; k<i && fossil_isspace(z[k]); k++){}
780
if( k==i ){
781
blob_appendf(ob, "<pre><code>%#h</code></pre>", n-i, z+i);
782
}else{
783
for(j=k+1; j<i && !fossil_isspace(z[j]); j++){}
784
if( j-k==6 && strncmp(z+k,"pikchr",6)==0 ){
785
while( j<i && fossil_isspace(z[j]) ){ j++; }
786
pikchr_to_html(ob, z+i, n-i, z+j, i-j);
787
}else{
788
blob_appendf(ob, "<pre><code class='language-%#h'>%#h</code></pre>",
789
j-k, z+k, n-i, z+i);
790
}
791
}
792
}
793
}
794
return 1;
795
}
796
797
static int html_double_emphasis(
798
struct Blob *ob,
799
struct Blob *text,
800
char c,
801
void *opaque
802
){
803
blob_append_literal(ob, "<strong>");
804
blob_appendb(ob, text);
805
blob_append_literal(ob, "</strong>");
806
return 1;
807
}
808
809
static int html_emphasis(
810
struct Blob *ob,
811
struct Blob *text,
812
char c,
813
void *opaque
814
){
815
blob_append_literal(ob, "<em>");
816
blob_appendb(ob, text);
817
blob_append_literal(ob, "</em>");
818
return 1;
819
}
820
821
static int html_image(
822
struct Blob *ob,
823
struct Blob *link,
824
struct Blob *title,
825
struct Blob *alt,
826
void *opaque
827
){
828
blob_append_literal(ob, "<img src=\"");
829
html_quote(ob, blob_buffer(link), blob_size(link));
830
blob_append_literal(ob, "\" alt=\"");
831
html_quote(ob, blob_buffer(alt), blob_size(alt));
832
if( title && blob_size(title)>0 ){
833
blob_append_literal(ob, "\" title=\"");
834
html_quote(ob, blob_buffer(title), blob_size(title));
835
}
836
blob_append_literal(ob, "\">");
837
return 1;
838
}
839
840
static int html_linebreak(struct Blob *ob, void *opaque){
841
blob_append_literal(ob, "<br>\n");
842
return 1;
843
}
844
845
static int html_link(
846
struct Blob *ob,
847
struct Blob *link,
848
struct Blob *title,
849
struct Blob *content,
850
void *opaque
851
){
852
char *zLink = blob_buffer(link);
853
char *zTitle = title!=0 && blob_size(title)>0 ? blob_str(title) : 0;
854
char zClose[20];
855
856
if( zLink==0 || zLink[0]==0 ){
857
zClose[0] = 0;
858
}else{
859
static const int flags =
860
WIKI_NOBADLINKS |
861
WIKI_MARKDOWNLINKS
862
;
863
wiki_resolve_hyperlink(ob, flags, zLink, zClose, sizeof(zClose), 0, zTitle);
864
}
865
if( blob_size(content)==0 ){
866
if( link ) blob_appendb(ob, link);
867
}else{
868
blob_appendb(ob, content);
869
}
870
blob_append(ob, zClose, -1);
871
return 1;
872
}
873
874
/* Invoked for @name and #tag tagged words, marked up in the
875
** output text in a way that JS and CSS can do something
876
** interesting with them. This isn't standard Markdown, so
877
** it's implementation-specific what occurs here. More, each
878
** Fossil feature using Markdown is free to apply styling and
879
** behavior to these in feature-specific ways.
880
*/
881
static int html_tagspan(
882
struct Blob *ob, /* Write the output here */
883
struct Blob *text, /* The word after the tag character */
884
enum mkd_tagspan type, /* Which type of tagspan we're creating */
885
void *opaque
886
){
887
if( text==0 ){
888
/* no-op */
889
}else{
890
char cPrefix = '!';
891
blob_append_literal(ob, "<span data-");
892
switch (type) {
893
case MKDT_HASHTAG:
894
cPrefix = '#'; blob_append_literal(ob, "hashtag"); break;
895
case MKDT_NUMTAG:
896
cPrefix = '#'; blob_append_literal(ob, "numtag"); break;
897
}
898
blob_append_literal(ob, "=\"");
899
html_quote(ob, blob_buffer(text), blob_size(text));
900
blob_append_literal(ob, "\"");
901
blob_appendf(ob, ">%c%b</span>", cPrefix,text);
902
}
903
return 1;
904
}
905
906
static int html_triple_emphasis(
907
struct Blob *ob,
908
struct Blob *text,
909
char c,
910
void *opaque
911
){
912
blob_append_literal(ob, "<strong><em>");
913
blob_appendb(ob, text);
914
blob_append_literal(ob, "</em></strong>");
915
return 1;
916
}
917
918
919
static void html_normal_text(struct Blob *ob, struct Blob *text, void *opaque){
920
html_escape(ob, blob_buffer(text), blob_size(text));
921
}
922
923
/*
924
** Convert markdown into HTML.
925
**
926
** The document title is placed in output_title if not NULL. Or if
927
** output_title is NULL, the document title appears in the body.
928
*/
929
void markdown_to_html(
930
struct Blob *input_markdown, /* Markdown content to be rendered */
931
struct Blob *output_title, /* Put title here. May be NULL */
932
struct Blob *output_body /* Put document body here. */
933
){
934
struct mkd_renderer html_renderer = {
935
/* prolog and epilog */
936
html_prolog,
937
html_epilog,
938
html_footnotes,
939
940
/* block level elements */
941
html_blockcode,
942
html_blockquote,
943
html_blockhtml,
944
html_header,
945
html_hrule,
946
html_list,
947
html_list_item,
948
html_paragraph,
949
html_table,
950
html_table_cell,
951
html_table_row,
952
html_footnote_item,
953
954
/* span level elements */
955
html_autolink,
956
html_codespan,
957
html_double_emphasis,
958
html_emphasis,
959
html_image,
960
html_linebreak,
961
html_link,
962
html_raw_html_tag,
963
html_tagspan,
964
html_triple_emphasis,
965
html_footnote_ref,
966
967
/* low level elements */
968
0, /* entity */
969
html_normal_text,
970
971
/* misc. parameters */
972
"*_", /* emph_chars */
973
0 /* opaque */
974
};
975
static int invocation = -1; /* no marker for the first document */
976
static const char* zRU = 0; /* REQUEST_URI with escaped quotes */
977
MarkdownToHtml context;
978
memset(&context, 0, sizeof(context));
979
context.output_title = output_title;
980
context.unique = to_base26(invocation++,1);
981
if( !zRU ) zRU = escape_quotes(PD("REQUEST_URI",""));
982
#ifndef FOOTNOTES_WITHOUT_URI
983
blob_set( &context.reqURI, zRU );
984
#endif
985
html_renderer.opaque = &context;
986
if( output_title ) blob_reset(output_title);
987
blob_reset(output_body);
988
markdown(output_body, input_markdown, &html_renderer);
989
}
990
991
/*
992
** Undo HTML escapes in Blob p. In other words convert:
993
**
994
** &amp; -> &
995
** &lt; -> <
996
** &gt; -> >
997
** &quot; -> "
998
** &#NNN; -> ascii character NNN
999
*/
1000
void markdown_dehtmlize_blob(Blob *p){
1001
char *z;
1002
unsigned int j, k;
1003
1004
z = p->aData;
1005
for(j=k=0; j<p->nUsed; j++){
1006
char c = z[j];
1007
if( c=='&' ){
1008
if( z[j+1]=='#' && fossil_isdigit(z[j+2]) ){
1009
int n = 3;
1010
int x = z[j+2] - '0';
1011
if( fossil_isdigit(z[j+3]) ){
1012
x = x*10 + z[j+3] - '0';
1013
n++;
1014
if( fossil_isdigit(z[j+4]) ){
1015
x = x*10 + z[j+4] - '0';
1016
n++;
1017
}
1018
}
1019
if( z[j+n]==';' ){
1020
z[k++] = (char)x;
1021
j += n;
1022
}else{
1023
z[k++] = c;
1024
}
1025
}else if( memcmp(&z[j],"&lt;",4)==0 ){
1026
z[k++] = '<';
1027
j += 3;
1028
}else if( memcmp(&z[j],"&gt;",4)==0 ){
1029
z[k++] = '>';
1030
j += 3;
1031
}else if( memcmp(&z[j],"&quot;",6)==0 ){
1032
z[k++] = '"';
1033
j += 5;
1034
}else if( memcmp(&z[j],"&amp;",5)==0 ){
1035
z[k++] = '&';
1036
j += 4;
1037
}else{
1038
z[k++] = c;
1039
}
1040
}else{
1041
z[k++] = c;
1042
}
1043
}
1044
z[k] = 0;
1045
p->nUsed = k;
1046
}
1047

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button