Fossil SCM
Initial support for [forum:/forumthread/d752446a4f63f390|footnotes in Markdown]. <br>This is WIP: support of multiline notes and code clean-up are pending.
Commit
ebce0f357e0732cacdcc4105623c94385d1691a0c9d6cfe7ef979ae71b96650f
Parent
605064e656ca56c…
2 files changed
+6
+152
-7
+6
| --- src/default.css | ||
| +++ src/default.css | ||
| @@ -1664,10 +1664,16 @@ | ||
| 1664 | 1664 | display: inline; |
| 1665 | 1665 | } |
| 1666 | 1666 | |
| 1667 | 1667 | .monospace { |
| 1668 | 1668 | font-family: monospace; |
| 1669 | +} | |
| 1670 | +div.content div.markdown > ol.footnotes { | |
| 1671 | + font-size: 90%; | |
| 1672 | +} | |
| 1673 | +div.content div.markdown > ol.footnotes > li { | |
| 1674 | + margin-bottom: 0.5em; | |
| 1669 | 1675 | } |
| 1670 | 1676 | |
| 1671 | 1677 | /* Objects in the "desktoponly" class are invisible on mobile */ |
| 1672 | 1678 | @media screen and (max-width: 600px) { |
| 1673 | 1679 | .desktoponly { |
| 1674 | 1680 |
| --- src/default.css | |
| +++ src/default.css | |
| @@ -1664,10 +1664,16 @@ | |
| 1664 | display: inline; |
| 1665 | } |
| 1666 | |
| 1667 | .monospace { |
| 1668 | font-family: monospace; |
| 1669 | } |
| 1670 | |
| 1671 | /* Objects in the "desktoponly" class are invisible on mobile */ |
| 1672 | @media screen and (max-width: 600px) { |
| 1673 | .desktoponly { |
| 1674 |
| --- src/default.css | |
| +++ src/default.css | |
| @@ -1664,10 +1664,16 @@ | |
| 1664 | display: inline; |
| 1665 | } |
| 1666 | |
| 1667 | .monospace { |
| 1668 | font-family: monospace; |
| 1669 | } |
| 1670 | div.content div.markdown > ol.footnotes { |
| 1671 | font-size: 90%; |
| 1672 | } |
| 1673 | div.content div.markdown > ol.footnotes > li { |
| 1674 | margin-bottom: 0.5em; |
| 1675 | } |
| 1676 | |
| 1677 | /* Objects in the "desktoponly" class are invisible on mobile */ |
| 1678 | @media screen and (max-width: 600px) { |
| 1679 | .desktoponly { |
| 1680 |
+152
-7
| --- src/markdown.c | ||
| +++ src/markdown.c | ||
| @@ -129,14 +129,20 @@ | ||
| 129 | 129 | * LOCAL TYPES * |
| 130 | 130 | ***************/ |
| 131 | 131 | |
| 132 | 132 | /* link_ref -- reference to a link */ |
| 133 | 133 | struct link_ref { |
| 134 | - struct Blob id; | |
| 134 | + struct Blob id; /* must be the first field as in footnote struct */ | |
| 135 | 135 | struct Blob link; |
| 136 | 136 | struct Blob title; |
| 137 | 137 | }; |
| 138 | + | |
| 139 | +struct footnote { | |
| 140 | + struct Blob id; /* must be the first field as in link_ref struct */ | |
| 141 | + struct Blob text; /* footnote's content that is rendered at the end */ | |
| 142 | + int index; /* serial number, in the order of appearance */ | |
| 143 | +}; | |
| 138 | 144 | |
| 139 | 145 | |
| 140 | 146 | /* char_trigger -- function pointer to render active chars */ |
| 141 | 147 | /* returns the number of chars taken care of */ |
| 142 | 148 | /* data is the pointer of the beginning of the span */ |
| @@ -152,16 +158,17 @@ | ||
| 152 | 158 | |
| 153 | 159 | /* render -- structure containing one particular render */ |
| 154 | 160 | struct render { |
| 155 | 161 | struct mkd_renderer make; |
| 156 | 162 | struct Blob refs; |
| 163 | + struct Blob footnotes; | |
| 157 | 164 | char_trigger active_char[256]; |
| 158 | 165 | int iDepth; /* Depth of recursion */ |
| 159 | 166 | int nBlobCache; /* Number of entries in aBlobCache */ |
| 160 | 167 | struct Blob *aBlobCache[20]; /* Cache of Blobs available for reuse */ |
| 168 | + int nLabels; /* Footnotes counter for the second pass */ | |
| 161 | 169 | }; |
| 162 | - | |
| 163 | 170 | |
| 164 | 171 | /* html_tag -- structure for quick HTML tag search (inspired from discount) */ |
| 165 | 172 | struct html_tag { |
| 166 | 173 | const char *text; |
| 167 | 174 | int size; |
| @@ -247,10 +254,17 @@ | ||
| 247 | 254 | struct link_ref *lra = (void *)a; |
| 248 | 255 | struct link_ref *lrb = (void *)b; |
| 249 | 256 | return blob_compare(&lra->id, &lrb->id); |
| 250 | 257 | } |
| 251 | 258 | |
| 259 | +/* cmp_footnote_sort -- comparison function for footnotes qsort */ | |
| 260 | +static int cmp_footnote_sort(const void *a, const void *b){ | |
| 261 | + const struct footnote *fna = (void *)a, *fnb = (void *)b; | |
| 262 | + assert( fna->index > 0 && fnb->index > 0 ); | |
| 263 | + if( fna->index == fnb->index ) return 0; | |
| 264 | + return ( fna->index < fnb->index ? -1 : 1 ); | |
| 265 | +} | |
| 252 | 266 | |
| 253 | 267 | /* cmp_html_tag -- comparison function for bsearch() (stolen from discount) */ |
| 254 | 268 | static int cmp_html_tag(const void *a, const void *b){ |
| 255 | 269 | const struct html_tag *hta = a; |
| 256 | 270 | const struct html_tag *htb = b; |
| @@ -1003,10 +1017,39 @@ | ||
| 1003 | 1017 | blob_append(link, blob_buffer(&lr->link), blob_size(&lr->link)); |
| 1004 | 1018 | blob_append(title, blob_buffer(&lr->title), blob_size(&lr->title)); |
| 1005 | 1019 | return 0; |
| 1006 | 1020 | } |
| 1007 | 1021 | |
| 1022 | +/* get_footnote -- resolve footnote by its id | |
| 1023 | + * on success: fill text and return positive footnote's index | |
| 1024 | + * on failure: return -1 */ | |
| 1025 | +static int get_footnote( | |
| 1026 | + struct render *rndr, | |
| 1027 | + struct Blob *text, | |
| 1028 | + const char *data, | |
| 1029 | + size_t size | |
| 1030 | +){ | |
| 1031 | + struct footnote *fn; | |
| 1032 | + | |
| 1033 | + blob_reset(text); /* use text for temporary storage */ | |
| 1034 | + if( build_ref_id(text, data, size)<0 ) return -1; | |
| 1035 | + | |
| 1036 | + fn = bsearch(text, | |
| 1037 | + blob_buffer(&rndr->footnotes), | |
| 1038 | + blob_size(&rndr->footnotes)/sizeof(struct footnote), | |
| 1039 | + sizeof (struct footnote), | |
| 1040 | + cmp_link_ref); | |
| 1041 | + if( !fn ) return -1; | |
| 1042 | + | |
| 1043 | + if( fn->index == 0 ){ /* the first reference to the footnote */ | |
| 1044 | + fn->index = ++(rndr->nLabels); | |
| 1045 | + } | |
| 1046 | + assert( fn->index > 0 ); | |
| 1047 | + blob_reset(text); | |
| 1048 | + blob_append(text, blob_buffer(&fn->text), blob_size(&fn->text)); | |
| 1049 | + return fn->index; | |
| 1050 | +} | |
| 1008 | 1051 | |
| 1009 | 1052 | /* char_link -- '[': parsing a link or an image */ |
| 1010 | 1053 | static size_t char_link( |
| 1011 | 1054 | struct Blob *ob, |
| 1012 | 1055 | struct render *rndr, |
| @@ -1017,10 +1060,11 @@ | ||
| 1017 | 1060 | int is_img = (offset && data[-1] == '!'), level; |
| 1018 | 1061 | size_t i = 1, txt_e; |
| 1019 | 1062 | struct Blob *content = 0; |
| 1020 | 1063 | struct Blob *link = 0; |
| 1021 | 1064 | struct Blob *title = 0; |
| 1065 | + const int is_note = (size && data[1] == '^'); | |
| 1022 | 1066 | int ret; |
| 1023 | 1067 | |
| 1024 | 1068 | /* checking whether the correct renderer exists */ |
| 1025 | 1069 | if( (is_img && !rndr->make.image) || (!is_img && !rndr->make.link) ){ |
| 1026 | 1070 | return 0; |
| @@ -1083,20 +1127,26 @@ | ||
| 1083 | 1127 | }else{ |
| 1084 | 1128 | /* explicit id - between brackets */ |
| 1085 | 1129 | id_data = data+i+1; |
| 1086 | 1130 | id_size = id_end-(i+1); |
| 1087 | 1131 | } |
| 1088 | - | |
| 1089 | 1132 | if( get_link_ref(rndr, link, title, id_data, id_size)<0 ){ |
| 1090 | 1133 | goto char_link_cleanup; |
| 1091 | 1134 | } |
| 1092 | - | |
| 1093 | 1135 | i = id_end+1; |
| 1094 | 1136 | |
| 1095 | 1137 | /* shortcut reference style link */ |
| 1096 | 1138 | }else{ |
| 1097 | - if( get_link_ref(rndr, link, title, data+1, txt_e-1)<0 ){ | |
| 1139 | + if( is_note ){ | |
| 1140 | + const int lbl = get_footnote(rndr, link, data+1, txt_e-1); | |
| 1141 | + if( lbl <= 0 ){ | |
| 1142 | + goto char_link_cleanup; | |
| 1143 | + } | |
| 1144 | + blob_reset(link); | |
| 1145 | + blob_appendf(link, "#footnote-%i", lbl); | |
| 1146 | + blob_appendf(content,"<sup class='footnote'>%i</sup>",lbl); | |
| 1147 | + }else if( get_link_ref(rndr, link, title, data+1, txt_e-1)<0 ){ | |
| 1098 | 1148 | goto char_link_cleanup; |
| 1099 | 1149 | } |
| 1100 | 1150 | |
| 1101 | 1151 | /* rewinding the whitespace */ |
| 1102 | 1152 | i = txt_e+1; |
| @@ -1103,11 +1153,11 @@ | ||
| 1103 | 1153 | } |
| 1104 | 1154 | |
| 1105 | 1155 | /* building content: img alt is escaped, link content is parsed */ |
| 1106 | 1156 | if( txt_e>1 ){ |
| 1107 | 1157 | if( is_img ) blob_append(content, data+1, txt_e-1); |
| 1108 | - else parse_inline(content, rndr, data+1, txt_e-1); | |
| 1158 | + else if(!is_note) parse_inline(content, rndr, data+1, txt_e-1); | |
| 1109 | 1159 | } |
| 1110 | 1160 | |
| 1111 | 1161 | /* calling the relevant rendering function */ |
| 1112 | 1162 | if( is_img ){ |
| 1113 | 1163 | if( blob_size(ob)>0 && blob_buffer(ob)[blob_size(ob)-1]=='!' ) ob->nUsed--; |
| @@ -2095,11 +2145,11 @@ | ||
| 2095 | 2145 | } |
| 2096 | 2146 | } |
| 2097 | 2147 | i += beg; |
| 2098 | 2148 | |
| 2099 | 2149 | /* id part: anything but a newline between brackets */ |
| 2100 | - if( data[i]!='[' ) return 0; | |
| 2150 | + if( data[i]!='[' || data[i+1]=='^' ) return 0; | |
| 2101 | 2151 | i++; |
| 2102 | 2152 | id_offset = i; |
| 2103 | 2153 | while( i<end && data[i]!='\n' && data[i]!='\r' && data[i]!=']' ){ i++; } |
| 2104 | 2154 | if( i>=end || data[i]!=']' ) return 0; |
| 2105 | 2155 | id_end = i; |
| @@ -2184,11 +2234,72 @@ | ||
| 2184 | 2234 | } |
| 2185 | 2235 | blob_append(refs, (char *)&lr, sizeof lr); |
| 2186 | 2236 | return 1; |
| 2187 | 2237 | } |
| 2188 | 2238 | |
| 2239 | +/********************* | |
| 2240 | + * FOOTNOTE PARSING * | |
| 2241 | + *********************/ | |
| 2242 | + | |
| 2243 | +/* is_footnote -- returns whether a line is a footnote or not */ | |
| 2244 | +static int is_footnote( | |
| 2245 | + char *data, /* input text */ | |
| 2246 | + size_t beg, /* offset of the beginning of the line */ | |
| 2247 | + size_t end, /* offset of the end of the text */ | |
| 2248 | + size_t *last, /* last character of the link */ | |
| 2249 | + struct Blob * footnotes /* FIXME: struct render *rndr */ | |
| 2250 | +){ | |
| 2251 | + size_t i = 0; | |
| 2252 | + size_t id_offset, id_end; | |
| 2253 | + size_t note_offset, note_end; | |
| 2254 | + size_t line_end; | |
| 2255 | + struct footnote fn = { empty_blob, empty_blob, 0 }; | |
| 2256 | + | |
| 2257 | + /* footnote definition must start at the begining of a line */ | |
| 2258 | + if( beg+4>=end ) return 0; | |
| 2259 | + i += beg; | |
| 2260 | + | |
| 2261 | + /* id part: anything but a newline between brackets */ | |
| 2262 | + if( data[i]!='[' || data[i+1]!='^' ) return 0; | |
| 2263 | + i++; | |
| 2264 | + id_offset = i; | |
| 2265 | + while( i<end && data[i]!=']' && data[i]!='\n' && data[i]!='\r' ){ i++; } | |
| 2266 | + if( i>=end || data[i]!=']' ) return 0; | |
| 2267 | + id_end = i; | |
| 2268 | + | |
| 2269 | + /* spacer: colon (space | tab)* newline? (space | tab)* */ | |
| 2270 | + i++; | |
| 2271 | + if( i>=end || data[i]!=':' ) return 0; | |
| 2272 | + i++; | |
| 2273 | + while( i<end && (data[i]==' ' || data[i]=='\t') ){ i++; } | |
| 2274 | + if( i<end && (data[i]=='\n' || data[i]=='\r') ){ | |
| 2275 | + i++; | |
| 2276 | + if( i<end && data[i]=='\r' && data[i-1] == '\n' ) i++; | |
| 2277 | + } | |
| 2278 | + while( i<end && (data[i]==' ' || data[i]=='\t') ){ i++; } | |
| 2279 | + if( i>=end ) return 0; | |
| 2280 | + | |
| 2281 | + /* note is a single line of text (FIXME: support multiline notes) */ | |
| 2282 | + note_offset = i; | |
| 2283 | + while( i<end && data[i]!='\r' && data[i]!='\n' ){ i++; } | |
| 2284 | + note_end = i; | |
| 2285 | + | |
| 2286 | + /* computing end-of-line */ | |
| 2287 | + line_end = 0; | |
| 2288 | + if( i >=end || data[i]=='\r' || data[ i ]=='\n' ) line_end = i; | |
| 2289 | + if( i+1<end && data[i]=='\n' && data[i+1]=='\r' ) line_end = i+1; | |
| 2290 | + | |
| 2291 | + if( !line_end ) return 0; /* garbage after the link */ | |
| 2189 | 2292 | |
| 2293 | + /* a valid note has been found, filling-in note's text */ | |
| 2294 | + if( last ) *last = line_end; | |
| 2295 | + if( !footnotes ) return 1; | |
| 2296 | + if( build_ref_id(&fn.id, data+id_offset, id_end-id_offset)<0 ) return 0; | |
| 2297 | + blob_append(&fn.text, data+note_offset, note_end-note_offset); | |
| 2298 | + blob_append(footnotes, (char *)&fn, sizeof fn); | |
| 2299 | + return 1; | |
| 2300 | +} | |
| 2190 | 2301 | |
| 2191 | 2302 | /********************** |
| 2192 | 2303 | * EXPORTED FUNCTIONS * |
| 2193 | 2304 | **********************/ |
| 2194 | 2305 | |
| @@ -2197,10 +2308,11 @@ | ||
| 2197 | 2308 | struct Blob *ob, /* output blob for rendered text */ |
| 2198 | 2309 | struct Blob *ib, /* input blob in markdown */ |
| 2199 | 2310 | const struct mkd_renderer *rndrer /* renderer descriptor (callbacks) */ |
| 2200 | 2311 | ){ |
| 2201 | 2312 | struct link_ref *lr; |
| 2313 | + struct footnote *fn; | |
| 2202 | 2314 | size_t i, beg, end = 0; |
| 2203 | 2315 | struct render rndr; |
| 2204 | 2316 | char *ib_data; |
| 2205 | 2317 | Blob text = BLOB_INITIALIZER; |
| 2206 | 2318 | |
| @@ -2208,10 +2320,12 @@ | ||
| 2208 | 2320 | if( !rndrer ) return; |
| 2209 | 2321 | rndr.make = *rndrer; |
| 2210 | 2322 | rndr.nBlobCache = 0; |
| 2211 | 2323 | rndr.iDepth = 0; |
| 2212 | 2324 | rndr.refs = empty_blob; |
| 2325 | + rndr.footnotes = empty_blob; | |
| 2326 | + rndr.nLabels = 0; | |
| 2213 | 2327 | for(i=0; i<256; i++) rndr.active_char[i] = 0; |
| 2214 | 2328 | if( (rndr.make.emphasis |
| 2215 | 2329 | || rndr.make.double_emphasis |
| 2216 | 2330 | || rndr.make.triple_emphasis) |
| 2217 | 2331 | && rndr.make.emph_chars |
| @@ -2230,10 +2344,13 @@ | ||
| 2230 | 2344 | /* first pass: looking for references, copying everything else */ |
| 2231 | 2345 | beg = 0; |
| 2232 | 2346 | ib_data = blob_buffer(ib); |
| 2233 | 2347 | while( beg<blob_size(ib) ){ /* iterating over lines */ |
| 2234 | 2348 | if( is_ref(ib_data, beg, blob_size(ib), &end, &rndr.refs) ){ |
| 2349 | + beg = end; | |
| 2350 | + }else if( is_footnote(ib_data, beg, blob_size(ib), &end, &rndr.footnotes) ){ | |
| 2351 | + /* FIXME: fossil_print("\nfootnote found at %i\n", beg); */ | |
| 2235 | 2352 | beg = end; |
| 2236 | 2353 | }else{ /* skipping to the next line */ |
| 2237 | 2354 | end = beg; |
| 2238 | 2355 | while( end<blob_size(ib) && ib_data[end]!='\n' && ib_data[end]!='\r' ){ |
| 2239 | 2356 | end += 1; |
| @@ -2258,14 +2375,42 @@ | ||
| 2258 | 2375 | qsort(blob_buffer(&rndr.refs), |
| 2259 | 2376 | blob_size(&rndr.refs)/sizeof(struct link_ref), |
| 2260 | 2377 | sizeof(struct link_ref), |
| 2261 | 2378 | cmp_link_ref_sort); |
| 2262 | 2379 | } |
| 2380 | + /* sorting the footnotes array by id */ | |
| 2381 | + if( blob_size(&rndr.footnotes) ){ | |
| 2382 | + qsort(blob_buffer(&rndr.footnotes), | |
| 2383 | + blob_size(&rndr.footnotes)/sizeof(struct footnote), | |
| 2384 | + sizeof(struct footnote), | |
| 2385 | + cmp_link_ref_sort); | |
| 2386 | + } | |
| 2263 | 2387 | |
| 2264 | 2388 | /* second pass: actual rendering */ |
| 2265 | 2389 | if( rndr.make.prolog ) rndr.make.prolog(ob, rndr.make.opaque); |
| 2266 | 2390 | parse_block(ob, &rndr, blob_buffer(&text), blob_size(&text)); |
| 2391 | + | |
| 2392 | + /* sorting the footnotes array by index */ | |
| 2393 | + if( blob_size(&rndr.footnotes) ){ | |
| 2394 | + qsort(blob_buffer(&rndr.footnotes), | |
| 2395 | + blob_size(&rndr.footnotes)/sizeof(struct footnote), | |
| 2396 | + sizeof(struct footnote), | |
| 2397 | + cmp_footnote_sort); | |
| 2398 | + } | |
| 2399 | + /* FIXME: decouple parsing and HTML-specific rendering of footnotes */ | |
| 2400 | + if( rndr.nLabels ){ | |
| 2401 | + fn = (struct footnote *)blob_buffer(&rndr.footnotes); | |
| 2402 | + end = blob_size(&rndr.footnotes)/sizeof(struct footnote); | |
| 2403 | + blob_appendf(ob, "\n<ol class='footnotes'>\n"); | |
| 2404 | + for(i=0; i<end; i++){ | |
| 2405 | + if(fn[i].index == 0) continue; | |
| 2406 | + blob_appendf(ob, "<li id='footnote-%i'>\n ", fn[i].index ); | |
| 2407 | + parse_inline(ob,&rndr,blob_buffer(&fn[i].text),blob_size(&fn[i].text)); | |
| 2408 | + blob_append(ob,"\n</li>\n",7); | |
| 2409 | + } | |
| 2410 | + blob_append(ob, "</ol>\n", 7); | |
| 2411 | + } | |
| 2267 | 2412 | if( rndr.make.epilog ) rndr.make.epilog(ob, rndr.make.opaque); |
| 2268 | 2413 | |
| 2269 | 2414 | /* clean-up */ |
| 2270 | 2415 | assert( rndr.iDepth==0 ); |
| 2271 | 2416 | blob_reset(&text); |
| 2272 | 2417 |
| --- src/markdown.c | |
| +++ src/markdown.c | |
| @@ -129,14 +129,20 @@ | |
| 129 | * LOCAL TYPES * |
| 130 | ***************/ |
| 131 | |
| 132 | /* link_ref -- reference to a link */ |
| 133 | struct link_ref { |
| 134 | struct Blob id; |
| 135 | struct Blob link; |
| 136 | struct Blob title; |
| 137 | }; |
| 138 | |
| 139 | |
| 140 | /* char_trigger -- function pointer to render active chars */ |
| 141 | /* returns the number of chars taken care of */ |
| 142 | /* data is the pointer of the beginning of the span */ |
| @@ -152,16 +158,17 @@ | |
| 152 | |
| 153 | /* render -- structure containing one particular render */ |
| 154 | struct render { |
| 155 | struct mkd_renderer make; |
| 156 | struct Blob refs; |
| 157 | char_trigger active_char[256]; |
| 158 | int iDepth; /* Depth of recursion */ |
| 159 | int nBlobCache; /* Number of entries in aBlobCache */ |
| 160 | struct Blob *aBlobCache[20]; /* Cache of Blobs available for reuse */ |
| 161 | }; |
| 162 | |
| 163 | |
| 164 | /* html_tag -- structure for quick HTML tag search (inspired from discount) */ |
| 165 | struct html_tag { |
| 166 | const char *text; |
| 167 | int size; |
| @@ -247,10 +254,17 @@ | |
| 247 | struct link_ref *lra = (void *)a; |
| 248 | struct link_ref *lrb = (void *)b; |
| 249 | return blob_compare(&lra->id, &lrb->id); |
| 250 | } |
| 251 | |
| 252 | |
| 253 | /* cmp_html_tag -- comparison function for bsearch() (stolen from discount) */ |
| 254 | static int cmp_html_tag(const void *a, const void *b){ |
| 255 | const struct html_tag *hta = a; |
| 256 | const struct html_tag *htb = b; |
| @@ -1003,10 +1017,39 @@ | |
| 1003 | blob_append(link, blob_buffer(&lr->link), blob_size(&lr->link)); |
| 1004 | blob_append(title, blob_buffer(&lr->title), blob_size(&lr->title)); |
| 1005 | return 0; |
| 1006 | } |
| 1007 | |
| 1008 | |
| 1009 | /* char_link -- '[': parsing a link or an image */ |
| 1010 | static size_t char_link( |
| 1011 | struct Blob *ob, |
| 1012 | struct render *rndr, |
| @@ -1017,10 +1060,11 @@ | |
| 1017 | int is_img = (offset && data[-1] == '!'), level; |
| 1018 | size_t i = 1, txt_e; |
| 1019 | struct Blob *content = 0; |
| 1020 | struct Blob *link = 0; |
| 1021 | struct Blob *title = 0; |
| 1022 | int ret; |
| 1023 | |
| 1024 | /* checking whether the correct renderer exists */ |
| 1025 | if( (is_img && !rndr->make.image) || (!is_img && !rndr->make.link) ){ |
| 1026 | return 0; |
| @@ -1083,20 +1127,26 @@ | |
| 1083 | }else{ |
| 1084 | /* explicit id - between brackets */ |
| 1085 | id_data = data+i+1; |
| 1086 | id_size = id_end-(i+1); |
| 1087 | } |
| 1088 | |
| 1089 | if( get_link_ref(rndr, link, title, id_data, id_size)<0 ){ |
| 1090 | goto char_link_cleanup; |
| 1091 | } |
| 1092 | |
| 1093 | i = id_end+1; |
| 1094 | |
| 1095 | /* shortcut reference style link */ |
| 1096 | }else{ |
| 1097 | if( get_link_ref(rndr, link, title, data+1, txt_e-1)<0 ){ |
| 1098 | goto char_link_cleanup; |
| 1099 | } |
| 1100 | |
| 1101 | /* rewinding the whitespace */ |
| 1102 | i = txt_e+1; |
| @@ -1103,11 +1153,11 @@ | |
| 1103 | } |
| 1104 | |
| 1105 | /* building content: img alt is escaped, link content is parsed */ |
| 1106 | if( txt_e>1 ){ |
| 1107 | if( is_img ) blob_append(content, data+1, txt_e-1); |
| 1108 | else parse_inline(content, rndr, data+1, txt_e-1); |
| 1109 | } |
| 1110 | |
| 1111 | /* calling the relevant rendering function */ |
| 1112 | if( is_img ){ |
| 1113 | if( blob_size(ob)>0 && blob_buffer(ob)[blob_size(ob)-1]=='!' ) ob->nUsed--; |
| @@ -2095,11 +2145,11 @@ | |
| 2095 | } |
| 2096 | } |
| 2097 | i += beg; |
| 2098 | |
| 2099 | /* id part: anything but a newline between brackets */ |
| 2100 | if( data[i]!='[' ) return 0; |
| 2101 | i++; |
| 2102 | id_offset = i; |
| 2103 | while( i<end && data[i]!='\n' && data[i]!='\r' && data[i]!=']' ){ i++; } |
| 2104 | if( i>=end || data[i]!=']' ) return 0; |
| 2105 | id_end = i; |
| @@ -2184,11 +2234,72 @@ | |
| 2184 | } |
| 2185 | blob_append(refs, (char *)&lr, sizeof lr); |
| 2186 | return 1; |
| 2187 | } |
| 2188 | |
| 2189 | |
| 2190 | |
| 2191 | /********************** |
| 2192 | * EXPORTED FUNCTIONS * |
| 2193 | **********************/ |
| 2194 | |
| @@ -2197,10 +2308,11 @@ | |
| 2197 | struct Blob *ob, /* output blob for rendered text */ |
| 2198 | struct Blob *ib, /* input blob in markdown */ |
| 2199 | const struct mkd_renderer *rndrer /* renderer descriptor (callbacks) */ |
| 2200 | ){ |
| 2201 | struct link_ref *lr; |
| 2202 | size_t i, beg, end = 0; |
| 2203 | struct render rndr; |
| 2204 | char *ib_data; |
| 2205 | Blob text = BLOB_INITIALIZER; |
| 2206 | |
| @@ -2208,10 +2320,12 @@ | |
| 2208 | if( !rndrer ) return; |
| 2209 | rndr.make = *rndrer; |
| 2210 | rndr.nBlobCache = 0; |
| 2211 | rndr.iDepth = 0; |
| 2212 | rndr.refs = empty_blob; |
| 2213 | for(i=0; i<256; i++) rndr.active_char[i] = 0; |
| 2214 | if( (rndr.make.emphasis |
| 2215 | || rndr.make.double_emphasis |
| 2216 | || rndr.make.triple_emphasis) |
| 2217 | && rndr.make.emph_chars |
| @@ -2230,10 +2344,13 @@ | |
| 2230 | /* first pass: looking for references, copying everything else */ |
| 2231 | beg = 0; |
| 2232 | ib_data = blob_buffer(ib); |
| 2233 | while( beg<blob_size(ib) ){ /* iterating over lines */ |
| 2234 | if( is_ref(ib_data, beg, blob_size(ib), &end, &rndr.refs) ){ |
| 2235 | beg = end; |
| 2236 | }else{ /* skipping to the next line */ |
| 2237 | end = beg; |
| 2238 | while( end<blob_size(ib) && ib_data[end]!='\n' && ib_data[end]!='\r' ){ |
| 2239 | end += 1; |
| @@ -2258,14 +2375,42 @@ | |
| 2258 | qsort(blob_buffer(&rndr.refs), |
| 2259 | blob_size(&rndr.refs)/sizeof(struct link_ref), |
| 2260 | sizeof(struct link_ref), |
| 2261 | cmp_link_ref_sort); |
| 2262 | } |
| 2263 | |
| 2264 | /* second pass: actual rendering */ |
| 2265 | if( rndr.make.prolog ) rndr.make.prolog(ob, rndr.make.opaque); |
| 2266 | parse_block(ob, &rndr, blob_buffer(&text), blob_size(&text)); |
| 2267 | if( rndr.make.epilog ) rndr.make.epilog(ob, rndr.make.opaque); |
| 2268 | |
| 2269 | /* clean-up */ |
| 2270 | assert( rndr.iDepth==0 ); |
| 2271 | blob_reset(&text); |
| 2272 |
| --- src/markdown.c | |
| +++ src/markdown.c | |
| @@ -129,14 +129,20 @@ | |
| 129 | * LOCAL TYPES * |
| 130 | ***************/ |
| 131 | |
| 132 | /* link_ref -- reference to a link */ |
| 133 | struct link_ref { |
| 134 | struct Blob id; /* must be the first field as in footnote struct */ |
| 135 | struct Blob link; |
| 136 | struct Blob title; |
| 137 | }; |
| 138 | |
| 139 | struct footnote { |
| 140 | struct Blob id; /* must be the first field as in link_ref struct */ |
| 141 | struct Blob text; /* footnote's content that is rendered at the end */ |
| 142 | int index; /* serial number, in the order of appearance */ |
| 143 | }; |
| 144 | |
| 145 | |
| 146 | /* char_trigger -- function pointer to render active chars */ |
| 147 | /* returns the number of chars taken care of */ |
| 148 | /* data is the pointer of the beginning of the span */ |
| @@ -152,16 +158,17 @@ | |
| 158 | |
| 159 | /* render -- structure containing one particular render */ |
| 160 | struct render { |
| 161 | struct mkd_renderer make; |
| 162 | struct Blob refs; |
| 163 | struct Blob footnotes; |
| 164 | char_trigger active_char[256]; |
| 165 | int iDepth; /* Depth of recursion */ |
| 166 | int nBlobCache; /* Number of entries in aBlobCache */ |
| 167 | struct Blob *aBlobCache[20]; /* Cache of Blobs available for reuse */ |
| 168 | int nLabels; /* Footnotes counter for the second pass */ |
| 169 | }; |
| 170 | |
| 171 | /* html_tag -- structure for quick HTML tag search (inspired from discount) */ |
| 172 | struct html_tag { |
| 173 | const char *text; |
| 174 | int size; |
| @@ -247,10 +254,17 @@ | |
| 254 | struct link_ref *lra = (void *)a; |
| 255 | struct link_ref *lrb = (void *)b; |
| 256 | return blob_compare(&lra->id, &lrb->id); |
| 257 | } |
| 258 | |
| 259 | /* cmp_footnote_sort -- comparison function for footnotes qsort */ |
| 260 | static int cmp_footnote_sort(const void *a, const void *b){ |
| 261 | const struct footnote *fna = (void *)a, *fnb = (void *)b; |
| 262 | assert( fna->index > 0 && fnb->index > 0 ); |
| 263 | if( fna->index == fnb->index ) return 0; |
| 264 | return ( fna->index < fnb->index ? -1 : 1 ); |
| 265 | } |
| 266 | |
| 267 | /* cmp_html_tag -- comparison function for bsearch() (stolen from discount) */ |
| 268 | static int cmp_html_tag(const void *a, const void *b){ |
| 269 | const struct html_tag *hta = a; |
| 270 | const struct html_tag *htb = b; |
| @@ -1003,10 +1017,39 @@ | |
| 1017 | blob_append(link, blob_buffer(&lr->link), blob_size(&lr->link)); |
| 1018 | blob_append(title, blob_buffer(&lr->title), blob_size(&lr->title)); |
| 1019 | return 0; |
| 1020 | } |
| 1021 | |
| 1022 | /* get_footnote -- resolve footnote by its id |
| 1023 | * on success: fill text and return positive footnote's index |
| 1024 | * on failure: return -1 */ |
| 1025 | static int get_footnote( |
| 1026 | struct render *rndr, |
| 1027 | struct Blob *text, |
| 1028 | const char *data, |
| 1029 | size_t size |
| 1030 | ){ |
| 1031 | struct footnote *fn; |
| 1032 | |
| 1033 | blob_reset(text); /* use text for temporary storage */ |
| 1034 | if( build_ref_id(text, data, size)<0 ) return -1; |
| 1035 | |
| 1036 | fn = bsearch(text, |
| 1037 | blob_buffer(&rndr->footnotes), |
| 1038 | blob_size(&rndr->footnotes)/sizeof(struct footnote), |
| 1039 | sizeof (struct footnote), |
| 1040 | cmp_link_ref); |
| 1041 | if( !fn ) return -1; |
| 1042 | |
| 1043 | if( fn->index == 0 ){ /* the first reference to the footnote */ |
| 1044 | fn->index = ++(rndr->nLabels); |
| 1045 | } |
| 1046 | assert( fn->index > 0 ); |
| 1047 | blob_reset(text); |
| 1048 | blob_append(text, blob_buffer(&fn->text), blob_size(&fn->text)); |
| 1049 | return fn->index; |
| 1050 | } |
| 1051 | |
| 1052 | /* char_link -- '[': parsing a link or an image */ |
| 1053 | static size_t char_link( |
| 1054 | struct Blob *ob, |
| 1055 | struct render *rndr, |
| @@ -1017,10 +1060,11 @@ | |
| 1060 | int is_img = (offset && data[-1] == '!'), level; |
| 1061 | size_t i = 1, txt_e; |
| 1062 | struct Blob *content = 0; |
| 1063 | struct Blob *link = 0; |
| 1064 | struct Blob *title = 0; |
| 1065 | const int is_note = (size && data[1] == '^'); |
| 1066 | int ret; |
| 1067 | |
| 1068 | /* checking whether the correct renderer exists */ |
| 1069 | if( (is_img && !rndr->make.image) || (!is_img && !rndr->make.link) ){ |
| 1070 | return 0; |
| @@ -1083,20 +1127,26 @@ | |
| 1127 | }else{ |
| 1128 | /* explicit id - between brackets */ |
| 1129 | id_data = data+i+1; |
| 1130 | id_size = id_end-(i+1); |
| 1131 | } |
| 1132 | if( get_link_ref(rndr, link, title, id_data, id_size)<0 ){ |
| 1133 | goto char_link_cleanup; |
| 1134 | } |
| 1135 | i = id_end+1; |
| 1136 | |
| 1137 | /* shortcut reference style link */ |
| 1138 | }else{ |
| 1139 | if( is_note ){ |
| 1140 | const int lbl = get_footnote(rndr, link, data+1, txt_e-1); |
| 1141 | if( lbl <= 0 ){ |
| 1142 | goto char_link_cleanup; |
| 1143 | } |
| 1144 | blob_reset(link); |
| 1145 | blob_appendf(link, "#footnote-%i", lbl); |
| 1146 | blob_appendf(content,"<sup class='footnote'>%i</sup>",lbl); |
| 1147 | }else if( get_link_ref(rndr, link, title, data+1, txt_e-1)<0 ){ |
| 1148 | goto char_link_cleanup; |
| 1149 | } |
| 1150 | |
| 1151 | /* rewinding the whitespace */ |
| 1152 | i = txt_e+1; |
| @@ -1103,11 +1153,11 @@ | |
| 1153 | } |
| 1154 | |
| 1155 | /* building content: img alt is escaped, link content is parsed */ |
| 1156 | if( txt_e>1 ){ |
| 1157 | if( is_img ) blob_append(content, data+1, txt_e-1); |
| 1158 | else if(!is_note) parse_inline(content, rndr, data+1, txt_e-1); |
| 1159 | } |
| 1160 | |
| 1161 | /* calling the relevant rendering function */ |
| 1162 | if( is_img ){ |
| 1163 | if( blob_size(ob)>0 && blob_buffer(ob)[blob_size(ob)-1]=='!' ) ob->nUsed--; |
| @@ -2095,11 +2145,11 @@ | |
| 2145 | } |
| 2146 | } |
| 2147 | i += beg; |
| 2148 | |
| 2149 | /* id part: anything but a newline between brackets */ |
| 2150 | if( data[i]!='[' || data[i+1]=='^' ) return 0; |
| 2151 | i++; |
| 2152 | id_offset = i; |
| 2153 | while( i<end && data[i]!='\n' && data[i]!='\r' && data[i]!=']' ){ i++; } |
| 2154 | if( i>=end || data[i]!=']' ) return 0; |
| 2155 | id_end = i; |
| @@ -2184,11 +2234,72 @@ | |
| 2234 | } |
| 2235 | blob_append(refs, (char *)&lr, sizeof lr); |
| 2236 | return 1; |
| 2237 | } |
| 2238 | |
| 2239 | /********************* |
| 2240 | * FOOTNOTE PARSING * |
| 2241 | *********************/ |
| 2242 | |
| 2243 | /* is_footnote -- returns whether a line is a footnote or not */ |
| 2244 | static int is_footnote( |
| 2245 | char *data, /* input text */ |
| 2246 | size_t beg, /* offset of the beginning of the line */ |
| 2247 | size_t end, /* offset of the end of the text */ |
| 2248 | size_t *last, /* last character of the link */ |
| 2249 | struct Blob * footnotes /* FIXME: struct render *rndr */ |
| 2250 | ){ |
| 2251 | size_t i = 0; |
| 2252 | size_t id_offset, id_end; |
| 2253 | size_t note_offset, note_end; |
| 2254 | size_t line_end; |
| 2255 | struct footnote fn = { empty_blob, empty_blob, 0 }; |
| 2256 | |
| 2257 | /* footnote definition must start at the begining of a line */ |
| 2258 | if( beg+4>=end ) return 0; |
| 2259 | i += beg; |
| 2260 | |
| 2261 | /* id part: anything but a newline between brackets */ |
| 2262 | if( data[i]!='[' || data[i+1]!='^' ) return 0; |
| 2263 | i++; |
| 2264 | id_offset = i; |
| 2265 | while( i<end && data[i]!=']' && data[i]!='\n' && data[i]!='\r' ){ i++; } |
| 2266 | if( i>=end || data[i]!=']' ) return 0; |
| 2267 | id_end = i; |
| 2268 | |
| 2269 | /* spacer: colon (space | tab)* newline? (space | tab)* */ |
| 2270 | i++; |
| 2271 | if( i>=end || data[i]!=':' ) return 0; |
| 2272 | i++; |
| 2273 | while( i<end && (data[i]==' ' || data[i]=='\t') ){ i++; } |
| 2274 | if( i<end && (data[i]=='\n' || data[i]=='\r') ){ |
| 2275 | i++; |
| 2276 | if( i<end && data[i]=='\r' && data[i-1] == '\n' ) i++; |
| 2277 | } |
| 2278 | while( i<end && (data[i]==' ' || data[i]=='\t') ){ i++; } |
| 2279 | if( i>=end ) return 0; |
| 2280 | |
| 2281 | /* note is a single line of text (FIXME: support multiline notes) */ |
| 2282 | note_offset = i; |
| 2283 | while( i<end && data[i]!='\r' && data[i]!='\n' ){ i++; } |
| 2284 | note_end = i; |
| 2285 | |
| 2286 | /* computing end-of-line */ |
| 2287 | line_end = 0; |
| 2288 | if( i >=end || data[i]=='\r' || data[ i ]=='\n' ) line_end = i; |
| 2289 | if( i+1<end && data[i]=='\n' && data[i+1]=='\r' ) line_end = i+1; |
| 2290 | |
| 2291 | if( !line_end ) return 0; /* garbage after the link */ |
| 2292 | |
| 2293 | /* a valid note has been found, filling-in note's text */ |
| 2294 | if( last ) *last = line_end; |
| 2295 | if( !footnotes ) return 1; |
| 2296 | if( build_ref_id(&fn.id, data+id_offset, id_end-id_offset)<0 ) return 0; |
| 2297 | blob_append(&fn.text, data+note_offset, note_end-note_offset); |
| 2298 | blob_append(footnotes, (char *)&fn, sizeof fn); |
| 2299 | return 1; |
| 2300 | } |
| 2301 | |
| 2302 | /********************** |
| 2303 | * EXPORTED FUNCTIONS * |
| 2304 | **********************/ |
| 2305 | |
| @@ -2197,10 +2308,11 @@ | |
| 2308 | struct Blob *ob, /* output blob for rendered text */ |
| 2309 | struct Blob *ib, /* input blob in markdown */ |
| 2310 | const struct mkd_renderer *rndrer /* renderer descriptor (callbacks) */ |
| 2311 | ){ |
| 2312 | struct link_ref *lr; |
| 2313 | struct footnote *fn; |
| 2314 | size_t i, beg, end = 0; |
| 2315 | struct render rndr; |
| 2316 | char *ib_data; |
| 2317 | Blob text = BLOB_INITIALIZER; |
| 2318 | |
| @@ -2208,10 +2320,12 @@ | |
| 2320 | if( !rndrer ) return; |
| 2321 | rndr.make = *rndrer; |
| 2322 | rndr.nBlobCache = 0; |
| 2323 | rndr.iDepth = 0; |
| 2324 | rndr.refs = empty_blob; |
| 2325 | rndr.footnotes = empty_blob; |
| 2326 | rndr.nLabels = 0; |
| 2327 | for(i=0; i<256; i++) rndr.active_char[i] = 0; |
| 2328 | if( (rndr.make.emphasis |
| 2329 | || rndr.make.double_emphasis |
| 2330 | || rndr.make.triple_emphasis) |
| 2331 | && rndr.make.emph_chars |
| @@ -2230,10 +2344,13 @@ | |
| 2344 | /* first pass: looking for references, copying everything else */ |
| 2345 | beg = 0; |
| 2346 | ib_data = blob_buffer(ib); |
| 2347 | while( beg<blob_size(ib) ){ /* iterating over lines */ |
| 2348 | if( is_ref(ib_data, beg, blob_size(ib), &end, &rndr.refs) ){ |
| 2349 | beg = end; |
| 2350 | }else if( is_footnote(ib_data, beg, blob_size(ib), &end, &rndr.footnotes) ){ |
| 2351 | /* FIXME: fossil_print("\nfootnote found at %i\n", beg); */ |
| 2352 | beg = end; |
| 2353 | }else{ /* skipping to the next line */ |
| 2354 | end = beg; |
| 2355 | while( end<blob_size(ib) && ib_data[end]!='\n' && ib_data[end]!='\r' ){ |
| 2356 | end += 1; |
| @@ -2258,14 +2375,42 @@ | |
| 2375 | qsort(blob_buffer(&rndr.refs), |
| 2376 | blob_size(&rndr.refs)/sizeof(struct link_ref), |
| 2377 | sizeof(struct link_ref), |
| 2378 | cmp_link_ref_sort); |
| 2379 | } |
| 2380 | /* sorting the footnotes array by id */ |
| 2381 | if( blob_size(&rndr.footnotes) ){ |
| 2382 | qsort(blob_buffer(&rndr.footnotes), |
| 2383 | blob_size(&rndr.footnotes)/sizeof(struct footnote), |
| 2384 | sizeof(struct footnote), |
| 2385 | cmp_link_ref_sort); |
| 2386 | } |
| 2387 | |
| 2388 | /* second pass: actual rendering */ |
| 2389 | if( rndr.make.prolog ) rndr.make.prolog(ob, rndr.make.opaque); |
| 2390 | parse_block(ob, &rndr, blob_buffer(&text), blob_size(&text)); |
| 2391 | |
| 2392 | /* sorting the footnotes array by index */ |
| 2393 | if( blob_size(&rndr.footnotes) ){ |
| 2394 | qsort(blob_buffer(&rndr.footnotes), |
| 2395 | blob_size(&rndr.footnotes)/sizeof(struct footnote), |
| 2396 | sizeof(struct footnote), |
| 2397 | cmp_footnote_sort); |
| 2398 | } |
| 2399 | /* FIXME: decouple parsing and HTML-specific rendering of footnotes */ |
| 2400 | if( rndr.nLabels ){ |
| 2401 | fn = (struct footnote *)blob_buffer(&rndr.footnotes); |
| 2402 | end = blob_size(&rndr.footnotes)/sizeof(struct footnote); |
| 2403 | blob_appendf(ob, "\n<ol class='footnotes'>\n"); |
| 2404 | for(i=0; i<end; i++){ |
| 2405 | if(fn[i].index == 0) continue; |
| 2406 | blob_appendf(ob, "<li id='footnote-%i'>\n ", fn[i].index ); |
| 2407 | parse_inline(ob,&rndr,blob_buffer(&fn[i].text),blob_size(&fn[i].text)); |
| 2408 | blob_append(ob,"\n</li>\n",7); |
| 2409 | } |
| 2410 | blob_append(ob, "</ol>\n", 7); |
| 2411 | } |
| 2412 | if( rndr.make.epilog ) rndr.make.epilog(ob, rndr.make.opaque); |
| 2413 | |
| 2414 | /* clean-up */ |
| 2415 | assert( rndr.iDepth==0 ); |
| 2416 | blob_reset(&text); |
| 2417 |