Fossil SCM

fossil-scm / src / wikiformat.c
Blame History Raw 3189 lines
1
/*
2
** Copyright (c) 2007 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 code to do formatting of wiki text.
19
*/
20
#include "config.h"
21
#include <assert.h>
22
#include "wikiformat.h"
23
24
#if INTERFACE
25
/*
26
** Allowed wiki transformation operations
27
*/
28
#define WIKI_HTMLONLY 0x0001 /* HTML markup only. No wiki */
29
#define WIKI_INLINE 0x0002 /* Do not surround with <p>..</p> */
30
/* available for reuse: 0x0004 --- formerly WIKI_NOBLOCK */
31
#define WIKI_BUTTONS 0x0008 /* Allow sub-menu buttons */
32
#define WIKI_NOBADLINKS 0x0010 /* Ignore broken hyperlinks */
33
#define WIKI_LINKSONLY 0x0020 /* No markup. Only decorate links */
34
#define WIKI_NEWLINE 0x0040 /* Honor \n - break lines at each \n */
35
#define WIKI_MARKDOWNLINKS 0x0080 /* Resolve hyperlinks as in markdown */
36
#define WIKI_SAFE 0x0100 /* Make the result safe for embedding */
37
#define WIKI_TARGET_BLANK 0x0200 /* Hyperlinks go to a new window */
38
#define WIKI_NOBRACKET 0x0400 /* Omit extra [..] around hyperlinks */
39
#define WIKI_ADMIN 0x0800 /* Ignore g.perm.Hyperlink */
40
#define WIKI_MARK 0x1000 /* Add <mark>..</mark> around problems */
41
42
/*
43
** Return values from wiki_convert
44
*/
45
#define RENDER_LINK 0x0001 /* One or more hyperlinks rendered */
46
#define RENDER_ENTITY 0x0002 /* One or more HTML entities (ex: &lt;) */
47
#define RENDER_TAG 0x0004 /* One or more HTML tags */
48
#define RENDER_BLOCKTAG 0x0008 /* One or more HTML block tags (ex: <p>) */
49
#define RENDER_BLOCK 0x0010 /* Block wiki (paragraphs, etc.) */
50
#define RENDER_MARK 0x0020 /* Output contains <mark>..</mark> */
51
#define RENDER_BADLINK 0x0100 /* Bad hyperlink syntax seen */
52
#define RENDER_BADTARGET 0x0200 /* Bad hyperlink target */
53
#define RENDER_BADTAG 0x0400 /* Bad HTML tag or tag syntax */
54
#define RENDER_BADENTITY 0x0800 /* Bad HTML entity syntax */
55
#define RENDER_BADHTML 0x1000 /* Bad HTML seen */
56
#define RENDER_ERROR 0x8000 /* Some other kind of error */
57
/* Composite values: */
58
#define RENDER_ANYERROR 0x9f00 /* Mask for any kind of error */
59
60
#endif /* INTERFACE */
61
62
63
/*
64
** These are the only markup attributes allowed.
65
*/
66
enum allowed_attr_t {
67
ATTR_ALIGN = 1,
68
ATTR_ALT,
69
ATTR_BGCOLOR,
70
ATTR_BORDER,
71
ATTR_CELLPADDING,
72
ATTR_CELLSPACING,
73
ATTR_CITE,
74
ATTR_CLASS,
75
ATTR_CLEAR,
76
ATTR_COLOR,
77
ATTR_COLSPAN,
78
ATTR_COMPACT,
79
ATTR_DATETIME,
80
ATTR_FACE,
81
ATTR_FOR,
82
ATTR_HEIGHT,
83
ATTR_HIGH,
84
ATTR_HREF,
85
ATTR_HSPACE,
86
ATTR_ID,
87
ATTR_LINKS,
88
ATTR_LOW,
89
ATTR_MAX,
90
ATTR_MEDIA,
91
ATTR_MIN,
92
ATTR_NAME,
93
ATTR_OPEN,
94
ATTR_OPTIMUM,
95
ATTR_ROWSPAN,
96
ATTR_SIZE,
97
ATTR_SIZES,
98
ATTR_SRC,
99
ATTR_SRCSET,
100
ATTR_START,
101
ATTR_STYLE,
102
ATTR_TARGET,
103
ATTR_TITLE,
104
ATTR_TYPE,
105
ATTR_VALIGN,
106
ATTR_VALUE,
107
ATTR_VSPACE,
108
ATTR_WIDTH
109
};
110
111
typedef uint64_t amsk_t;
112
#define AMSK_ALIGN ((amsk_t)1 << 0)
113
#define AMSK_ALT ((amsk_t)1 << 1)
114
#define AMSK_BGCOLOR ((amsk_t)1 << 2)
115
#define AMSK_BORDER ((amsk_t)1 << 3)
116
#define AMSK_CELLPADDING ((amsk_t)1 << 4)
117
#define AMSK_CELLSPACING ((amsk_t)1 << 5)
118
#define AMSK_CLASS ((amsk_t)1 << 6)
119
#define AMSK_CLEAR ((amsk_t)1 << 7)
120
#define AMSK_COLOR ((amsk_t)1 << 8)
121
#define AMSK_COLSPAN ((amsk_t)1 << 9)
122
#define AMSK_COMPACT ((amsk_t)1 << 10)
123
#define AMSK_FACE ((amsk_t)1 << 11)
124
#define AMSK_HEIGHT ((amsk_t)1 << 12)
125
#define AMSK_HREF ((amsk_t)1 << 13)
126
#define AMSK_HSPACE ((amsk_t)1 << 14)
127
#define AMSK_ID ((amsk_t)1 << 15)
128
#define AMSK_LINKS ((amsk_t)1 << 16)
129
#define AMSK_NAME ((amsk_t)1 << 17)
130
#define AMSK_OPEN ((amsk_t)1 << 18)
131
#define AMSK_ROWSPAN ((amsk_t)1 << 19)
132
#define AMSK_SIZE ((amsk_t)1 << 20)
133
#define AMSK_SRC ((amsk_t)1 << 21)
134
#define AMSK_START ((amsk_t)1 << 22)
135
#define AMSK_STYLE ((amsk_t)1 << 23)
136
#define AMSK_TARGET ((amsk_t)1 << 24)
137
#define AMSK_TITLE ((amsk_t)1 << 25)
138
#define AMSK_TYPE ((amsk_t)1 << 26)
139
#define AMSK_VALIGN ((amsk_t)1 << 27)
140
#define AMSK_VALUE ((amsk_t)1 << 28)
141
#define AMSK_VSPACE ((amsk_t)1 << 29)
142
#define AMSK_WIDTH ((amsk_t)1 << 30)
143
#define AMSK_CITE ((amsk_t)1 << 31)
144
#define AMSK_DATETIME ((amsk_t)1 << 32)
145
146
static const struct AllowedAttribute {
147
const char *zName;
148
amsk_t iMask;
149
} aAttribute[] = {
150
/* These indexes MUST line up with their
151
corresponding allowed_attr_t enum values.
152
*/
153
{ 0, 0 },
154
{ "align", AMSK_ALIGN },
155
{ "alt", AMSK_ALT },
156
{ "bgcolor", AMSK_BGCOLOR },
157
{ "border", AMSK_BORDER },
158
{ "cellpadding", AMSK_CELLPADDING },
159
{ "cellspacing", AMSK_CELLSPACING },
160
{ "cite", AMSK_CITE },
161
{ "class", AMSK_CLASS },
162
{ "clear", AMSK_CLEAR },
163
{ "color", AMSK_COLOR },
164
{ "colspan", AMSK_COLSPAN },
165
{ "compact", AMSK_COMPACT },
166
{ "datetime", AMSK_DATETIME },
167
{ "face", AMSK_FACE },
168
{ "for", 0 },
169
{ "height", AMSK_HEIGHT },
170
{ "high", 0 },
171
{ "href", AMSK_HREF },
172
{ "hspace", AMSK_HSPACE },
173
{ "id", AMSK_ID },
174
{ "links", AMSK_LINKS },
175
{ "low", 0 },
176
{ "max", 0 },
177
{ "media", 0 },
178
{ "min", 0 },
179
{ "name", AMSK_NAME },
180
{ "open", AMSK_OPEN },
181
{ "optimum", 0 },
182
{ "rowspan", AMSK_ROWSPAN },
183
{ "size", AMSK_SIZE },
184
{ "sizes", 0 },
185
{ "src", AMSK_SRC },
186
{ "srcset", 0 },
187
{ "start", AMSK_START },
188
{ "style", AMSK_STYLE },
189
{ "target", AMSK_TARGET },
190
{ "title", AMSK_TITLE },
191
{ "type", AMSK_TYPE },
192
{ "valign", AMSK_VALIGN },
193
{ "value", AMSK_VALUE },
194
{ "vspace", AMSK_VSPACE },
195
{ "width", AMSK_WIDTH },
196
};
197
198
/*
199
** Use binary search to locate a tag in the aAttribute[] table.
200
*/
201
static int findAttr(const char *z){
202
int i, c, first, last;
203
first = 1;
204
last = count(aAttribute) - 1;
205
while( first<=last ){
206
i = (first+last)/2;
207
c = fossil_strcmp(aAttribute[i].zName, z);
208
if( c==0 ){
209
return i;
210
}else if( c<0 ){
211
first = i+1;
212
}else{
213
last = i-1;
214
}
215
}
216
return 0;
217
}
218
219
220
221
/*
222
** Allowed markup.
223
**
224
** Except for MARKUP_INVALID, this must all be in alphabetical order
225
** and in numerical sequence. The first markup type must be zero.
226
** The value for MARKUP_XYZ must correspond to the <xyz> entry
227
** in aMarkup[].
228
*/
229
enum markup_t {
230
MARKUP_INVALID = 0,
231
MARKUP_A,
232
MARKUP_ABBR,
233
MARKUP_ADDRESS,
234
MARKUP_HTML5_ARTICLE,
235
MARKUP_HTML5_ASIDE,
236
MARKUP_B,
237
MARKUP_BIG,
238
MARKUP_BLOCKQUOTE,
239
MARKUP_BR,
240
MARKUP_CAPTION,
241
MARKUP_CENTER,
242
MARKUP_CITE,
243
MARKUP_CODE,
244
MARKUP_COL,
245
MARKUP_COLGROUP,
246
MARKUP_DD,
247
MARKUP_DEL,
248
MARKUP_DETAILS,
249
MARKUP_DFN,
250
MARKUP_DIV,
251
MARKUP_DL,
252
MARKUP_DT,
253
MARKUP_EM,
254
MARKUP_FIGCAPTION,
255
MARKUP_FIGURE,
256
MARKUP_FONT,
257
MARKUP_HTML5_FOOTER,
258
MARKUP_H1,
259
MARKUP_H2,
260
MARKUP_H3,
261
MARKUP_H4,
262
MARKUP_H5,
263
MARKUP_H6,
264
MARKUP_HTML5_HEADER,
265
MARKUP_HR,
266
MARKUP_I,
267
MARKUP_IMG,
268
MARKUP_INS,
269
MARKUP_KBD,
270
MARKUP_LABEL,
271
MARKUP_LI,
272
MARKUP_MARK,
273
MARKUP_METER,
274
MARKUP_HTML5_NAV,
275
MARKUP_NOBR,
276
MARKUP_NOWIKI,
277
MARKUP_OL,
278
MARKUP_P,
279
MARKUP_PICTURE,
280
MARKUP_PRE,
281
MARKUP_PROGRESS,
282
MARKUP_Q,
283
MARKUP_S,
284
MARKUP_SAMP,
285
MARKUP_HTML5_SECTION,
286
MARKUP_SMALL,
287
MARKUP_SOURCE,
288
MARKUP_SPAN,
289
MARKUP_STRIKE,
290
MARKUP_STRONG,
291
MARKUP_SUB,
292
MARKUP_SUMMARY,
293
MARKUP_SUP,
294
MARKUP_TABLE,
295
MARKUP_TBODY,
296
MARKUP_TD,
297
MARKUP_TFOOT,
298
MARKUP_TH,
299
MARKUP_THEAD,
300
MARKUP_TIME,
301
MARKUP_TITLE,
302
MARKUP_TR,
303
MARKUP_TT,
304
MARKUP_U,
305
MARKUP_UL,
306
MARKUP_VAR,
307
MARKUP_VERBATIM,
308
MARKUP_WBR
309
};
310
311
/*
312
** The various markup is divided into the following types:
313
*/
314
#define MUTYPE_SINGLE 0x0001 /* <img>, <br>, or <hr> */
315
#define MUTYPE_BLOCK 0x0002 /* Forms a new paragraph. ex: <p>, <h2> */
316
#define MUTYPE_FONT 0x0004 /* Font changes. ex: <b>, <font>, <sub> */
317
#define MUTYPE_LIST 0x0010 /* Lists. <ol>, <ul>, or <dl> */
318
#define MUTYPE_LI 0x0020 /* List items. <li>, <dd>, <dt> */
319
#define MUTYPE_TABLE 0x0040 /* <table> */
320
#define MUTYPE_TR 0x0080 /* <tr> */
321
#define MUTYPE_TD 0x0100 /* <td> or <th> */
322
#define MUTYPE_SPECIAL 0x0200 /* <nowiki> or <verbatim> */
323
#define MUTYPE_HYPERLINK 0x0400 /* <a> */
324
325
/* MUTYPE values for elements that require strictly nested end-tags */
326
#define MUTYPE_Nested 0x0656
327
328
/*
329
** These markup types must have an end tag.
330
*/
331
#define MUTYPE_STACK (MUTYPE_BLOCK | MUTYPE_FONT | MUTYPE_LIST | MUTYPE_TABLE)
332
333
/*
334
** This markup types are allowed for "inline" text.
335
*/
336
#define MUTYPE_INLINE (MUTYPE_FONT | MUTYPE_HYPERLINK)
337
338
static const struct AllowedMarkup {
339
const char *zName; /* Name of the markup */
340
char iCode; /* The MARKUP_* code */
341
short int iType; /* The MUTYPE_* code */
342
amsk_t allowedAttr; /* Allowed attributes on this markup */
343
} aMarkup[] = {
344
{ 0, MARKUP_INVALID, 0, 0 },
345
{ "a", MARKUP_A, MUTYPE_HYPERLINK,
346
AMSK_HREF|AMSK_NAME|AMSK_CLASS|AMSK_TARGET|AMSK_STYLE|
347
AMSK_TITLE},
348
{ "abbr", MARKUP_ABBR, MUTYPE_FONT,
349
AMSK_ID|AMSK_CLASS|AMSK_STYLE|AMSK_TITLE },
350
{ "address", MARKUP_ADDRESS, MUTYPE_BLOCK, AMSK_STYLE },
351
{ "article", MARKUP_HTML5_ARTICLE, MUTYPE_BLOCK,
352
AMSK_ID|AMSK_CLASS|AMSK_STYLE },
353
{ "aside", MARKUP_HTML5_ASIDE, MUTYPE_BLOCK,
354
AMSK_ID|AMSK_CLASS|AMSK_STYLE },
355
{ "b", MARKUP_B, MUTYPE_FONT, AMSK_STYLE },
356
{ "big", MARKUP_BIG, MUTYPE_FONT, AMSK_STYLE },
357
{ "blockquote", MARKUP_BLOCKQUOTE, MUTYPE_BLOCK, AMSK_STYLE },
358
{ "br", MARKUP_BR, MUTYPE_SINGLE, AMSK_CLEAR },
359
{ "caption", MARKUP_CAPTION, MUTYPE_BLOCK,
360
AMSK_ID|AMSK_CLASS|AMSK_STYLE },
361
{ "center", MARKUP_CENTER, MUTYPE_BLOCK, AMSK_STYLE },
362
{ "cite", MARKUP_CITE, MUTYPE_FONT, AMSK_STYLE },
363
{ "code", MARKUP_CODE, MUTYPE_FONT, AMSK_STYLE },
364
{ "col", MARKUP_COL, MUTYPE_SINGLE,
365
AMSK_ALIGN|AMSK_CLASS|AMSK_COLSPAN|AMSK_WIDTH|AMSK_STYLE },
366
{ "colgroup", MARKUP_COLGROUP, MUTYPE_BLOCK,
367
AMSK_ALIGN|AMSK_CLASS|AMSK_COLSPAN|AMSK_WIDTH|AMSK_STYLE},
368
{ "dd", MARKUP_DD, MUTYPE_LI, AMSK_STYLE },
369
{ "del", MARKUP_DEL, MUTYPE_FONT, AMSK_STYLE },
370
{ "details", MARKUP_DETAILS, MUTYPE_BLOCK,
371
AMSK_ID|AMSK_CLASS|AMSK_STYLE|AMSK_OPEN },
372
{ "dfn", MARKUP_DFN, MUTYPE_FONT, AMSK_STYLE },
373
{ "div", MARKUP_DIV, MUTYPE_BLOCK,
374
AMSK_ID|AMSK_CLASS|AMSK_STYLE },
375
{ "dl", MARKUP_DL, MUTYPE_LIST,
376
AMSK_COMPACT|AMSK_STYLE },
377
{ "dt", MARKUP_DT, MUTYPE_LI, AMSK_STYLE },
378
{ "em", MARKUP_EM, MUTYPE_FONT, AMSK_STYLE },
379
{ "figcaption", MARKUP_FIGCAPTION, MUTYPE_BLOCK,
380
AMSK_ID|AMSK_CLASS|AMSK_STYLE },
381
{ "figure", MARKUP_FIGURE, MUTYPE_BLOCK,
382
AMSK_ID|AMSK_CLASS|AMSK_STYLE },
383
{ "font", MARKUP_FONT, MUTYPE_FONT,
384
AMSK_COLOR|AMSK_FACE|AMSK_SIZE|AMSK_STYLE },
385
{ "footer", MARKUP_HTML5_FOOTER, MUTYPE_BLOCK,
386
AMSK_ID|AMSK_CLASS|AMSK_STYLE },
387
{ "h1", MARKUP_H1, MUTYPE_BLOCK,
388
AMSK_ALIGN|AMSK_CLASS|AMSK_STYLE },
389
{ "h2", MARKUP_H2, MUTYPE_BLOCK,
390
AMSK_ALIGN|AMSK_CLASS|AMSK_STYLE },
391
{ "h3", MARKUP_H3, MUTYPE_BLOCK,
392
AMSK_ALIGN|AMSK_CLASS|AMSK_STYLE },
393
{ "h4", MARKUP_H4, MUTYPE_BLOCK,
394
AMSK_ALIGN|AMSK_CLASS|AMSK_STYLE },
395
{ "h5", MARKUP_H5, MUTYPE_BLOCK,
396
AMSK_ALIGN|AMSK_CLASS|AMSK_STYLE },
397
{ "h6", MARKUP_H6, MUTYPE_BLOCK,
398
AMSK_ALIGN|AMSK_CLASS|AMSK_STYLE },
399
{ "header", MARKUP_HTML5_HEADER, MUTYPE_BLOCK,
400
AMSK_ID|AMSK_CLASS|AMSK_STYLE },
401
{ "hr", MARKUP_HR, MUTYPE_SINGLE,
402
AMSK_ALIGN|AMSK_COLOR|AMSK_SIZE|AMSK_WIDTH|
403
AMSK_STYLE|AMSK_CLASS },
404
{ "i", MARKUP_I, MUTYPE_FONT, AMSK_STYLE },
405
{ "img", MARKUP_IMG, MUTYPE_SINGLE,
406
AMSK_ALIGN|AMSK_ALT|AMSK_BORDER|AMSK_HEIGHT|
407
AMSK_HSPACE|AMSK_SRC|AMSK_VSPACE|AMSK_WIDTH|AMSK_STYLE },
408
{ "ins", MARKUP_INS, MUTYPE_FONT, AMSK_STYLE },
409
{ "kbd", MARKUP_KBD, MUTYPE_FONT, AMSK_STYLE },
410
{ "label", MARKUP_LABEL, MUTYPE_FONT,
411
AMSK_ID|AMSK_CLASS|AMSK_STYLE },
412
{ "li", MARKUP_LI, MUTYPE_LI,
413
AMSK_TYPE|AMSK_VALUE|AMSK_STYLE },
414
{ "mark", MARKUP_MARK, MUTYPE_FONT,
415
AMSK_ID|AMSK_CLASS|AMSK_STYLE },
416
{ "meter", MARKUP_METER, MUTYPE_FONT,
417
AMSK_ID|AMSK_CLASS|AMSK_STYLE|AMSK_VALUE },
418
{ "nav", MARKUP_HTML5_NAV, MUTYPE_BLOCK,
419
AMSK_ID|AMSK_CLASS|AMSK_STYLE },
420
{ "nobr", MARKUP_NOBR, MUTYPE_FONT, 0 },
421
{ "nowiki", MARKUP_NOWIKI, MUTYPE_SPECIAL, 0 },
422
{ "ol", MARKUP_OL, MUTYPE_LIST,
423
AMSK_START|AMSK_TYPE|AMSK_COMPACT|AMSK_STYLE },
424
{ "p", MARKUP_P, MUTYPE_BLOCK,
425
AMSK_ALIGN|AMSK_CLASS|AMSK_STYLE },
426
{ "picture", MARKUP_PICTURE, MUTYPE_BLOCK,
427
AMSK_ID|AMSK_CLASS|AMSK_STYLE },
428
{ "pre", MARKUP_PRE, MUTYPE_BLOCK, AMSK_STYLE },
429
{ "progress", MARKUP_PROGRESS, MUTYPE_FONT,
430
AMSK_ID|AMSK_CLASS|AMSK_STYLE|AMSK_VALUE },
431
{ "q", MARKUP_Q, MUTYPE_FONT,
432
AMSK_CLASS|AMSK_STYLE|AMSK_CITE },
433
{ "s", MARKUP_S, MUTYPE_FONT, AMSK_STYLE },
434
{ "samp", MARKUP_SAMP, MUTYPE_FONT, AMSK_STYLE },
435
{ "section", MARKUP_HTML5_SECTION, MUTYPE_BLOCK,
436
AMSK_ID|AMSK_CLASS|AMSK_STYLE },
437
{ "small", MARKUP_SMALL, MUTYPE_FONT, AMSK_STYLE },
438
{ "source", MARKUP_SOURCE, MUTYPE_SINGLE,
439
AMSK_TYPE|AMSK_ID|AMSK_CLASS|AMSK_STYLE },
440
{ "span", MARKUP_SPAN, MUTYPE_BLOCK,
441
AMSK_ALIGN|AMSK_CLASS|AMSK_STYLE },
442
{ "strike", MARKUP_STRIKE, MUTYPE_FONT, AMSK_STYLE },
443
{ "strong", MARKUP_STRONG, MUTYPE_FONT, AMSK_STYLE },
444
{ "sub", MARKUP_SUB, MUTYPE_FONT, AMSK_STYLE },
445
{ "summary", MARKUP_SUMMARY, MUTYPE_BLOCK,
446
AMSK_ALIGN|AMSK_CLASS|AMSK_STYLE },
447
{ "sup", MARKUP_SUP, MUTYPE_FONT, AMSK_STYLE },
448
{ "table", MARKUP_TABLE, MUTYPE_TABLE,
449
AMSK_ALIGN|AMSK_BGCOLOR|AMSK_BORDER|AMSK_CELLPADDING|
450
AMSK_CELLSPACING|AMSK_HSPACE|AMSK_VSPACE|AMSK_CLASS|
451
AMSK_STYLE },
452
{ "tbody", MARKUP_TBODY, MUTYPE_BLOCK,
453
AMSK_ALIGN|AMSK_CLASS|AMSK_STYLE },
454
{ "td", MARKUP_TD, MUTYPE_TD,
455
AMSK_ALIGN|AMSK_BGCOLOR|AMSK_COLSPAN|
456
AMSK_ROWSPAN|AMSK_VALIGN|AMSK_CLASS|AMSK_STYLE },
457
{ "tfoot", MARKUP_TFOOT, MUTYPE_BLOCK,
458
AMSK_ALIGN|AMSK_CLASS|AMSK_STYLE },
459
{ "th", MARKUP_TH, MUTYPE_TD,
460
AMSK_ALIGN|AMSK_BGCOLOR|AMSK_COLSPAN|
461
AMSK_ROWSPAN|AMSK_VALIGN|AMSK_CLASS|AMSK_STYLE },
462
{ "thead", MARKUP_THEAD, MUTYPE_BLOCK,
463
AMSK_ALIGN|AMSK_CLASS|AMSK_STYLE },
464
{ "time", MARKUP_TIME, MUTYPE_FONT,
465
AMSK_DATETIME|AMSK_ID|AMSK_CLASS|AMSK_STYLE|AMSK_TITLE },
466
{ "title", MARKUP_TITLE, MUTYPE_BLOCK, 0 },
467
{ "tr", MARKUP_TR, MUTYPE_TR,
468
AMSK_ALIGN|AMSK_BGCOLOR|AMSK_VALIGN|AMSK_CLASS|AMSK_STYLE },
469
{ "tt", MARKUP_TT, MUTYPE_FONT, AMSK_STYLE },
470
{ "u", MARKUP_U, MUTYPE_FONT, AMSK_STYLE },
471
{ "ul", MARKUP_UL, MUTYPE_LIST,
472
AMSK_TYPE|AMSK_COMPACT|AMSK_STYLE },
473
{ "var", MARKUP_VAR, MUTYPE_FONT, AMSK_STYLE },
474
{ "verbatim", MARKUP_VERBATIM, MUTYPE_SPECIAL,
475
AMSK_ID|AMSK_TYPE },
476
{ "wbr", MARKUP_WBR, MUTYPE_SINGLE, 0 },
477
};
478
479
void show_allowed_wiki_markup( void ){
480
int i; /* loop over allowedAttr */
481
for( i=1 ; i<=count(aMarkup) - 1 ; i++ ){
482
@ &lt;%s(aMarkup[i].zName)&gt;
483
}
484
}
485
486
/*
487
** Use binary search to locate a tag in the aMarkup[] table.
488
*/
489
static int findTag(const char *z){
490
int i, c, first, last;
491
first = 1;
492
last = count(aMarkup) - 1;
493
while( first<=last ){
494
i = (first+last)/2;
495
c = fossil_strcmp(aMarkup[i].zName, z);
496
if( c==0 ){
497
assert( aMarkup[i].iCode==i );
498
return i;
499
}else if( c<0 ){
500
first = i+1;
501
}else{
502
last = i-1;
503
}
504
}
505
return MARKUP_INVALID;
506
}
507
508
/*
509
** Token types
510
*/
511
#define TOKEN_MARKUP 1 /* <...> */
512
#define TOKEN_CHARACTER 2 /* "&" or "<" not part of markup */
513
#define TOKEN_LINK 3 /* [...] */
514
#define TOKEN_PARAGRAPH 4 /* blank lines */
515
#define TOKEN_NEWLINE 5 /* A single "\n" */
516
#define TOKEN_BUL_LI 6 /* " * " */
517
#define TOKEN_NUM_LI 7 /* " # " */
518
#define TOKEN_ENUM 8 /* " \(?\d+[.)]? " */
519
#define TOKEN_INDENT 9 /* " " */
520
#define TOKEN_RAW 10 /* Output exactly (used when wiki-use-html==1) */
521
#define TOKEN_TEXT 11 /* None of the above */
522
523
/*
524
** State flags. Save the lower 16 bits for the WIKI_* flags.
525
*/
526
#define AT_NEWLINE 0x0010000 /* At start of a line */
527
#define AT_PARAGRAPH 0x0020000 /* At start of a paragraph */
528
#define ALLOW_WIKI 0x0040000 /* Allow wiki markup */
529
#define ALLOW_LINKS 0x0080000 /* Allow [...] hyperlinks */
530
#define FONT_MARKUP_ONLY 0x0100000 /* Only allow MUTYPE_FONT markup */
531
#define IN_LIST 0x0200000 /* Within wiki <ul> or <ol> */
532
533
/*
534
** Current state of the rendering engine
535
*/
536
typedef struct Renderer Renderer;
537
struct Renderer {
538
Blob *pOut; /* Output appended to this blob */
539
int state; /* Flag that govern rendering */
540
int mRender; /* Mask of RENDER_* values to return */
541
unsigned renderFlags; /* Flags from the client */
542
int wikiList; /* Current wiki list type */
543
int inVerbatim; /* True in <verbatim> mode */
544
int preVerbState; /* Value of state prior to verbatim */
545
int wantAutoParagraph; /* True if a <p> is desired */
546
int inAutoParagraph; /* True if within an automatic paragraph */
547
int pikchrHtmlFlags; /* Flags for pikchr_to_html() */
548
const char *zVerbatimId; /* The id= attribute of <verbatim> */
549
int nStack; /* Number of elements on the stack */
550
int nAlloc; /* Space allocated for aStack */
551
struct sStack {
552
short iCode; /* Markup code */
553
short allowWiki; /* ALLOW_WIKI if wiki allowed before tag */
554
const char *zId; /* ID attribute or NULL */
555
} *aStack;
556
};
557
558
/*
559
** Return TRUE if HTML should be used as the sole markup language for wiki.
560
**
561
** On first invocation, this routine consults the "wiki-use-html" setting.
562
** It caches the result for subsequent invocations, under the assumption
563
** that the setting will not change.
564
*/
565
static int wikiUsesHtml(void){
566
static int r = -1;
567
if( r<0 ) r = db_get_boolean("wiki-use-html", 0);
568
return r;
569
}
570
571
/*
572
** z points to a "<" character. Check to see if this is the start of
573
** a valid markup. If it is, return the total number of characters in
574
** the markup including the initial "<" and the terminating ">". If
575
** it is not well-formed markup, return 0.
576
*/
577
int html_tag_length(const char *z){
578
int n = 1;
579
int inparen = 0;
580
int c;
581
if( z[n]=='/' ){ n++; }
582
if( !fossil_isalpha(z[n]) ) return 0;
583
while( fossil_isalnum(z[n]) || z[n]=='-' ){ n++; }
584
c = z[n];
585
if( c=='/' && z[n+1]=='>' ){ return n+2; }
586
if( c!='>' && !fossil_isspace(c) ) return 0;
587
while( (c = z[n])!=0 && (c!='>' || inparen) ){
588
if( c==inparen ){
589
inparen = 0;
590
}else if( inparen==0 && (c=='"' || c=='\'') ){
591
inparen = c;
592
}
593
n++;
594
}
595
if( z[n]!='>' ) return 0;
596
return n+1;
597
}
598
599
/*
600
** z points to a "\n" character. Check to see if this newline is
601
** followed by one or more blank lines. If it is, return the number
602
** of characters through the closing "\n". If not, return 0.
603
*/
604
static int paragraphBreakLength(const char *z){
605
int i, n;
606
int nNewline = 1;
607
for(i=1, n=0; fossil_isspace(z[i]); i++){
608
if( z[i]=='\n' ){
609
nNewline++;
610
n = i;
611
}
612
}
613
if( nNewline>=2 ){
614
return n+1;
615
}else{
616
return 0;
617
}
618
}
619
620
/*
621
** Return the number of characters until the next "interesting"
622
** characters.
623
**
624
** Interesting characters are:
625
**
626
** <
627
** &
628
** \n
629
** [
630
**
631
** The "[" is only considered if flags contain ALLOW_LINKS or ALLOW_WIKI.
632
** The "\n" is only considered interesting if the flags contains ALLOW_WIKI.
633
*/
634
static int textLength(const char *z, int flags){
635
const char *zReject;
636
if( flags & ALLOW_WIKI ){
637
zReject = "<&[\n";
638
}else if( flags & ALLOW_LINKS ){
639
zReject = "<&[";
640
}else{
641
zReject = "<&";
642
}
643
return strcspn(z, zReject);
644
}
645
646
/*
647
** Return true if z[] begins with an HTML character element.
648
*/
649
static int isElement(const char *z){
650
int i;
651
assert( z[0]=='&' );
652
if( z[1]=='#' ){
653
for(i=2; fossil_isdigit(z[i]); i++){}
654
return i>2 && z[i]==';';
655
}else{
656
for(i=1; fossil_isalpha(z[i]); i++){}
657
return i>1 && z[i]==';';
658
}
659
}
660
661
/*
662
** Check to see if the z[] string is the beginning of a wiki list item.
663
** If it is, return the length of the bullet text. Otherwise return 0.
664
*/
665
static int listItemLength(const char *z, const char listChar){
666
int i, n;
667
n = 0;
668
i = 0;
669
while( z[n]==' ' || z[n]=='\t' ){
670
if( z[n]=='\t' ) i++;
671
i++;
672
n++;
673
}
674
if( i<2 || z[n]!=listChar ) return 0;
675
n++;
676
i = 0;
677
while( z[n]==' ' || z[n]=='\t' ){
678
if( z[n]=='\t' ) i++;
679
i++;
680
n++;
681
}
682
if( i<2 || fossil_isspace(z[n]) ) return 0;
683
return n;
684
}
685
686
/*
687
** Check to see if the z[] string is the beginning of an enumeration value.
688
** If it is, return the length of the bullet text. Otherwise return 0.
689
**
690
** Syntax:
691
** * a tab or two or more spaces
692
** * one or more digits
693
** * optional "."
694
** * another tab or two ore more spaces.
695
**
696
*/
697
static int enumLength(const char *z){
698
int i, n;
699
n = 0;
700
i = 0;
701
while( z[n]==' ' || z[n]=='\t' ){
702
if( z[n]=='\t' ) i++;
703
i++;
704
n++;
705
}
706
if( i<2 ) return 0;
707
for(i=0; fossil_isdigit(z[n]); i++, n++){}
708
if( i==0 ) return 0;
709
if( z[n]=='.' ){
710
n++;
711
}
712
i = 0;
713
while( z[n]==' ' || z[n]=='\t' ){
714
if( z[n]=='\t' ) i++;
715
i++;
716
n++;
717
}
718
if( i<2 || fossil_isspace(z[n]) ) return 0;
719
return n;
720
}
721
722
/*
723
** Check to see if the z[] string is the beginning of an indented
724
** paragraph. If it is, return the length of the indent. Otherwise
725
** return 0.
726
*/
727
static int indentLength(const char *z){
728
int i, n;
729
n = 0;
730
i = 0;
731
while( z[n]==' ' || z[n]=='\t' ){
732
if( z[n]=='\t' ) i++;
733
i++;
734
n++;
735
}
736
if( i<2 || fossil_isspace(z[n]) ) return 0;
737
return n;
738
}
739
740
/*
741
** Check to see if the z[] string is a wiki hyperlink. If it is,
742
** return the length of the hyperlink. Otherwise return 0.
743
*/
744
static int linkLength(const char *z){
745
int n;
746
assert( z[0]=='[' );
747
for(n=0; z[n] && z[n]!=']'; n++){}
748
if( z[n]==']' ){
749
return n+1;
750
}else{
751
return 0;
752
}
753
}
754
755
/*
756
** Get the next wiki token.
757
**
758
** z points to the start of a token. Return the number of
759
** characters in that token. Write the token type into *pTokenType.
760
*/
761
static int nextWikiToken(const char *z, Renderer *p, int *pTokenType){
762
int n;
763
if( z[0]=='<' ){
764
n = html_tag_length(z);
765
if( n>0 ){
766
p->mRender |= RENDER_TAG;
767
*pTokenType = TOKEN_MARKUP;
768
return n;
769
}else{
770
p->mRender |= RENDER_BADTAG;
771
*pTokenType = TOKEN_CHARACTER;
772
return 1;
773
}
774
}
775
if( z[0]=='&' ){
776
p->mRender |= RENDER_ENTITY;
777
if( (p->inVerbatim || !isElement(z)) ){
778
*pTokenType = TOKEN_CHARACTER;
779
return 1;
780
}
781
}
782
if( (p->state & ALLOW_WIKI)!=0 ){
783
if( z[0]=='\n' ){
784
n = paragraphBreakLength(z);
785
if( n>0 ){
786
*pTokenType = TOKEN_PARAGRAPH;
787
return n;
788
}else{
789
*pTokenType = TOKEN_NEWLINE;
790
return 1;
791
}
792
}
793
if( (p->state & AT_NEWLINE)!=0 && fossil_isspace(z[0]) ){
794
n = listItemLength(z, '*');
795
if( n>0 ){
796
*pTokenType = TOKEN_BUL_LI;
797
return n;
798
}
799
n = listItemLength(z, '#');
800
if( n>0 ){
801
*pTokenType = TOKEN_NUM_LI;
802
return n;
803
}
804
n = enumLength(z);
805
if( n>0 ){
806
*pTokenType = TOKEN_ENUM;
807
return n;
808
}
809
}
810
if( (p->state & AT_PARAGRAPH)!=0 && fossil_isspace(z[0]) ){
811
n = indentLength(z);
812
if( n>0 ){
813
*pTokenType = TOKEN_INDENT;
814
return n;
815
}
816
}
817
if( z[0]=='[' ){
818
if( (n = linkLength(z))>0 ){
819
*pTokenType = TOKEN_LINK;
820
return n;
821
}else if( p->state & WIKI_MARK ){
822
blob_append_string(p->pOut, "<mark>");
823
p->mRender |= RENDER_BADLINK|RENDER_MARK;
824
}else{
825
p->mRender |= RENDER_BADLINK;
826
}
827
}
828
}else if( (p->state & ALLOW_LINKS)!=0 && z[0]=='[' ){
829
if( (n = linkLength(z))>0 ){
830
*pTokenType = TOKEN_LINK;
831
return n;
832
}else if( p->state & WIKI_MARK ){
833
blob_append_string(p->pOut, "<mark>");
834
p->mRender |= RENDER_BADLINK|RENDER_MARK;
835
}else{
836
p->mRender |= RENDER_BADLINK;
837
}
838
}
839
*pTokenType = TOKEN_TEXT;
840
return 1 + textLength(z+1, p->state);
841
}
842
843
/*
844
** Parse only Wiki links, return everything else as TOKEN_RAW.
845
**
846
** z points to the start of a token. Return the number of
847
** characters in that token. Write the token type into *pTokenType.
848
*/
849
static int nextRawToken(const char *z, Renderer *p, int *pTokenType){
850
int n;
851
if( z[0]=='[' ){
852
if( (n = linkLength(z))>0 ){
853
*pTokenType = TOKEN_LINK;
854
return n;
855
}else if( p->state & WIKI_MARK ){
856
blob_append_string(p->pOut, "<mark>");
857
p->mRender |= RENDER_BADLINK|RENDER_MARK;
858
}else{
859
p->mRender |= RENDER_BADLINK;
860
}
861
}
862
*pTokenType = TOKEN_RAW;
863
return 1 + textLength(z+1, p->state);
864
}
865
866
/*
867
** A single markup is parsed into an instance of the following
868
** structure.
869
*/
870
typedef struct ParsedMarkup ParsedMarkup;
871
struct ParsedMarkup {
872
unsigned char endTag; /* True if </...> instead of <...> */
873
unsigned char iCode; /* MARKUP_* */
874
unsigned char nAttr; /* Number of attributes */
875
unsigned short iType; /* MUTYPE_* */
876
struct {
877
unsigned char iACode; /* ATTR_* */
878
char *zValue; /* Argument to this attribute. Might be NULL */
879
char cTerm; /* Original argument termination character */
880
} aAttr[10];
881
};
882
883
/*
884
** Return true if attribute iACode has already been parsed into p.
885
*/
886
static int markupHasAttr(const ParsedMarkup *p, int iACode){
887
int i;
888
for(i=0; i<p->nAttr; i++){
889
if( p->aAttr[i].iACode==iACode ) return 1;
890
}
891
return 0;
892
}
893
894
/*
895
** z[] is an HTML markup element - something that begins with '<'.
896
** Parse this element into the p structure.
897
**
898
** The content of z[] might be modified by converting characters
899
** to lowercase and by inserting some "\000" characters.
900
*/
901
static amsk_t parseMarkup(ParsedMarkup *p, char *z){
902
int i, j, c;
903
int iACode;
904
char *zValue;
905
amsk_t seen = 0;
906
char zTag[100];
907
908
if( z[1]=='/' ){
909
p->endTag = 1;
910
i = 2;
911
}else{
912
p->endTag = 0;
913
i = 1;
914
}
915
j = 0;
916
while( fossil_isalnum(z[i]) ){
917
if( j<(int)sizeof(zTag)-1 ) zTag[j++] = fossil_tolower(z[i]);
918
i++;
919
}
920
zTag[j] = 0;
921
p->iCode = findTag(zTag);
922
p->iType = aMarkup[p->iCode].iType;
923
p->nAttr = 0;
924
c = 0;
925
if( z[i]=='-' ){
926
p->aAttr[0].iACode = iACode = ATTR_ID;
927
i++;
928
p->aAttr[0].zValue = &z[i];
929
while( fossil_isalnum(z[i]) ){ i++; }
930
p->aAttr[0].cTerm = c = z[i];
931
z[i++] = 0;
932
p->nAttr = 1;
933
if( c=='>' ) return 0;
934
}
935
while( fossil_isspace(z[i]) ){ i++; }
936
while( c!='>' && p->nAttr<8 && fossil_isalpha(z[i]) ){
937
int attrOk; /* True to preserve attribute. False to ignore it */
938
j = 0;
939
while( fossil_isalnum(z[i]) ){
940
if( j<(int)sizeof(zTag)-1 ) zTag[j++] = fossil_tolower(z[i]);
941
i++;
942
}
943
zTag[j] = 0;
944
p->aAttr[p->nAttr].iACode = iACode = findAttr(zTag);
945
attrOk = iACode!=0 && !markupHasAttr(p, iACode);
946
while( fossil_isspace(z[i]) ){ z++; }
947
if( z[i]!='=' ){
948
p->aAttr[p->nAttr].zValue = 0;
949
p->aAttr[p->nAttr].cTerm = 0;
950
c = 0;
951
}else{
952
i++;
953
while( fossil_isspace(z[i]) ){ z++; }
954
if( z[i]=='"' ){
955
i++;
956
zValue = &z[i];
957
while( z[i] && z[i]!='"' ){ i++; }
958
}else if( z[i]=='\'' ){
959
i++;
960
zValue = &z[i];
961
while( z[i] && z[i]!='\'' ){ i++; }
962
}else{
963
zValue = &z[i];
964
while( !fossil_isspace(z[i]) && z[i]!='>' ){
965
if( z[i]=='\'' || z[i]=='"' ) attrOk = 0;
966
i++;
967
}
968
}
969
if( attrOk ){
970
p->aAttr[p->nAttr].zValue = zValue;
971
p->aAttr[p->nAttr].cTerm = c = z[i];
972
if( z[i]==0 ){
973
i--;
974
}else{
975
z[i] = 0;
976
}
977
}
978
i++;
979
}
980
if( attrOk ){
981
if( aAttribute[iACode].iMask ) seen |= aAttribute[iACode].iMask;
982
p->nAttr++;
983
}
984
while( fossil_isspace(z[i]) ){ i++; }
985
if( z[i]==0 || z[i]=='>' || (z[i]=='/' && z[i+1]=='>') ) break;
986
}
987
return seen;
988
}
989
990
/*
991
** Append an HTML attribute value, rewriting root-relative URLs in src
992
** and srcset the same way (%R prefix).
993
*/
994
static void appendRewrittenAttrValue(Blob *pOut, int iACode, const char *zVal){
995
if( iACode==ATTR_SRC && zVal[0]=='/' ){
996
blob_appendf(pOut, "=\"%R%s\"", zVal);
997
return;
998
}
999
if( iACode==ATTR_SRCSET ){
1000
const char *z = zVal;
1001
int nCand = 0;
1002
blob_append_char(pOut, '=');
1003
blob_append_char(pOut, '"');
1004
while( z[0] ){
1005
const char *zComma = strchr(z, ',');
1006
int len = zComma ? (int)(zComma - z) : (int)strlen(z);
1007
const char *zCand = z;
1008
int urlStart = 0;
1009
int urlLen;
1010
if( len>0 ){
1011
while( urlStart<len && fossil_isspace(zCand[urlStart]) ) urlStart++;
1012
urlLen = urlStart;
1013
while( urlLen<len && !fossil_isspace(zCand[urlLen]) ) urlLen++;
1014
urlLen -= urlStart;
1015
if( nCand++ ) blob_append_char(pOut, ',');
1016
if( urlStart>0 ){
1017
blob_append(pOut, zCand, urlStart);
1018
}
1019
if( urlLen>0 && zCand[urlStart]=='/' ){
1020
blob_appendf(pOut, "%R%.*s", urlLen, zCand+urlStart);
1021
}else if( urlLen>0 ){
1022
blob_appendf(pOut, "%.*s", urlLen, zCand+urlStart);
1023
}
1024
if( urlStart+urlLen < len ){
1025
blob_appendf(pOut, "%.*s", len-urlStart-urlLen, zCand+urlStart+urlLen);
1026
}
1027
}
1028
if( zComma==0 ) break;
1029
z = zComma + 1;
1030
}
1031
blob_append_char(pOut, '"');
1032
return;
1033
}
1034
blob_appendf(pOut, "=\"%s\"", zVal);
1035
}
1036
1037
/*
1038
** Render markup on the given blob.
1039
*/
1040
static void renderMarkup(Blob *pOut, ParsedMarkup *p){
1041
int i;
1042
if( p->endTag ){
1043
blob_appendf(pOut, "</%s>", aMarkup[p->iCode].zName);
1044
}else{
1045
blob_appendf(pOut, "<%s", aMarkup[p->iCode].zName);
1046
for(i=0; i<p->nAttr; i++){
1047
blob_appendf(pOut, " %s", aAttribute[p->aAttr[i].iACode].zName);
1048
if( p->aAttr[i].zValue ){
1049
appendRewrittenAttrValue(pOut, p->aAttr[i].iACode, p->aAttr[i].zValue);
1050
}
1051
}
1052
if (p->iType & MUTYPE_SINGLE){
1053
blob_append_string(pOut, " /");
1054
}
1055
blob_append_char(pOut, '>');
1056
}
1057
}
1058
1059
/*
1060
** When the markup was parsed, some "\000" may have been inserted.
1061
** This routine restores to those "\000" values back to their
1062
** original content.
1063
*/
1064
static void unparseMarkup(ParsedMarkup *p){
1065
int i, n;
1066
for(i=0; i<p->nAttr; i++){
1067
char *z = p->aAttr[i].zValue;
1068
if( z==0 ) continue;
1069
if( p->aAttr[i].cTerm ){
1070
n = strlen(z);
1071
z[n] = p->aAttr[i].cTerm;
1072
}
1073
}
1074
}
1075
1076
/*
1077
** Return the value of attribute attrId. Return NULL if there is no
1078
** ID attribute.
1079
*/
1080
static const char *attributeValue(ParsedMarkup *p, int attrId){
1081
int i;
1082
for(i=0; i<p->nAttr; i++){
1083
if( p->aAttr[i].iACode==attrId ){
1084
return p->aAttr[i].zValue;
1085
}
1086
}
1087
return 0;
1088
}
1089
1090
/*
1091
** Return the ID attribute for markup. Return NULL if there is no
1092
** ID attribute.
1093
*/
1094
static const char *markupId(ParsedMarkup *p){
1095
return attributeValue(p, ATTR_ID);
1096
}
1097
1098
/*
1099
** Check markup pMarkup to see if it is a hyperlink with class "button"
1100
** that is follows by simple text and an </a> only. Example:
1101
**
1102
** <a class="button" href="../index.wiki">Index</a>
1103
**
1104
** If the markup matches this pattern, and if the WIKI_BUTTONS flag was
1105
** passed to wiki_convert(), then transform this link into a submenu
1106
** button, skip the text, and set *pN equal to the total length of the
1107
** text through the end of </a> and return true. If the markup does
1108
** not match or if WIKI_BUTTONS is not set, then make no changes to *pN
1109
** and return false.
1110
*/
1111
static int isButtonHyperlink(
1112
Renderer *p, /* Renderer state */
1113
ParsedMarkup *pMarkup, /* Potential button markup */
1114
const char *z, /* Complete text of Wiki */
1115
int *pN /* Characters of z[] consumed */
1116
){
1117
const char *zClass;
1118
const char *zHref;
1119
char *zTag;
1120
int i, j;
1121
if( (p->state & WIKI_BUTTONS)==0 ) return 0;
1122
zClass = attributeValue(pMarkup, ATTR_CLASS);
1123
if( zClass==0 ) return 0;
1124
if( fossil_strcmp(zClass, "button")!=0 ) return 0;
1125
zHref = attributeValue(pMarkup, ATTR_HREF);
1126
if( zHref==0 ) return 0;
1127
i = *pN;
1128
while( z[i] && z[i]!='<' ){ i++; }
1129
if( fossil_strnicmp(&z[i], "</a>",4)!=0 ) return 0;
1130
for(j=*pN; fossil_isspace(z[j]); j++){}
1131
zTag = mprintf("%.*s", i-j, &z[j]);
1132
j = (int)strlen(zTag);
1133
while( j>0 && fossil_isspace(zTag[j-1]) ){ j--; }
1134
if( j==0 ) return 0;
1135
style_submenu_element(zTag, "%s", zHref);
1136
*pN = i+4;
1137
return 1;
1138
}
1139
1140
/*
1141
** Pop a single element off of the stack. As the element is popped,
1142
** output its end tag if it is not a </div> tag.
1143
*/
1144
static void popStack(Renderer *p){
1145
if( p->nStack ){
1146
int iCode;
1147
p->nStack--;
1148
iCode = p->aStack[p->nStack].iCode;
1149
if( (iCode!=MARKUP_DIV || p->aStack[p->nStack].zId==0) && p->pOut ){
1150
blob_appendf(p->pOut, "</%s>", aMarkup[iCode].zName);
1151
}
1152
}
1153
}
1154
1155
/*
1156
** Push a new markup value onto the stack. Enlarge the stack
1157
** if necessary.
1158
*/
1159
static void pushStackWithId(Renderer *p, int elem, const char *zId, int w){
1160
if( p->nStack>=p->nAlloc ){
1161
p->nAlloc = p->nAlloc*2 + 100;
1162
p->aStack = fossil_realloc(p->aStack, p->nAlloc*sizeof(p->aStack[0]));
1163
}
1164
p->aStack[p->nStack].iCode = elem;
1165
p->aStack[p->nStack].zId = zId;
1166
p->aStack[p->nStack].allowWiki = w;
1167
p->nStack++;
1168
}
1169
static void pushStack(Renderer *p, int elem){
1170
pushStackWithId(p, elem, 0, 0);
1171
}
1172
1173
/*
1174
** Pop the stack until the top-most iTag element is removed.
1175
** If there is no iTag element on the stack, this routine
1176
** is a no-op.
1177
*/
1178
static void popStackToTag(Renderer *p, int iTag){
1179
int i;
1180
for(i=p->nStack-1; i>=0; i--){
1181
if( p->aStack[i].iCode!=iTag ) continue;
1182
if( p->aStack[i].zId ) continue;
1183
break;
1184
}
1185
if( i<0 ) return;
1186
while( p->nStack>i ){
1187
popStack(p);
1188
}
1189
}
1190
1191
/*
1192
** Attempt to find a find a tag of type iTag with id zId. Return -1
1193
** if not found. If found, return its stack level.
1194
*/
1195
static int findTagWithId(Renderer *p, int iTag, const char *zId){
1196
int i;
1197
assert( zId!=0 );
1198
for(i=p->nStack-1; i>=0; i--){
1199
if( p->aStack[i].iCode!=iTag ) continue;
1200
if( p->aStack[i].zId==0 ) continue;
1201
if( fossil_strcmp(zId, p->aStack[i].zId)!=0 ) continue;
1202
break;
1203
}
1204
return i;
1205
}
1206
1207
/*
1208
** Pop the stack until the top-most element of the stack
1209
** is an element that matches the type in iMask. Return
1210
** code of the markup element that is on left on top of the stack.
1211
** If the stack does not have an element
1212
** that matches iMask, then leave the stack unchanged and
1213
** return false (MARKUP_INVALID).
1214
*/
1215
static int backupToType(Renderer *p, int iMask){
1216
int i;
1217
for(i=p->nStack-1; i>=0; i--){
1218
if( aMarkup[p->aStack[i].iCode].iType & iMask ) break;
1219
}
1220
if( i<0 ) return 0;
1221
i++;
1222
while( p->nStack>i ){
1223
popStack(p);
1224
}
1225
return p->aStack[i-1].iCode;
1226
}
1227
1228
/*
1229
** Begin a new paragraph if that something that is needed.
1230
*/
1231
static void startAutoParagraph(Renderer *p){
1232
if( p->wantAutoParagraph==0 ) return;
1233
if( p->state & WIKI_LINKSONLY ) return;
1234
if( p->wikiList==MARKUP_OL || p->wikiList==MARKUP_UL ) return;
1235
blob_append_string(p->pOut, "<p>");
1236
p->wantAutoParagraph = 0;
1237
p->inAutoParagraph = 1;
1238
}
1239
1240
/*
1241
** End a paragraph if we are in one.
1242
*/
1243
static void endAutoParagraph(Renderer *p){
1244
if( p->inAutoParagraph ){
1245
p->inAutoParagraph = 0;
1246
}
1247
}
1248
1249
/*
1250
** If the input string corresponds to an existing baseline,
1251
** return true.
1252
*/
1253
static int is_valid_hname(const char *z){
1254
int n = strlen(z);
1255
if( n<4 || n>HNAME_MAX ) return 0;
1256
if( !validate16(z, n) ) return 0;
1257
return 1;
1258
}
1259
1260
/*
1261
** Return TRUE if a hash name corresponds to an artifact in this
1262
** repository.
1263
*/
1264
static int in_this_repo(const char *zUuid){
1265
static Stmt q;
1266
int rc;
1267
int n;
1268
char zU2[HNAME_MAX+1];
1269
db_static_prepare(&q,
1270
"SELECT 1 FROM blob WHERE uuid>=:u AND uuid<:u2"
1271
);
1272
db_bind_text(&q, ":u", zUuid);
1273
n = (int)strlen(zUuid);
1274
if( n>=(int)sizeof(zU2) ) n = sizeof(zU2)-1;
1275
memcpy(zU2, zUuid, n);
1276
zU2[n-1]++;
1277
zU2[n] = 0;
1278
db_bind_text(&q, ":u2", zU2);
1279
rc = db_step(&q);
1280
db_reset(&q);
1281
return rc==SQLITE_ROW;
1282
}
1283
1284
/*
1285
** zTarget is guaranteed to be a UUID. It might be the UUID of a ticket.
1286
** If it is, store in *pClosed a true or false depending on whether or not
1287
** the ticket is closed and return true. If zTarget
1288
** is not the UUID of a ticket, return false.
1289
*/
1290
int is_ticket(
1291
const char *zTarget, /* Ticket UUID */
1292
int *pClosed /* True if the ticket is closed */
1293
){
1294
static Stmt q;
1295
int n;
1296
int rc;
1297
char zLower[HNAME_MAX+1];
1298
char zUpper[HNAME_MAX+1];
1299
n = strlen(zTarget);
1300
memcpy(zLower, zTarget, n+1);
1301
canonical16(zLower, n+1);
1302
memcpy(zUpper, zLower, n+1);
1303
zUpper[n-1]++;
1304
if( !db_static_stmt_is_init(&q) ){
1305
char *zClosedExpr = db_get("ticket-closed-expr", "status='Closed'");
1306
db_static_prepare(&q,
1307
"SELECT %z FROM ticket "
1308
" WHERE tkt_uuid>=:lwr AND tkt_uuid<:upr",
1309
zClosedExpr /*safe-for-%s*/
1310
);
1311
}
1312
db_bind_text(&q, ":lwr", zLower);
1313
db_bind_text(&q, ":upr", zUpper);
1314
if( db_step(&q)==SQLITE_ROW ){
1315
rc = 1;
1316
*pClosed = db_column_int(&q, 0);
1317
}else{
1318
rc = 0;
1319
}
1320
db_reset(&q);
1321
return rc;
1322
}
1323
1324
/*
1325
** Return a pointer to the name part of zTarget (skipping the "wiki:" prefix
1326
** if there is one) if zTarget is a valid wiki page name. Return NULL if
1327
** zTarget names a page that does not exist.
1328
*/
1329
static const char *validWikiPageName(int mFlags, const char *zTarget){
1330
if( strncmp(zTarget, "wiki:", 5)==0
1331
&& wiki_name_is_wellformed((const unsigned char*)zTarget) ){
1332
return zTarget+5;
1333
}
1334
if( strcmp(zTarget, "Sandbox")==0 ) return zTarget;
1335
if( wiki_name_is_wellformed((const unsigned char *)zTarget)
1336
&& ((mFlags & WIKI_NOBADLINKS)==0 ||
1337
db_exists("SELECT 1 FROM tag WHERE tagname GLOB 'wiki-%q'"
1338
" AND (SELECT value FROM tagxref WHERE tagid=tag.tagid"
1339
" ORDER BY mtime DESC LIMIT 1) > 0", zTarget))
1340
){
1341
return zTarget;
1342
}
1343
return 0;
1344
}
1345
1346
static const char *wikiOverrideHash = 0;
1347
1348
/*
1349
** Fossil-wiki hyperlinks to wiki pages should be overridden to the
1350
** hash value supplied. If the value is NULL, then override is cancelled
1351
** and all overwrites operate normally.
1352
*/
1353
void wiki_hyperlink_override(const char *zUuid){
1354
wikiOverrideHash = zUuid;
1355
}
1356
1357
1358
/*
1359
** If links to wiki page zTarget should be redirected to some historical
1360
** version of that page, then return the hash of the historical version.
1361
** If no override is required, return NULL.
1362
*/
1363
static const char *wiki_is_overridden(const char *zTarget){
1364
if( wikiOverrideHash==0 ) return 0;
1365
/* The override should only happen if the override version is not the
1366
** latest version of the wiki page. */
1367
if( !db_exists(
1368
"SELECT 1 FROM tag, blob, tagxref AS xA, tagxref AS xB "
1369
" WHERE tag.tagname GLOB 'wiki-%q*'"
1370
" AND blob.uuid GLOB '%q'"
1371
" AND xA.tagid=tag.tagid AND xA.rid=blob.rid"
1372
" AND xB.tagid=tag.tagid AND xB.mtime>xA.mtime",
1373
zTarget, wikiOverrideHash
1374
) ){
1375
return 0;
1376
}
1377
return wikiOverrideHash;
1378
}
1379
1380
/*
1381
** Resolve a hyperlink. The zTarget argument is the content of the [...]
1382
** in the wiki. Append to the output string whatever text is appropriate
1383
** for opening the hyperlink. Write into zClose[0...nClose-1] text that will
1384
** close the markup.
1385
**
1386
** If this routine determines that no hyperlink should be generated, then
1387
** set zClose[0] to 0.
1388
**
1389
** Actually, this routine might or might not append the hyperlink, depending
1390
** on current rendering rules: specifically does the current user have
1391
** "History" permission.
1392
**
1393
** [http://fossil-scm.org/]
1394
** [https://fossil-scm.org/]
1395
** [ftp://fossil-scm.org/]
1396
** [mailto:[email protected]]
1397
**
1398
** [/path] -> Refers to the root of the Fossil hierarchy, not
1399
** the root of the URI domain
1400
**
1401
** [./relpath]
1402
** [../relpath]
1403
**
1404
** [#fragment]
1405
**
1406
** [0123456789abcdef]
1407
**
1408
** [WikiPageName]
1409
** [wiki:WikiPageName]
1410
**
1411
** [2010-02-27 07:13]
1412
**
1413
** [InterMap:Link] -> Interwiki link
1414
**
1415
** The return value is a mask of RENDER_* values indicating what happened.
1416
** Probably the return value is 0 on success and RENDER_BADTARGET or
1417
** RENDER_BADLINK if there are problems.
1418
*/
1419
int wiki_resolve_hyperlink(
1420
Blob *pOut, /* Write the HTML output here */
1421
int mFlags, /* Rendering option flags */
1422
const char *zTarget, /* Hyperlink target; text within [...] */
1423
char *zClose, /* Write hyperlink closing text here */
1424
int nClose, /* Bytes available in zClose[] */
1425
const char *zOrig, /* Complete document text */
1426
const char *zTitle /* Title of the link */
1427
){
1428
const char *zTerm = "</a>";
1429
const char *z;
1430
char *zExtra = 0;
1431
const char *zExtraNS = 0;
1432
char *zRemote = 0;
1433
int rc = 0;
1434
1435
if( zTitle ){
1436
zExtra = mprintf(" title='%h'", zTitle);
1437
zExtraNS = zExtra+1;
1438
}else if( mFlags & WIKI_TARGET_BLANK ){
1439
zExtra = mprintf(" target='_blank'");
1440
zExtraNS = zExtra+1;
1441
}
1442
assert( nClose>=20 );
1443
if( strncmp(zTarget, "http:", 5)==0
1444
|| strncmp(zTarget, "https:", 6)==0
1445
|| strncmp(zTarget, "ftp:", 4)==0
1446
|| strncmp(zTarget, "mailto:", 7)==0
1447
){
1448
blob_appendf(pOut, "<a href=\"%s\"%s>", zTarget, zExtra);
1449
}else if( zTarget[0]=='/' ){
1450
blob_appendf(pOut, "<a href=\"%R%h\"%s>", zTarget, zExtra);
1451
}else if( zTarget[0]=='.'
1452
&& (zTarget[1]=='/' || (zTarget[1]=='.' && zTarget[2]=='/'))
1453
&& (mFlags & WIKI_LINKSONLY)==0 ){
1454
blob_appendf(pOut, "<a href=\"%h\"%s>", zTarget, zExtra);
1455
}else if( zTarget[0]=='#' ){
1456
blob_appendf(pOut, "<a href=\"%h\"%s>", zTarget, zExtra);
1457
}else if( is_valid_hname(zTarget) ){
1458
int isClosed = 0;
1459
const char *zLB = (mFlags & WIKI_NOBRACKET)==0 ? "[" : "";
1460
if( strlen(zTarget)<=HNAME_MAX && is_ticket(zTarget, &isClosed) ){
1461
/* Special display processing for tickets. Display the hyperlink
1462
** as crossed out if the ticket is closed.
1463
*/
1464
if( isClosed ){
1465
if( g.perm.Hyperlink ){
1466
blob_appendf(pOut,
1467
"%z<span class=\"wikiTagCancelled\">%s",
1468
xhref(zExtraNS,"%R/info/%s",zTarget), zLB
1469
);
1470
zTerm = "]</span></a>";
1471
}else{
1472
blob_appendf(pOut,"<span class=\"wikiTagCancelled\">%s", zLB);
1473
zTerm = "]</span>";
1474
}
1475
}else{
1476
if( g.perm.Hyperlink ){
1477
blob_appendf(pOut,"%z%s", xhref(zExtraNS,"%R/info/%s", zTarget),zLB);
1478
zTerm = "]</a>";
1479
}else{
1480
blob_appendf(pOut, "%s", zLB);
1481
zTerm = "]";
1482
}
1483
}
1484
}else if( !in_this_repo(zTarget) ){
1485
if( (mFlags & (WIKI_LINKSONLY|WIKI_NOBADLINKS))!=0 ){
1486
zTerm = "";
1487
}else if( (mFlags & WIKI_MARK)!=0 ){
1488
blob_appendf(pOut, "<mark>%s", zLB);
1489
zTerm = "]</mark>";
1490
rc |= RENDER_MARK;
1491
}else{
1492
blob_appendf(pOut, "<span class=\"brokenlink\">%s", zLB);
1493
zTerm = "]</span>";
1494
}
1495
rc |= RENDER_BADTARGET;
1496
}else if( g.perm.Hyperlink || (mFlags & WIKI_ADMIN)!=0 ){
1497
blob_appendf(pOut, "%z%s",xhref(zExtraNS, "%R/info/%s", zTarget), zLB);
1498
zTerm = "]</a>";
1499
}else{
1500
zTerm = "";
1501
}
1502
if( zTerm[0]==']' && (mFlags & WIKI_NOBRACKET)!=0 ) zTerm++;
1503
}else if( (zRemote = interwiki_url(zTarget))!=0 ){
1504
blob_appendf(pOut, "<a href=\"%z\"%s>", zRemote, zExtra);
1505
zTerm = "</a>";
1506
}else if( (z = validWikiPageName(mFlags, zTarget))!=0 ){
1507
/* The link is to a valid wiki page name */
1508
const char *zOverride = wiki_is_overridden(zTarget);
1509
if( zOverride ){
1510
blob_appendf(pOut, "<a href=\"%R/info/%S\"%s>", zOverride, zExtra);
1511
}else{
1512
blob_appendf(pOut, "<a href=\"%R/wiki?name=%T\"%s>", z, zExtra);
1513
}
1514
}else if( strlen(zTarget)>=10 && fossil_isdigit(zTarget[0]) && zTarget[4]=='-'
1515
&& db_int(0, "SELECT datetime(%Q) NOT NULL", zTarget) ){
1516
/* Dates or date-and-times in ISO8601 resolve to a link to the
1517
** timeline for that date */
1518
blob_appendf(pOut, "<a href=\"%R/timeline?c=%T\"%s>", zTarget, zExtra);
1519
}else if( mFlags & WIKI_MARKDOWNLINKS ){
1520
/* If none of the above, and if rendering links for markdown, then
1521
** create a link to the literal text of the target */
1522
blob_appendf(pOut, "<a href=\"%h\"%s>", zTarget, zExtra);
1523
}else if( mFlags & WIKI_MARK ){
1524
blob_appendf(pOut, "<mark>[");
1525
zTerm = "]</mark>";
1526
rc |= RENDER_BADTARGET|RENDER_MARK;
1527
}else if( zOrig && zTarget>=&zOrig[2]
1528
&& zTarget[-1]=='[' && !fossil_isspace(zTarget[-2]) ){
1529
/* If the hyperlink markup is not preceded by whitespace, then it
1530
** is probably a C-language subscript or similar, not really a
1531
** hyperlink. Just ignore it. */
1532
zTerm = "";
1533
}else if( (mFlags & (WIKI_NOBADLINKS|WIKI_LINKSONLY))!=0 ){
1534
/* Also ignore the link if various flags are set */
1535
zTerm = "";
1536
rc |= RENDER_BADTARGET;
1537
}else{
1538
blob_appendf(pOut, "<span class=\"brokenlink\">[%h]", zTarget);
1539
zTerm = "</span>";
1540
rc |= RENDER_BADTARGET;
1541
}
1542
if( zExtra ) fossil_free(zExtra);
1543
assert( (int)strlen(zTerm)<nClose );
1544
sqlite3_snprintf(nClose, zClose, "%s", zTerm);
1545
return rc;
1546
}
1547
1548
/*
1549
** Check zTarget to see if it looks like a valid hyperlink target.
1550
** Return true if it does seem valid and false if not.
1551
*/
1552
int wiki_valid_link_target(char *zTarget){
1553
char zClose[30];
1554
Blob notUsed;
1555
blob_init(&notUsed, 0, 0);
1556
wiki_resolve_hyperlink(&notUsed, WIKI_NOBADLINKS|WIKI_ADMIN,
1557
zTarget, zClose, sizeof(zClose)-1, 0, 0);
1558
blob_reset(&notUsed);
1559
return zClose[0]!=0;
1560
}
1561
1562
/*
1563
** Check to see if the given parsed markup is the correct
1564
** </verbatim> tag.
1565
*/
1566
static int endVerbatim(Renderer *p, ParsedMarkup *pMarkup){
1567
char *z;
1568
assert( p->inVerbatim );
1569
if( pMarkup->iCode!=MARKUP_VERBATIM ) return 0;
1570
if( !pMarkup->endTag ) return 0;
1571
if( p->zVerbatimId==0 ) return 1;
1572
if( pMarkup->nAttr!=1 ) return 0;
1573
z = pMarkup->aAttr[0].zValue;
1574
return fossil_strcmp(z, p->zVerbatimId)==0;
1575
}
1576
1577
/*
1578
** z[] points to the text that immediately follows markup of the form:
1579
**
1580
** <verbatim type='pikchr ...'>
1581
**
1582
** zClass is the argument to "type". This routine will process the
1583
** Pikchr text through the next matching </verbatim> (or until end-of-file)
1584
** and append the resulting SVG output onto p. It then returns the
1585
** number of bytes of text processed, including the closing </verbatim>.
1586
*/
1587
static int wiki_process_pikchr(Renderer *p, char *z, const char *zClass){
1588
ParsedMarkup m; /* Parsed closing tag */
1589
int i = 0; /* For looping over z[] in search of </verbatim> */
1590
int iRet = 0; /* Value to return */
1591
int atEnd = 0; /* True if se have found the </verbatim> */
1592
int nMarkup = 0; /* Length of a markup we are checking */
1593
1594
/* Search for the closing </verbatim> tag */
1595
while( z[i]!=0 ){
1596
char *zEnd = strchr(z+i, '<');
1597
if( zEnd==0 ){
1598
i += (int)strlen(z+i);
1599
iRet = i;
1600
break;
1601
}
1602
nMarkup = html_tag_length(zEnd);
1603
if( nMarkup<11 || fossil_strnicmp(zEnd, "</verbatim", 10)!=0 ){
1604
i = (int)(zEnd - z) + 1;
1605
continue;
1606
}
1607
(void)parseMarkup(&m, z+i);
1608
atEnd = endVerbatim(p, &m);
1609
unparseMarkup(&m);
1610
if( atEnd ){
1611
iRet = i + nMarkup;
1612
break;
1613
}
1614
i++;
1615
}
1616
1617
/* The Pikchr source text should be i character in length and iRet is
1618
** i plus the number of bytes in the </verbatim>. Generate the reply.
1619
*/
1620
assert( strncmp(zClass,"pikchr",6)==0 );
1621
zClass += 6;
1622
while( fossil_isspace(zClass[0]) ) zClass++;
1623
blob_append(p->pOut, "<p>", 3);
1624
pikchr_to_html(p->pOut, z, i, zClass, (int)strlen(zClass));
1625
blob_append(p->pOut, "</p>\n", 5);
1626
return iRet;
1627
}
1628
1629
/*
1630
** Return the MUTYPE for the top of the stack.
1631
*/
1632
static int stackTopType(Renderer *p){
1633
if( p->nStack<=0 ) return 0;
1634
return aMarkup[p->aStack[p->nStack-1].iCode].iType;
1635
}
1636
1637
/*
1638
** Convert the wiki in z[] into html in the renderer p. The
1639
** renderer has already been initialized.
1640
**
1641
** This routine will probably modify the content of z[].
1642
*/
1643
static void wiki_render(Renderer *p, char *z){
1644
int tokenType;
1645
ParsedMarkup markup;
1646
int n;
1647
int wikiHtmlOnly = (p->state & (WIKI_HTMLONLY | WIKI_LINKSONLY))!=0;
1648
int linksOnly = (p->state & WIKI_LINKSONLY)!=0;
1649
char *zOrig = z;
1650
1651
/* Make sure the attribute constants and names still align
1652
** following changes in the attribute list. */
1653
assert( fossil_strcmp(aAttribute[ATTR_WIDTH].zName, "width")==0 );
1654
1655
while( z[0] ){
1656
if( wikiHtmlOnly ){
1657
n = nextRawToken(z, p, &tokenType);
1658
}else{
1659
n = nextWikiToken(z, p, &tokenType);
1660
}
1661
p->state &= ~(AT_NEWLINE|AT_PARAGRAPH);
1662
switch( tokenType ){
1663
case TOKEN_PARAGRAPH: {
1664
if( p->wikiList ){
1665
popStackToTag(p, p->wikiList);
1666
p->wikiList = 0;
1667
}
1668
endAutoParagraph(p);
1669
blob_append_string(p->pOut, "\n\n");
1670
p->wantAutoParagraph = 1;
1671
p->state |= AT_PARAGRAPH|AT_NEWLINE;
1672
break;
1673
}
1674
case TOKEN_NEWLINE: {
1675
if( p->renderFlags & WIKI_NEWLINE ){
1676
blob_append_string(p->pOut, "<br>\n");
1677
}else{
1678
blob_append_string(p->pOut, "\n");
1679
}
1680
p->state |= AT_NEWLINE;
1681
break;
1682
}
1683
case TOKEN_BUL_LI: {
1684
p->mRender |= RENDER_BLOCK;
1685
if( p->wikiList!=MARKUP_UL ){
1686
if( p->wikiList ){
1687
popStackToTag(p, p->wikiList);
1688
}
1689
endAutoParagraph(p);
1690
pushStack(p, MARKUP_UL);
1691
blob_append_string(p->pOut, "<ul>");
1692
p->wikiList = MARKUP_UL;
1693
}
1694
popStackToTag(p, MARKUP_LI);
1695
startAutoParagraph(p);
1696
pushStack(p, MARKUP_LI);
1697
blob_append_string(p->pOut, "<li>");
1698
break;
1699
}
1700
case TOKEN_NUM_LI: {
1701
p->mRender |= RENDER_BLOCK;
1702
if( p->wikiList!=MARKUP_OL ){
1703
if( p->wikiList ){
1704
popStackToTag(p, p->wikiList);
1705
}
1706
endAutoParagraph(p);
1707
pushStack(p, MARKUP_OL);
1708
blob_append_string(p->pOut, "<ol>");
1709
p->wikiList = MARKUP_OL;
1710
}
1711
popStackToTag(p, MARKUP_LI);
1712
startAutoParagraph(p);
1713
pushStack(p, MARKUP_LI);
1714
blob_append_string(p->pOut, "<li>");
1715
break;
1716
}
1717
case TOKEN_ENUM: {
1718
p->mRender |= RENDER_BLOCK;
1719
if( p->wikiList!=MARKUP_OL ){
1720
if( p->wikiList ){
1721
popStackToTag(p, p->wikiList);
1722
}
1723
endAutoParagraph(p);
1724
pushStack(p, MARKUP_OL);
1725
blob_append_string(p->pOut, "<ol>");
1726
p->wikiList = MARKUP_OL;
1727
}
1728
popStackToTag(p, MARKUP_LI);
1729
startAutoParagraph(p);
1730
pushStack(p, MARKUP_LI);
1731
blob_appendf(p->pOut, "<li value=\"%d\">", atoi(z));
1732
break;
1733
}
1734
case TOKEN_INDENT: {
1735
p->mRender |= RENDER_BLOCK;
1736
assert( p->wikiList==0 );
1737
pushStack(p, MARKUP_BLOCKQUOTE);
1738
blob_append_string(p->pOut, "<blockquote>");
1739
p->wantAutoParagraph = 0;
1740
p->wikiList = MARKUP_BLOCKQUOTE;
1741
break;
1742
}
1743
case TOKEN_CHARACTER: {
1744
startAutoParagraph(p);
1745
if( p->state & WIKI_MARK ){
1746
blob_append_string(p->pOut, "<mark>");
1747
p->mRender |= RENDER_MARK;
1748
}
1749
if( z[0]=='<' ){
1750
p->mRender |= RENDER_BADTAG;
1751
blob_append_string(p->pOut, "&lt;");
1752
}else if( z[0]=='&' ){
1753
p->mRender |= RENDER_BADENTITY;
1754
blob_append_string(p->pOut, "&amp;");
1755
}
1756
if( p->state & WIKI_MARK ){
1757
if( fossil_isalnum(z[1]) || (z[1]=='/' && fossil_isalnum(z[2])) ){
1758
int kk;
1759
for(kk=2; fossil_isalnum(z[kk]); kk++){}
1760
blob_append(p->pOut, &z[1], kk-1);
1761
n = kk;
1762
}
1763
blob_append_string(p->pOut, "</mark>");
1764
}
1765
break;
1766
}
1767
case TOKEN_LINK: {
1768
char *zTarget;
1769
char *zDisplay = 0;
1770
int i, j;
1771
int savedState;
1772
char zClose[20];
1773
char cS1 = 0;
1774
int iS1 = 0;
1775
1776
startAutoParagraph(p);
1777
p->mRender |= RENDER_LINK;
1778
zTarget = &z[1];
1779
for(i=1; z[i] && z[i]!=']'; i++){
1780
if( z[i]=='|' && zDisplay==0 ){
1781
zDisplay = &z[i+1];
1782
for(j=i; j>0 && fossil_isspace(z[j-1]); j--){}
1783
iS1 = j;
1784
cS1 = z[j];
1785
z[j] = 0;
1786
}
1787
}
1788
z[i] = 0;
1789
if( zDisplay==0 ){
1790
zDisplay = zTarget + interwiki_removable_prefix(zTarget);
1791
}else{
1792
while( fossil_isspace(*zDisplay) ) zDisplay++;
1793
}
1794
p->mRender |= wiki_resolve_hyperlink(p->pOut, p->state,
1795
zTarget, zClose, sizeof(zClose), zOrig, 0);
1796
if( linksOnly || zClose[0]==0 || p->inVerbatim ){
1797
if( cS1 ) z[iS1] = cS1;
1798
if( zClose[0]!=']' ){
1799
blob_appendf(p->pOut, "[%h]%s", zTarget, zClose);
1800
}else{
1801
blob_appendf(p->pOut, "%h%s", zTarget, zClose);
1802
}
1803
}else{
1804
savedState = p->state;
1805
p->state &= ~ALLOW_WIKI;
1806
p->state |= FONT_MARKUP_ONLY;
1807
wiki_render(p, zDisplay);
1808
p->state = savedState;
1809
blob_append(p->pOut, zClose, -1);
1810
}
1811
break;
1812
}
1813
case TOKEN_TEXT: {
1814
int i;
1815
for(i=0; i<n && fossil_isspace(z[i]); i++){}
1816
if( i<n ) startAutoParagraph(p);
1817
blob_append(p->pOut, z, n);
1818
break;
1819
}
1820
case TOKEN_RAW: {
1821
if( linksOnly ){
1822
htmlize_to_blob(p->pOut, z, n);
1823
}else{
1824
blob_append(p->pOut, z, n);
1825
}
1826
break;
1827
}
1828
case TOKEN_MARKUP: {
1829
const char *zId;
1830
int iDiv;
1831
(void)parseMarkup(&markup, z);
1832
1833
/* Convert <title> to <h1 align='center'> */
1834
if( markup.iCode==MARKUP_TITLE && !p->inVerbatim ){
1835
markup.iCode = MARKUP_H1;
1836
markup.nAttr = 1;
1837
markup.aAttr[0].iACode = ATTR_ALIGN;
1838
markup.aAttr[0].zValue = "center";
1839
markup.aAttr[0].cTerm = 0;
1840
}
1841
1842
/* Markup of the form </div id=ID> where there is a matching
1843
** ID somewhere on the stack. Exit any contained verbatim.
1844
** Pop the stack up to the matching <div>. Discard the </div>
1845
*/
1846
if( markup.iCode==MARKUP_DIV && markup.endTag &&
1847
(zId = markupId(&markup))!=0 &&
1848
(iDiv = findTagWithId(p, MARKUP_DIV, zId))>=0
1849
){
1850
if( p->inVerbatim ){
1851
p->inVerbatim = 0;
1852
p->state = p->preVerbState;
1853
blob_append_string(p->pOut, "</pre>");
1854
}
1855
while( p->nStack>iDiv+1 ) popStack(p);
1856
if( p->aStack[iDiv].allowWiki ){
1857
p->state |= ALLOW_WIKI;
1858
}else{
1859
p->state &= ~ALLOW_WIKI;
1860
}
1861
assert( p->nStack==iDiv+1 );
1862
p->nStack--;
1863
}else
1864
1865
/* If within <verbatim id=ID> ignore everything other than
1866
** </verbatim id=ID> and the </dev id=ID2> above.
1867
*/
1868
if( p->inVerbatim ){
1869
if( endVerbatim(p, &markup) ){
1870
p->inVerbatim = 0;
1871
p->state = p->preVerbState;
1872
blob_append_string(p->pOut, "</pre>");
1873
}else{
1874
unparseMarkup(&markup);
1875
blob_append_string(p->pOut, "&lt;");
1876
n = 1;
1877
}
1878
}else
1879
1880
/* Render invalid markup literally. The markup appears in the
1881
** final output as plain text.
1882
*/
1883
if( markup.iCode==MARKUP_INVALID ){
1884
p->mRender |= RENDER_BADTAG;
1885
unparseMarkup(&markup);
1886
startAutoParagraph(p);
1887
if( p->state & WIKI_MARK ){
1888
p->mRender |= RENDER_MARK;
1889
blob_append_string(p->pOut, "<mark>");
1890
htmlize_to_blob(p->pOut, z, n);
1891
blob_append_string(p->pOut, "</mark>");
1892
}else{
1893
blob_append_string(p->pOut, "&lt;");
1894
htmlize_to_blob(p->pOut, z+1, n-1);
1895
}
1896
}else
1897
1898
/* If the markup is not font-change markup ignore it if the
1899
** font-change-only flag is set.
1900
*/
1901
if( (markup.iType&MUTYPE_FONT)==0 && (p->state & FONT_MARKUP_ONLY)!=0 ){
1902
/* Do nothing */
1903
}else
1904
1905
if( markup.iCode==MARKUP_NOWIKI ){
1906
if( markup.endTag ){
1907
p->state |= ALLOW_WIKI;
1908
}else{
1909
p->state &= ~ALLOW_WIKI;
1910
}
1911
}else
1912
1913
/* Generate end-tags */
1914
if( markup.endTag ){
1915
popStackToTag(p, markup.iCode);
1916
}else
1917
1918
/* Push <div> markup onto the stack together with the id=ID attribute.
1919
*/
1920
if( markup.iCode==MARKUP_DIV && attributeValue(&markup, ATTR_ID)!=0 ){
1921
pushStackWithId(p, markup.iCode, markupId(&markup),
1922
(p->state & ALLOW_WIKI)!=0);
1923
}else
1924
1925
/* Enter <verbatim> processing. With verbatim enabled, all other
1926
** markup other than the corresponding end-tag with the same ID is
1927
** ignored.
1928
*/
1929
if( markup.iCode==MARKUP_VERBATIM ){
1930
int ii; /*, vAttrDidAppend=0;*/
1931
const char *zClass = 0;
1932
p->zVerbatimId = 0;
1933
p->inVerbatim = 1;
1934
p->preVerbState = p->state;
1935
p->state &= ~ALLOW_WIKI;
1936
for(ii=0; ii<markup.nAttr; ii++){
1937
if( markup.aAttr[ii].iACode == ATTR_ID ){
1938
p->zVerbatimId = markup.aAttr[ii].zValue;
1939
}else if( markup.aAttr[ii].iACode==ATTR_TYPE ){
1940
zClass = markup.aAttr[ii].zValue;
1941
}else if( markup.aAttr[ii].iACode==ATTR_LINKS
1942
&& !is_false(markup.aAttr[ii].zValue) ){
1943
p->state |= ALLOW_LINKS;
1944
}
1945
}
1946
endAutoParagraph(p);
1947
if( zClass==0 ){
1948
blob_append_string(p->pOut, "<pre class='verbatim'>");
1949
}else if( strncmp(zClass,"pikchr",6)==0 &&
1950
(fossil_isspace(zClass[6]) || zClass[6]==0) ){
1951
n += wiki_process_pikchr(p, z+n, zClass);
1952
p->inVerbatim = 0;
1953
p->state = p->preVerbState;
1954
}else{
1955
blob_appendf(p->pOut, "<pre name='code' class='%h'>",
1956
zClass);
1957
}
1958
p->wantAutoParagraph = 0;
1959
}else
1960
if( markup.iType==MUTYPE_LI ){
1961
if( backupToType(p, MUTYPE_LIST)==0 ){
1962
endAutoParagraph(p);
1963
pushStack(p, MARKUP_UL);
1964
blob_append_string(p->pOut, "<ul>");
1965
}
1966
pushStack(p, MARKUP_LI);
1967
renderMarkup(p->pOut, &markup);
1968
}else
1969
if( markup.iType==MUTYPE_TR ){
1970
if( backupToType(p, MUTYPE_TABLE) ){
1971
pushStack(p, MARKUP_TR);
1972
renderMarkup(p->pOut, &markup);
1973
}
1974
}else
1975
if( markup.iType==MUTYPE_TD ){
1976
if( backupToType(p, MUTYPE_TABLE|MUTYPE_TR) ){
1977
if( stackTopType(p)==MUTYPE_TABLE ){
1978
pushStack(p, MARKUP_TR);
1979
blob_append_string(p->pOut, "<tr>");
1980
}
1981
p->wantAutoParagraph = 0;
1982
pushStack(p, markup.iCode);
1983
renderMarkup(p->pOut, &markup);
1984
}
1985
}else
1986
if( markup.iType==MUTYPE_HYPERLINK ){
1987
if( !isButtonHyperlink(p, &markup, z, &n) ){
1988
popStackToTag(p, markup.iCode);
1989
startAutoParagraph(p);
1990
renderMarkup(p->pOut, &markup);
1991
pushStack(p, markup.iCode);
1992
}
1993
}else
1994
{
1995
if( markup.iType==MUTYPE_FONT ){
1996
startAutoParagraph(p);
1997
}else if( markup.iType==MUTYPE_BLOCK || markup.iType==MUTYPE_LIST ){
1998
p->mRender |= RENDER_BLOCKTAG;
1999
p->wantAutoParagraph = 0;
2000
}
2001
if( markup.iCode==MARKUP_HR
2002
|| markup.iCode==MARKUP_H1
2003
|| markup.iCode==MARKUP_H2
2004
|| markup.iCode==MARKUP_H3
2005
|| markup.iCode==MARKUP_H4
2006
|| markup.iCode==MARKUP_H5
2007
|| markup.iCode==MARKUP_P
2008
){
2009
endAutoParagraph(p);
2010
}
2011
if( (markup.iType & MUTYPE_STACK )!=0 ){
2012
pushStack(p, markup.iCode);
2013
}
2014
renderMarkup(p->pOut, &markup);
2015
}
2016
break;
2017
}
2018
}
2019
z += n;
2020
}
2021
}
2022
2023
/*
2024
** Transform the text in the pIn blob. Write the results
2025
** into the pOut blob. The pOut blob should already be
2026
** initialized. The output is merely appended to pOut.
2027
** If pOut is NULL, then the output is appended to the CGI
2028
** reply.
2029
**
2030
** Return a mask of RENDER_ flags indicating what happened.
2031
*/
2032
int wiki_convert(Blob *pIn, Blob *pOut, int flags){
2033
Renderer renderer;
2034
2035
memset(&renderer, 0, sizeof(renderer));
2036
renderer.renderFlags = flags;
2037
renderer.state = ALLOW_WIKI|AT_NEWLINE|AT_PARAGRAPH|flags;
2038
if( flags & WIKI_INLINE ){
2039
renderer.wantAutoParagraph = 0;
2040
}else{
2041
renderer.wantAutoParagraph = 1;
2042
}
2043
if( wikiUsesHtml() ){
2044
renderer.state |= WIKI_HTMLONLY;
2045
}
2046
if( pOut ){
2047
renderer.pOut = pOut;
2048
}else{
2049
renderer.pOut = cgi_output_blob();
2050
}
2051
2052
blob_to_utf8_no_bom(pIn, 0);
2053
wiki_render(&renderer, blob_str(pIn));
2054
endAutoParagraph(&renderer);
2055
while( renderer.nStack ){
2056
popStack(&renderer);
2057
}
2058
blob_append_char(renderer.pOut, '\n');
2059
free(renderer.aStack);
2060
return renderer.mRender;
2061
}
2062
2063
/*
2064
** COMMAND: test-wiki-render
2065
**
2066
** Usage: %fossil test-wiki-render FILE [OPTIONS]
2067
**
2068
** Translate the input FILE from Fossil-wiki into HTML and write
2069
** the resulting HTML on standard output.
2070
**
2071
** Options:
2072
** --buttons Set the WIKI_BUTTONS flag
2073
** --dark-pikchr Render pikchrs in dark mode
2074
** --flow Render as text using comment_format
2075
** --htmlonly Set the WIKI_HTMLONLY flag
2076
** --inline Set the WIKI_INLINE flag
2077
** --linksonly Set the WIKI_LINKSONLY flag
2078
** -m TEXT Use TEXT in place of the content of FILE
2079
** --mark Add <mark>...</mark> around problems
2080
** --nobadlinks Set the WIKI_NOBADLINKS flag
2081
** --text Run the output through html_to_plaintext()
2082
** --type Break down the return code from wiki_convert()
2083
*/
2084
void test_wiki_render(void){
2085
Blob in, out;
2086
int flags = 0;
2087
int bText;
2088
int bFlow = 0;
2089
int showType = 0;
2090
int mType;
2091
const char *zIn;
2092
if( find_option("buttons",0,0)!=0 ) flags |= WIKI_BUTTONS;
2093
if( find_option("htmlonly",0,0)!=0 ) flags |= WIKI_HTMLONLY;
2094
if( find_option("linksonly",0,0)!=0 ) flags |= WIKI_LINKSONLY;
2095
if( find_option("nobadlinks",0,0)!=0 ) flags |= WIKI_NOBADLINKS;
2096
if( find_option("inline",0,0)!=0 ) flags |= WIKI_INLINE;
2097
if( find_option("mark",0,0)!=0 ) flags |= WIKI_MARK;
2098
if( find_option("dark-pikchr",0,0)!=0 ){
2099
pikchr_to_html_add_flags( PIKCHR_PROCESS_DARK_MODE );
2100
}
2101
bText = find_option("text",0,0)!=0;
2102
bFlow = find_option("flow",0,0)!=0;
2103
showType = find_option("type",0,0)!=0;
2104
zIn = find_option("msg","m",1);
2105
db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE,0);
2106
verify_all_options();
2107
if( (zIn==0 && g.argc!=3) || (zIn!=0 && g.argc!=2) ) usage("FILE");
2108
blob_zero(&out);
2109
if( zIn ){
2110
blob_init(&in, zIn, -1);
2111
}else{
2112
blob_read_from_file(&in, g.argv[2], ExtFILE);
2113
}
2114
mType = wiki_convert(&in, &out, flags);
2115
if( bText ){
2116
Blob txt;
2117
int htot = HTOT_TRIM;
2118
if( terminal_is_vt100() ) htot |= HTOT_VT100;
2119
if( bFlow ) htot |= HTOT_FLOW;
2120
blob_init(&txt, 0, 0);
2121
html_to_plaintext(blob_str(&out),&txt, htot);
2122
blob_reset(&out);
2123
out = txt;
2124
}
2125
if( bFlow ){
2126
fossil_print(" ");
2127
comment_print(blob_str(&out), 0, 3, terminal_get_width(80)-3,
2128
get_comment_format());
2129
}else{
2130
blob_write_to_file(&out, "-");
2131
}
2132
if( showType ){
2133
fossil_print("%.*c\nResult Codes:", terminal_get_width(80)-1, '*');
2134
if( mType & RENDER_LINK ) fossil_print(" LINK");
2135
if( mType & RENDER_ENTITY ) fossil_print(" ENTITY");
2136
if( mType & RENDER_TAG ) fossil_print(" TAG");
2137
if( mType & RENDER_BLOCKTAG ) fossil_print(" BLOCKTAG");
2138
if( mType & RENDER_BLOCK ) fossil_print(" BLOCK");
2139
if( mType & RENDER_MARK ) fossil_print(" MARK");
2140
if( mType & RENDER_BADLINK ) fossil_print(" BADLINK");
2141
if( mType & RENDER_BADTARGET ) fossil_print(" BADTARGET");
2142
if( mType & RENDER_BADTAG ) fossil_print(" BADTAG");
2143
if( mType & RENDER_BADENTITY ) fossil_print(" BADENTITY");
2144
if( mType & RENDER_BADHTML ) fossil_print(" BADHTML");
2145
if( mType & RENDER_ERROR ) fossil_print(" ERROR");
2146
fossil_print("\n");
2147
}
2148
}
2149
2150
/*
2151
** COMMAND: test-markdown-render
2152
**
2153
** Usage: %fossil test-markdown-render TEXT ...
2154
**
2155
** Render markdown in TEXT as HTML on stdout, where TEXT
2156
** may be a file name or a markdown-formatted string.
2157
**
2158
** Options:
2159
**
2160
** --dark-pikchr Render pikchrs in dark mode
2161
** --lint-footnotes Print stats for footnotes-related issues
2162
** --safe Restrict the output to use only "safe" HTML
2163
** --text Run the output through html_to_plaintext().
2164
*/
2165
void test_markdown_render(void){
2166
Blob in, out;
2167
int i;
2168
int bSafe = 0, bFnLint = 0, bText = 0;
2169
db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE,0);
2170
bSafe = find_option("safe",0,0)!=0;
2171
bFnLint = find_option("lint-footnotes",0,0)!=0;
2172
if( find_option("dark-pikchr",0,0)!=0 ){
2173
pikchr_to_html_add_flags( PIKCHR_PROCESS_DARK_MODE );
2174
}
2175
bText = find_option("text",0,0)!=0;
2176
verify_all_options();
2177
for(i=2; i<g.argc; i++){
2178
blob_zero(&out);
2179
if(file_isfile(g.argv[i], ExtFILE)){
2180
blob_read_from_file(&in, g.argv[i], ExtFILE);
2181
if( g.argc>3 ){
2182
fossil_print("<!------ %h ------->\n", g.argv[i]);
2183
}
2184
}else{
2185
blob_init(&in, g.argv[i], -1);
2186
if( g.argc>3 ){
2187
fossil_print("<!------ script #%d ------->\n", i-1);
2188
}
2189
}
2190
markdown_to_html(&in, 0, &out);
2191
safe_html_context( bSafe ? DOCSRC_UNTRUSTED : DOCSRC_TRUSTED );
2192
safe_html(&out);
2193
if( bText ){
2194
Blob txt;
2195
blob_init(&txt, 0, 0);
2196
html_to_plaintext(blob_str(&out), &txt, HTOT_VT100);
2197
blob_reset(&out);
2198
out = txt;
2199
}
2200
blob_write_to_file(&out, "-");
2201
blob_reset(&in);
2202
blob_reset(&out);
2203
}
2204
if( bFnLint && (g.ftntsIssues[0] || g.ftntsIssues[1]
2205
|| g.ftntsIssues[2] || g.ftntsIssues[3] )){
2206
fossil_fatal("There were issues with footnotes:\n"
2207
" %8d misreference%s\n"
2208
" %8d unreferenced\n"
2209
" %8d split\n"
2210
" %8d overnested",
2211
g.ftntsIssues[0], g.ftntsIssues[0]==1?"":"s",
2212
g.ftntsIssues[1], g.ftntsIssues[2], g.ftntsIssues[3]);
2213
}
2214
}
2215
2216
/*
2217
** Search for a <title>...</title> at the beginning of a wiki page.
2218
** Return true (nonzero) if a title is found. Return zero if there is
2219
** not title.
2220
**
2221
** If a title is found, initialize the pTitle blob to be the content
2222
** of the title and initialize pTail to be the text that follows the
2223
** title.
2224
*/
2225
int wiki_find_title(Blob *pIn, Blob *pTitle, Blob *pTail){
2226
char *z;
2227
int i;
2228
int iStart;
2229
blob_to_utf8_no_bom(pIn, 0);
2230
z = blob_str(pIn);
2231
for(i=0; fossil_isspace(z[i]); i++){}
2232
if( z[i]!='<' ) return 0;
2233
i++;
2234
if( strncmp(&z[i],"title>", 6)!=0 ) return 0;
2235
for(iStart=i+6; fossil_isspace(z[iStart]); iStart++){}
2236
for(i=iStart; z[i] && (z[i]!='<' || strncmp(&z[i],"</title>",8)!=0); i++){}
2237
if( strncmp(&z[i],"</title>",8)!=0 ){
2238
blob_init(pTitle, 0, 0);
2239
blob_init(pTail, &z[iStart], -1);
2240
return 1;
2241
}
2242
if( i-iStart>0 ){
2243
blob_init(pTitle, &z[iStart], i-iStart);
2244
}else{
2245
blob_init(pTitle, 0, 0);
2246
}
2247
blob_init(pTail, &z[i+8], -1);
2248
return 1;
2249
}
2250
2251
/*
2252
** Parse text looking for wiki hyperlinks in one of the formats:
2253
**
2254
** [target]
2255
** [target|...]
2256
**
2257
** Where "target" can be either an artifact ID prefix or a wiki page
2258
** name. For each such hyperlink found, add an entry to the
2259
** backlink table.
2260
**
2261
** The return value is a mask of RENDER_ flags.
2262
*/
2263
int wiki_extract_links(
2264
char *z, /* The wiki text from which to extract links */
2265
Backlink *pBklnk, /* Backlink extraction context */
2266
int flags /* wiki parsing flags */
2267
){
2268
Renderer renderer;
2269
int tokenType;
2270
ParsedMarkup markup;
2271
int n;
2272
int wikiHtmlOnly = 0;
2273
2274
memset(&renderer, 0, sizeof(renderer));
2275
renderer.state = ALLOW_WIKI|AT_NEWLINE|AT_PARAGRAPH;
2276
if( wikiUsesHtml() ){
2277
renderer.state |= WIKI_HTMLONLY;
2278
wikiHtmlOnly = 1;
2279
}
2280
2281
while( z[0] ){
2282
if( wikiHtmlOnly ){
2283
n = nextRawToken(z, &renderer, &tokenType);
2284
}else{
2285
n = nextWikiToken(z, &renderer, &tokenType);
2286
}
2287
switch( tokenType ){
2288
case TOKEN_LINK: {
2289
char *zTarget;
2290
int i;
2291
2292
zTarget = &z[1];
2293
for(i=0; zTarget[i] && zTarget[i]!='|' && zTarget[i]!=']'; i++){}
2294
while(i>1 && zTarget[i-1]==' '){ i--; }
2295
backlink_create(pBklnk, zTarget, i);
2296
break;
2297
}
2298
case TOKEN_MARKUP: {
2299
const char *zId;
2300
int iDiv;
2301
parseMarkup(&markup, z);
2302
2303
/* Markup of the form </div id=ID> where there is a matching
2304
** ID somewhere on the stack. Exit the verbatim if were are in
2305
** it. Pop the stack up to the matching <div>. Discard the
2306
** </div>
2307
*/
2308
if( markup.iCode==MARKUP_DIV && markup.endTag &&
2309
(zId = markupId(&markup))!=0 &&
2310
(iDiv = findTagWithId(&renderer, MARKUP_DIV, zId))>=0
2311
){
2312
if( renderer.inVerbatim ){
2313
renderer.inVerbatim = 0;
2314
renderer.state = renderer.preVerbState;
2315
}
2316
while( renderer.nStack>iDiv+1 ) popStack(&renderer);
2317
if( renderer.aStack[iDiv].allowWiki ){
2318
renderer.state |= ALLOW_WIKI;
2319
}else{
2320
renderer.state &= ~ALLOW_WIKI;
2321
}
2322
renderer.nStack--;
2323
}else
2324
2325
/* If within <verbatim id=ID> ignore everything other than
2326
** </verbatim id=ID> and the </dev id=ID2> above.
2327
*/
2328
if( renderer.inVerbatim ){
2329
if( endVerbatim(&renderer, &markup) ){
2330
renderer.inVerbatim = 0;
2331
renderer.state = renderer.preVerbState;
2332
}else{
2333
n = 1;
2334
}
2335
}else
2336
2337
/* Render invalid markup literally. The markup appears in the
2338
** final output as plain text.
2339
*/
2340
if( markup.iCode==MARKUP_INVALID ){
2341
n = 1;
2342
}else
2343
2344
/* If the markup is not font-change markup ignore it if the
2345
** font-change-only flag is set.
2346
*/
2347
if( (markup.iType&MUTYPE_FONT)==0 &&
2348
(renderer.state & FONT_MARKUP_ONLY)!=0 ){
2349
/* Do nothing */
2350
}else
2351
2352
if( markup.iCode==MARKUP_NOWIKI ){
2353
if( markup.endTag ){
2354
renderer.state |= ALLOW_WIKI;
2355
}else{
2356
renderer.state &= ~ALLOW_WIKI;
2357
}
2358
}else
2359
2360
/* Generate end-tags */
2361
if( markup.endTag ){
2362
popStackToTag(&renderer, markup.iCode);
2363
}else
2364
2365
/* Push <div> markup onto the stack together with the id=ID attribute.
2366
*/
2367
if( markup.iCode==MARKUP_DIV ){
2368
pushStackWithId(&renderer, markup.iCode, markupId(&markup),
2369
(renderer.state & ALLOW_WIKI)!=0);
2370
}else
2371
2372
/* Enter <verbatim> processing. With verbatim enabled, all other
2373
** markup other than the corresponding end-tag with the same ID is
2374
** ignored.
2375
*/
2376
if( markup.iCode==MARKUP_VERBATIM ){
2377
int vAttrIdx;
2378
renderer.zVerbatimId = 0;
2379
renderer.inVerbatim = 1;
2380
renderer.preVerbState = renderer.state;
2381
renderer.state &= ~ALLOW_WIKI;
2382
for (vAttrIdx = 0; vAttrIdx < markup.nAttr; vAttrIdx++){
2383
if( markup.aAttr[vAttrIdx].iACode == ATTR_ID ){
2384
renderer.zVerbatimId = markup.aAttr[0].zValue;
2385
}
2386
}
2387
renderer.wantAutoParagraph = 0;
2388
}
2389
2390
/* Restore the input text to its original configuration
2391
*/
2392
unparseMarkup(&markup);
2393
break;
2394
}
2395
default: {
2396
break;
2397
}
2398
}
2399
z += n;
2400
}
2401
free(renderer.aStack);
2402
return renderer.mRender;
2403
}
2404
2405
/*
2406
** Return the length, in bytes, of the HTML token that z is pointing to.
2407
*/
2408
int html_token_length(const char *z){
2409
int n;
2410
char c;
2411
if( (c=z[0])=='<' ){
2412
n = html_tag_length(z);
2413
if( n<=0 ) n = 1;
2414
}else if( fossil_isspace(c) ){
2415
for(n=1; z[n] && fossil_isspace(z[n]); n++){}
2416
}else if( c=='&' ){
2417
n = z[1]=='#' ? 2 : 1;
2418
while( fossil_isalnum(z[n]) ) n++;
2419
if( z[n]==';' ) n++;
2420
}else{
2421
n = 1;
2422
for(n=1; 1; n++){
2423
if( (c = z[n]) > '<' ) continue;
2424
if( c=='<' || c=='&' || fossil_isspace(c) || c==0 ) break;
2425
}
2426
}
2427
return n;
2428
}
2429
2430
/*
2431
** z points to someplace in the middle of HTML markup. Return the length
2432
** of the subtoken that starts on z.
2433
*/
2434
int html_subtoken_length(const char *z){
2435
int n;
2436
char c;
2437
c = z[0];
2438
if( fossil_isspace(c) ){
2439
for(n=1; z[n] && fossil_isspace(z[n]); n++){}
2440
return n;
2441
}
2442
if( c=='"' || c=='\'' ){
2443
for(n=1; z[n] && z[n]!=c && z[n]!='>'; n++){}
2444
if( z[n]==c ) n++;
2445
return n;
2446
}
2447
if( c=='>' ){
2448
return 0;
2449
}
2450
if( c=='=' ){
2451
return 1;
2452
}
2453
if( fossil_isalnum(c) || c=='/' ){
2454
for(n=1; (c=z[n])!=0 && (fossil_isalnum(c) || c=='-' || c=='_'); n++){}
2455
return n;
2456
}
2457
return 1;
2458
}
2459
2460
/*
2461
** z points to an HTML markup token: <TAG ATTR=VALUE ...>
2462
** This routine looks for the VALUE associated with zAttr and returns
2463
** a pointer to the start of that value and sets *pLen to be the length
2464
** in bytes for the value. Or it returns NULL if no such attr exists.
2465
*/
2466
const char *html_attribute(const char *zMarkup, const char *zAttr, int *pLen){
2467
int i = 1;
2468
int n;
2469
int nAttr;
2470
int iMatchCnt = 0;
2471
assert( zMarkup[0]=='<' );
2472
assert( zMarkup[1]!=0 );
2473
n = html_subtoken_length(zMarkup+i);
2474
if( n==0 ) return 0;
2475
i += n;
2476
nAttr = (int)strlen(zAttr);
2477
while( 1 ){
2478
const char *zStart = zMarkup+i;
2479
n = html_subtoken_length(zStart);
2480
if( n==0 ) break;
2481
i += n;
2482
if( fossil_isspace(zStart[0]) ) continue;
2483
if( n==nAttr && fossil_strnicmp(zAttr,zStart,nAttr)==0 ){
2484
iMatchCnt = 1;
2485
}else if( n==1 && zStart[0]=='=' && iMatchCnt==1 ){
2486
iMatchCnt = 2;
2487
}else if( iMatchCnt==2 ){
2488
if( (zStart[0]=='"' || zStart[0]=='\'') && zStart[n-1]==zStart[0] ){
2489
zStart++;
2490
n -= 2;
2491
}
2492
*pLen = n;
2493
return zStart;
2494
}else{
2495
iMatchCnt = 0;
2496
}
2497
}
2498
return 0;
2499
}
2500
2501
/*
2502
** COMMAND: test-html-tokenize
2503
**
2504
** Tokenize an HTML file. Return the offset and length and text of
2505
** each token - one token per line. Omit white-space tokens.
2506
*/
2507
void test_html_tokenize(void){
2508
Blob in;
2509
char *z;
2510
int i;
2511
int iOfst, n;
2512
2513
for(i=2; i<g.argc; i++){
2514
blob_read_from_file(&in, g.argv[i], ExtFILE);
2515
z = blob_str(&in);
2516
for(iOfst=0; z[iOfst]; iOfst+=n){
2517
n = html_token_length(z+iOfst);
2518
if( fossil_isspace(z[iOfst]) ) continue;
2519
fossil_print("%d %d %.*s\n", iOfst, n, n, z+iOfst);
2520
if( z[iOfst]=='<' && n>1 ){
2521
int j,k;
2522
for(j=iOfst+1; (k = html_subtoken_length(z+j))>0; j+=k){
2523
if( fossil_isspace(z[j]) || z[j]=='=' ) continue;
2524
fossil_print("# %d %d %.*s\n", j, k, k, z+j);
2525
}
2526
}
2527
}
2528
blob_reset(&in);
2529
}
2530
}
2531
2532
/*
2533
** Attempt to reformat messy HTML to be easily readable by humans.
2534
**
2535
** * Try to keep lines less than 80 characters in length
2536
** * Collapse white space into a single space
2537
** * Put a blank line before:
2538
** <blockquote><center><code><hN><p><pre><table>
2539
** * Put a newline after <br> and <hr>
2540
** * Start each of the following elements on a new line:
2541
** <address><cite><dd><div><dl><dt><li><ol><samp>
2542
** <tbody><td><tfoot><th><thead><tr><ul>
2543
**
2544
** Except, do not do any reformatting inside of <pre>...</pre>
2545
*/
2546
void htmlTidy(const char *zIn, Blob *pOut){
2547
int n;
2548
int nPre = 0;
2549
int iCur = 0;
2550
int wantSpace = 0;
2551
int omitSpace = 1;
2552
while( zIn[0] ){
2553
n = html_token_length(zIn);
2554
if( zIn[0]=='<' && n>1 ){
2555
int i, j;
2556
int isCloseTag;
2557
int eTag;
2558
int eType;
2559
char zTag[32];
2560
isCloseTag = zIn[1]=='/';
2561
for(i=0, j=1+isCloseTag; i<30 && fossil_isalnum(zIn[j]); i++, j++){
2562
zTag[i] = fossil_tolower(zIn[j]);
2563
}
2564
zTag[i] = 0;
2565
eTag = findTag(zTag);
2566
eType = aMarkup[eTag].iType;
2567
if( eTag==MARKUP_PRE ){
2568
if( isCloseTag ){
2569
nPre--;
2570
blob_append(pOut, zIn, n);
2571
zIn += n;
2572
if( nPre==0 ){ blob_append_char(pOut, '\n'); iCur = 0; }
2573
continue;
2574
}else{
2575
if( iCur && nPre==0 ){ blob_append_char(pOut, '\n'); iCur = 0; }
2576
nPre++;
2577
}
2578
}else if( eType & (MUTYPE_BLOCK|MUTYPE_TABLE) ){
2579
if( !isCloseTag && nPre==0 && blob_size(pOut)>0 ){
2580
blob_append(pOut, "\n\n", 1 + (iCur>0));
2581
iCur = 0;
2582
}
2583
wantSpace = 0;
2584
omitSpace = 1;
2585
}else if( (eType & (MUTYPE_LIST|MUTYPE_LI|MUTYPE_TR|MUTYPE_TD))!=0
2586
|| eTag==MARKUP_HR
2587
){
2588
if( nPre==0 && (!isCloseTag || (eType&MUTYPE_LIST)!=0) && iCur>0 ){
2589
blob_append_char(pOut, '\n');
2590
iCur = 0;
2591
}
2592
wantSpace = 0;
2593
omitSpace = 1;
2594
}
2595
if( wantSpace && nPre==0 ){
2596
if( iCur+n+1>=80 ){
2597
blob_append_char(pOut, '\n');
2598
iCur = 0;
2599
}else{
2600
blob_append_char(pOut, ' ');
2601
iCur++;
2602
}
2603
}
2604
blob_append(pOut, zIn, n);
2605
iCur += n;
2606
wantSpace = 0;
2607
if( eTag==MARKUP_BR || eTag==MARKUP_HR ){
2608
blob_append_char(pOut, '\n');
2609
iCur = 0;
2610
}
2611
}else if( fossil_isspace(zIn[0]) ){
2612
if( nPre ){
2613
blob_append(pOut, zIn, n);
2614
}else{
2615
wantSpace = !omitSpace;
2616
}
2617
}else{
2618
if( wantSpace && nPre==0 ){
2619
if( iCur+n+1>=80 ){
2620
blob_append_char(pOut, '\n');
2621
iCur = 0;
2622
}else{
2623
blob_append_char(pOut, ' ');
2624
iCur++;
2625
}
2626
}
2627
blob_append(pOut, zIn, n);
2628
iCur += n;
2629
wantSpace = omitSpace = 0;
2630
}
2631
zIn += n;
2632
}
2633
if( iCur ) blob_append_char(pOut, '\n');
2634
}
2635
2636
/*
2637
** COMMAND: test-html-tidy
2638
**
2639
** Run the htmlTidy() routine on the content of all files named on
2640
** the command-line and write the results to standard output.
2641
*/
2642
void test_html_tidy(void){
2643
Blob in, out;
2644
int i;
2645
2646
for(i=2; i<g.argc; i++){
2647
blob_read_from_file(&in, g.argv[i], ExtFILE);
2648
blob_zero(&out);
2649
htmlTidy(blob_str(&in), &out);
2650
blob_reset(&in);
2651
fossil_puts(blob_buffer(&out), 0, blob_size(&out));
2652
blob_reset(&out);
2653
}
2654
}
2655
2656
#if INTERFACE
2657
/*
2658
** Allowed flag options for html_to_plaintext().
2659
*/
2660
#define HTOT_VT100 0x01 /* <mark> becomes ^[[91m */
2661
#define HTOT_FLOW 0x02 /* Collapse internal whitespace to a single space */
2662
#define HTOT_TRIM 0x04 /* Trim off leading and trailing whitespace */
2663
2664
#endif /* INTERFACE */
2665
2666
/*
2667
** Add <MARK> or </MARK> to the output, or similar VT-100 escape
2668
** codes.
2669
*/
2670
static void addMark(Blob *pOut, int mFlags, int isClose){
2671
static const char *az[4] = { "<MARK>", "</MARK>", "\033[91m", "\033[0m" };
2672
int i = 0;
2673
if( isClose ) i++;
2674
if( mFlags & HTOT_VT100 ) i += 2;
2675
blob_append(pOut, az[i], -1);
2676
}
2677
2678
/*
2679
** Remove all HTML markup from the input text. The output written into
2680
** pOut is pure text.
2681
**
2682
** Put the title on the first line, if there is any <title> markup.
2683
** If there is no <title>, then create a blank first line.
2684
*/
2685
void html_to_plaintext(const char *zIn, Blob *pOut, int mFlags){
2686
int n;
2687
int i, j;
2688
int bFlow = 0; /* Transform internal WS into a single space */
2689
int prevWS = 1; /* Previous output was whitespace or start of msg */
2690
int nMark = 0; /* True if inside of <mark>..</mark> */
2691
2692
for(i=0; fossil_isspace(zIn[i]); i++){}
2693
if( i>0 && (mFlags & HTOT_TRIM)==0 ){
2694
blob_append(pOut, zIn, i);
2695
}
2696
zIn += i;
2697
if( mFlags & HTOT_FLOW ) bFlow = 1;
2698
while( zIn[0] ){
2699
n = html_token_length(zIn);
2700
if( zIn[0]=='<' && n>1 ){
2701
int isCloseTag;
2702
int eTag;
2703
int eType;
2704
char zTag[32];
2705
prevWS = 0;
2706
isCloseTag = zIn[1]=='/';
2707
for(i=0, j=1+isCloseTag; i<30 && fossil_isalnum(zIn[j]); i++, j++){
2708
zTag[i] = fossil_tolower(zIn[j]);
2709
}
2710
zTag[i] = 0;
2711
eTag = findTag(zTag);
2712
eType = aMarkup[eTag].iType;
2713
if( eTag==MARKUP_INVALID && fossil_strnicmp(zIn,"<style",6)==0 ){
2714
zIn += n;
2715
while( zIn[0] ){
2716
n = html_token_length(zIn);
2717
if( fossil_strnicmp(zIn, "</style",7)==0 ) break;
2718
zIn += n;
2719
}
2720
if( zIn[0]=='<' ) zIn += n;
2721
continue;
2722
}
2723
if( eTag==MARKUP_INVALID && strcmp(zTag,"mark")==0 ){
2724
if( isCloseTag && nMark ){
2725
addMark(pOut, mFlags, 1);
2726
nMark = 0;
2727
}else if( !isCloseTag && !nMark ){
2728
addMark(pOut, mFlags, 0);
2729
nMark = 1;
2730
}
2731
zIn += n;
2732
continue;
2733
}
2734
if( eTag==MARKUP_TITLE ){
2735
if( isCloseTag && (mFlags & HTOT_FLOW)==0 ){
2736
bFlow = 0;
2737
}else{
2738
bFlow = 1;
2739
}
2740
}
2741
if( !isCloseTag && (eType & (MUTYPE_BLOCK|MUTYPE_TABLE))!=0 ){
2742
blob_append_char(pOut, '\n');
2743
}
2744
}else if( fossil_isspace(zIn[0]) ){
2745
if( bFlow==0 ){
2746
if( zIn[n]==0 && (mFlags & HTOT_TRIM) ) break;
2747
blob_append(pOut, zIn, n);
2748
}else if( !prevWS ){
2749
prevWS = 1;
2750
blob_append_char(pOut, ' ');
2751
zIn += n;
2752
n = 0;
2753
}
2754
}else if( zIn[0]=='&' ){
2755
u32 c = '?';
2756
prevWS = 0;
2757
if( zIn[1]=='#' ){
2758
c = atoi(&zIn[2]);
2759
if( c==0 ) c = '?';
2760
}else{
2761
static const struct { int n; u32 c; char *z; } aEntity[] = {
2762
{ 5, '&', "&amp;" },
2763
{ 4, '<', "&lt;" },
2764
{ 4, '>', "&gt;" },
2765
{ 6, ' ', "&nbsp;" },
2766
{ 6, '"', "&quot;" },
2767
};
2768
int jj;
2769
for(jj=0; jj<count(aEntity); jj++){
2770
if( aEntity[jj].n==n && strncmp(aEntity[jj].z,zIn,n)==0 ){
2771
c = aEntity[jj].c;
2772
break;
2773
}
2774
}
2775
}
2776
if( c<0x00080 ){
2777
blob_append_char(pOut, c & 0xff);
2778
}else if( c<0x00800 ){
2779
blob_append_char(pOut, 0xc0 + (u8)((c>>6)&0x1f));
2780
blob_append_char(pOut, 0x80 + (u8)(c&0x3f));
2781
}else if( c<0x10000 ){
2782
blob_append_char(pOut, 0xe0 + (u8)((c>>12)&0x0f));
2783
blob_append_char(pOut, 0x80 + (u8)((c>>6)&0x3f));
2784
blob_append_char(pOut, 0x80 + (u8)(c&0x3f));
2785
}else{
2786
blob_append_char(pOut, 0xf0 + (u8)((c>>18)&0x07));
2787
blob_append_char(pOut, 0x80 + (u8)((c>>12)&0x3f));
2788
blob_append_char(pOut, 0x80 + (u8)((c>>6)&0x3f));
2789
blob_append_char(pOut, 0x80 + (u8)(c&0x3f));
2790
}
2791
}else{
2792
prevWS = 0;
2793
blob_append(pOut, zIn, n);
2794
}
2795
zIn += n;
2796
}
2797
if( nMark ){
2798
addMark(pOut, mFlags, 1);
2799
}
2800
}
2801
2802
/*
2803
** COMMAND: test-html-to-text
2804
**
2805
** Usage: %fossil test-html-to-text [OPTIONS] FILE ...
2806
**
2807
** Read all files named on the command-line. Convert the file
2808
** content from HTML to text and write the results on standard
2809
** output.
2810
**
2811
** This command is intended as a test and debug interface for
2812
** the html_to_plaintext() routine.
2813
**
2814
** Options:
2815
**
2816
** --vt100 Translate <mark> and </mark> into ANSI/VT100
2817
** escapes to highlight the contained text.
2818
*/
2819
void test_html_to_text(void){
2820
Blob in, out;
2821
int i;
2822
int mFlags = 0;
2823
if( find_option("vt100",0,0)!=0 ) mFlags |= HTOT_VT100;
2824
2825
for(i=2; i<g.argc; i++){
2826
blob_read_from_file(&in, g.argv[i], ExtFILE);
2827
blob_zero(&out);
2828
html_to_plaintext(blob_str(&in), &out, mFlags);
2829
blob_reset(&in);
2830
fossil_puts(blob_buffer(&out), 0, blob_size(&out));
2831
blob_reset(&out);
2832
}
2833
}
2834
2835
/****************************************************************************
2836
** safe-html:
2837
**
2838
** An interface for preventing HTML constructs (ex: <style>, <form>, etc)
2839
** from being inserted into Wiki and Forum posts using Markdown. See the
2840
** comment on safe_html_append() for additional information on what is meant
2841
** by "safe".
2842
**
2843
** The safe-html restrictions only apply to Markdown, as Fossil-Wiki only
2844
** allows safe-html by design - unsafe-HTML is never and has never been
2845
** allowed in Fossil-Wiki.
2846
**
2847
** This code is in the wikiformat.c file so that it can have access to the
2848
** white-list of acceptable HTML in the aMarkup[] array.
2849
*/
2850
2851
/*
2852
** An instance of this object keeps track of the nesting of HTML
2853
** elements for safe_html_append().
2854
*/
2855
typedef struct HtmlTagStack HtmlTagStack;
2856
struct HtmlTagStack {
2857
int n; /* Current tag stack depth */
2858
int nAlloc; /* Space allocated for aStack[] */
2859
int *aStack; /* The stack of tags */
2860
int aSpace[10]; /* Initial static space, to avoid malloc() */
2861
};
2862
2863
/*
2864
** Initialize bulk memory to a valid empty tagstack.
2865
*/
2866
static void html_tagstack_init(HtmlTagStack *p){
2867
p->n = 0;
2868
p->nAlloc = 0;
2869
p->aStack = p->aSpace;
2870
}
2871
2872
/*
2873
** Push a new element onto the tag stack
2874
*/
2875
static void html_tagstack_push(HtmlTagStack *p, int e){
2876
if( p->n>=ArraySize(p->aSpace) && p->n>=p->nAlloc ){
2877
if( p->nAlloc==0 ){
2878
int *aNew;
2879
p->nAlloc = 50;
2880
aNew = fossil_malloc( sizeof(p->aStack[0])*p->nAlloc );
2881
memcpy(aNew, p->aStack, sizeof(p->aStack[0])*p->n );
2882
p->aStack = aNew;
2883
}else{
2884
p->nAlloc *= 2;
2885
p->aStack = fossil_realloc(p->aStack, sizeof(p->aStack[0])*p->nAlloc );
2886
}
2887
}
2888
p->aStack[p->n++] = e;
2889
}
2890
2891
/*
2892
** Clear a tag stack, reclaiming any memory allocations.
2893
*/
2894
static void html_tagstack_clear(HtmlTagStack *p){
2895
if( p->nAlloc ){
2896
fossil_free(p->aStack);
2897
p->nAlloc = 0;
2898
p->aStack = p->aSpace;
2899
}
2900
p->n = 0;
2901
}
2902
2903
/*
2904
** The HTML end-tag eEnd wants to be added to pBlob.
2905
**
2906
** If an open-tag for eEnd exists anywhere on the stack, then
2907
** pop it and all prior elements from the task, issuing appropriate
2908
** end-tags as you go.
2909
**
2910
** If there is no open-tag for eEnd on the stack, then this
2911
** routine is a no-op.
2912
*/
2913
static void html_tagstack_pop(HtmlTagStack *p, Blob *pBlob, int eEnd){
2914
int i, e;
2915
if( eEnd!=0 ){
2916
for(i=p->n-1; i>=0 && p->aStack[i]!=eEnd; i--){}
2917
if( i<0 ){
2918
blob_appendf(pBlob, "<span class='error'>&lt;/%s&gt;</span>",
2919
aMarkup[eEnd].zName);
2920
return;
2921
}
2922
}else if( p->n==0 ){
2923
return;
2924
}
2925
do{
2926
e = p->aStack[--p->n];
2927
if( e==eEnd || (aMarkup[e].iType & MUTYPE_Nested)!=0 ){
2928
blob_appendf(pBlob, "</%s>", aMarkup[e].zName);
2929
}
2930
}while( e!=eEnd && p->n>0 );
2931
}
2932
2933
/*
2934
** Return a nonce to indicate that safe_html() can allow code through
2935
** without censoring.
2936
**
2937
** When safe_html() is asked to sanitize some HTML, it will ignore
2938
** any text in between two consecutive instances of the nonce. The
2939
** nonce itself is an HTML comment so it is harmless to keep the
2940
** nonce in the middle of the HTML stream. A different nonce is
2941
** chosen each time Fossil is run, using a lot of randomness, so
2942
** an attacker will be unable to guess the nonce in advance.
2943
**
2944
** The original use-case for this mechanism is to allow Pikchr-generated
2945
** SVG in the middle of HTML generated from Markdown. The Markdown
2946
** output will normally be processed by safe_html() to prevent accidental
2947
** or malicious introduction of harmful HTML (ex: <script>) in the
2948
** output stream. The safe_html() only lets through HTML elements
2949
** that are on its allow-list and SVG is not on that list. Hence, in order
2950
** to allow the Pikchr-generated SVG through, it must be surrounded by
2951
** the nonce.
2952
*/
2953
const char *safe_html_nonce(int bGenerate){
2954
static char *zNonce = 0;
2955
if( zNonce==0 && bGenerate ){
2956
zNonce = db_text(0, "SELECT '<!--'||hex(randomblob(32))||'-->';");
2957
}
2958
return zNonce;
2959
}
2960
#define SAFE_NONCE_SIZE (4+64+3)
2961
2962
/*
2963
** Append a safe translation of HTML text to a Blob object.
2964
**
2965
** Restriction: The input to this routine must be writable.
2966
* Temporary changes may be made to the input, but the input is restored
2967
** to its original state prior to returning. If zHtml[nHtml] is not a
2968
** zero character, then a zero might be written in that position
2969
** temporarily, but that slot will also be restored before this routine
2970
** returns.
2971
*/
2972
static void safe_html_append(Blob *pBlob, char *zHtml, int nHtml){
2973
char cLast;
2974
int i, j, n;
2975
HtmlTagStack s;
2976
ParsedMarkup markup;
2977
const char *zNonce;
2978
char *z;
2979
2980
if( nHtml<=0 ) return;
2981
cLast = zHtml[nHtml];
2982
zHtml[nHtml] = 0;
2983
html_tagstack_init(&s);
2984
2985
i = 0;
2986
while( i<nHtml ){
2987
if( zHtml[i]=='<' ){
2988
j = i;
2989
}else{
2990
z = strchr(zHtml+i, '<');
2991
if( z==0 ){
2992
blob_append(pBlob, zHtml+i, nHtml-i);
2993
break;
2994
}
2995
j = (int)(z - zHtml);
2996
blob_append(pBlob, zHtml+i, j-i);
2997
}
2998
if( zHtml[j+1]=='!'
2999
&& j+2*SAFE_NONCE_SIZE<nHtml
3000
&& (zNonce = safe_html_nonce(0))!=0
3001
&& strncmp(zHtml+j,zNonce,SAFE_NONCE_SIZE)==0
3002
&& (z = strstr(zHtml+j+SAFE_NONCE_SIZE,zNonce))!=0
3003
){
3004
i = (int)(z - zHtml) + SAFE_NONCE_SIZE;
3005
blob_append(pBlob, zHtml+j, i-j);
3006
continue;
3007
}
3008
n = html_tag_length(zHtml+j);
3009
if( n==0 ){
3010
blob_append(pBlob, "&lt;", 4);
3011
i = j+1;
3012
continue;
3013
}else{
3014
i = j + n;
3015
}
3016
parseMarkup(&markup, zHtml+j);
3017
if( markup.iCode==MARKUP_INVALID ){
3018
unparseMarkup(&markup);
3019
blob_appendf(pBlob, "<span class='error'>&lt;%.*s&gt;</span>",
3020
n-2, zHtml+j+1);
3021
continue;
3022
}
3023
if( (markup.iType & MUTYPE_Nested)==0 || markup.iCode==MARKUP_P ){
3024
renderMarkup(pBlob, &markup);
3025
}else{
3026
if( markup.endTag ){
3027
html_tagstack_pop(&s, pBlob, markup.iCode);
3028
}else{
3029
renderMarkup(pBlob, &markup);
3030
html_tagstack_push(&s, markup.iCode);
3031
}
3032
}
3033
unparseMarkup(&markup);
3034
}
3035
html_tagstack_pop(&s, pBlob, 0);
3036
html_tagstack_clear(&s);
3037
zHtml[nHtml] = cLast;
3038
}
3039
3040
/*
3041
** This local variable is true if the safe_html() function is enabled.
3042
** In other words, this is true if the output of Markdown should be
3043
** restricted to use only "safe" HTML.
3044
*/
3045
static int safeHtmlEnable = 1;
3046
3047
3048
#if INTERFACE
3049
/*
3050
** Allowed values for the eTrust parameter to safe_html_context().
3051
*/
3052
#define DOCSRC_FILE 1 /* Document is a checked-in file */
3053
#define DOCSRC_FORUM 2 /* Document is a forum post */
3054
#define DOCSRC_TICKET 3 /* Document is a ticket comment */
3055
#define DOCSRC_WIKI 4 /* Document is a wiki page */
3056
#define DOCSRC_TRUSTED 5 /* safe_html() is always a no-op */
3057
#define DOCSRC_UNTRUSTED 6 /* safe_html() is always enabled */
3058
#endif /* INTERFACE */
3059
3060
3061
/*
3062
** Specify the context in which a markdown document with potentially
3063
** unsafe HTML will be rendered.
3064
*/
3065
void safe_html_context(int eTrust){
3066
static const char *zSafeHtmlSetting = 0;
3067
char cPerm = 0;
3068
if( eTrust==DOCSRC_TRUSTED ){
3069
safeHtmlEnable = 0;
3070
return;
3071
}
3072
if( eTrust==DOCSRC_UNTRUSTED ){
3073
safeHtmlEnable = 1;
3074
return;
3075
}
3076
if( zSafeHtmlSetting==0 ){
3077
zSafeHtmlSetting = db_get("safe-html", "");
3078
}
3079
switch( eTrust ){
3080
case DOCSRC_FILE: cPerm = 'b'; break;
3081
case DOCSRC_FORUM: cPerm = 'f'; break;
3082
case DOCSRC_TICKET: cPerm = 't'; break;
3083
case DOCSRC_WIKI: cPerm = 'w'; break;
3084
}
3085
safeHtmlEnable = (strchr(zSafeHtmlSetting,cPerm)==0);
3086
}
3087
3088
/*
3089
** SETTING: safe-html width=8
3090
** This setting controls whether or not unsafe HTML elements
3091
** (such as SCRIPT or STYLE tags) are allowed in Markdown-formatted
3092
** documents. Unsafe HTML is disabled by default. If this setting
3093
** exists and is a string, then letters in that string can enable
3094
** unsafe HTML in various contexts:
3095
**
3096
** - b Unsafe HTML allowed in embedded documentation
3097
** - f Unsafe HTML allowed in forum posts
3098
** - t Unsafe HTML allowed in tickets
3099
** - w Unsafe HTML allowed on wiki pages
3100
*/
3101
/*
3102
** The input blob contains HTML. If safe-html is enabled, then
3103
** convert the input into "safe HTML". The following modifications
3104
** are made:
3105
**
3106
** 1. Remove any elements that are not on the AllowedMarkup list.
3107
** (ex: <script>, <form>, etc.)
3108
**
3109
** 2. Remove any attributes that are not on the AllowedMarkup list.
3110
** (ex: onload=, etc.)
3111
**
3112
** 3. Omit any surplus close-tags. This prevents the script from
3113
** terminating an <div> or similar in the outer context.
3114
**
3115
** 4. Insert additional close-tags as necessary so that any
3116
** tag in the input that needs a close-tag has one. This
3117
** prevents tags in the embedded script from affecting the
3118
** display of content that follows this script in the enclosing
3119
** context.
3120
**
3121
** These modifications are intended to make the generated HTML safe
3122
** to be embedded in a larger HTML document, such that the embedded
3123
** HTML has no influence on the formatting and operation of the
3124
** larger document.
3125
**
3126
** If safe-html is disabled, then this routine is a no-op.
3127
*/
3128
void safe_html(Blob *in){
3129
Blob out; /* Holding area for the revised text during construction */
3130
char *z; /* Original input text */
3131
int n; /* Number of bytes in the original input text */
3132
int k;
3133
3134
if( safeHtmlEnable==0 ) return;
3135
z = blob_str(in);
3136
n = blob_size(in);
3137
blob_init(&out, 0, 0);
3138
while( fossil_isspace(z[0]) ){ z++; n--; }
3139
for(k=n-1; k>5 && fossil_isspace(z[k]); k--){}
3140
3141
if( fossil_strnicmp(z, "<div",4)==0 && !fossil_isalpha(z[4])
3142
&& fossil_strnicmp(z+k-5, "</div>",6)==0
3143
){
3144
/* The input contains an outer <div>...</div>. Preserve the
3145
** full scope of that <div>. */
3146
int m = html_tag_length(z);
3147
k -= 5;
3148
blob_append(&out, z, m);
3149
safe_html_append(&out, z+m, k-m);
3150
blob_append(&out, z+k, n-k);
3151
}else{
3152
safe_html_append(&out, z, n);
3153
}
3154
blob_reset(in);
3155
*in = out;
3156
}
3157
3158
/*
3159
** COMMAND: test-safe-html
3160
**
3161
** Usage: %fossil test-safe-html ?OPTIONS? FILE ...
3162
**
3163
** Read files named on the command-line. Send the text of each file
3164
** through safe_html_append() and then write the result on
3165
** standard output.
3166
**
3167
** Options:
3168
** --top PATH Use PATH as g.zTop when rewriting root-relative URLs
3169
*/
3170
void test_safe_html_cmd(void){
3171
int i;
3172
Blob x;
3173
const char *zTop = find_option("top",0,1);
3174
verify_all_options();
3175
if( zTop ) g.zTop = (char*)zTop;
3176
for(i=2; i<g.argc; i++){
3177
char *z;
3178
int n;
3179
blob_read_from_file(&x, g.argv[i], ExtFILE);
3180
blob_terminate(&x);
3181
safe_html(&x);
3182
z = blob_str(&x);
3183
n = blob_size(&x);
3184
while( n>0 && (z[n-1]=='\n' || z[n-1]=='\r') ) n--;
3185
fossil_print("%.*s\n", n, z);
3186
blob_reset(&x);
3187
}
3188
}
3189

Keyboard Shortcuts

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