|
1
|
/* |
|
2
|
** Copyright (c) 2012 D. Richard Hipp |
|
3
|
** |
|
4
|
** This program is free software; you can redistribute it and/or |
|
5
|
** modify it under the terms of the Simplified BSD License (also |
|
6
|
** known as the "2-Clause License" or "FreeBSD License".) |
|
7
|
|
|
8
|
** This program is distributed in the hope that it will be useful, |
|
9
|
** but without any warranty; without even the implied warranty of |
|
10
|
** merchantability or fitness for a particular purpose. |
|
11
|
** |
|
12
|
** Author contact information: |
|
13
|
** [email protected] |
|
14
|
** http://www.hwaci.com/drh/ |
|
15
|
** |
|
16
|
******************************************************************************* |
|
17
|
** |
|
18
|
** This file contains code to parse a blob containing markdown text, |
|
19
|
** using an external renderer. |
|
20
|
*/ |
|
21
|
|
|
22
|
#include "config.h" |
|
23
|
#include "markdown.h" |
|
24
|
|
|
25
|
#include <assert.h> |
|
26
|
#include <string.h> |
|
27
|
#include <stdlib.h> |
|
28
|
|
|
29
|
#define MKD_LI_END 8 /* internal list flag */ |
|
30
|
|
|
31
|
/******************** |
|
32
|
* TYPE DEFINITIONS * |
|
33
|
********************/ |
|
34
|
|
|
35
|
#if INTERFACE |
|
36
|
|
|
37
|
/* mkd_autolink -- type of autolink */ |
|
38
|
enum mkd_autolink { |
|
39
|
MKDA_NOT_AUTOLINK, /* used internally when it is not an autolink*/ |
|
40
|
MKDA_NORMAL, /* normal http/http/ftp link */ |
|
41
|
MKDA_EXPLICIT_EMAIL, /* e-mail link with explicit mailto: */ |
|
42
|
MKDA_IMPLICIT_EMAIL /* e-mail link without mailto: */ |
|
43
|
}; |
|
44
|
|
|
45
|
/* mkd_renderer -- functions for rendering parsed data */ |
|
46
|
struct mkd_renderer { |
|
47
|
/* document level callbacks */ |
|
48
|
void (*prolog)(struct Blob *ob, void *opaque); |
|
49
|
void (*epilog)(struct Blob *ob, void *opaque); |
|
50
|
void (*footnotes)(struct Blob *ob, const struct Blob *items, void *opaque); |
|
51
|
|
|
52
|
/* block level callbacks - NULL skips the block */ |
|
53
|
void (*blockcode)(struct Blob *ob, struct Blob *text, void *opaque); |
|
54
|
void (*blockquote)(struct Blob *ob, struct Blob *text, void *opaque); |
|
55
|
void (*blockhtml)(struct Blob *ob, struct Blob *text, void *opaque); |
|
56
|
void (*header)(struct Blob *ob, struct Blob *text, |
|
57
|
int level, void *opaque); |
|
58
|
void (*hrule)(struct Blob *ob, void *opaque); |
|
59
|
void (*list)(struct Blob *ob, struct Blob *text, int flags, void *opaque); |
|
60
|
void (*listitem)(struct Blob *ob, struct Blob *text, |
|
61
|
int flags, void *opaque); |
|
62
|
void (*paragraph)(struct Blob *ob, struct Blob *text, void *opaque); |
|
63
|
void (*table)(struct Blob *ob, struct Blob *head_row, struct Blob *rows, |
|
64
|
void *opaque); |
|
65
|
void (*table_cell)(struct Blob *ob, struct Blob *text, int flags, |
|
66
|
void *opaque); |
|
67
|
void (*table_row)(struct Blob *ob, struct Blob *cells, int flags, |
|
68
|
void *opaque); |
|
69
|
void (*footnote_item)(struct Blob *ob, const struct Blob *text, |
|
70
|
int index, int nUsed, void *opaque); |
|
71
|
|
|
72
|
/* span level callbacks - NULL or return 0 prints the span verbatim */ |
|
73
|
int (*autolink)(struct Blob *ob, struct Blob *link, |
|
74
|
enum mkd_autolink type, void *opaque); |
|
75
|
int (*codespan)(struct Blob *ob, struct Blob *text, int nSep, void *opaque); |
|
76
|
int (*double_emphasis)(struct Blob *ob, struct Blob *text, |
|
77
|
char c, void *opaque); |
|
78
|
int (*emphasis)(struct Blob *ob, struct Blob *text, char c,void*opaque); |
|
79
|
int (*image)(struct Blob *ob, struct Blob *link, struct Blob *title, |
|
80
|
struct Blob *alt, void *opaque); |
|
81
|
int (*linebreak)(struct Blob *ob, void *opaque); |
|
82
|
int (*link)(struct Blob *ob, struct Blob *link, struct Blob *title, |
|
83
|
struct Blob *content, void *opaque); |
|
84
|
int (*raw_html_tag)(struct Blob *ob, struct Blob *tag, void *opaque); |
|
85
|
int (*triple_emphasis)(struct Blob *ob, struct Blob *text, |
|
86
|
char c, void *opaque); |
|
87
|
int (*footnote_ref)(struct Blob *ob, const struct Blob *span, |
|
88
|
const struct Blob *upc, int index, int locus, void *opaque); |
|
89
|
|
|
90
|
/* low level callbacks - NULL copies input directly into the output */ |
|
91
|
void (*entity)(struct Blob *ob, struct Blob *entity, void *opaque); |
|
92
|
void (*normal_text)(struct Blob *ob, struct Blob *text, void *opaque); |
|
93
|
|
|
94
|
/* renderer data */ |
|
95
|
const char *emph_chars; /* chars that trigger emphasis rendering */ |
|
96
|
void *opaque; /* opaque data send to every rendering callback */ |
|
97
|
}; |
|
98
|
|
|
99
|
|
|
100
|
|
|
101
|
/********* |
|
102
|
* FLAGS * |
|
103
|
*********/ |
|
104
|
|
|
105
|
/* list/listitem flags */ |
|
106
|
#define MKD_LIST_ORDERED 1 |
|
107
|
#define MKD_LI_BLOCK 2 /* <li> containing block data */ |
|
108
|
|
|
109
|
/* table cell flags */ |
|
110
|
#define MKD_CELL_ALIGN_DEFAULT 0 |
|
111
|
#define MKD_CELL_ALIGN_LEFT 1 |
|
112
|
#define MKD_CELL_ALIGN_RIGHT 2 |
|
113
|
#define MKD_CELL_ALIGN_CENTER 3 /* LEFT | RIGHT */ |
|
114
|
#define MKD_CELL_ALIGN_MASK 3 |
|
115
|
#define MKD_CELL_HEAD 4 |
|
116
|
|
|
117
|
|
|
118
|
#endif /* INTERFACE */ |
|
119
|
|
|
120
|
#define BLOB_COUNT(pBlob,el_type) (blob_size(pBlob)/sizeof(el_type)) |
|
121
|
#define COUNT_FOOTNOTES(pBlob) BLOB_COUNT(pBlob,struct footnote) |
|
122
|
#define CAST_AS_FOOTNOTES(pBlob) ((struct footnote*)blob_buffer(pBlob)) |
|
123
|
|
|
124
|
/*************** |
|
125
|
* LOCAL TYPES * |
|
126
|
***************/ |
|
127
|
|
|
128
|
/* |
|
129
|
** link_ref -- reference to a link. |
|
130
|
*/ |
|
131
|
struct link_ref { |
|
132
|
struct Blob id; /* must be the first field as in footnote struct */ |
|
133
|
struct Blob link; |
|
134
|
struct Blob title; |
|
135
|
}; |
|
136
|
|
|
137
|
/* |
|
138
|
** A footnote's data. |
|
139
|
** id, text, and upc fields must be in that particular order. |
|
140
|
*/ |
|
141
|
struct footnote { |
|
142
|
struct Blob id; /* must be the first field as in link_ref struct */ |
|
143
|
struct Blob text; /* footnote's content that is rendered at the end */ |
|
144
|
struct Blob upc; /* user-provided classes .ASCII-alnum.or-hypen: */ |
|
145
|
int bRndred; /* indicates if `text` holds a rendered content */ |
|
146
|
|
|
147
|
int defno; /* serial number of definition, set during the first pass */ |
|
148
|
int index; /* set to the index within array after ordering by id */ |
|
149
|
int iMark; /* user-visible numeric marker, assigned upon the first use*/ |
|
150
|
int nUsed; /* counts references to this note, increments upon each use*/ |
|
151
|
}; |
|
152
|
#define FOOTNOTE_INITIALIZER {empty_blob,empty_blob,empty_blob, 0,0,0,0,0} |
|
153
|
|
|
154
|
/* char_trigger -- function pointer to render active chars */ |
|
155
|
/* returns the number of chars taken care of */ |
|
156
|
/* data is the pointer of the beginning of the span */ |
|
157
|
/* offset is the number of valid chars before data */ |
|
158
|
struct render; |
|
159
|
typedef size_t (*char_trigger)( |
|
160
|
struct Blob *ob, |
|
161
|
struct render *rndr, |
|
162
|
char *data, |
|
163
|
size_t offset, |
|
164
|
size_t size); |
|
165
|
|
|
166
|
|
|
167
|
/* render -- structure containing one particular render */ |
|
168
|
struct render { |
|
169
|
struct mkd_renderer make; |
|
170
|
struct Blob refs; |
|
171
|
char_trigger active_char[256]; |
|
172
|
int iDepth; /* Depth of recursion */ |
|
173
|
int nBlobCache; /* Number of entries in aBlobCache */ |
|
174
|
struct Blob *aBlobCache[20]; /* Cache of Blobs available for reuse */ |
|
175
|
|
|
176
|
struct { |
|
177
|
Blob all; /* Buffer that holds array of footnotes. Its underline |
|
178
|
memory may be reallocated when a new footnote is added. */ |
|
179
|
int nLbled; /* number of labeled footnotes found during the first pass */ |
|
180
|
int nMarks; /* counts distinct indices found during the second pass */ |
|
181
|
struct footnote misref; /* nUsed counts misreferences, iMark must be -1 */ |
|
182
|
} notes; |
|
183
|
}; |
|
184
|
|
|
185
|
/* html_tag -- structure for quick HTML tag search (inspired from discount) */ |
|
186
|
struct html_tag { |
|
187
|
const char *text; |
|
188
|
int size; |
|
189
|
}; |
|
190
|
|
|
191
|
|
|
192
|
|
|
193
|
/******************** |
|
194
|
* GLOBAL VARIABLES * |
|
195
|
********************/ |
|
196
|
|
|
197
|
/* block_tags -- recognised block tags, sorted by cmp_html_tag. |
|
198
|
** |
|
199
|
** When these HTML tags are separated from other text by newlines |
|
200
|
** then they are rendered verbatim. Their content is not interpreted |
|
201
|
** in any way. |
|
202
|
*/ |
|
203
|
static const struct html_tag block_tags[] = { |
|
204
|
{ "html", 4 }, |
|
205
|
{ "pre", 3 }, |
|
206
|
{ "script", 6 }, |
|
207
|
}; |
|
208
|
|
|
209
|
/*************************** |
|
210
|
* STATIC HELPER FUNCTIONS * |
|
211
|
***************************/ |
|
212
|
|
|
213
|
/* |
|
214
|
** build_ref_id -- collapse whitespace from input text to make it a ref id. |
|
215
|
** Potential TODO: maybe also handle CR+LF line endings? |
|
216
|
*/ |
|
217
|
static int build_ref_id(struct Blob *id, const char *data, size_t size){ |
|
218
|
size_t beg, i; |
|
219
|
char *id_data; |
|
220
|
|
|
221
|
/* skip leading whitespace */ |
|
222
|
while( size>0 && (data[0]==' ' || data[0]=='\t' || data[0]=='\n') ){ |
|
223
|
data++; |
|
224
|
size--; |
|
225
|
} |
|
226
|
|
|
227
|
/* skip trailing whitespace */ |
|
228
|
while( size>0 && (data[size-1]==' ' |
|
229
|
|| data[size-1]=='\t' |
|
230
|
|| data[size-1]=='\n') |
|
231
|
){ |
|
232
|
size--; |
|
233
|
} |
|
234
|
if( size==0 ) return -1; |
|
235
|
|
|
236
|
/* making the ref id */ |
|
237
|
i = 0; |
|
238
|
blob_reset(id); |
|
239
|
while( i<size ){ |
|
240
|
/* copy non-whitespace into the output buffer */ |
|
241
|
beg = i; |
|
242
|
while( i<size && !(data[i]==' ' || data[i]=='\t' || data[i]=='\n') ){ |
|
243
|
i++; |
|
244
|
} |
|
245
|
blob_append(id, data+beg, i-beg); |
|
246
|
|
|
247
|
/* add a single space and skip all consecutive whitespace */ |
|
248
|
if( i<size ) blob_append_char(id, ' '); |
|
249
|
while( i<size && (data[i]==' ' || data[i]=='\t' || data[i]=='\n') ){ i++; } |
|
250
|
} |
|
251
|
|
|
252
|
/* turn upper-case ASCII into their lower-case counterparts */ |
|
253
|
id_data = blob_buffer(id); |
|
254
|
for(i=0; i<blob_size(id); i++){ |
|
255
|
if( id_data[i]>='A' && id_data[i]<='Z' ) id_data[i] += 'a' - 'A'; |
|
256
|
} |
|
257
|
return 0; |
|
258
|
} |
|
259
|
|
|
260
|
|
|
261
|
/* cmp_link_ref -- comparison function for link_ref sorted arrays */ |
|
262
|
static int cmp_link_ref(const void *key, const void *array_entry){ |
|
263
|
struct link_ref *lr = (void *)array_entry; |
|
264
|
return blob_compare((void *)key, &lr->id); |
|
265
|
} |
|
266
|
|
|
267
|
|
|
268
|
/* cmp_link_ref_sort -- comparison function for link_ref qsort */ |
|
269
|
static int cmp_link_ref_sort(const void *a, const void *b){ |
|
270
|
struct link_ref *lra = (void *)a; |
|
271
|
struct link_ref *lrb = (void *)b; |
|
272
|
return blob_compare(&lra->id, &lrb->id); |
|
273
|
} |
|
274
|
|
|
275
|
/* |
|
276
|
** cmp_footnote_id -- comparison function for footnotes qsort. |
|
277
|
** Empty IDs sort last (in undetermined order). |
|
278
|
** Equal IDs are sorted in the order of definition in the source. |
|
279
|
*/ |
|
280
|
static int cmp_footnote_id(const void *fna, const void *fnb){ |
|
281
|
const struct footnote *a = fna, *b = fnb; |
|
282
|
const int szA = blob_size(&a->id), szB = blob_size(&b->id); |
|
283
|
if( szA ){ |
|
284
|
if( szB ){ |
|
285
|
int cmp = blob_compare(&a->id, &b->id); |
|
286
|
if( cmp ) return cmp; |
|
287
|
}else return -1; |
|
288
|
}else return szB ? 1 : 0; |
|
289
|
/* IDs are equal and non-empty */ |
|
290
|
if( a->defno < b->defno ) return -1; |
|
291
|
if( a->defno > b->defno ) return 1; |
|
292
|
assert(!"reachable"); |
|
293
|
return 0; /* should never reach here */ |
|
294
|
} |
|
295
|
|
|
296
|
/* |
|
297
|
** cmp_footnote_sort -- comparison function for footnotes qsort. |
|
298
|
** Unreferenced footnotes (when nUsed == 0) sort last and |
|
299
|
** are sorted in the order of definition in the source. |
|
300
|
*/ |
|
301
|
static int cmp_footnote_sort(const void *fna, const void *fnb){ |
|
302
|
const struct footnote *a = fna, *b = fnb; |
|
303
|
int i, j; |
|
304
|
assert( a->nUsed >= 0 ); |
|
305
|
assert( b->nUsed >= 0 ); |
|
306
|
assert( a->defno >= 0 ); |
|
307
|
assert( b->defno >= 0 ); |
|
308
|
if( a->nUsed ){ |
|
309
|
assert( a->iMark > 0 ); |
|
310
|
if( !b->nUsed ) return -1; |
|
311
|
assert( b->iMark > 0 ); |
|
312
|
i = a->iMark; |
|
313
|
j = b->iMark; |
|
314
|
}else{ |
|
315
|
if( b->nUsed ) return 1; |
|
316
|
i = a->defno; |
|
317
|
j = b->defno; |
|
318
|
} |
|
319
|
if( i < j ) return -1; |
|
320
|
if( i > j ) return 1; |
|
321
|
return 0; |
|
322
|
} |
|
323
|
|
|
324
|
/* cmp_html_tag -- comparison function for bsearch() (stolen from discount) */ |
|
325
|
static int cmp_html_tag(const void *a, const void *b){ |
|
326
|
const struct html_tag *hta = a; |
|
327
|
const struct html_tag *htb = b; |
|
328
|
int sz = hta->size; |
|
329
|
int c; |
|
330
|
if( htb->size<sz ) sz = htb->size; |
|
331
|
c = fossil_strnicmp(hta->text, htb->text, sz); |
|
332
|
if( c==0 ) c = hta->size - htb->size; |
|
333
|
return c; |
|
334
|
} |
|
335
|
|
|
336
|
|
|
337
|
/* find_block_tag -- returns the current block tag */ |
|
338
|
static const struct html_tag *find_block_tag(const char *data, size_t size){ |
|
339
|
size_t i = 0; |
|
340
|
struct html_tag key; |
|
341
|
|
|
342
|
/* looking for the word end */ |
|
343
|
while( i<size |
|
344
|
&& ((data[i]>='0' && data[i]<='9') |
|
345
|
|| (data[i]>='A' && data[i]<='Z') |
|
346
|
|| (data[i]>='a' && data[i]<='z')) |
|
347
|
){ |
|
348
|
i++; |
|
349
|
} |
|
350
|
if( i>=size ) return 0; |
|
351
|
|
|
352
|
/* binary search of the tag */ |
|
353
|
key.text = data; |
|
354
|
key.size = i; |
|
355
|
return bsearch(&key, |
|
356
|
block_tags, |
|
357
|
count(block_tags), |
|
358
|
sizeof block_tags[0], |
|
359
|
cmp_html_tag); |
|
360
|
} |
|
361
|
|
|
362
|
/* return true if recursion has gone too deep */ |
|
363
|
static int too_deep(struct render *rndr){ |
|
364
|
return rndr->iDepth>200; |
|
365
|
} |
|
366
|
|
|
367
|
/* get a new working buffer from the cache or create one. return NULL |
|
368
|
** if failIfDeep is true and the depth of recursion has gone too deep. */ |
|
369
|
static struct Blob *new_work_buffer(struct render *rndr){ |
|
370
|
struct Blob *ret; |
|
371
|
rndr->iDepth++; |
|
372
|
if( rndr->nBlobCache ){ |
|
373
|
ret = rndr->aBlobCache[--rndr->nBlobCache]; |
|
374
|
}else{ |
|
375
|
ret = fossil_malloc(sizeof(*ret)); |
|
376
|
} |
|
377
|
*ret = empty_blob; |
|
378
|
return ret; |
|
379
|
} |
|
380
|
|
|
381
|
|
|
382
|
/* release the given working buffer back to the cache */ |
|
383
|
static void release_work_buffer(struct render *rndr, struct Blob *buf){ |
|
384
|
if( !buf ) return; |
|
385
|
rndr->iDepth--; |
|
386
|
blob_reset(buf); |
|
387
|
if( rndr->nBlobCache < |
|
388
|
(int)(sizeof(rndr->aBlobCache)/sizeof(rndr->aBlobCache[0])) ){ |
|
389
|
rndr->aBlobCache[rndr->nBlobCache++] = buf; |
|
390
|
}else{ |
|
391
|
fossil_free(buf); |
|
392
|
} |
|
393
|
} |
|
394
|
|
|
395
|
|
|
396
|
|
|
397
|
/**************************** |
|
398
|
* INLINE PARSING FUNCTIONS * |
|
399
|
****************************/ |
|
400
|
|
|
401
|
/* is_mail_autolink -- looks for the address part of a mail autolink and '>' */ |
|
402
|
/* this is less strict than the original markdown e-mail address matching */ |
|
403
|
static size_t is_mail_autolink(char *data, size_t size){ |
|
404
|
size_t i = 0, nb = 0; |
|
405
|
/* address is assumed to be: [-@._a-zA-Z0-9]+ with exactly one '@' */ |
|
406
|
while( i<size && (data[i]=='-' |
|
407
|
|| data[i]=='.' |
|
408
|
|| data[i]=='_' |
|
409
|
|| data[i]=='@' |
|
410
|
|| (data[i]>='a' && data[i]<='z') |
|
411
|
|| (data[i]>='A' && data[i]<='Z') |
|
412
|
|| (data[i]>='0' && data[i]<='9')) |
|
413
|
){ |
|
414
|
if( data[i]=='@' ) nb++; |
|
415
|
i++; |
|
416
|
} |
|
417
|
if( i>=size || data[i]!='>' || nb!=1 ) return 0; |
|
418
|
return i+1; |
|
419
|
} |
|
420
|
|
|
421
|
|
|
422
|
/* tag_length -- returns the length of the given tag, or 0 if it's not valid */ |
|
423
|
static size_t tag_length(char *data, size_t size, enum mkd_autolink *autolink){ |
|
424
|
size_t i, j; |
|
425
|
|
|
426
|
/* a valid tag can't be shorter than 3 chars */ |
|
427
|
if( size<3 ) return 0; |
|
428
|
|
|
429
|
/* begins with a '<' optionally followed by '/', followed by letter */ |
|
430
|
if( data[0]!='<' ) return 0; |
|
431
|
i = (data[1]=='/') ? 2 : 1; |
|
432
|
if( (data[i]<'a' || data[i]>'z') && (data[i]<'A' || data[i]>'Z') ){ |
|
433
|
if( data[1]=='!' && size>=7 && data[2]=='-' && data[3]=='-' ){ |
|
434
|
for(i=6; i<size && (data[i]!='>'||data[i-1]!='-'|| data[i-2]!='-');i++){} |
|
435
|
if( i<size ) return i; |
|
436
|
} |
|
437
|
return 0; |
|
438
|
} |
|
439
|
|
|
440
|
/* scheme test */ |
|
441
|
*autolink = MKDA_NOT_AUTOLINK; |
|
442
|
if( size>6 |
|
443
|
&& fossil_strnicmp(data+1, "http", 4)==0 |
|
444
|
&& (data[5]==':' |
|
445
|
|| ((data[5]=='s' || data[5]=='S') && data[6]==':')) |
|
446
|
){ |
|
447
|
i = (data[5]==':') ? 6 : 7; |
|
448
|
*autolink = MKDA_NORMAL; |
|
449
|
}else if( size>5 && fossil_strnicmp(data+1, "ftp:", 4)==0 ){ |
|
450
|
i = 5; |
|
451
|
*autolink = MKDA_NORMAL; |
|
452
|
}else if( size>7 && fossil_strnicmp(data+1, "mailto:", 7)==0 ){ |
|
453
|
i = 8; |
|
454
|
/* not changing *autolink to go to the address test */ |
|
455
|
} |
|
456
|
|
|
457
|
/* completing autolink test: no whitespace or ' or " */ |
|
458
|
if( i>=size || i=='>' ){ |
|
459
|
*autolink = MKDA_NOT_AUTOLINK; |
|
460
|
}else if( *autolink ){ |
|
461
|
j = i; |
|
462
|
while( i<size |
|
463
|
&& data[i]!='>' |
|
464
|
&& data[i]!='\'' |
|
465
|
&& data[i]!='"' |
|
466
|
&& data[i]!=' ' |
|
467
|
&& data[i]!='\t' |
|
468
|
&& data[i]!='\n' |
|
469
|
){ |
|
470
|
i++; |
|
471
|
} |
|
472
|
if( i>=size ) return 0; |
|
473
|
if( i>j && data[i]=='>' ) return i+1; |
|
474
|
/* one of the forbidden chars has been found */ |
|
475
|
*autolink = MKDA_NOT_AUTOLINK; |
|
476
|
}else if( (j = is_mail_autolink(data+i, size-i))!=0 ){ |
|
477
|
*autolink = (i==8) ? MKDA_EXPLICIT_EMAIL : MKDA_IMPLICIT_EMAIL; |
|
478
|
return i+j; |
|
479
|
} |
|
480
|
|
|
481
|
/* looking for something looking like a tag end */ |
|
482
|
while( i<size && data[i]!='>' ){ i++; } |
|
483
|
if( i>=size ) return 0; |
|
484
|
return i+1; |
|
485
|
} |
|
486
|
|
|
487
|
|
|
488
|
/* parse_inline -- parses inline markdown elements */ |
|
489
|
static void parse_inline( |
|
490
|
struct Blob *ob, |
|
491
|
struct render *rndr, |
|
492
|
char *data, |
|
493
|
size_t size |
|
494
|
){ |
|
495
|
size_t i = 0, end = 0; |
|
496
|
char_trigger action = 0; |
|
497
|
struct Blob work = BLOB_INITIALIZER; |
|
498
|
|
|
499
|
if( too_deep(rndr) ){ |
|
500
|
blob_append(ob, data, size); |
|
501
|
return; |
|
502
|
} |
|
503
|
while( i<size ){ |
|
504
|
/* copying inactive chars into the output */ |
|
505
|
while( end<size |
|
506
|
&& (action = rndr->active_char[(unsigned char)data[end]])==0 |
|
507
|
){ |
|
508
|
end++; |
|
509
|
} |
|
510
|
if( end>i ){ |
|
511
|
if( rndr->make.normal_text ){ |
|
512
|
blob_init(&work, data+i, end-i); |
|
513
|
rndr->make.normal_text(ob, &work, rndr->make.opaque); |
|
514
|
}else{ |
|
515
|
blob_append(ob, data+i, end-i); |
|
516
|
} |
|
517
|
} |
|
518
|
if( end>=size ) break; |
|
519
|
i = end; |
|
520
|
|
|
521
|
/* calling the trigger */ |
|
522
|
end = action(ob, rndr, data+i, i, size-i); |
|
523
|
if( !end ){ |
|
524
|
/* no action from the callback */ |
|
525
|
end = i+1; |
|
526
|
}else{ |
|
527
|
i += end; |
|
528
|
end = i; |
|
529
|
} |
|
530
|
} |
|
531
|
} |
|
532
|
|
|
533
|
/* |
|
534
|
** data[*pI] should be a "`" character that introduces a code-span. |
|
535
|
** The code-span boundary mark can be any number of one or more "`" |
|
536
|
** characters. We do not know the size of the boundary marker, only |
|
537
|
** that there is at least one "`" at data[*pI]. |
|
538
|
** |
|
539
|
** This routine increases *pI to move it past the code-span, including |
|
540
|
** the closing boundary mark. Or, if the code-span is unterminated, |
|
541
|
** this routine moves *pI past the opening boundary mark only. |
|
542
|
*/ |
|
543
|
static void skip_codespan(const char *data, size_t size, size_t *pI){ |
|
544
|
size_t i = *pI; |
|
545
|
size_t span_nb; /* Number of "`" characters in the boundary mark */ |
|
546
|
size_t bt; |
|
547
|
|
|
548
|
assert( i<size ); |
|
549
|
assert( data[i]=='`' ); |
|
550
|
data += i; |
|
551
|
size -= i; |
|
552
|
|
|
553
|
/* counting the number of opening backticks */ |
|
554
|
i = 0; |
|
555
|
span_nb = 0; |
|
556
|
while( i<size && data[i]=='`' ){ |
|
557
|
i++; |
|
558
|
span_nb++; |
|
559
|
} |
|
560
|
if( i>=size ){ |
|
561
|
*pI += span_nb; |
|
562
|
return; |
|
563
|
} |
|
564
|
|
|
565
|
/* finding the matching closing sequence */ |
|
566
|
bt = 0; |
|
567
|
while( i<size && bt<span_nb ){ |
|
568
|
if( data[i]=='`' ) bt += 1; else bt = 0; |
|
569
|
i++; |
|
570
|
} |
|
571
|
*pI += (bt == span_nb) ? i : span_nb; |
|
572
|
} |
|
573
|
|
|
574
|
|
|
575
|
/* find_emph_char -- looks for the next emph char, skipping other constructs */ |
|
576
|
static size_t find_emph_char(char *data, size_t size, char c){ |
|
577
|
size_t i = data[0]!='`'; |
|
578
|
|
|
579
|
while( i<size ){ |
|
580
|
while( i<size && data[i]!=c && data[i]!='`' && data[i]!='[' ){ i++; } |
|
581
|
if( i>=size ) return 0; |
|
582
|
|
|
583
|
/* not counting escaped chars */ |
|
584
|
if( i && data[i-1]=='\\' ){ |
|
585
|
i++; |
|
586
|
continue; |
|
587
|
} |
|
588
|
|
|
589
|
if( data[i]==c ) return i; |
|
590
|
|
|
591
|
if( data[i]=='`' ){ /* skip a code span */ |
|
592
|
skip_codespan(data, size, &i); |
|
593
|
}else if( data[i]=='[' ){ /* skip a link */ |
|
594
|
size_t tmp_i = 0; |
|
595
|
char cc; |
|
596
|
i++; |
|
597
|
while( i<size && data[i]!=']' ){ |
|
598
|
if( !tmp_i && data[i]==c ) tmp_i = i; |
|
599
|
i++; |
|
600
|
} |
|
601
|
i++; |
|
602
|
while( i<size && (data[i]==' ' || data[i]=='\t' || data[i]=='\n') ){ |
|
603
|
i++; |
|
604
|
} |
|
605
|
if( i>=size ) return tmp_i; |
|
606
|
if( data[i]!='[' && data[i]!='(' ){ /* not a link*/ |
|
607
|
if( tmp_i ) return tmp_i; else continue; |
|
608
|
} |
|
609
|
cc = data[i]; |
|
610
|
i++; |
|
611
|
while( i<size && data[i]!=cc ){ |
|
612
|
if( !tmp_i && data[i]==c ) tmp_i = i; |
|
613
|
i++; |
|
614
|
} |
|
615
|
if( i>=size ) return tmp_i; |
|
616
|
i++; |
|
617
|
} |
|
618
|
} |
|
619
|
return 0; |
|
620
|
} |
|
621
|
|
|
622
|
/* CommonMark defines separate "right-flanking" and "left-flanking" |
|
623
|
** deliminators for emphasis. Whether a deliminator is left- or |
|
624
|
** right-flanking, or both, or neither depends on the characters |
|
625
|
** immediately before and after. |
|
626
|
** |
|
627
|
** before after example left-flanking right-flanking |
|
628
|
** ------ ----- ------- ------------- -------------- |
|
629
|
** space space * no no |
|
630
|
** space punct *) yes no |
|
631
|
** space alnum *x yes no |
|
632
|
** punct space (* no yes |
|
633
|
** punct punct (*) yes yes |
|
634
|
** punct alnum (*x yes no |
|
635
|
** alnum space a* no yes |
|
636
|
** alnum punct a*( no yes |
|
637
|
** alnum alnum a*x yes yes |
|
638
|
** |
|
639
|
** The following routines determine whether a delimiter is left |
|
640
|
** or right flanking. |
|
641
|
*/ |
|
642
|
static int left_flanking(char before, char after){ |
|
643
|
if( fossil_isspace(after) ) return 0; |
|
644
|
if( fossil_isalnum(after) ) return 1; |
|
645
|
if( fossil_isalnum(before) ) return 0; |
|
646
|
return 1; |
|
647
|
} |
|
648
|
static int right_flanking(char before, char after){ |
|
649
|
if( fossil_isspace(before) ) return 0; |
|
650
|
if( fossil_isalnum(before) ) return 1; |
|
651
|
if( fossil_isalnum(after) ) return 0; |
|
652
|
return 1; |
|
653
|
} |
|
654
|
|
|
655
|
|
|
656
|
/* |
|
657
|
** parse_emph1 -- parsing single emphasis. |
|
658
|
** closed by a symbol not preceded by whitespace and not followed by symbol. |
|
659
|
*/ |
|
660
|
static size_t parse_emph1( |
|
661
|
struct Blob *ob, |
|
662
|
struct render *rndr, |
|
663
|
char *data, |
|
664
|
size_t size, |
|
665
|
char c |
|
666
|
){ |
|
667
|
size_t i = 0, len; |
|
668
|
struct Blob *work = 0; |
|
669
|
int r; |
|
670
|
char after; |
|
671
|
|
|
672
|
if( !rndr->make.emphasis ) return 0; |
|
673
|
|
|
674
|
/* skipping one symbol if coming from emph3 */ |
|
675
|
if( size>1 && data[0]==c && data[1]==c ) i = 1; |
|
676
|
|
|
677
|
while( i<size ){ |
|
678
|
len = find_emph_char(data+i, size-i, c); |
|
679
|
if( !len ) return 0; |
|
680
|
i += len; |
|
681
|
if( i>=size ) return 0; |
|
682
|
|
|
683
|
if( i+1<size && data[i+1]==c ){ |
|
684
|
i++; |
|
685
|
continue; |
|
686
|
} |
|
687
|
after = i+1<size ? data[i+1] : ' '; |
|
688
|
if( data[i]==c |
|
689
|
&& right_flanking(data[i-1],after) |
|
690
|
&& (c!='_' || !fossil_isalnum(after)) |
|
691
|
&& !too_deep(rndr) |
|
692
|
){ |
|
693
|
work = new_work_buffer(rndr); |
|
694
|
parse_inline(work, rndr, data, i); |
|
695
|
r = rndr->make.emphasis(ob, work, c, rndr->make.opaque); |
|
696
|
release_work_buffer(rndr, work); |
|
697
|
return r ? i+1 : 0; |
|
698
|
} |
|
699
|
} |
|
700
|
return 0; |
|
701
|
} |
|
702
|
|
|
703
|
/* |
|
704
|
** parse_emph2 -- parsing single emphasis. |
|
705
|
*/ |
|
706
|
static size_t parse_emph2( |
|
707
|
struct Blob *ob, |
|
708
|
struct render *rndr, |
|
709
|
char *data, |
|
710
|
size_t size, |
|
711
|
char c |
|
712
|
){ |
|
713
|
size_t i = 0, len; |
|
714
|
struct Blob *work = 0; |
|
715
|
int r; |
|
716
|
char after; |
|
717
|
|
|
718
|
if( !rndr->make.double_emphasis ) return 0; |
|
719
|
|
|
720
|
while( i<size ){ |
|
721
|
len = find_emph_char(data+i, size-i, c); |
|
722
|
if( !len ) return 0; |
|
723
|
i += len; |
|
724
|
after = i+2<size ? data[i+2] : ' '; |
|
725
|
if( i+1<size |
|
726
|
&& data[i]==c |
|
727
|
&& data[i+1]==c |
|
728
|
&& right_flanking(data[i-1],after) |
|
729
|
&& (c!='_' || !fossil_isalnum(after)) |
|
730
|
&& !too_deep(rndr) |
|
731
|
){ |
|
732
|
work = new_work_buffer(rndr); |
|
733
|
parse_inline(work, rndr, data, i); |
|
734
|
r = rndr->make.double_emphasis(ob, work, c, rndr->make.opaque); |
|
735
|
release_work_buffer(rndr, work); |
|
736
|
return r ? i+2 : 0; |
|
737
|
} |
|
738
|
i++; |
|
739
|
} |
|
740
|
return 0; |
|
741
|
} |
|
742
|
|
|
743
|
/* |
|
744
|
** parse_emph3 -- parsing single emphasis. |
|
745
|
** finds the first closing tag, and delegates to the other emph. |
|
746
|
*/ |
|
747
|
static size_t parse_emph3( |
|
748
|
struct Blob *ob, |
|
749
|
struct render *rndr, |
|
750
|
char *data, |
|
751
|
size_t size, |
|
752
|
char c |
|
753
|
){ |
|
754
|
size_t i = 0, len; |
|
755
|
int r; |
|
756
|
|
|
757
|
while( i<size ){ |
|
758
|
len = find_emph_char(data+i, size-i, c); |
|
759
|
if( !len ) return 0; |
|
760
|
i += len; |
|
761
|
|
|
762
|
/* skip whitespace preceded symbols */ |
|
763
|
if( data[i]!=c || data[i-1]==' ' || data[i-1]=='\t' || data[i-1]=='\n' ){ |
|
764
|
continue; |
|
765
|
} |
|
766
|
|
|
767
|
if( i+2<size |
|
768
|
&& data[i+1]==c |
|
769
|
&& data[i+2] == c |
|
770
|
&& rndr->make.triple_emphasis |
|
771
|
&& !too_deep(rndr) |
|
772
|
){ |
|
773
|
/* triple symbol found */ |
|
774
|
struct Blob *work = new_work_buffer(rndr); |
|
775
|
parse_inline(work, rndr, data, i); |
|
776
|
r = rndr->make.triple_emphasis(ob, work, c, rndr->make.opaque); |
|
777
|
release_work_buffer(rndr, work); |
|
778
|
return r ? i+3 : 0; |
|
779
|
}else if( i+1<size && data[i+1]==c ){ |
|
780
|
/* double symbol found, handing over to emph1 */ |
|
781
|
len = parse_emph1(ob, rndr, data-2, size+2, c); |
|
782
|
return len ? len-2 : 0; |
|
783
|
}else{ |
|
784
|
/* single symbol found, handing over to emph2 */ |
|
785
|
len = parse_emph2(ob, rndr, data-1, size+1, c); |
|
786
|
return len ? len-1 : 0; |
|
787
|
} |
|
788
|
} |
|
789
|
return 0; |
|
790
|
} |
|
791
|
|
|
792
|
/* |
|
793
|
** char_emphasis -- single and double emphasis parsing. |
|
794
|
*/ |
|
795
|
static size_t char_emphasis( |
|
796
|
struct Blob *ob, |
|
797
|
struct render *rndr, |
|
798
|
char *data, |
|
799
|
size_t offset, |
|
800
|
size_t size |
|
801
|
){ |
|
802
|
char c = data[0]; |
|
803
|
char before = offset>0 ? data[-1] : ' '; |
|
804
|
size_t ret; |
|
805
|
|
|
806
|
if( size>2 && data[1]!=c ){ |
|
807
|
if( !left_flanking(before, data[1]) |
|
808
|
|| (c=='_' && fossil_isalnum(before)) |
|
809
|
|| (ret = parse_emph1(ob, rndr, data+1, size-1, c))==0 |
|
810
|
){ |
|
811
|
return 0; |
|
812
|
} |
|
813
|
return ret+1; |
|
814
|
} |
|
815
|
|
|
816
|
if( size>3 && data[1]==c && data[2]!=c ){ |
|
817
|
if( !left_flanking(before, data[2]) |
|
818
|
|| (c=='_' && fossil_isalnum(before)) |
|
819
|
|| (ret = parse_emph2(ob, rndr, data+2, size-2, c))==0 |
|
820
|
){ |
|
821
|
return 0; |
|
822
|
} |
|
823
|
return ret+2; |
|
824
|
} |
|
825
|
|
|
826
|
if( size>4 && data[1]==c && data[2]==c && data[3]!=c ){ |
|
827
|
if( !left_flanking(before, data[3]) |
|
828
|
|| (c=='_' && fossil_isalnum(before)) |
|
829
|
|| (ret = parse_emph3(ob, rndr, data+3, size-3, c))==0 |
|
830
|
){ |
|
831
|
return 0; |
|
832
|
} |
|
833
|
return ret+3; |
|
834
|
} |
|
835
|
return 0; |
|
836
|
} |
|
837
|
|
|
838
|
/* |
|
839
|
** char_linebreak -- '\n' preceded by two spaces (assuming linebreak != 0). |
|
840
|
*/ |
|
841
|
static size_t char_linebreak( |
|
842
|
struct Blob *ob, |
|
843
|
struct render *rndr, |
|
844
|
char *data, |
|
845
|
size_t offset, |
|
846
|
size_t size |
|
847
|
){ |
|
848
|
if( offset<2 || data[-1]!=' ' || data[-2]!=' ' ) return 0; |
|
849
|
/* removing the last space from ob and rendering */ |
|
850
|
if( blob_size(ob)>0 && blob_buffer(ob)[blob_size(ob)-1]==' ' ) ob->nUsed--; |
|
851
|
return rndr->make.linebreak(ob, rndr->make.opaque) ? 1 : 0; |
|
852
|
} |
|
853
|
|
|
854
|
/* |
|
855
|
** char_codespan -- '`' parsing a code span (assuming codespan != 0). |
|
856
|
*/ |
|
857
|
static size_t char_codespan( |
|
858
|
struct Blob *ob, |
|
859
|
struct render *rndr, |
|
860
|
char *data, |
|
861
|
size_t offset, |
|
862
|
size_t size |
|
863
|
){ |
|
864
|
size_t end, nb = 0, i, f_begin, f_end; |
|
865
|
char delim = data[0]; |
|
866
|
|
|
867
|
/* counting the number of backticks in the delimiter */ |
|
868
|
while( nb<size && data[nb]==delim ){ nb++; } |
|
869
|
|
|
870
|
/* finding the next delimiter */ |
|
871
|
i = 0; |
|
872
|
for(end=nb; end<size && i<nb; end++){ |
|
873
|
if( data[end]==delim ) i++; else i = 0; |
|
874
|
} |
|
875
|
if( i<nb && end>=size ) return 0; /* no matching delimiter */ |
|
876
|
|
|
877
|
/* trimming outside whitespaces */ |
|
878
|
f_begin = nb; |
|
879
|
while( f_begin<end && (data[f_begin]==' ' || data[f_begin]=='\t') ){ |
|
880
|
f_begin++; |
|
881
|
} |
|
882
|
f_end = end-nb; |
|
883
|
while( f_end>nb && (data[f_end-1]==' ' || data[f_end-1]=='\t') ){ f_end--; } |
|
884
|
|
|
885
|
/* real code span */ |
|
886
|
if( f_begin<f_end ){ |
|
887
|
struct Blob work = BLOB_INITIALIZER; |
|
888
|
blob_init(&work, data+f_begin, f_end-f_begin); |
|
889
|
if( !rndr->make.codespan(ob, &work, nb, rndr->make.opaque) ) end = 0; |
|
890
|
}else{ |
|
891
|
if( !rndr->make.codespan(ob, 0, nb, rndr->make.opaque) ) end = 0; |
|
892
|
} |
|
893
|
return end; |
|
894
|
} |
|
895
|
|
|
896
|
|
|
897
|
/* |
|
898
|
** char_escape -- '\\' backslash escape. |
|
899
|
*/ |
|
900
|
static size_t char_escape( |
|
901
|
struct Blob *ob, |
|
902
|
struct render *rndr, |
|
903
|
char *data, |
|
904
|
size_t offset, |
|
905
|
size_t size |
|
906
|
){ |
|
907
|
struct Blob work = BLOB_INITIALIZER; |
|
908
|
if( size>1 ){ |
|
909
|
if( rndr->make.normal_text ){ |
|
910
|
blob_init(&work, data+1,1); |
|
911
|
rndr->make.normal_text(ob, &work, rndr->make.opaque); |
|
912
|
}else{ |
|
913
|
blob_append(ob, data+1, 1); |
|
914
|
} |
|
915
|
} |
|
916
|
return 2; |
|
917
|
} |
|
918
|
|
|
919
|
/* |
|
920
|
** char_entity -- '&' escaped when it doesn't belong to an entity. |
|
921
|
** valid entities are assumed to be anything matching &#?[A-Za-z0-9]+; |
|
922
|
*/ |
|
923
|
static size_t char_entity( |
|
924
|
struct Blob *ob, |
|
925
|
struct render *rndr, |
|
926
|
char *data, |
|
927
|
size_t offset, |
|
928
|
size_t size |
|
929
|
){ |
|
930
|
size_t end = 1; |
|
931
|
struct Blob work = BLOB_INITIALIZER; |
|
932
|
if( end<size && data[end]=='#' ) end++; |
|
933
|
while( end<size |
|
934
|
&& ((data[end]>='0' && data[end]<='9') |
|
935
|
|| (data[end]>='a' && data[end]<='z') |
|
936
|
|| (data[end]>='A' && data[end]<='Z')) |
|
937
|
){ |
|
938
|
end++; |
|
939
|
} |
|
940
|
if( end<size && data[end]==';' ){ |
|
941
|
/* real entity */ |
|
942
|
end++; |
|
943
|
}else{ |
|
944
|
/* lone '&' */ |
|
945
|
return 0; |
|
946
|
} |
|
947
|
if( rndr->make.entity ){ |
|
948
|
blob_init(&work, data, end); |
|
949
|
rndr->make.entity(ob, &work, rndr->make.opaque); |
|
950
|
}else{ |
|
951
|
blob_append(ob, data, end); |
|
952
|
} |
|
953
|
return end; |
|
954
|
} |
|
955
|
|
|
956
|
/* |
|
957
|
** char_langle_tag -- '<' when tags or autolinks are allowed. |
|
958
|
*/ |
|
959
|
static size_t char_langle_tag( |
|
960
|
struct Blob *ob, |
|
961
|
struct render *rndr, |
|
962
|
char *data, |
|
963
|
size_t offset, |
|
964
|
size_t size |
|
965
|
){ |
|
966
|
enum mkd_autolink altype = MKDA_NOT_AUTOLINK; |
|
967
|
size_t end = tag_length(data, size, &altype); |
|
968
|
struct Blob work = BLOB_INITIALIZER; |
|
969
|
int ret = 0; |
|
970
|
if( end ){ |
|
971
|
if( rndr->make.autolink && altype!=MKDA_NOT_AUTOLINK ){ |
|
972
|
blob_init(&work, data+1, end-2); |
|
973
|
ret = rndr->make.autolink(ob, &work, altype, rndr->make.opaque); |
|
974
|
}else if( rndr->make.raw_html_tag ){ |
|
975
|
blob_init(&work, data, end); |
|
976
|
ret = rndr->make.raw_html_tag(ob, &work, rndr->make.opaque); |
|
977
|
} |
|
978
|
} |
|
979
|
|
|
980
|
if( !ret ){ |
|
981
|
return 0; |
|
982
|
}else{ |
|
983
|
return end; |
|
984
|
} |
|
985
|
} |
|
986
|
|
|
987
|
/* |
|
988
|
** get_link_inline -- extract inline-style link and title from |
|
989
|
** parenthesed data |
|
990
|
*/ |
|
991
|
static int get_link_inline( |
|
992
|
struct Blob *link, |
|
993
|
struct Blob *title, |
|
994
|
char *data, |
|
995
|
size_t size |
|
996
|
){ |
|
997
|
size_t i = 0, mark; |
|
998
|
size_t link_b, link_e; |
|
999
|
size_t title_b = 0, title_e = 0; |
|
1000
|
|
|
1001
|
/* skipping initial whitespace */ |
|
1002
|
while( i<size && (data[i]==' ' || data[i]=='\t' || data[i]=='\n') ){ i++; } |
|
1003
|
link_b = i; |
|
1004
|
|
|
1005
|
/* looking for link end: ' " */ |
|
1006
|
while( i<size && data[i]!='\'' && data[i]!='"' ){ i++; } |
|
1007
|
link_e = i; |
|
1008
|
|
|
1009
|
/* looking for title end if present */ |
|
1010
|
if( data[i]=='\'' || data[i]=='"' ){ |
|
1011
|
i++; |
|
1012
|
title_b = i; |
|
1013
|
|
|
1014
|
/* skipping whitespaces after title */ |
|
1015
|
title_e = size-1; |
|
1016
|
while( title_e>title_b |
|
1017
|
&& (data[title_e]==' ' |
|
1018
|
|| data[title_e]=='\t' |
|
1019
|
|| data[title_e]=='\n') |
|
1020
|
){ |
|
1021
|
title_e--; |
|
1022
|
} |
|
1023
|
|
|
1024
|
/* checking for closing quote presence */ |
|
1025
|
if (data[title_e] != '\'' && data[title_e] != '"') { |
|
1026
|
title_b = title_e = 0; |
|
1027
|
link_e = i; |
|
1028
|
} |
|
1029
|
} |
|
1030
|
|
|
1031
|
/* remove whitespace at the end of the link */ |
|
1032
|
while( link_e>link_b |
|
1033
|
&& (data[link_e-1]==' ' |
|
1034
|
|| data[link_e-1]=='\t' |
|
1035
|
|| data[link_e-1]=='\n') |
|
1036
|
){ |
|
1037
|
link_e--; |
|
1038
|
} |
|
1039
|
|
|
1040
|
/* remove optional angle brackets around the link */ |
|
1041
|
if( data[link_b]=='<' ) link_b += 1; |
|
1042
|
if( link_e && data[link_e-1]=='>' ) link_e -= 1; |
|
1043
|
|
|
1044
|
/* escape backslashed character from link */ |
|
1045
|
blob_reset(link); |
|
1046
|
i = link_b; |
|
1047
|
while( i<link_e ){ |
|
1048
|
mark = i; |
|
1049
|
while( i<link_e && data[i]!='\\' ){ i++; } |
|
1050
|
blob_append(link, data+mark, i-mark); |
|
1051
|
while( i<link_e && data[i]=='\\' ){ i++; } |
|
1052
|
} |
|
1053
|
|
|
1054
|
/* handing back title */ |
|
1055
|
blob_reset(title); |
|
1056
|
if( title_e>title_b ) blob_append(title, data+title_b, title_e-title_b); |
|
1057
|
|
|
1058
|
/* this function always succeed */ |
|
1059
|
return 0; |
|
1060
|
} |
|
1061
|
|
|
1062
|
|
|
1063
|
/* |
|
1064
|
** get_link_ref -- extract referenced link and title from id. |
|
1065
|
*/ |
|
1066
|
static int get_link_ref( |
|
1067
|
struct render *rndr, |
|
1068
|
struct Blob *link, |
|
1069
|
struct Blob *title, |
|
1070
|
char *data, |
|
1071
|
size_t size |
|
1072
|
){ |
|
1073
|
struct link_ref *lr; |
|
1074
|
const size_t sz = blob_size(&rndr->refs); |
|
1075
|
|
|
1076
|
/* find the link from its id (stored temporarily in link) */ |
|
1077
|
blob_reset(link); |
|
1078
|
if( !sz || build_ref_id(link, data, size)<0 ) return -1; |
|
1079
|
lr = bsearch(link, |
|
1080
|
blob_buffer(&rndr->refs), |
|
1081
|
sz/sizeof(struct link_ref), |
|
1082
|
sizeof (struct link_ref), |
|
1083
|
cmp_link_ref); |
|
1084
|
if( !lr ) return -1; |
|
1085
|
|
|
1086
|
/* fill the output buffers */ |
|
1087
|
blob_reset(link); |
|
1088
|
blob_reset(title); |
|
1089
|
blob_appendb(link, &lr->link); |
|
1090
|
blob_appendb(title, &lr->title); |
|
1091
|
return 0; |
|
1092
|
} |
|
1093
|
|
|
1094
|
/* |
|
1095
|
** get_footnote() -- find a footnote by label, invoked during the 2nd pass. |
|
1096
|
** If found then return a shallow copy of the corresponding footnote; |
|
1097
|
** otherwise return a shallow copy of rndr->notes.misref. |
|
1098
|
** In both cases corresponding `nUsed` field is incremented before return. |
|
1099
|
*/ |
|
1100
|
static struct footnote get_footnote( |
|
1101
|
struct render *rndr, |
|
1102
|
const char *data, |
|
1103
|
size_t size |
|
1104
|
){ |
|
1105
|
struct footnote *fn = 0; |
|
1106
|
struct Blob *id; |
|
1107
|
if( !rndr->notes.nLbled ) goto fallback; |
|
1108
|
id = new_work_buffer(rndr); |
|
1109
|
if( build_ref_id(id, data, size)<0 ) goto cleanup; |
|
1110
|
fn = bsearch(id, blob_buffer(&rndr->notes.all), |
|
1111
|
rndr->notes.nLbled, |
|
1112
|
sizeof (struct footnote), |
|
1113
|
cmp_link_ref); |
|
1114
|
if( !fn ) goto cleanup; |
|
1115
|
|
|
1116
|
if( fn->nUsed == 0 ){ /* the first reference to the footnote */ |
|
1117
|
assert( fn->iMark == 0 ); |
|
1118
|
fn->iMark = ++(rndr->notes.nMarks); |
|
1119
|
} |
|
1120
|
assert( fn->iMark > 0 ); |
|
1121
|
cleanup: |
|
1122
|
release_work_buffer( rndr, id ); |
|
1123
|
fallback: |
|
1124
|
if( !fn ) fn = &rndr->notes.misref; |
|
1125
|
fn->nUsed++; |
|
1126
|
assert( fn->nUsed > 0 ); |
|
1127
|
return *fn; |
|
1128
|
} |
|
1129
|
|
|
1130
|
/* |
|
1131
|
** Counts characters in the blank prefix within at most nHalfLines. |
|
1132
|
** A sequence of spaces and tabs counts as odd halfline, |
|
1133
|
** a newline counts as even halfline. |
|
1134
|
** If nHalfLines < 0 then proceed without constraints. |
|
1135
|
*/ |
|
1136
|
static inline size_t sizeof_blank_prefix( |
|
1137
|
const char *data, size_t size, int nHalfLines |
|
1138
|
){ |
|
1139
|
const char *p = data; |
|
1140
|
const char * const end = data+size; |
|
1141
|
if( nHalfLines < 0 ){ |
|
1142
|
while( p!=end && fossil_isspace(*p) ){ |
|
1143
|
p++; |
|
1144
|
} |
|
1145
|
}else while( nHalfLines > 0 ){ |
|
1146
|
while( p!=end && (*p==' ' || *p=='\t' ) ){ p++; } |
|
1147
|
if( p==end || --nHalfLines == 0 ) break; |
|
1148
|
if( *p=='\n' || *p=='\r' ){ |
|
1149
|
p++; |
|
1150
|
if( p==end ) break; |
|
1151
|
if( *p=='\n' && p[-1]=='\r' ){ |
|
1152
|
p++; |
|
1153
|
} |
|
1154
|
} |
|
1155
|
nHalfLines--; |
|
1156
|
} |
|
1157
|
return p-data; |
|
1158
|
} |
|
1159
|
|
|
1160
|
/* |
|
1161
|
** Check if the data starts with a classlist token of the special form. |
|
1162
|
** If so then return the length of that token, otherwise return 0. |
|
1163
|
** |
|
1164
|
** The token must start with a dot and must end with a colon; |
|
1165
|
** in between of these it must be a dot-separated list of words; |
|
1166
|
** each word may contain only alphanumeric characters and hyphens. |
|
1167
|
** |
|
1168
|
** If `bBlank` is non-zero then a blank character must follow |
|
1169
|
** the token's ending colon: otherwise function returns 0 |
|
1170
|
** despite the well-formed token. |
|
1171
|
*/ |
|
1172
|
static size_t is_footnote_classlist(const char * const data, size_t size, |
|
1173
|
int bBlank){ |
|
1174
|
const char *p; |
|
1175
|
const char * const end = data+size; |
|
1176
|
if( data==end || *data != '.' ) return 0; |
|
1177
|
for(p=data+1; p!=end; p++){ |
|
1178
|
if( fossil_isalnum(*p) || *p=='-' ) continue; |
|
1179
|
if( p[-1]=='.' ) break; |
|
1180
|
if( *p==':' ){ |
|
1181
|
p++; |
|
1182
|
if( bBlank ){ |
|
1183
|
if( p==end || !fossil_isspace(*p) ) break; |
|
1184
|
} |
|
1185
|
return p-data; |
|
1186
|
} |
|
1187
|
if( *p!='.' ) break; |
|
1188
|
} |
|
1189
|
return 0; |
|
1190
|
} |
|
1191
|
|
|
1192
|
/* |
|
1193
|
** Adds unlabeled footnote to the rndr->notes.all. |
|
1194
|
** On success puts a shallow copy of the constructed footnote into pFN |
|
1195
|
** and returns 1, otherwise pFN is unchanged and 0 is returned. |
|
1196
|
*/ |
|
1197
|
static inline int add_inline_footnote( |
|
1198
|
struct render *rndr, |
|
1199
|
const char *text, |
|
1200
|
size_t size, |
|
1201
|
struct footnote* pFN |
|
1202
|
){ |
|
1203
|
struct footnote fn = FOOTNOTE_INITIALIZER, *last; |
|
1204
|
const char *zUPC = 0; |
|
1205
|
size_t nUPC = 0, n = sizeof_blank_prefix(text, size, 3); |
|
1206
|
if( n >= size ) return 0; |
|
1207
|
text += n; |
|
1208
|
size -= n; |
|
1209
|
nUPC = is_footnote_classlist(text, size, 1); |
|
1210
|
if( nUPC ){ |
|
1211
|
assert( nUPC<size ); |
|
1212
|
zUPC = text; |
|
1213
|
text += nUPC; |
|
1214
|
size -= nUPC; |
|
1215
|
} |
|
1216
|
if( sizeof_blank_prefix(text,size,-1)==size ){ |
|
1217
|
if( !nUPC ) return 0; /* empty inline footnote */ |
|
1218
|
text = zUPC; |
|
1219
|
size = nUPC; /* bare classlist is treated */ |
|
1220
|
nUPC = 0; /* as plain text */ |
|
1221
|
} |
|
1222
|
fn.iMark = ++(rndr->notes.nMarks); |
|
1223
|
fn.nUsed = 1; |
|
1224
|
fn.index = COUNT_FOOTNOTES(&rndr->notes.all); |
|
1225
|
assert( fn.iMark > 0 ); |
|
1226
|
blob_append(&fn.text, text, size); |
|
1227
|
if(nUPC) blob_append(&fn.upc, zUPC, nUPC); |
|
1228
|
blob_append(&rndr->notes.all, (char *)&fn, sizeof fn); |
|
1229
|
last = (struct footnote*)( blob_buffer(&rndr->notes.all) |
|
1230
|
+( blob_size(&rndr->notes.all)-sizeof fn )); |
|
1231
|
assert( pFN ); |
|
1232
|
memcpy( pFN, last, sizeof fn ); |
|
1233
|
return 1; |
|
1234
|
} |
|
1235
|
|
|
1236
|
/* |
|
1237
|
** Return the byte offset of the matching closing bracket or 0 if not |
|
1238
|
** found. begin[0] must be either '[' or '('. |
|
1239
|
** |
|
1240
|
** TODO: It seems that things like "\\(" are not handled correctly. |
|
1241
|
** That is historical behavior for a corner-case, |
|
1242
|
** so it's left as it is until somebody complains. |
|
1243
|
*/ |
|
1244
|
static inline size_t matching_bracket_offset( |
|
1245
|
const char* begin, |
|
1246
|
const char* end |
|
1247
|
){ |
|
1248
|
const char *i; |
|
1249
|
int level; |
|
1250
|
const char bra = *begin; |
|
1251
|
const char ket = bra=='[' ? ']' : ')'; |
|
1252
|
assert( bra=='[' || bra=='(' ); |
|
1253
|
for(i=begin+1,level=1; i!=end; i++){ |
|
1254
|
if( *i=='\n' ) /* do nothing */; |
|
1255
|
else if( i[-1]=='\\' ) continue; |
|
1256
|
else if( *i==bra ) level++; |
|
1257
|
else if( *i==ket ){ |
|
1258
|
if( --level<=0 ) return i-begin; |
|
1259
|
} |
|
1260
|
} |
|
1261
|
return 0; |
|
1262
|
} |
|
1263
|
|
|
1264
|
/* |
|
1265
|
** char_footnote -- '(': parsing a standalone inline footnote. |
|
1266
|
*/ |
|
1267
|
static size_t char_footnote( |
|
1268
|
struct Blob *ob, |
|
1269
|
struct render *rndr, |
|
1270
|
char *data, |
|
1271
|
size_t offset, |
|
1272
|
size_t size |
|
1273
|
){ |
|
1274
|
size_t end; |
|
1275
|
struct footnote fn; |
|
1276
|
|
|
1277
|
if( size<4 || data[1]!='^' ) return 0; |
|
1278
|
end = matching_bracket_offset(data, data+size); |
|
1279
|
if( !end ) return 0; |
|
1280
|
if( !add_inline_footnote(rndr, data+2, end-2, &fn) ) return 0; |
|
1281
|
if( rndr->make.footnote_ref ){ |
|
1282
|
rndr->make.footnote_ref(ob,0,&fn.upc,fn.iMark,1,rndr->make.opaque); |
|
1283
|
} |
|
1284
|
return end+1; |
|
1285
|
} |
|
1286
|
|
|
1287
|
/* |
|
1288
|
** char_link -- '[': parsing a link or an image. |
|
1289
|
*/ |
|
1290
|
static size_t char_link( |
|
1291
|
struct Blob *ob, |
|
1292
|
struct render *rndr, |
|
1293
|
char *data, |
|
1294
|
size_t offset, |
|
1295
|
size_t size /* parse_inline() ensures that size > 0 */ |
|
1296
|
){ |
|
1297
|
const int bFsfn = (size>3 && data[1]=='^'); /*free-standing footnote ref*/ |
|
1298
|
const int bImg = !bFsfn && (offset && data[-1] == '!'); |
|
1299
|
size_t i, txt_e; |
|
1300
|
struct Blob *content; |
|
1301
|
struct Blob *link; |
|
1302
|
struct Blob *title; |
|
1303
|
struct footnote fn; |
|
1304
|
int ret; |
|
1305
|
|
|
1306
|
/* checking whether the correct renderer exists */ |
|
1307
|
if( !bFsfn ){ |
|
1308
|
if( (bImg && !rndr->make.image) || (!bImg && !rndr->make.link) ){ |
|
1309
|
return 0; |
|
1310
|
} |
|
1311
|
} |
|
1312
|
|
|
1313
|
/* looking for the matching closing bracket */ |
|
1314
|
txt_e = matching_bracket_offset(data, data+size); |
|
1315
|
if( !txt_e ) return 0; |
|
1316
|
i = txt_e + 1; |
|
1317
|
ret = 0; /* error if we don't get to the callback */ |
|
1318
|
|
|
1319
|
/* free-standing footnote reference */ |
|
1320
|
if( bFsfn ){ |
|
1321
|
fn = get_footnote(rndr, data+2, txt_e-2); |
|
1322
|
content = link = title = 0; |
|
1323
|
}else{ |
|
1324
|
fn.nUsed = 0; |
|
1325
|
|
|
1326
|
/* skip "inter-bracket-whitespace" - any amount of whitespace or newline */ |
|
1327
|
/* (this is much more lax than original markdown syntax) */ |
|
1328
|
while( i<size && (data[i]==' ' || data[i]=='\t' || data[i]=='\n') ){ i++; } |
|
1329
|
|
|
1330
|
/* allocate temporary buffers to store content, link and title */ |
|
1331
|
title = new_work_buffer(rndr); |
|
1332
|
content = new_work_buffer(rndr); |
|
1333
|
link = new_work_buffer(rndr); |
|
1334
|
|
|
1335
|
if( i<size && data[i]=='(' ){ |
|
1336
|
|
|
1337
|
if( i+2<size && data[i+1]=='^' ){ /* span-bounded inline footnote */ |
|
1338
|
|
|
1339
|
const size_t k = matching_bracket_offset(data+i, data+size); |
|
1340
|
if( !k ) goto char_link_cleanup; |
|
1341
|
add_inline_footnote(rndr, data+(i+2), k-2, &fn); |
|
1342
|
i += k+1; |
|
1343
|
}else{ /* inline style link */ |
|
1344
|
size_t span_end = i; |
|
1345
|
while( span_end<size |
|
1346
|
&& !(data[span_end]==')' |
|
1347
|
&& (span_end==i || data[span_end-1]!='\\')) ){ |
|
1348
|
span_end++; |
|
1349
|
} |
|
1350
|
if( span_end>=size |
|
1351
|
|| get_link_inline(link, title, data+i+1, span_end-(i+1))<0 ){ |
|
1352
|
goto char_link_cleanup; |
|
1353
|
} |
|
1354
|
i = span_end+1; |
|
1355
|
} |
|
1356
|
/* reference style link or span-bounded footnote reference */ |
|
1357
|
}else if( i<size && data[i]=='[' ){ |
|
1358
|
char *id_data; |
|
1359
|
size_t id_size, id_end = i; |
|
1360
|
int bFootnote; |
|
1361
|
|
|
1362
|
while( id_end<size && data[id_end]!=']' ){ id_end++; } |
|
1363
|
if( id_end>=size ) goto char_link_cleanup; |
|
1364
|
bFootnote = data[i+1]=='^'; |
|
1365
|
if( i+1==id_end || (bFootnote && i+2==id_end) ){ |
|
1366
|
/* implicit id - use the contents */ |
|
1367
|
id_data = data+1; |
|
1368
|
id_size = txt_e-1; |
|
1369
|
}else{ |
|
1370
|
/* explicit id - between brackets */ |
|
1371
|
id_data = data+i+1; |
|
1372
|
id_size = id_end-(i+1); |
|
1373
|
if( bFootnote ){ |
|
1374
|
id_data++; |
|
1375
|
id_size--; |
|
1376
|
} |
|
1377
|
} |
|
1378
|
if( bFootnote ){ |
|
1379
|
fn = get_footnote(rndr, id_data, id_size); |
|
1380
|
}else if( get_link_ref(rndr, link, title, id_data, id_size)<0 ){ |
|
1381
|
goto char_link_cleanup; |
|
1382
|
} |
|
1383
|
i = id_end+1; |
|
1384
|
/* shortcut reference style link */ |
|
1385
|
}else{ |
|
1386
|
if( get_link_ref(rndr, link, title, data+1, txt_e-1)<0 ){ |
|
1387
|
goto char_link_cleanup; |
|
1388
|
} |
|
1389
|
/* rewinding an "inter-bracket-whitespace" */ |
|
1390
|
i = txt_e+1; |
|
1391
|
} |
|
1392
|
} |
|
1393
|
/* building content: img alt is escaped, link content is parsed */ |
|
1394
|
if( txt_e>1 && content ){ |
|
1395
|
if( bImg ) blob_append(content, data+1, txt_e-1); |
|
1396
|
else parse_inline(content, rndr, data+1, txt_e-1); |
|
1397
|
} |
|
1398
|
|
|
1399
|
/* calling the relevant rendering function */ |
|
1400
|
if( bImg ){ |
|
1401
|
if( blob_size(ob)>0 && blob_buffer(ob)[blob_size(ob)-1]=='!' ){ |
|
1402
|
ob->nUsed--; |
|
1403
|
} |
|
1404
|
ret = rndr->make.image(ob, link, title, content, rndr->make.opaque); |
|
1405
|
}else if( fn.nUsed ){ |
|
1406
|
if( rndr->make.footnote_ref ){ |
|
1407
|
ret = rndr->make.footnote_ref(ob, content, &fn.upc, fn.iMark, |
|
1408
|
fn.nUsed, rndr->make.opaque); |
|
1409
|
} |
|
1410
|
}else{ |
|
1411
|
ret = rndr->make.link(ob, link, title, content, rndr->make.opaque); |
|
1412
|
} |
|
1413
|
|
|
1414
|
/* cleanup */ |
|
1415
|
char_link_cleanup: |
|
1416
|
release_work_buffer(rndr, title); |
|
1417
|
release_work_buffer(rndr, link); |
|
1418
|
release_work_buffer(rndr, content); |
|
1419
|
return ret ? i : 0; |
|
1420
|
} |
|
1421
|
|
|
1422
|
|
|
1423
|
/********************************* |
|
1424
|
* BLOCK-LEVEL PARSING FUNCTIONS * |
|
1425
|
*********************************/ |
|
1426
|
|
|
1427
|
/* is_empty -- returns the line length when it is empty, 0 otherwise */ |
|
1428
|
static size_t is_empty(const char *data, size_t size){ |
|
1429
|
size_t i; |
|
1430
|
for(i=0; i<size && data[i]!='\n'; i++){ |
|
1431
|
if( data[i]!=' ' && data[i]!='\t' ) return 0; |
|
1432
|
} |
|
1433
|
return i+1; |
|
1434
|
} |
|
1435
|
|
|
1436
|
|
|
1437
|
/* is_hrule -- returns whether a line is a horizontal rule */ |
|
1438
|
static int is_hrule(char *data, size_t size){ |
|
1439
|
size_t i = 0, n = 0; |
|
1440
|
char c; |
|
1441
|
|
|
1442
|
/* skipping initial spaces */ |
|
1443
|
if( size<3 ) return 0; |
|
1444
|
if( data[0]==' ' ){ |
|
1445
|
i++; |
|
1446
|
if( data[1]==' ' ){ |
|
1447
|
i++; |
|
1448
|
if( data[2]==' ' ){ |
|
1449
|
i++; |
|
1450
|
} |
|
1451
|
} |
|
1452
|
} |
|
1453
|
|
|
1454
|
/* looking at the hrule char */ |
|
1455
|
if( i+2>=size || (data[i]!='*' && data[i]!='-' && data[i]!='_') ) return 0; |
|
1456
|
c = data[i]; |
|
1457
|
|
|
1458
|
/* the whole line must be the char or whitespace */ |
|
1459
|
while (i < size && data[i] != '\n') { |
|
1460
|
if( data[i]==c ){ |
|
1461
|
n += 1; |
|
1462
|
}else if( data[i]!=' ' && data[i]!='\t' ){ |
|
1463
|
return 0; |
|
1464
|
} |
|
1465
|
i++; |
|
1466
|
} |
|
1467
|
|
|
1468
|
return n>=3; |
|
1469
|
} |
|
1470
|
|
|
1471
|
|
|
1472
|
/* is_headerline -- returns whether the line is a setext-style hdr underline */ |
|
1473
|
static int is_headerline(char *data, size_t size){ |
|
1474
|
size_t i = 0; |
|
1475
|
|
|
1476
|
/* test of level 1 header */ |
|
1477
|
if( data[i]=='=' ){ |
|
1478
|
for(i=1; i<size && data[i]=='='; i++); |
|
1479
|
while( i<size && (data[i]==' ' || data[i]=='\t') ){ i++; } |
|
1480
|
return (i>=size || data[i]=='\n') ? 1 : 0; |
|
1481
|
} |
|
1482
|
|
|
1483
|
/* test of level 2 header */ |
|
1484
|
if( data[i]=='-' ){ |
|
1485
|
for(i=1; i<size && data[i]=='-'; i++); |
|
1486
|
while( i<size && (data[i]==' ' || data[i]=='\t') ){ i++; } |
|
1487
|
return (i>=size || data[i]=='\n') ? 2 : 0; |
|
1488
|
} |
|
1489
|
|
|
1490
|
return 0; |
|
1491
|
} |
|
1492
|
|
|
1493
|
|
|
1494
|
/* is_table_sep -- returns whether there is a table separator at pos */ |
|
1495
|
static int is_table_sep(char *data, size_t pos){ |
|
1496
|
return data[pos]=='|' && (pos==0 || data[pos-1]!='\\'); |
|
1497
|
} |
|
1498
|
|
|
1499
|
|
|
1500
|
/* is_tableline -- returns the number of column tables in the given line */ |
|
1501
|
static int is_tableline(char *data, size_t size){ |
|
1502
|
size_t i = 0; |
|
1503
|
int n_sep = 0, outer_sep = 0; |
|
1504
|
|
|
1505
|
/* skip initial blanks */ |
|
1506
|
while( i<size && (data[i]==' ' || data[i]=='\t') ){ i++; } |
|
1507
|
|
|
1508
|
/* check for initial '|' */ |
|
1509
|
if( i<size && data[i]=='|') outer_sep++; |
|
1510
|
|
|
1511
|
/* count the number of pipes in the line */ |
|
1512
|
for(n_sep=0; i<size && data[i]!='\n'; i++){ |
|
1513
|
if( is_table_sep(data, i) ) n_sep++; |
|
1514
|
if( data[i]=='`' ){ |
|
1515
|
skip_codespan(data, size, &i); |
|
1516
|
i--; |
|
1517
|
} |
|
1518
|
} |
|
1519
|
|
|
1520
|
/* march back to check for optional last '|' before blanks and EOL */ |
|
1521
|
while( i && (data[i-1]==' ' || data[i-1]=='\t' || data[i-1]=='\n') ){ i--; } |
|
1522
|
if( i && is_table_sep(data, i-1) ) outer_sep += 1; |
|
1523
|
|
|
1524
|
/* return the number of column or 0 if it's not a table line */ |
|
1525
|
return (n_sep>0) ? (n_sep-outer_sep+1) : 0; |
|
1526
|
} |
|
1527
|
|
|
1528
|
|
|
1529
|
/* prefix_quote -- returns blockquote prefix length */ |
|
1530
|
static size_t prefix_quote(char *data, size_t size){ |
|
1531
|
size_t i = 0; |
|
1532
|
if( i<size && data[i]==' ' ) i++; |
|
1533
|
if( i<size && data[i]==' ' ) i++; |
|
1534
|
if( i<size && data[i]==' ' ) i++; |
|
1535
|
if( i<size && data[i]=='>' ){ |
|
1536
|
if( i+1<size && (data[i+1]==' ' || data[i+1]=='\t') ){ |
|
1537
|
return i + 2; |
|
1538
|
}else{ |
|
1539
|
return i + 1; |
|
1540
|
} |
|
1541
|
}else{ |
|
1542
|
return 0; |
|
1543
|
} |
|
1544
|
} |
|
1545
|
|
|
1546
|
|
|
1547
|
/* prefix_code -- returns prefix length for block code */ |
|
1548
|
static size_t prefix_code(char *data, size_t size){ |
|
1549
|
if( size>0 && data[0]=='\t' ) return 1; |
|
1550
|
if( size>3 && data[0]==' ' && data[1]==' ' && data[2]==' ' && data[3]==' ' ){ |
|
1551
|
return 4; |
|
1552
|
} |
|
1553
|
return 0; |
|
1554
|
} |
|
1555
|
|
|
1556
|
/* Return the number of characters in the delimiter of a fenced code |
|
1557
|
** block. */ |
|
1558
|
static size_t prefix_fencedcode(char *data, size_t size){ |
|
1559
|
char c = data[0]; |
|
1560
|
int nb; |
|
1561
|
if( c!='`' && c!='~' ) return 0; |
|
1562
|
for(nb=1; nb<(int)size-3 && data[nb]==c; nb++){} |
|
1563
|
if( nb<3 ) return 0; |
|
1564
|
if( nb>=(int)size-nb ) return 0; |
|
1565
|
return nb; |
|
1566
|
} |
|
1567
|
|
|
1568
|
/* prefix_oli -- returns ordered list item prefix */ |
|
1569
|
static size_t prefix_oli(char *data, size_t size){ |
|
1570
|
size_t i = 0; |
|
1571
|
if( i<size && data[i]==' ') i++; |
|
1572
|
if( i<size && data[i]==' ') i++; |
|
1573
|
if( i<size && data[i]==' ') i++; |
|
1574
|
|
|
1575
|
if( i>=size || data[i]<'0' || data[i]>'9' ) return 0; |
|
1576
|
while( i<size && data[i]>='0' && data[i]<='9' ){ i++; } |
|
1577
|
|
|
1578
|
if( i+1>=size |
|
1579
|
|| (data[i]!='.' && data[i]!=')') |
|
1580
|
|| (data[i+1]!=' ' && data[i+1]!='\t') |
|
1581
|
){ |
|
1582
|
return 0; |
|
1583
|
} |
|
1584
|
i = i+2; |
|
1585
|
while( i<size && (data[i]==' ' || data[i]=='\t') ){ i++; } |
|
1586
|
return i; |
|
1587
|
} |
|
1588
|
|
|
1589
|
|
|
1590
|
/* prefix_uli -- returns ordered list item prefix */ |
|
1591
|
static size_t prefix_uli(char *data, size_t size){ |
|
1592
|
size_t i = 0; |
|
1593
|
if( i<size && data[i]==' ') i++; |
|
1594
|
if( i<size && data[i]==' ') i++; |
|
1595
|
if( i<size && data[i]==' ') i++; |
|
1596
|
if( i+1>=size |
|
1597
|
|| (data[i]!='*' && data[i]!='+' && data[i]!='-') |
|
1598
|
|| (data[i+1]!=' ' && data[i+1]!='\t') |
|
1599
|
){ |
|
1600
|
return 0; |
|
1601
|
} |
|
1602
|
i = i+2; |
|
1603
|
while( i<size && (data[i]==' ' || data[i]=='\t') ){ i++; } |
|
1604
|
return i; |
|
1605
|
} |
|
1606
|
|
|
1607
|
|
|
1608
|
/* parse_block predeclaration */ |
|
1609
|
static void parse_block( |
|
1610
|
struct Blob *ob, |
|
1611
|
struct render *rndr, |
|
1612
|
char *data, |
|
1613
|
size_t size); |
|
1614
|
|
|
1615
|
|
|
1616
|
/* parse_blockquote -- handles parsing of a blockquote fragment */ |
|
1617
|
static size_t parse_blockquote( |
|
1618
|
struct Blob *ob, |
|
1619
|
struct render *rndr, |
|
1620
|
char *data, |
|
1621
|
size_t size |
|
1622
|
){ |
|
1623
|
size_t beg, end = 0, pre, work_size = 0, nb, endFence = 0; |
|
1624
|
char *work_data = 0; |
|
1625
|
struct Blob *out = new_work_buffer(rndr); |
|
1626
|
|
|
1627
|
/* Check to see if this is a quote of a fenced code block, because |
|
1628
|
** if it is, then blank lines do not terminated the quoted text. Ex: |
|
1629
|
** |
|
1630
|
** > ~~~~ |
|
1631
|
** First line |
|
1632
|
** |
|
1633
|
** Line after blank |
|
1634
|
** ~~~~ |
|
1635
|
** |
|
1636
|
** If this is a quoted fenced block, then set endFence to be the |
|
1637
|
** offset of the end of the fenced block. |
|
1638
|
*/ |
|
1639
|
pre = prefix_quote(data,size); |
|
1640
|
pre += is_empty(data+pre,size-pre); |
|
1641
|
nb = prefix_fencedcode(data+pre,size-pre); |
|
1642
|
if( nb ){ |
|
1643
|
size_t i = 0; |
|
1644
|
char delim = data[pre]; |
|
1645
|
for(end=pre+nb; end<size && i<nb; end++){ |
|
1646
|
if( data[end]==delim ) i++; else i = 0; |
|
1647
|
} |
|
1648
|
if( i>=nb ) endFence = end; |
|
1649
|
} |
|
1650
|
|
|
1651
|
beg = 0; |
|
1652
|
while( beg<size ){ |
|
1653
|
for(end=beg+1; end<size && data[end-1]!='\n'; end++); |
|
1654
|
pre = prefix_quote(data+beg, end-beg); |
|
1655
|
if( pre ){ |
|
1656
|
beg += pre; /* skipping prefix */ |
|
1657
|
}else if( is_empty(data+beg, end-beg) |
|
1658
|
&& (end>=size |
|
1659
|
|| (end>endFence |
|
1660
|
&& prefix_quote(data+end, size-end)==0 |
|
1661
|
&& !is_empty(data+end, size-end))) |
|
1662
|
){ |
|
1663
|
/* empty line followed by non-quote line */ |
|
1664
|
break; |
|
1665
|
} |
|
1666
|
if( beg<end ){ /* copy into the in-place working buffer */ |
|
1667
|
if( !work_data ){ |
|
1668
|
work_data = data+beg; |
|
1669
|
}else if( (data+beg)!=(work_data+work_size) ){ |
|
1670
|
memmove(work_data+work_size, data+beg, end-beg); |
|
1671
|
} |
|
1672
|
work_size += end-beg; |
|
1673
|
} |
|
1674
|
beg = end; |
|
1675
|
} |
|
1676
|
|
|
1677
|
if( rndr->make.blockquote ){ |
|
1678
|
if( !too_deep(rndr) ){ |
|
1679
|
parse_block(out, rndr, work_data, work_size); |
|
1680
|
}else{ |
|
1681
|
blob_append(out, work_data, work_size); |
|
1682
|
} |
|
1683
|
rndr->make.blockquote(ob, out, rndr->make.opaque); |
|
1684
|
} |
|
1685
|
release_work_buffer(rndr, out); |
|
1686
|
return end; |
|
1687
|
} |
|
1688
|
|
|
1689
|
|
|
1690
|
/* parse_paragraph -- handles parsing of a regular paragraph */ |
|
1691
|
static size_t parse_paragraph( |
|
1692
|
struct Blob *ob, |
|
1693
|
struct render *rndr, |
|
1694
|
char *data, |
|
1695
|
size_t size |
|
1696
|
){ |
|
1697
|
size_t i = 0, end = 0; |
|
1698
|
int level = 0; |
|
1699
|
char *work_data = data; |
|
1700
|
size_t work_size = 0; |
|
1701
|
|
|
1702
|
while( i<size ){ |
|
1703
|
char *zEnd = memchr(data+i, '\n', size-i-1); |
|
1704
|
end = zEnd==0 ? size : (size_t)(zEnd - (data-1)); |
|
1705
|
/* The above is the same as: |
|
1706
|
** for(end=i+1; end<size && data[end-1]!='\n'; end++); |
|
1707
|
** "end" is left with a value such that data[end] is one byte |
|
1708
|
** past the first '\n' or one byte past the end of the string */ |
|
1709
|
if( is_empty(data+i, size-i) |
|
1710
|
|| (level = is_headerline(data+i, size-i))!= 0 |
|
1711
|
){ |
|
1712
|
break; |
|
1713
|
} |
|
1714
|
if( (i && data[i]=='#') |
|
1715
|
|| is_hrule(data+i, size-i) |
|
1716
|
|| prefix_uli(data+i, size-i) |
|
1717
|
|| prefix_oli(data+i, size-i) |
|
1718
|
){ |
|
1719
|
end = i; |
|
1720
|
break; |
|
1721
|
} |
|
1722
|
i = end; |
|
1723
|
} |
|
1724
|
|
|
1725
|
work_size = i; |
|
1726
|
while( work_size && data[work_size-1]=='\n' ){ work_size--; } |
|
1727
|
|
|
1728
|
if( !level ){ |
|
1729
|
if( rndr->make.paragraph ){ |
|
1730
|
struct Blob *tmp = new_work_buffer(rndr); |
|
1731
|
parse_inline(tmp, rndr, work_data, work_size); |
|
1732
|
rndr->make.paragraph(ob, tmp, rndr->make.opaque); |
|
1733
|
release_work_buffer(rndr, tmp); |
|
1734
|
} |
|
1735
|
}else{ |
|
1736
|
if( work_size ){ |
|
1737
|
size_t beg; |
|
1738
|
i = work_size; |
|
1739
|
work_size -= 1; |
|
1740
|
while( work_size && data[work_size]!='\n' ){ work_size--; } |
|
1741
|
beg = work_size+1; |
|
1742
|
while( work_size && data[work_size-1]=='\n'){ work_size--; } |
|
1743
|
if( work_size ){ |
|
1744
|
struct Blob *tmp = new_work_buffer(rndr); |
|
1745
|
parse_inline(tmp, rndr, work_data, work_size); |
|
1746
|
if( rndr->make.paragraph ){ |
|
1747
|
rndr->make.paragraph(ob, tmp, rndr->make.opaque); |
|
1748
|
} |
|
1749
|
release_work_buffer(rndr, tmp); |
|
1750
|
work_data += beg; |
|
1751
|
work_size = i - beg; |
|
1752
|
}else{ |
|
1753
|
work_size = i; |
|
1754
|
} |
|
1755
|
} |
|
1756
|
|
|
1757
|
if( rndr->make.header ){ |
|
1758
|
struct Blob *span = new_work_buffer(rndr); |
|
1759
|
parse_inline(span, rndr, work_data, work_size); |
|
1760
|
rndr->make.header(ob, span, level, rndr->make.opaque); |
|
1761
|
release_work_buffer(rndr, span); |
|
1762
|
} |
|
1763
|
} |
|
1764
|
return end; |
|
1765
|
} |
|
1766
|
|
|
1767
|
|
|
1768
|
/* parse_blockcode -- handles parsing of a block-level code fragment */ |
|
1769
|
static size_t parse_blockcode( |
|
1770
|
struct Blob *ob, |
|
1771
|
struct render *rndr, |
|
1772
|
char *data, |
|
1773
|
size_t size |
|
1774
|
){ |
|
1775
|
size_t beg, end, pre; |
|
1776
|
struct Blob *work = new_work_buffer(rndr); |
|
1777
|
|
|
1778
|
beg = 0; |
|
1779
|
while( beg<size ){ |
|
1780
|
char *zEnd = memchr(data+beg, '\n', size-beg-1); |
|
1781
|
end = zEnd==0 ? size : (size_t)(zEnd - (data-1)); |
|
1782
|
/* The above is the same as: |
|
1783
|
** for(end=beg+1; end<size && data[end-1]!='\n'; end++); |
|
1784
|
** "end" is left with a value such that data[end] is one byte |
|
1785
|
** past the first \n or past then end of the string. */ |
|
1786
|
pre = prefix_code(data+beg, end-beg); |
|
1787
|
if( pre ){ |
|
1788
|
beg += pre; /* skipping prefix */ |
|
1789
|
}else if( !is_empty(data+beg, end-beg) ){ |
|
1790
|
/* non-empty non-prefixed line breaks the pre */ |
|
1791
|
break; |
|
1792
|
} |
|
1793
|
if( beg<end ){ |
|
1794
|
/* verbatim copy to the working buffer, escaping entities */ |
|
1795
|
if( is_empty(data + beg, end - beg) ){ |
|
1796
|
blob_append_char(work, '\n'); |
|
1797
|
}else{ |
|
1798
|
blob_append(work, data+beg, end-beg); |
|
1799
|
} |
|
1800
|
} |
|
1801
|
beg = end; |
|
1802
|
} |
|
1803
|
|
|
1804
|
end = blob_size(work); |
|
1805
|
while( end>0 && blob_buffer(work)[end-1]=='\n' ){ end--; } |
|
1806
|
work->nUsed = end; |
|
1807
|
blob_append_char(work, '\n'); |
|
1808
|
|
|
1809
|
if( work!=ob ){ |
|
1810
|
if( rndr->make.blockcode ){ |
|
1811
|
rndr->make.blockcode(ob, work, rndr->make.opaque); |
|
1812
|
} |
|
1813
|
release_work_buffer(rndr, work); |
|
1814
|
} |
|
1815
|
return beg; |
|
1816
|
} |
|
1817
|
|
|
1818
|
|
|
1819
|
/* parse_listitem -- parsing of a single list item */ |
|
1820
|
/* assuming initial prefix is already removed */ |
|
1821
|
static size_t parse_listitem( |
|
1822
|
struct Blob *ob, |
|
1823
|
struct render *rndr, |
|
1824
|
char *data, |
|
1825
|
size_t size, |
|
1826
|
int *flags |
|
1827
|
){ |
|
1828
|
struct Blob *work = 0, *inter = 0; |
|
1829
|
size_t beg = 0, end, pre, sublist = 0, orgpre = 0, i; |
|
1830
|
int in_empty = 0, has_inside_empty = 0; |
|
1831
|
|
|
1832
|
/* keeping track of the first indentation prefix */ |
|
1833
|
if( size>1 && data[0]==' ' ){ |
|
1834
|
orgpre = 1; |
|
1835
|
if( size>2 && data[1]==' ' ){ |
|
1836
|
orgpre = 2; |
|
1837
|
if( size>3 && data[2]==' ' ){ |
|
1838
|
orgpre = 3; |
|
1839
|
} |
|
1840
|
} |
|
1841
|
} |
|
1842
|
beg = prefix_uli(data, size); |
|
1843
|
if( !beg ) beg = prefix_oli(data, size); |
|
1844
|
if( !beg ) return 0; |
|
1845
|
/* skipping to the beginning of the following line */ |
|
1846
|
end = beg; |
|
1847
|
while( end<size && data[end-1]!='\n' ){ end++; } |
|
1848
|
|
|
1849
|
/* getting working buffers */ |
|
1850
|
work = new_work_buffer(rndr); |
|
1851
|
inter = new_work_buffer(rndr); |
|
1852
|
|
|
1853
|
/* putting the first line into the working buffer */ |
|
1854
|
blob_append(work, data+beg, end-beg); |
|
1855
|
beg = end; |
|
1856
|
|
|
1857
|
/* process the following lines */ |
|
1858
|
while( beg<size ){ |
|
1859
|
end++; |
|
1860
|
while( end<size && data[end-1]!='\n' ){ end++; } |
|
1861
|
|
|
1862
|
/* process an empty line */ |
|
1863
|
if( is_empty(data+beg, end-beg) ){ |
|
1864
|
in_empty = 1; |
|
1865
|
beg = end; |
|
1866
|
continue; |
|
1867
|
} |
|
1868
|
|
|
1869
|
/* computing the indentation */ |
|
1870
|
i = 0; |
|
1871
|
if( end-beg>1 && data[beg]==' ' ){ |
|
1872
|
i = 1; |
|
1873
|
if( end-beg>2 && data[beg+1]==' ' ){ |
|
1874
|
i = 2; |
|
1875
|
if( end-beg>3 && data[beg+2]==' ' ){ |
|
1876
|
i = 3; |
|
1877
|
if( end-beg>3 && data[beg+3]==' ' ){ |
|
1878
|
i = 4; |
|
1879
|
} |
|
1880
|
} |
|
1881
|
} |
|
1882
|
} |
|
1883
|
pre = i; |
|
1884
|
if( data[beg]=='\t' ){ i = 1; pre = 8; } |
|
1885
|
|
|
1886
|
/* checking for a new item */ |
|
1887
|
if( (prefix_uli(data+beg+i, end-beg-i) && !is_hrule(data+beg+i, end-beg-i)) |
|
1888
|
|| prefix_oli(data+beg+i, end-beg-i) |
|
1889
|
){ |
|
1890
|
if( in_empty ) has_inside_empty = 1; |
|
1891
|
if( pre == orgpre ){ /* the following item must have */ |
|
1892
|
break; /* the same indentation */ |
|
1893
|
} |
|
1894
|
if( !sublist ) sublist = blob_size(work); |
|
1895
|
|
|
1896
|
/* joining only indented stuff after empty lines */ |
|
1897
|
}else if( in_empty && i<4 && data[beg]!='\t' ){ |
|
1898
|
*flags |= MKD_LI_END; |
|
1899
|
break; |
|
1900
|
}else if( in_empty ){ |
|
1901
|
blob_append_char(work, '\n'); |
|
1902
|
has_inside_empty = 1; |
|
1903
|
} |
|
1904
|
in_empty = 0; |
|
1905
|
|
|
1906
|
/* adding the line without prefix into the working buffer */ |
|
1907
|
blob_append(work, data+beg+i, end-beg-i); |
|
1908
|
beg = end; |
|
1909
|
} |
|
1910
|
|
|
1911
|
/* non-recursive fallback when working buffer stack is full */ |
|
1912
|
if( !inter ){ |
|
1913
|
if( rndr->make.listitem ){ |
|
1914
|
rndr->make.listitem(ob, work, *flags, rndr->make.opaque); |
|
1915
|
} |
|
1916
|
release_work_buffer(rndr, work); |
|
1917
|
return beg; |
|
1918
|
} |
|
1919
|
|
|
1920
|
/* render of li contents */ |
|
1921
|
if( has_inside_empty ) *flags |= MKD_LI_BLOCK; |
|
1922
|
if( *flags & MKD_LI_BLOCK ){ |
|
1923
|
/* intermediate render of block li */ |
|
1924
|
if( sublist && sublist<blob_size(work) ){ |
|
1925
|
parse_block(inter, rndr, blob_buffer(work), sublist); |
|
1926
|
parse_block(inter, |
|
1927
|
rndr, |
|
1928
|
blob_buffer(work)+sublist, |
|
1929
|
blob_size(work)-sublist); |
|
1930
|
}else{ |
|
1931
|
parse_block(inter, rndr, blob_buffer(work), blob_size(work)); |
|
1932
|
} |
|
1933
|
}else{ |
|
1934
|
/* intermediate render of inline li */ |
|
1935
|
if( sublist && sublist<blob_size(work) ){ |
|
1936
|
parse_inline(inter, rndr, blob_buffer(work), sublist); |
|
1937
|
parse_block(inter, |
|
1938
|
rndr, |
|
1939
|
blob_buffer(work)+sublist, |
|
1940
|
blob_size(work)-sublist); |
|
1941
|
}else{ |
|
1942
|
parse_inline(inter, rndr, blob_buffer(work), blob_size(work)); |
|
1943
|
} |
|
1944
|
} |
|
1945
|
|
|
1946
|
/* render of li itself */ |
|
1947
|
if( rndr->make.listitem ){ |
|
1948
|
rndr->make.listitem(ob, inter, *flags, rndr->make.opaque); |
|
1949
|
} |
|
1950
|
release_work_buffer(rndr, inter); |
|
1951
|
release_work_buffer(rndr, work); |
|
1952
|
return beg; |
|
1953
|
} |
|
1954
|
|
|
1955
|
|
|
1956
|
/* parse_list -- parsing ordered or unordered list block */ |
|
1957
|
static size_t parse_list( |
|
1958
|
struct Blob *ob, |
|
1959
|
struct render *rndr, |
|
1960
|
char *data, |
|
1961
|
size_t size, |
|
1962
|
int flags |
|
1963
|
){ |
|
1964
|
struct Blob *work = new_work_buffer(rndr); |
|
1965
|
size_t i = 0, j; |
|
1966
|
|
|
1967
|
while( i<size ){ |
|
1968
|
j = parse_listitem(work, rndr, data+i, size-i, &flags); |
|
1969
|
i += j; |
|
1970
|
if( !j || (flags & MKD_LI_END) ) break; |
|
1971
|
} |
|
1972
|
|
|
1973
|
if( rndr->make.list ) rndr->make.list(ob, work, flags, rndr->make.opaque); |
|
1974
|
release_work_buffer(rndr, work); |
|
1975
|
return i; |
|
1976
|
} |
|
1977
|
|
|
1978
|
|
|
1979
|
/* parse_atxheader -- parsing of atx-style headers */ |
|
1980
|
static size_t parse_atxheader( |
|
1981
|
struct Blob *ob, |
|
1982
|
struct render *rndr, |
|
1983
|
char *data, |
|
1984
|
size_t size |
|
1985
|
){ |
|
1986
|
int level = 0; |
|
1987
|
size_t i, end, skip, span_beg, span_size; |
|
1988
|
|
|
1989
|
if( !size || data[0]!='#' ) return 0; |
|
1990
|
|
|
1991
|
while( level<(int)size && level<6 && data[level]=='#' ){ level++; } |
|
1992
|
for(i=level; i<size && (data[i]==' ' || data[i]=='\t'); i++); |
|
1993
|
if ( (int)i == level ) return parse_paragraph(ob, rndr, data, size); |
|
1994
|
span_beg = i; |
|
1995
|
|
|
1996
|
for(end=i; end<size && data[end]!='\n'; end++); |
|
1997
|
skip = end; |
|
1998
|
if( end<=i ) return parse_paragraph(ob, rndr, data, size); |
|
1999
|
while( end && data[end-1]=='#' ){ end--; } |
|
2000
|
while( end && (data[end-1]==' ' || data[end-1]=='\t') ){ end--; } |
|
2001
|
if( end<=i ) return parse_paragraph(ob, rndr, data, size); |
|
2002
|
|
|
2003
|
span_size = end-span_beg; |
|
2004
|
if( rndr->make.header ){ |
|
2005
|
struct Blob *span = new_work_buffer(rndr); |
|
2006
|
parse_inline(span, rndr, data+span_beg, span_size); |
|
2007
|
rndr->make.header(ob, span, level, rndr->make.opaque); |
|
2008
|
release_work_buffer(rndr, span); |
|
2009
|
} |
|
2010
|
return skip; |
|
2011
|
} |
|
2012
|
|
|
2013
|
|
|
2014
|
/* htmlblock_end -- checking end of HTML block : </tag>[ \t]*\n[ \t*]\n */ |
|
2015
|
/* returns the length on match, 0 otherwise */ |
|
2016
|
static size_t htmlblock_end( |
|
2017
|
const struct html_tag *tag, |
|
2018
|
const char *data, |
|
2019
|
size_t size |
|
2020
|
){ |
|
2021
|
size_t i, w; |
|
2022
|
|
|
2023
|
/* assuming data[0]=='<' && data[1]=='/' already tested */ |
|
2024
|
|
|
2025
|
/* checking tag is a match */ |
|
2026
|
if( (tag->size+3)>(int)size |
|
2027
|
|| fossil_strnicmp(data+2, tag->text, tag->size) |
|
2028
|
|| data[tag->size+2]!='>' |
|
2029
|
){ |
|
2030
|
return 0; |
|
2031
|
} |
|
2032
|
|
|
2033
|
/* checking white lines */ |
|
2034
|
i = tag->size + 3; |
|
2035
|
w = 0; |
|
2036
|
if( i<size && (w = is_empty(data+i, size-i))==0 ){ |
|
2037
|
return 0; /* non-blank after tag */ |
|
2038
|
} |
|
2039
|
i += w; |
|
2040
|
w = 0; |
|
2041
|
|
|
2042
|
if( i<size && (w = is_empty(data + i, size - i))==0 ){ |
|
2043
|
return 0; /* non-blank line after tag line */ |
|
2044
|
} |
|
2045
|
return i+w; |
|
2046
|
} |
|
2047
|
|
|
2048
|
|
|
2049
|
/* parse_htmlblock -- parsing of inline HTML block */ |
|
2050
|
static size_t parse_htmlblock( |
|
2051
|
struct Blob *ob, |
|
2052
|
struct render *rndr, |
|
2053
|
char *data, |
|
2054
|
size_t size |
|
2055
|
){ |
|
2056
|
size_t i, j = 0; |
|
2057
|
const struct html_tag *curtag; |
|
2058
|
int found; |
|
2059
|
size_t work_size = 0; |
|
2060
|
struct Blob work = BLOB_INITIALIZER; |
|
2061
|
|
|
2062
|
/* identification of the opening tag */ |
|
2063
|
if( size<2 || data[0]!='<' ) return 0; |
|
2064
|
curtag = find_block_tag(data+1, size-1); |
|
2065
|
|
|
2066
|
/* handling of special cases */ |
|
2067
|
if( !curtag ){ |
|
2068
|
|
|
2069
|
/* HTML comment, laxist form */ |
|
2070
|
if( size>5 && data[1]=='!' && data[2]=='-' && data[3]=='-' ){ |
|
2071
|
i = 5; |
|
2072
|
while( i<size && !(data[i-2]=='-' && data[i-1]=='-' && data[i]=='>') ){ |
|
2073
|
i++; |
|
2074
|
} |
|
2075
|
i++; |
|
2076
|
if( i<size ){ |
|
2077
|
j = is_empty(data+i, size-i); |
|
2078
|
if( j ){ |
|
2079
|
work_size = i+j; |
|
2080
|
if( !rndr->make.blockhtml ) return work_size; |
|
2081
|
blob_init(&work, data, work_size); |
|
2082
|
rndr->make.blockhtml(ob, &work, rndr->make.opaque); |
|
2083
|
return work_size; |
|
2084
|
} |
|
2085
|
} |
|
2086
|
} |
|
2087
|
|
|
2088
|
/* HR, which is the only self-closing block tag considered */ |
|
2089
|
if( size>4 |
|
2090
|
&& (data[1]=='h' || data[1]=='H') |
|
2091
|
&& (data[2]=='r' || data[2]=='R') |
|
2092
|
){ |
|
2093
|
i = 3; |
|
2094
|
while( i<size && data[i]!='>' ){ i++; } |
|
2095
|
if( i+1<size ){ |
|
2096
|
i += 1; |
|
2097
|
j = is_empty(data+i, size-i); |
|
2098
|
if( j ){ |
|
2099
|
work_size = i+j; |
|
2100
|
if( !rndr->make.blockhtml ) return work_size; |
|
2101
|
blob_init(&work, data, work_size); |
|
2102
|
rndr->make.blockhtml(ob, &work, rndr->make.opaque); |
|
2103
|
return work_size; |
|
2104
|
} |
|
2105
|
} |
|
2106
|
} |
|
2107
|
|
|
2108
|
/* no special case recognised */ |
|
2109
|
return 0; |
|
2110
|
} |
|
2111
|
|
|
2112
|
/* looking for an matching closing tag */ |
|
2113
|
/* followed by a blank line */ |
|
2114
|
i = 1; |
|
2115
|
found = 0; |
|
2116
|
while( i<size ){ |
|
2117
|
i++; |
|
2118
|
while( i<size && !(data[i-1]=='<' && data[i]=='/') ){ i++; } |
|
2119
|
if( (i+2+curtag->size)>size ) break; |
|
2120
|
j = htmlblock_end(curtag, data+i-1, size-i+1); |
|
2121
|
if (j) { |
|
2122
|
i += j-1; |
|
2123
|
found = 1; |
|
2124
|
break; |
|
2125
|
} |
|
2126
|
} |
|
2127
|
if( !found ) return 0; |
|
2128
|
|
|
2129
|
/* the end of the block has been found */ |
|
2130
|
if( strcmp(curtag->text,"html")==0 ){ |
|
2131
|
/* Omit <html> tags */ |
|
2132
|
enum mkd_autolink dummy; |
|
2133
|
int k = tag_length(data, size, &dummy); |
|
2134
|
int sz = i - (j+k); |
|
2135
|
if( sz>0 ) blob_init(&work, data+k, sz); |
|
2136
|
}else{ |
|
2137
|
blob_init(&work, data, i); |
|
2138
|
} |
|
2139
|
if( rndr->make.blockhtml ){ |
|
2140
|
rndr->make.blockhtml(ob, &work, rndr->make.opaque); |
|
2141
|
} |
|
2142
|
return i; |
|
2143
|
} |
|
2144
|
|
|
2145
|
|
|
2146
|
/* parse_table_cell -- parse a cell inside a table */ |
|
2147
|
static void parse_table_cell( |
|
2148
|
struct Blob *ob, /* output blob */ |
|
2149
|
struct render *rndr, /* renderer description */ |
|
2150
|
char *data, /* input text */ |
|
2151
|
size_t size, /* input text size */ |
|
2152
|
int flags /* table flags */ |
|
2153
|
){ |
|
2154
|
struct Blob *span = new_work_buffer(rndr); |
|
2155
|
parse_inline(span, rndr, data, size); |
|
2156
|
rndr->make.table_cell(ob, span, flags, rndr->make.opaque); |
|
2157
|
release_work_buffer(rndr, span); |
|
2158
|
} |
|
2159
|
|
|
2160
|
|
|
2161
|
/* parse_table_row -- parse an input line into a table row */ |
|
2162
|
static size_t parse_table_row( |
|
2163
|
struct Blob *ob, /* output blob for rendering */ |
|
2164
|
struct render *rndr, /* renderer description */ |
|
2165
|
char *data, /* input text */ |
|
2166
|
size_t size, /* input text size */ |
|
2167
|
int *aligns, /* array of default alignment for columns */ |
|
2168
|
size_t align_size, /* number of columns with default alignment */ |
|
2169
|
int flags /* table flags */ |
|
2170
|
){ |
|
2171
|
size_t i = 0, col = 0; |
|
2172
|
size_t beg, end, total = 0; |
|
2173
|
struct Blob *cells = new_work_buffer(rndr); |
|
2174
|
int align; |
|
2175
|
|
|
2176
|
/* skip leading blanks and separator */ |
|
2177
|
while( i<size && (data[i]==' ' || data[i]=='\t') ){ i++; } |
|
2178
|
if( i<size && data[i]=='|' ) i++; |
|
2179
|
|
|
2180
|
/* go over all the cells */ |
|
2181
|
while( i<size && total==0 ){ |
|
2182
|
/* check optional left/center align marker */ |
|
2183
|
align = 0; |
|
2184
|
if( data[i]==':' ){ |
|
2185
|
align |= MKD_CELL_ALIGN_LEFT; |
|
2186
|
i++; |
|
2187
|
} |
|
2188
|
|
|
2189
|
/* skip blanks */ |
|
2190
|
while( i<size && (data[i]==' ' || data[i]=='\t') ){ i++; } |
|
2191
|
beg = i; |
|
2192
|
|
|
2193
|
/* forward to the next separator or EOL */ |
|
2194
|
while( i<size && !is_table_sep(data, i) && data[i]!='\n' ){ |
|
2195
|
if( data[i]=='`' ){ |
|
2196
|
skip_codespan(data, size, &i); |
|
2197
|
}else{ |
|
2198
|
i++; |
|
2199
|
} |
|
2200
|
} |
|
2201
|
end = i; |
|
2202
|
if( i<size ){ |
|
2203
|
i++; |
|
2204
|
if( data[i-1]=='\n' ) total = i; |
|
2205
|
} |
|
2206
|
|
|
2207
|
/* check optional right/center align marker */ |
|
2208
|
if( i>beg && data[end-1]==':' ){ |
|
2209
|
align |= MKD_CELL_ALIGN_RIGHT; |
|
2210
|
end--; |
|
2211
|
} |
|
2212
|
|
|
2213
|
/* remove trailing blanks */ |
|
2214
|
while( end>beg && (data[end-1]==' ' || data[end-1]=='\t') ){ end--; } |
|
2215
|
|
|
2216
|
/* skip the last cell if it was only blanks */ |
|
2217
|
/* (because it is only the optional end separator) */ |
|
2218
|
if( total && end<=beg ) continue; |
|
2219
|
|
|
2220
|
/* fallback on default alignment if not explicit */ |
|
2221
|
if( align==0 && aligns && col<align_size ) align = aligns[col]; |
|
2222
|
|
|
2223
|
/* render cells */ |
|
2224
|
if( cells && end>=beg ){ |
|
2225
|
parse_table_cell(cells, rndr, data+beg, end-beg, align|flags); |
|
2226
|
} |
|
2227
|
|
|
2228
|
col++; |
|
2229
|
} |
|
2230
|
|
|
2231
|
/* render the whole row and clean up */ |
|
2232
|
rndr->make.table_row(ob, cells, flags, rndr->make.opaque); |
|
2233
|
release_work_buffer(rndr, cells); |
|
2234
|
return total ? total : size; |
|
2235
|
} |
|
2236
|
|
|
2237
|
|
|
2238
|
/* parse_table -- parsing of a whole table */ |
|
2239
|
static size_t parse_table( |
|
2240
|
struct Blob *ob, |
|
2241
|
struct render *rndr, |
|
2242
|
char *data, |
|
2243
|
size_t size |
|
2244
|
){ |
|
2245
|
size_t i = 0, head_end, col; |
|
2246
|
size_t align_size = 0; |
|
2247
|
int *aligns = 0; |
|
2248
|
struct Blob *head = 0; |
|
2249
|
struct Blob *rows = new_work_buffer(rndr); |
|
2250
|
|
|
2251
|
/* skip the first (presumably header) line */ |
|
2252
|
while( i<size && data[i]!='\n' ){ i++; } |
|
2253
|
head_end = i; |
|
2254
|
|
|
2255
|
/* fallback on end of input */ |
|
2256
|
if( i>=size ){ |
|
2257
|
parse_table_row(rows, rndr, data, size, 0, 0, 0); |
|
2258
|
rndr->make.table(ob, 0, rows, rndr->make.opaque); |
|
2259
|
release_work_buffer(rndr, rows); |
|
2260
|
return i; |
|
2261
|
} |
|
2262
|
|
|
2263
|
/* attempt to parse a table rule, i.e. blanks, dash, colons and sep */ |
|
2264
|
i++; |
|
2265
|
col = 0; |
|
2266
|
while( i<size |
|
2267
|
&& (data[i]==' ' |
|
2268
|
|| data[i]=='\t' |
|
2269
|
|| data[i]=='-' |
|
2270
|
|| data[i] == ':' |
|
2271
|
|| data[i] =='|') |
|
2272
|
){ |
|
2273
|
if( data[i] == '|' ) align_size++; |
|
2274
|
if( data[i] == ':' ) col = 1; |
|
2275
|
i += 1; |
|
2276
|
} |
|
2277
|
|
|
2278
|
if( i<size && data[i]=='\n' ){ |
|
2279
|
align_size++; |
|
2280
|
|
|
2281
|
/* render the header row */ |
|
2282
|
head = new_work_buffer(rndr); |
|
2283
|
parse_table_row(head, rndr, data, head_end, 0, 0, MKD_CELL_HEAD); |
|
2284
|
|
|
2285
|
/* parse alignments if provided */ |
|
2286
|
if( col && (aligns=fossil_malloc(align_size * sizeof *aligns))!=0 ){ |
|
2287
|
for(i=0; i<align_size; i++) aligns[i] = 0; |
|
2288
|
col = 0; |
|
2289
|
i = head_end+1; |
|
2290
|
|
|
2291
|
/* skip initial white space and optional separator */ |
|
2292
|
while( i<size && (data[i]==' ' || data[i]=='\t') ){ i++; } |
|
2293
|
if( data[i]=='|' ) i++; |
|
2294
|
|
|
2295
|
/* compute default alignment for each column */ |
|
2296
|
while (i < size && data[i] != '\n') { |
|
2297
|
if (data[i] == ':') |
|
2298
|
aligns[col] |= MKD_CELL_ALIGN_LEFT; |
|
2299
|
while (i < size |
|
2300
|
&& data[i] != '|' && data[i] != '\n') |
|
2301
|
i += 1; |
|
2302
|
if (data[i - 1] == ':') |
|
2303
|
aligns[col] |= MKD_CELL_ALIGN_RIGHT; |
|
2304
|
if (i < size && data[i] == '|') |
|
2305
|
i += 1; |
|
2306
|
col += 1; } |
|
2307
|
} |
|
2308
|
|
|
2309
|
/* point i to the beginning of next line/row */ |
|
2310
|
i++; |
|
2311
|
|
|
2312
|
}else{ |
|
2313
|
/* there is no valid ruler, continuing without header */ |
|
2314
|
i = 0; |
|
2315
|
} |
|
2316
|
|
|
2317
|
/* render the table body lines */ |
|
2318
|
while( i<size && is_tableline(data + i, size - i) ){ |
|
2319
|
i += parse_table_row(rows, rndr, data+i, size-i, aligns, align_size, 0); |
|
2320
|
} |
|
2321
|
|
|
2322
|
/* render the full table */ |
|
2323
|
rndr->make.table(ob, head, rows, rndr->make.opaque); |
|
2324
|
|
|
2325
|
/* cleanup */ |
|
2326
|
release_work_buffer(rndr, head); |
|
2327
|
release_work_buffer(rndr, rows); |
|
2328
|
fossil_free(aligns); |
|
2329
|
return i; |
|
2330
|
} |
|
2331
|
|
|
2332
|
|
|
2333
|
/* parse_block -- parsing of one block, returning next char to parse */ |
|
2334
|
static void parse_block( |
|
2335
|
struct Blob *ob, /* output blob */ |
|
2336
|
struct render *rndr, /* renderer internal state */ |
|
2337
|
char *data, /* input text */ |
|
2338
|
size_t size /* input text size */ |
|
2339
|
){ |
|
2340
|
size_t beg, end, i; |
|
2341
|
char *txt_data; |
|
2342
|
int has_table; |
|
2343
|
if( !size ) return; |
|
2344
|
has_table = (rndr->make.table |
|
2345
|
&& rndr->make.table_row |
|
2346
|
&& rndr->make.table_cell |
|
2347
|
&& memchr(data, '|', size)!=0); |
|
2348
|
|
|
2349
|
beg = 0; |
|
2350
|
while( beg<size ){ |
|
2351
|
txt_data = data+beg; |
|
2352
|
end = size-beg; |
|
2353
|
if( data[beg]=='#' ){ |
|
2354
|
beg += parse_atxheader(ob, rndr, txt_data, end); |
|
2355
|
}else if( data[beg]=='<' |
|
2356
|
&& rndr->make.blockhtml |
|
2357
|
&& (i = parse_htmlblock(ob, rndr, txt_data, end))!=0 |
|
2358
|
){ |
|
2359
|
beg += i; |
|
2360
|
}else if( (i=is_empty(txt_data, end))!=0 ){ |
|
2361
|
beg += i; |
|
2362
|
}else if( is_hrule(txt_data, end) ){ |
|
2363
|
if( rndr->make.hrule ) rndr->make.hrule(ob, rndr->make.opaque); |
|
2364
|
while( beg<size && data[beg]!='\n' ){ beg++; } |
|
2365
|
beg++; |
|
2366
|
}else if( prefix_quote(txt_data, end) ){ |
|
2367
|
beg += parse_blockquote(ob, rndr, txt_data, end); |
|
2368
|
}else if( prefix_code(txt_data, end) ){ |
|
2369
|
beg += parse_blockcode(ob, rndr, txt_data, end); |
|
2370
|
}else if( prefix_uli(txt_data, end) ){ |
|
2371
|
beg += parse_list(ob, rndr, txt_data, end, 0); |
|
2372
|
}else if( prefix_oli(txt_data, end) ){ |
|
2373
|
beg += parse_list(ob, rndr, txt_data, end, MKD_LIST_ORDERED); |
|
2374
|
}else if( has_table && is_tableline(txt_data, end) ){ |
|
2375
|
beg += parse_table(ob, rndr, txt_data, end); |
|
2376
|
}else if( prefix_fencedcode(txt_data, end) |
|
2377
|
&& (i = char_codespan(ob, rndr, txt_data, 0, end))!=0 |
|
2378
|
){ |
|
2379
|
beg += i; |
|
2380
|
}else{ |
|
2381
|
beg += parse_paragraph(ob, rndr, txt_data, end); |
|
2382
|
} |
|
2383
|
} |
|
2384
|
} |
|
2385
|
|
|
2386
|
|
|
2387
|
|
|
2388
|
/********************* |
|
2389
|
* REFERENCE PARSING * |
|
2390
|
*********************/ |
|
2391
|
|
|
2392
|
/* is_ref -- returns whether a line is a reference or not */ |
|
2393
|
static int is_ref( |
|
2394
|
const char *data, /* input text */ |
|
2395
|
size_t beg, /* offset of the beginning of the line */ |
|
2396
|
size_t end, /* offset of the end of the text */ |
|
2397
|
size_t *last, /* last character of the link */ |
|
2398
|
struct Blob *refs /* array of link references */ |
|
2399
|
){ |
|
2400
|
size_t i = 0; |
|
2401
|
size_t id_offset, id_end; |
|
2402
|
size_t link_offset, link_end; |
|
2403
|
size_t title_offset, title_end; |
|
2404
|
size_t line_end; |
|
2405
|
struct link_ref lr = { |
|
2406
|
BLOB_INITIALIZER, |
|
2407
|
BLOB_INITIALIZER, |
|
2408
|
BLOB_INITIALIZER |
|
2409
|
}; |
|
2410
|
|
|
2411
|
/* up to 3 optional leading spaces */ |
|
2412
|
if( beg+3>=end ) return 0; |
|
2413
|
if( data[beg]==' ' ){ |
|
2414
|
i = 1; |
|
2415
|
if( data[beg+1]==' ' ){ |
|
2416
|
i = 2; |
|
2417
|
if( data[beg+2]==' ' ){ |
|
2418
|
i = 3; |
|
2419
|
if( data[beg+3]==' ' ) return 0; |
|
2420
|
} |
|
2421
|
} |
|
2422
|
} |
|
2423
|
i += beg; |
|
2424
|
|
|
2425
|
/* id part: anything but a newline between brackets */ |
|
2426
|
if( data[i]!='[' ) return 0; |
|
2427
|
i++; |
|
2428
|
if( i>=end || data[i]=='^' ) return 0; /* see is_footnote() */ |
|
2429
|
|
|
2430
|
id_offset = i; |
|
2431
|
while( i<end && data[i]!='\n' && data[i]!='\r' && data[i]!=']' ){ i++; } |
|
2432
|
if( i>=end || data[i]!=']' ) return 0; |
|
2433
|
id_end = i; |
|
2434
|
|
|
2435
|
/* spacer: colon (space | tab)* newline? (space | tab)* */ |
|
2436
|
i++; |
|
2437
|
if( i>=end || data[i]!=':' ) return 0; |
|
2438
|
i++; |
|
2439
|
while( i<end && (data[i]==' ' || data[i]=='\t') ){ i++; } |
|
2440
|
if( i<end && (data[i]=='\n' || data[i]=='\r') ){ |
|
2441
|
i++; |
|
2442
|
if( i<end && data[i]=='\r' && data[i-1] == '\n' ) i++; |
|
2443
|
} |
|
2444
|
while( i<end && (data[i]==' ' || data[i]=='\t') ){ i++; } |
|
2445
|
if( i>=end ) return 0; |
|
2446
|
|
|
2447
|
/* link: whitespace-free sequence, optionally between angle brackets */ |
|
2448
|
if( data[i]=='<' ) i++; |
|
2449
|
link_offset = i; |
|
2450
|
while( i<end |
|
2451
|
&& data[i]!=' ' |
|
2452
|
&& data[i]!='\t' |
|
2453
|
&& data[i]!='\n' |
|
2454
|
&& data[i]!='\r' |
|
2455
|
){ |
|
2456
|
i += 1; |
|
2457
|
} |
|
2458
|
/* TODO: maybe require both data[i-1]=='>' && data[link_offset-1]=='<' ? */ |
|
2459
|
if( data[i-1]=='>' ) link_end = i-1; else link_end = i; |
|
2460
|
|
|
2461
|
/* optional spacer: (space | tab)* (newline | '\'' | '"' | '(' ) */ |
|
2462
|
while( i<end && (data[i]==' ' || data[i]=='\t') ){ i++; } |
|
2463
|
if( i<end |
|
2464
|
&& data[i]!='\n' |
|
2465
|
&& data[i]!='\r' |
|
2466
|
&& data[i]!='\'' |
|
2467
|
&& data[i]!='"' |
|
2468
|
&& data[i]!='(' |
|
2469
|
){ |
|
2470
|
return 0; |
|
2471
|
} |
|
2472
|
line_end = 0; |
|
2473
|
/* computing end-of-line */ |
|
2474
|
if( i>=end || data[i]=='\r' || data[i]=='\n' ) line_end = i; |
|
2475
|
if( i+1<end && data[i]=='\n' && data[i+1]=='\r' ) line_end = i+1; |
|
2476
|
|
|
2477
|
/* optional (space|tab)* spacer after a newline */ |
|
2478
|
if( line_end ){ |
|
2479
|
i = line_end+1; |
|
2480
|
while( i<end && (data[i]==' ' || data[i]=='\t') ){ i++; } |
|
2481
|
} |
|
2482
|
|
|
2483
|
/* optional title: any non-newline sequence enclosed in '"() |
|
2484
|
alone on its line */ |
|
2485
|
title_offset = title_end = 0; |
|
2486
|
if( i+1<end && (data[i]=='\'' || data[i]=='"' || data[i]=='(') ){ |
|
2487
|
i += 1; |
|
2488
|
title_offset = i; |
|
2489
|
/* looking for EOL */ |
|
2490
|
while( i<end && data[i]!='\n' && data[i]!='\r' ){ i++; } |
|
2491
|
if( i+1<end && data[i]=='\n' && data[i+1]=='\r' ){ |
|
2492
|
title_end = i + 1; |
|
2493
|
}else{ |
|
2494
|
title_end = i; |
|
2495
|
} |
|
2496
|
/* stepping back */ |
|
2497
|
i--; |
|
2498
|
while( i>title_offset && (data[i]==' ' || data[i]=='\t') ){ i--; } |
|
2499
|
if( i>title_offset && (data[i]=='\'' || data[i]=='"' || data[i]==')') ){ |
|
2500
|
line_end = title_end; |
|
2501
|
title_end = i; |
|
2502
|
} |
|
2503
|
} |
|
2504
|
if( !line_end ) return 0; /* garbage after the link */ |
|
2505
|
|
|
2506
|
/* a valid ref has been found, filling-in return structures */ |
|
2507
|
if( last ) *last = line_end; |
|
2508
|
if( !refs ) return 1; |
|
2509
|
if( build_ref_id(&lr.id, data+id_offset, id_end-id_offset)<0 ) return 0; |
|
2510
|
blob_append(&lr.link, data+link_offset, link_end-link_offset); |
|
2511
|
if( title_end>title_offset ){ |
|
2512
|
blob_append(&lr.title, data+title_offset, title_end-title_offset); |
|
2513
|
} |
|
2514
|
blob_append(refs, (char *)&lr, sizeof lr); |
|
2515
|
return 1; |
|
2516
|
} |
|
2517
|
|
|
2518
|
/********************* |
|
2519
|
* FOOTNOTE PARSING * |
|
2520
|
*********************/ |
|
2521
|
|
|
2522
|
/* is_footnote -- check if data holds a definition of a labeled footnote. |
|
2523
|
* If so then append the corresponding element to `footnotes` array */ |
|
2524
|
static int is_footnote( |
|
2525
|
const char *data, /* input text */ |
|
2526
|
size_t beg, /* offset of the beginning of the line */ |
|
2527
|
size_t end, /* offset of the end of the text */ |
|
2528
|
size_t *last, /* last character of the link */ |
|
2529
|
struct Blob * footnotes |
|
2530
|
){ |
|
2531
|
size_t i, id_offset, id_end, upc_offset, upc_size; |
|
2532
|
struct footnote fn = FOOTNOTE_INITIALIZER; |
|
2533
|
|
|
2534
|
/* failfast if data is too short */ |
|
2535
|
if( beg+5>=end ) return 0; |
|
2536
|
i = beg; |
|
2537
|
|
|
2538
|
/* footnote definition must start at the beginning of a line */ |
|
2539
|
if( data[i]!='[' ) return 0; |
|
2540
|
i++; |
|
2541
|
if( data[i]!='^' ) return 0; |
|
2542
|
id_offset = ++i; |
|
2543
|
|
|
2544
|
/* id part: anything but a newline between brackets */ |
|
2545
|
while( i<end && data[i]!=']' && data[i]!='\n' && data[i]!='\r' ){ i++; } |
|
2546
|
if( i>=end || data[i]!=']' ) return 0; |
|
2547
|
id_end = i++; |
|
2548
|
|
|
2549
|
/* spacer: colon (space | tab)* */ |
|
2550
|
if( i>=end || data[i]!=':' ) return 0; |
|
2551
|
i++; |
|
2552
|
while( i<end && (data[i]==' ' || data[i]=='\t') ){ i++; } |
|
2553
|
|
|
2554
|
/* passthrough truncated footnote definition */ |
|
2555
|
if( i>=end ) return 0; |
|
2556
|
|
|
2557
|
if( build_ref_id(&fn.id, data+id_offset, id_end-id_offset)<0 ) return 0; |
|
2558
|
|
|
2559
|
/* footnote's text may start on the same line after [^id]: */ |
|
2560
|
upc_offset = upc_size = 0; |
|
2561
|
if( data[i]!='\n' && data[i]!='\r' ){ |
|
2562
|
size_t j; |
|
2563
|
upc_size = is_footnote_classlist(data+i, end-i, 1); |
|
2564
|
upc_offset = i; /* prevent further checks for a classlist */ |
|
2565
|
i += upc_size; |
|
2566
|
j = i; |
|
2567
|
while( i<end && data[i]!='\n' && data[i]!='\r' ){ i++; }; |
|
2568
|
if( i!=j )blob_append(&fn.text, data+j, i-j); |
|
2569
|
if( i<end ){ |
|
2570
|
blob_append_char(&fn.text, data[i]); |
|
2571
|
i++; |
|
2572
|
if( i<end && data[i]=='\n' && data[i-1]=='\r' ){ |
|
2573
|
blob_append_char(&fn.text, data[i]); |
|
2574
|
i++; |
|
2575
|
} |
|
2576
|
} |
|
2577
|
}else{ |
|
2578
|
i++; |
|
2579
|
if( i<end && data[i]=='\n' && data[i-1]=='\r' ) i++; |
|
2580
|
} |
|
2581
|
if( i<end ){ |
|
2582
|
|
|
2583
|
/* compute the indentation from the 2nd line */ |
|
2584
|
size_t indent = i; |
|
2585
|
const char *spaces = data+i; |
|
2586
|
while( indent<end && data[indent]==' ' ){ indent++; } |
|
2587
|
if( indent>=end ) goto footnote_finish; |
|
2588
|
indent -= i; |
|
2589
|
if( indent<2 ) goto footnote_finish; |
|
2590
|
|
|
2591
|
/* process the 2nd and subsequent lines */ |
|
2592
|
while( i+indent<end && memcmp(data+i,spaces,indent)==0 ){ |
|
2593
|
size_t j; |
|
2594
|
i += indent; |
|
2595
|
if( !upc_offset ){ |
|
2596
|
/* a classlist must be provided no later than at the 2nd line */ |
|
2597
|
upc_offset = i + sizeof_blank_prefix(data+i, end-i, 1); |
|
2598
|
upc_size = is_footnote_classlist(data+upc_offset, |
|
2599
|
end-upc_offset, 1); |
|
2600
|
if( upc_size ){ |
|
2601
|
i = upc_offset + upc_size; |
|
2602
|
} |
|
2603
|
} |
|
2604
|
j = i; |
|
2605
|
while( i<end && data[i]!='\n' && data[i]!='\r' ){ i++; } |
|
2606
|
if( i!=j ) blob_append(&fn.text, data+j, i-j); |
|
2607
|
if( i>=end ) break; |
|
2608
|
blob_append_char(&fn.text, data[i]); |
|
2609
|
i++; |
|
2610
|
if( i<end && data[i]=='\n' && data[i-1]=='\r' ){ |
|
2611
|
blob_append_char(&fn.text, data[i]); |
|
2612
|
i++; |
|
2613
|
} |
|
2614
|
} |
|
2615
|
} |
|
2616
|
footnote_finish: |
|
2617
|
if( !blob_size(&fn.text) ){ |
|
2618
|
blob_reset(&fn.id); |
|
2619
|
return 0; |
|
2620
|
} |
|
2621
|
if( !blob_trim(&fn.text) ){ /* if the content is all-blank */ |
|
2622
|
if( upc_size ){ /* interpret UPC as plain text */ |
|
2623
|
blob_append(&fn.text, data+upc_offset, upc_size); |
|
2624
|
upc_size = 0; |
|
2625
|
}else{ |
|
2626
|
blob_reset(&fn.id); /* or clean up and fail */ |
|
2627
|
blob_reset(&fn.text); |
|
2628
|
return 0; |
|
2629
|
} |
|
2630
|
} |
|
2631
|
/* a valid note has been found */ |
|
2632
|
if( last ) *last = i; |
|
2633
|
if( footnotes ){ |
|
2634
|
fn.defno = COUNT_FOOTNOTES( footnotes ); |
|
2635
|
if( upc_size ){ |
|
2636
|
assert( upc_offset && upc_offset+upc_size<end ); |
|
2637
|
blob_append(&fn.upc, data+upc_offset, upc_size); |
|
2638
|
} |
|
2639
|
blob_append(footnotes, (char *)&fn, sizeof fn); |
|
2640
|
} |
|
2641
|
return 1; |
|
2642
|
} |
|
2643
|
|
|
2644
|
/********************** |
|
2645
|
* EXPORTED FUNCTIONS * |
|
2646
|
**********************/ |
|
2647
|
|
|
2648
|
/* markdown -- parses the input buffer and renders it into the output buffer */ |
|
2649
|
void markdown( |
|
2650
|
struct Blob *ob, /* output blob for rendered text */ |
|
2651
|
const struct Blob *ib, /* input blob in markdown */ |
|
2652
|
const struct mkd_renderer *rndrer /* renderer descriptor (callbacks) */ |
|
2653
|
){ |
|
2654
|
struct link_ref *lr; |
|
2655
|
struct footnote *fn; |
|
2656
|
int i; |
|
2657
|
size_t beg, end = 0; |
|
2658
|
struct render rndr; |
|
2659
|
size_t size; |
|
2660
|
Blob text = BLOB_INITIALIZER; /* input after the first pass */ |
|
2661
|
Blob * const allNotes = &rndr.notes.all; |
|
2662
|
|
|
2663
|
/* filling the render structure */ |
|
2664
|
if( !rndrer ) return; |
|
2665
|
rndr.make = *rndrer; |
|
2666
|
rndr.nBlobCache = 0; |
|
2667
|
rndr.iDepth = 0; |
|
2668
|
rndr.refs = empty_blob; |
|
2669
|
rndr.notes.all = empty_blob; |
|
2670
|
rndr.notes.nMarks = 0; |
|
2671
|
rndr.notes.misref.id = empty_blob; |
|
2672
|
rndr.notes.misref.text = empty_blob; |
|
2673
|
rndr.notes.misref.upc = empty_blob; |
|
2674
|
rndr.notes.misref.bRndred = 0; |
|
2675
|
rndr.notes.misref.nUsed = 0; |
|
2676
|
rndr.notes.misref.iMark = -1; |
|
2677
|
|
|
2678
|
for(i=0; i<256; i++) rndr.active_char[i] = 0; |
|
2679
|
if( (rndr.make.emphasis |
|
2680
|
|| rndr.make.double_emphasis |
|
2681
|
|| rndr.make.triple_emphasis) |
|
2682
|
&& rndr.make.emph_chars |
|
2683
|
){ |
|
2684
|
for(i=0; rndr.make.emph_chars[i]; i++){ |
|
2685
|
rndr.active_char[(unsigned char)rndr.make.emph_chars[i]] = char_emphasis; |
|
2686
|
} |
|
2687
|
} |
|
2688
|
if( rndr.make.codespan ) rndr.active_char['`'] = char_codespan; |
|
2689
|
if( rndr.make.linebreak ) rndr.active_char['\n'] = char_linebreak; |
|
2690
|
if( rndr.make.image || rndr.make.link ) rndr.active_char['['] = char_link; |
|
2691
|
if( rndr.make.footnote_ref ) rndr.active_char['('] = char_footnote; |
|
2692
|
rndr.active_char['<'] = char_langle_tag; |
|
2693
|
rndr.active_char['\\'] = char_escape; |
|
2694
|
rndr.active_char['&'] = char_entity; |
|
2695
|
|
|
2696
|
/* first pass: iterate over lines looking for references, |
|
2697
|
* copying everything else into "text" */ |
|
2698
|
beg = 0; |
|
2699
|
for(size = blob_size(ib); beg<size ;){ |
|
2700
|
const char* const data = blob_buffer(ib); |
|
2701
|
if( is_ref(data, beg, size, &end, &rndr.refs) ){ |
|
2702
|
beg = end; |
|
2703
|
}else if(is_footnote(data, beg, size, &end, &rndr.notes.all)){ |
|
2704
|
beg = end; |
|
2705
|
}else{ /* skipping to the next line */ |
|
2706
|
end = beg; |
|
2707
|
while( end<size && data[end]!='\n' && data[end]!='\r' ){ |
|
2708
|
end += 1; |
|
2709
|
} |
|
2710
|
/* adding the line body if present */ |
|
2711
|
if( end>beg ) blob_append(&text, data + beg, end - beg); |
|
2712
|
while( end<size && (data[end]=='\n' || data[end]=='\r') ){ |
|
2713
|
/* add one \n per newline */ |
|
2714
|
if( data[end]=='\n' || (end+1<size && data[end+1]!='\n') ){ |
|
2715
|
blob_append_char(&text, '\n'); |
|
2716
|
} |
|
2717
|
end += 1; |
|
2718
|
} |
|
2719
|
beg = end; |
|
2720
|
} |
|
2721
|
} |
|
2722
|
|
|
2723
|
/* sorting the reference array */ |
|
2724
|
if( blob_size(&rndr.refs) ){ |
|
2725
|
qsort(blob_buffer(&rndr.refs), |
|
2726
|
blob_size(&rndr.refs)/sizeof(struct link_ref), |
|
2727
|
sizeof(struct link_ref), |
|
2728
|
cmp_link_ref_sort); |
|
2729
|
} |
|
2730
|
rndr.notes.nLbled = COUNT_FOOTNOTES( allNotes ); |
|
2731
|
|
|
2732
|
/* sort footnotes by ID and join duplicates */ |
|
2733
|
if( rndr.notes.nLbled > 1 ){ |
|
2734
|
int nDups = 0; |
|
2735
|
fn = CAST_AS_FOOTNOTES( allNotes ); |
|
2736
|
qsort(fn, rndr.notes.nLbled, sizeof(struct footnote), cmp_footnote_id); |
|
2737
|
|
|
2738
|
/* concatenate footnotes with equal labels */ |
|
2739
|
for(i=0; i<rndr.notes.nLbled ;){ |
|
2740
|
struct footnote *x = fn + i; |
|
2741
|
int j = i+1; |
|
2742
|
size_t k = blob_size(&x->text) + 64 + blob_size(&x->upc); |
|
2743
|
while(j<rndr.notes.nLbled && !blob_compare(&x->id, &fn[j].id)){ |
|
2744
|
k += blob_size(&fn[j].text) + 10 + blob_size(&fn[j].upc); |
|
2745
|
j++; |
|
2746
|
nDups++; |
|
2747
|
} |
|
2748
|
if( i+1<j ){ |
|
2749
|
Blob list = empty_blob; |
|
2750
|
blob_reserve(&list, k); |
|
2751
|
/* must match _joined_footnote_indicator in html_footnote_item() */ |
|
2752
|
blob_append_literal(&list, "<ul class='fn-joined'>\n"); |
|
2753
|
for(k=i; (int)k<j; k++){ |
|
2754
|
struct footnote *y = fn + k; |
|
2755
|
blob_append_literal(&list, "<li>"); |
|
2756
|
if( blob_size(&y->upc) ){ |
|
2757
|
blob_appendb(&list, &y->upc); |
|
2758
|
blob_reset(&y->upc); |
|
2759
|
} |
|
2760
|
blob_appendb(&list, &y->text); |
|
2761
|
blob_append_literal(&list, "</li>\n"); |
|
2762
|
|
|
2763
|
/* free memory buffer */ |
|
2764
|
blob_reset(&y->text); |
|
2765
|
if( (int)k!=i ) blob_reset(&y->id); |
|
2766
|
} |
|
2767
|
blob_append_literal(&list, "</ul>\n"); |
|
2768
|
x->text = list; |
|
2769
|
g.ftntsIssues[2]++; |
|
2770
|
} |
|
2771
|
i = j; |
|
2772
|
} |
|
2773
|
if( nDups ){ /* clean rndr.notes.all from invalidated footnotes */ |
|
2774
|
const int n = rndr.notes.nLbled - nDups; |
|
2775
|
struct Blob filtered = empty_blob; |
|
2776
|
blob_reserve(&filtered, n*sizeof(struct footnote)); |
|
2777
|
for(i=0; i<rndr.notes.nLbled; i++){ |
|
2778
|
if( blob_size(&fn[i].id) ){ |
|
2779
|
blob_append(&filtered, (char*)(fn+i), sizeof(struct footnote)); |
|
2780
|
} |
|
2781
|
} |
|
2782
|
blob_reset( allNotes ); |
|
2783
|
rndr.notes.all = filtered; |
|
2784
|
rndr.notes.nLbled = n; |
|
2785
|
assert( (int)(COUNT_FOOTNOTES(allNotes)) == rndr.notes.nLbled ); |
|
2786
|
} |
|
2787
|
} |
|
2788
|
fn = CAST_AS_FOOTNOTES( allNotes ); |
|
2789
|
for(i=0; i<rndr.notes.nLbled; i++){ |
|
2790
|
fn[i].index = i; |
|
2791
|
} |
|
2792
|
assert( rndr.notes.nMarks==0 ); |
|
2793
|
|
|
2794
|
/* second pass: actual rendering */ |
|
2795
|
if( rndr.make.prolog ) rndr.make.prolog(ob, rndr.make.opaque); |
|
2796
|
parse_block(ob, &rndr, blob_buffer(&text), blob_size(&text)); |
|
2797
|
|
|
2798
|
if( blob_size(allNotes) || rndr.notes.misref.nUsed ){ |
|
2799
|
|
|
2800
|
/* Footnotes must be parsed for the correct discovery of (back)links */ |
|
2801
|
Blob *notes = new_work_buffer( &rndr ); |
|
2802
|
if( blob_size(allNotes) ){ |
|
2803
|
Blob *tmp = new_work_buffer( &rndr ); |
|
2804
|
int nMarks = -1, maxDepth = 5; |
|
2805
|
|
|
2806
|
/* inline notes may get appended to rndr.notes.all while rendering */ |
|
2807
|
while(1){ |
|
2808
|
struct footnote *aNotes; |
|
2809
|
const int N = COUNT_FOOTNOTES( allNotes ); |
|
2810
|
|
|
2811
|
/* make a shallow copy of `allNotes` */ |
|
2812
|
blob_truncate(notes,0); |
|
2813
|
blob_appendb(notes, allNotes); |
|
2814
|
aNotes = CAST_AS_FOOTNOTES(notes); |
|
2815
|
qsort(aNotes, N, sizeof(struct footnote), cmp_footnote_sort); |
|
2816
|
|
|
2817
|
if( --maxDepth < 0 || nMarks == rndr.notes.nMarks ) break; |
|
2818
|
nMarks = rndr.notes.nMarks; |
|
2819
|
|
|
2820
|
for(i=0; i<N; i++){ |
|
2821
|
const int j = aNotes[i].index; |
|
2822
|
struct footnote *x = CAST_AS_FOOTNOTES(allNotes) + j; |
|
2823
|
assert( 0<=j && j<N ); |
|
2824
|
if( x->bRndred || !x->nUsed ) continue; |
|
2825
|
assert( x->iMark > 0 ); |
|
2826
|
assert( blob_size(&x->text) ); |
|
2827
|
blob_truncate(tmp,0); |
|
2828
|
|
|
2829
|
/* `allNotes` may be altered and extended through this call */ |
|
2830
|
parse_inline(tmp, &rndr, blob_buffer(&x->text), blob_size(&x->text)); |
|
2831
|
|
|
2832
|
x = CAST_AS_FOOTNOTES(allNotes) + j; |
|
2833
|
blob_truncate(&x->text,0); |
|
2834
|
blob_appendb(&x->text, tmp); |
|
2835
|
x->bRndred = 1; |
|
2836
|
} |
|
2837
|
} |
|
2838
|
release_work_buffer(&rndr,tmp); |
|
2839
|
} |
|
2840
|
|
|
2841
|
/* footnotes rendering */ |
|
2842
|
if( rndr.make.footnote_item && rndr.make.footnotes ){ |
|
2843
|
Blob *all_items = new_work_buffer(&rndr); |
|
2844
|
int j = -1; |
|
2845
|
|
|
2846
|
/* Assert that the in-memory layout of id, text and upc within |
|
2847
|
** footnote struct matches the expectations of html_footnote_item() |
|
2848
|
** If it doesn't then a compiler has done something very weird. |
|
2849
|
*/ |
|
2850
|
assert( &(rndr.notes.misref.id) == &(rndr.notes.misref.text) - 1 ); |
|
2851
|
assert( &(rndr.notes.misref.upc) == &(rndr.notes.misref.text) + 1 ); |
|
2852
|
|
|
2853
|
for(i=0; i<(int)(COUNT_FOOTNOTES(notes)); i++){ |
|
2854
|
const struct footnote* x = CAST_AS_FOOTNOTES(notes) + i; |
|
2855
|
const int xUsed = x->bRndred ? x->nUsed : 0; |
|
2856
|
if( !x->iMark ) break; |
|
2857
|
assert( x->nUsed ); |
|
2858
|
rndr.make.footnote_item(all_items, &x->text, x->iMark, |
|
2859
|
xUsed, rndr.make.opaque); |
|
2860
|
if( !xUsed ) g.ftntsIssues[3]++; /* an overnested footnote */ |
|
2861
|
j = i; |
|
2862
|
} |
|
2863
|
if( rndr.notes.misref.nUsed ){ |
|
2864
|
rndr.make.footnote_item(all_items, 0, -1, |
|
2865
|
rndr.notes.misref.nUsed, rndr.make.opaque); |
|
2866
|
g.ftntsIssues[0] += rndr.notes.misref.nUsed; |
|
2867
|
} |
|
2868
|
while( ++j < (int)(COUNT_FOOTNOTES(notes)) ){ |
|
2869
|
const struct footnote* x = CAST_AS_FOOTNOTES(notes) + j; |
|
2870
|
assert( !x->iMark ); |
|
2871
|
assert( !x->nUsed ); |
|
2872
|
assert( !x->bRndred ); |
|
2873
|
rndr.make.footnote_item(all_items,&x->text,0,0,rndr.make.opaque); |
|
2874
|
g.ftntsIssues[1]++; |
|
2875
|
} |
|
2876
|
rndr.make.footnotes(ob, all_items, rndr.make.opaque); |
|
2877
|
release_work_buffer(&rndr, all_items); |
|
2878
|
} |
|
2879
|
release_work_buffer(&rndr, notes); |
|
2880
|
} |
|
2881
|
if( rndr.make.epilog ) rndr.make.epilog(ob, rndr.make.opaque); |
|
2882
|
|
|
2883
|
/* clean-up */ |
|
2884
|
assert( rndr.iDepth==0 ); |
|
2885
|
blob_reset(&text); |
|
2886
|
lr = (struct link_ref *)blob_buffer(&rndr.refs); |
|
2887
|
end = blob_size(&rndr.refs)/sizeof(struct link_ref); |
|
2888
|
for(i=0; i<(int)end; i++){ |
|
2889
|
blob_reset(&lr[i].id); |
|
2890
|
blob_reset(&lr[i].link); |
|
2891
|
blob_reset(&lr[i].title); |
|
2892
|
} |
|
2893
|
blob_reset(&rndr.refs); |
|
2894
|
fn = CAST_AS_FOOTNOTES( allNotes ); |
|
2895
|
end = COUNT_FOOTNOTES( allNotes ); |
|
2896
|
for(i=0; i<(int)end; i++){ |
|
2897
|
if(blob_size(&fn[i].id)) blob_reset(&fn[i].id); |
|
2898
|
if(blob_size(&fn[i].upc)) blob_reset(&fn[i].upc); |
|
2899
|
blob_reset(&fn[i].text); |
|
2900
|
} |
|
2901
|
blob_reset(&rndr.notes.all); |
|
2902
|
for(i=0; i<rndr.nBlobCache; i++){ |
|
2903
|
fossil_free(rndr.aBlobCache[i]); |
|
2904
|
} |
|
2905
|
} |
|
2906
|
|