Fossil

Check-in [ebce0f357e]
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Initial support for [forum:/forumthread/d752446a4f63f390|footnotes in Markdown]. <br>This is WIP: support of multiline notes and code clean-up are pending.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | markdown-footnotes
Files: files | file ages | folders
SHA3-256: ebce0f357e0732cacdcc4105623c94385d1691a0c9d6cfe7ef979ae71b96650f
User & Date: george 2022-01-26 14:50:15.087
Context
2022-01-27
19:45
Minor code clean-up of src/markdown.c: add a few 'const' specifiers, reduce the scope of temporary variables and simplify their names. check-in: b9393a4e64 user: george tags: markdown-footnotes
2022-01-26
14:50
Initial support for [forum:/forumthread/d752446a4f63f390|footnotes in Markdown]. <br>This is WIP: support of multiline notes and code clean-up are pending. check-in: ebce0f357e user: george tags: markdown-footnotes
2022-01-25
17:44
Update the built-in SQLite to the latest 3.38.0 beta, for the purpose of beta testing SQLite. check-in: 605064e656 user: drh tags: trunk
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/default.css.
1662
1663
1664
1665
1666
1667
1668






1669
1670
1671
1672
1673
1674
1675
}
body.branch .submenu > a.timeline-link.selected {
  display: inline;
}

.monospace {
  font-family: monospace;






}

/* Objects in the "desktoponly" class are invisible on mobile */
@media screen and (max-width: 600px) {
  .desktoponly {
    display: none;
  }







>
>
>
>
>
>







1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
}
body.branch .submenu > a.timeline-link.selected {
  display: inline;
}

.monospace {
  font-family: monospace;
}
div.content  div.markdown > ol.footnotes {
  font-size: 90%;
}
div.content  div.markdown > ol.footnotes > li {
  margin-bottom: 0.5em;
}

/* Objects in the "desktoponly" class are invisible on mobile */
@media screen and (max-width: 600px) {
  .desktoponly {
    display: none;
  }
Changes to src/markdown.c.
127
128
129
130
131
132
133
134
135
136
137






138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156

157
158
159
160

161
162
163
164
165
166
167
168
169

/***************
 * LOCAL TYPES *
 ***************/

/* link_ref -- reference to a link */
struct link_ref {
  struct Blob id;
  struct Blob link;
  struct Blob title;
};








/* char_trigger -- function pointer to render active chars */
/*   returns the number of chars taken care of */
/*   data is the pointer of the beginning of the span */
/*   offset is the number of valid chars before data */
struct render;
typedef size_t (*char_trigger)(
  struct Blob *ob,
  struct render *rndr,
  char *data,
  size_t offset,
  size_t size);


/* render -- structure containing one particular render */
struct render {
  struct mkd_renderer make;
  struct Blob refs;

  char_trigger active_char[256];
  int iDepth;                    /* Depth of recursion */
  int nBlobCache;                /* Number of entries in aBlobCache */
  struct Blob *aBlobCache[20];   /* Cache of Blobs available for reuse */

};


/* html_tag -- structure for quick HTML tag search (inspired from discount) */
struct html_tag {
  const char *text;
  int size;
};








|



>
>
>
>
>
>



















>




>

<







127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169

170
171
172
173
174
175
176

/***************
 * LOCAL TYPES *
 ***************/

/* link_ref -- reference to a link */
struct link_ref {
  struct Blob id;     /* must be the first field as in footnote struct */
  struct Blob link;
  struct Blob title;
};

struct footnote {
  struct Blob id;   /* must be the first field as in link_ref struct  */
  struct Blob text; /* footnote's content that is rendered at the end */
  int    index;     /* serial number, in the order of appearance */
};


/* char_trigger -- function pointer to render active chars */
/*   returns the number of chars taken care of */
/*   data is the pointer of the beginning of the span */
/*   offset is the number of valid chars before data */
struct render;
typedef size_t (*char_trigger)(
  struct Blob *ob,
  struct render *rndr,
  char *data,
  size_t offset,
  size_t size);


/* render -- structure containing one particular render */
struct render {
  struct mkd_renderer make;
  struct Blob refs;
  struct Blob footnotes;
  char_trigger active_char[256];
  int iDepth;                    /* Depth of recursion */
  int nBlobCache;                /* Number of entries in aBlobCache */
  struct Blob *aBlobCache[20];   /* Cache of Blobs available for reuse */
  int    nLabels;                /* Footnotes counter for the second pass */
};


/* html_tag -- structure for quick HTML tag search (inspired from discount) */
struct html_tag {
  const char *text;
  int size;
};

245
246
247
248
249
250
251







252
253
254
255
256
257
258
/* cmp_link_ref_sort -- comparison function for link_ref qsort */
static int cmp_link_ref_sort(const void *a, const void *b){
  struct link_ref *lra = (void *)a;
  struct link_ref *lrb = (void *)b;
  return blob_compare(&lra->id, &lrb->id);
}









/* cmp_html_tag -- comparison function for bsearch() (stolen from discount) */
static int cmp_html_tag(const void *a, const void *b){
  const struct html_tag *hta = a;
  const struct html_tag *htb = b;
  int sz = hta->size;
  int c;







>
>
>
>
>
>
>







252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
/* cmp_link_ref_sort -- comparison function for link_ref qsort */
static int cmp_link_ref_sort(const void *a, const void *b){
  struct link_ref *lra = (void *)a;
  struct link_ref *lrb = (void *)b;
  return blob_compare(&lra->id, &lrb->id);
}

/* cmp_footnote_sort -- comparison function for footnotes qsort */
static int cmp_footnote_sort(const void *a, const void *b){
  const struct footnote *fna = (void *)a, *fnb = (void *)b;
  assert( fna->index > 0 && fnb->index > 0 );
  if( fna->index == fnb->index ) return 0;
  return ( fna->index < fnb->index ? -1 : 1 );
}

/* cmp_html_tag -- comparison function for bsearch() (stolen from discount) */
static int cmp_html_tag(const void *a, const void *b){
  const struct html_tag *hta = a;
  const struct html_tag *htb = b;
  int sz = hta->size;
  int c;
1001
1002
1003
1004
1005
1006
1007





























1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021

1022
1023
1024
1025
1026
1027
1028
  blob_reset(link);
  blob_reset(title);
  blob_append(link, blob_buffer(&lr->link), blob_size(&lr->link));
  blob_append(title, blob_buffer(&lr->title), blob_size(&lr->title));
  return 0;
}































/* char_link -- '[': parsing a link or an image */
static size_t char_link(
  struct Blob *ob,
  struct render *rndr,
  char *data,
  size_t offset,
  size_t size
){
  int is_img = (offset && data[-1] == '!'), level;
  size_t i = 1, txt_e;
  struct Blob *content = 0;
  struct Blob *link = 0;
  struct Blob *title = 0;

  int ret;

  /* checking whether the correct renderer exists */
  if( (is_img && !rndr->make.image) || (!is_img && !rndr->make.link) ){
    return 0;
  }








>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>














>







1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
  blob_reset(link);
  blob_reset(title);
  blob_append(link, blob_buffer(&lr->link), blob_size(&lr->link));
  blob_append(title, blob_buffer(&lr->title), blob_size(&lr->title));
  return 0;
}

/* get_footnote -- resolve footnote by its id
 * on success: fill text and return positive footnote's index
 * on failure: return -1 */
static int get_footnote(
  struct render *rndr,
  struct Blob *text,
  const char *data,
  size_t size
){
  struct footnote *fn;

  blob_reset(text);   /* use text for temporary storage */
  if( build_ref_id(text, data, size)<0 ) return -1;

  fn = bsearch(text,
               blob_buffer(&rndr->footnotes),
               blob_size(&rndr->footnotes)/sizeof(struct footnote),
               sizeof (struct footnote),
               cmp_link_ref);
  if( !fn ) return -1;

  if( fn->index == 0 ){  /* the first reference to the footnote */
    fn->index = ++(rndr->nLabels);
  }
  assert( fn->index > 0 );
  blob_reset(text);
  blob_append(text, blob_buffer(&fn->text), blob_size(&fn->text));
  return fn->index;
}

/* char_link -- '[': parsing a link or an image */
static size_t char_link(
  struct Blob *ob,
  struct render *rndr,
  char *data,
  size_t offset,
  size_t size
){
  int is_img = (offset && data[-1] == '!'), level;
  size_t i = 1, txt_e;
  struct Blob *content = 0;
  struct Blob *link = 0;
  struct Blob *title = 0;
  const int is_note = (size && data[1] == '^');
  int ret;

  /* checking whether the correct renderer exists */
  if( (is_img && !rndr->make.image) || (!is_img && !rndr->make.link) ){
    return 0;
  }

1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096








1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
      id_data = data+1;
      id_size = txt_e-1;
    }else{
      /* explicit id - between brackets */
      id_data = data+i+1;
      id_size = id_end-(i+1);
    }

    if( get_link_ref(rndr, link, title, id_data, id_size)<0 ){
      goto char_link_cleanup;
    }

    i = id_end+1;

  /* shortcut reference style link */
  }else{








    if( get_link_ref(rndr, link, title, data+1, txt_e-1)<0 ){
      goto char_link_cleanup;
    }

    /* rewinding the whitespace */
    i = txt_e+1;
  }

  /* building content: img alt is escaped, link content is parsed */
  if( txt_e>1 ){
    if( is_img ) blob_append(content, data+1, txt_e-1);
    else parse_inline(content, rndr, data+1, txt_e-1);
  }

  /* calling the relevant rendering function */
  if( is_img ){
    if( blob_size(ob)>0 && blob_buffer(ob)[blob_size(ob)-1]=='!' ) ob->nUsed--;
    ret = rndr->make.image(ob, link, title, content, rndr->make.opaque);
  }else{







<



<




>
>
>
>
>
>
>
>
|










|







1125
1126
1127
1128
1129
1130
1131

1132
1133
1134

1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
      id_data = data+1;
      id_size = txt_e-1;
    }else{
      /* explicit id - between brackets */
      id_data = data+i+1;
      id_size = id_end-(i+1);
    }

    if( get_link_ref(rndr, link, title, id_data, id_size)<0 ){
      goto char_link_cleanup;
    }

    i = id_end+1;

  /* shortcut reference style link */
  }else{
    if( is_note ){
      const int lbl = get_footnote(rndr, link, data+1, txt_e-1);
      if( lbl <= 0 ){
        goto char_link_cleanup;
      }
      blob_reset(link);
      blob_appendf(link, "#footnote-%i", lbl);
      blob_appendf(content,"<sup class='footnote'>%i</sup>",lbl);
    }else if( get_link_ref(rndr, link, title, data+1, txt_e-1)<0 ){
      goto char_link_cleanup;
    }

    /* rewinding the whitespace */
    i = txt_e+1;
  }

  /* building content: img alt is escaped, link content is parsed */
  if( txt_e>1 ){
    if( is_img ) blob_append(content, data+1, txt_e-1);
    else if(!is_note) parse_inline(content, rndr, data+1, txt_e-1);
  }

  /* calling the relevant rendering function */
  if( is_img ){
    if( blob_size(ob)>0 && blob_buffer(ob)[blob_size(ob)-1]=='!' ) ob->nUsed--;
    ret = rndr->make.image(ob, link, title, content, rndr->make.opaque);
  }else{
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
        if( data[beg+3]==' ' ) return 0;
      }
    }
  }
  i += beg;

  /* id part: anything but a newline between brackets */
  if( data[i]!='[' ) return 0;
  i++;
  id_offset = i;
  while( i<end && data[i]!='\n' && data[i]!='\r' && data[i]!=']' ){ i++; }
  if( i>=end || data[i]!=']' ) return 0;
  id_end = i;

  /* spacer: colon (space | tab)* newline? (space | tab)* */







|







2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
        if( data[beg+3]==' ' ) return 0;
      }
    }
  }
  i += beg;

  /* id part: anything but a newline between brackets */
  if( data[i]!='[' || data[i+1]=='^' ) return 0;
  i++;
  id_offset = i;
  while( i<end && data[i]!='\n' && data[i]!='\r' && data[i]!=']' ){ i++; }
  if( i>=end || data[i]!=']' ) return 0;
  id_end = i;

  /* spacer: colon (space | tab)* newline? (space | tab)* */
2182
2183
2184
2185
2186
2187
2188



2189


























































2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201

2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212


2213
2214
2215
2216
2217
2218
2219
  if( title_end>title_offset ){
    blob_append(&lr.title, data+title_offset, title_end-title_offset);
  }
  blob_append(refs, (char *)&lr, sizeof lr);
  return 1;
}
































































/**********************
 * EXPORTED FUNCTIONS *
 **********************/

/* markdown -- parses the input buffer and renders it into the output buffer */
void markdown(
  struct Blob *ob,                   /* output blob for rendered text */
  struct Blob *ib,                   /* input blob in markdown */
  const struct mkd_renderer *rndrer  /* renderer descriptor (callbacks) */
){
  struct link_ref *lr;

  size_t i, beg, end = 0;
  struct render rndr;
  char *ib_data;
  Blob text = BLOB_INITIALIZER;

  /* filling the render structure */
  if( !rndrer ) return;
  rndr.make = *rndrer;
  rndr.nBlobCache = 0;
  rndr.iDepth = 0;
  rndr.refs = empty_blob;


  for(i=0; i<256; i++) rndr.active_char[i] = 0;
  if( (rndr.make.emphasis
    || rndr.make.double_emphasis
    || rndr.make.triple_emphasis)
   && rndr.make.emph_chars
  ){
    for(i=0; rndr.make.emph_chars[i]; i++){







>
>
>

>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>












>











>
>







2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
  if( title_end>title_offset ){
    blob_append(&lr.title, data+title_offset, title_end-title_offset);
  }
  blob_append(refs, (char *)&lr, sizeof lr);
  return 1;
}

/*********************
 * FOOTNOTE PARSING  *
 *********************/

/* is_footnote -- returns whether a line is a footnote or not */
static int is_footnote(
  char *data,         /* input text */
  size_t beg,         /* offset of the beginning of the line */
  size_t end,         /* offset of the end of the text */
  size_t *last,       /* last character of the link */
  struct Blob * footnotes /* FIXME: struct render *rndr */
){
  size_t i = 0;
  size_t id_offset, id_end;
  size_t note_offset, note_end;
  size_t line_end;
  struct footnote fn = { empty_blob, empty_blob, 0 };

  /* footnote definition must start at the begining of a line */
  if( beg+4>=end ) return 0;
  i += beg;

  /* id part: anything but a newline between brackets */
  if( data[i]!='[' || data[i+1]!='^' ) return 0;
  i++;
  id_offset = i;
  while( i<end && data[i]!=']' && data[i]!='\n' && data[i]!='\r' ){ i++; }
  if( i>=end || data[i]!=']' ) return 0;
  id_end = i;

  /* spacer: colon (space | tab)* newline? (space | tab)* */
  i++;
  if( i>=end || data[i]!=':' ) return 0;
  i++;
  while( i<end && (data[i]==' ' || data[i]=='\t') ){ i++; }
  if( i<end && (data[i]=='\n' || data[i]=='\r') ){
    i++;
    if( i<end && data[i]=='\r' && data[i-1] == '\n' ) i++;
  }
  while( i<end && (data[i]==' ' || data[i]=='\t') ){ i++; }
  if( i>=end ) return 0;

  /* note is a single line of text (FIXME: support multiline notes) */
  note_offset = i;
  while( i<end && data[i]!='\r' && data[i]!='\n' ){ i++; }
  note_end = i;

  /* computing end-of-line */
  line_end = 0;
  if( i >=end || data[i]=='\r' || data[ i ]=='\n' ) line_end = i;
  if( i+1<end && data[i]=='\n' && data[i+1]=='\r' ) line_end = i+1;

  if( !line_end ) return 0; /* garbage after the link */

  /* a valid note has been found, filling-in note's text */
  if( last ) *last = line_end;
  if( !footnotes ) return 1;
  if( build_ref_id(&fn.id, data+id_offset, id_end-id_offset)<0 ) return 0;
  blob_append(&fn.text, data+note_offset, note_end-note_offset);
  blob_append(footnotes, (char *)&fn, sizeof fn);
  return 1;
}

/**********************
 * EXPORTED FUNCTIONS *
 **********************/

/* markdown -- parses the input buffer and renders it into the output buffer */
void markdown(
  struct Blob *ob,                   /* output blob for rendered text */
  struct Blob *ib,                   /* input blob in markdown */
  const struct mkd_renderer *rndrer  /* renderer descriptor (callbacks) */
){
  struct link_ref *lr;
  struct footnote *fn;
  size_t i, beg, end = 0;
  struct render rndr;
  char *ib_data;
  Blob text = BLOB_INITIALIZER;

  /* filling the render structure */
  if( !rndrer ) return;
  rndr.make = *rndrer;
  rndr.nBlobCache = 0;
  rndr.iDepth = 0;
  rndr.refs = empty_blob;
  rndr.footnotes = empty_blob;
  rndr.nLabels = 0;
  for(i=0; i<256; i++) rndr.active_char[i] = 0;
  if( (rndr.make.emphasis
    || rndr.make.double_emphasis
    || rndr.make.triple_emphasis)
   && rndr.make.emph_chars
  ){
    for(i=0; rndr.make.emph_chars[i]; i++){
2228
2229
2230
2231
2232
2233
2234



2235
2236
2237
2238
2239
2240
2241
  rndr.active_char['&'] = char_entity;

  /* first pass: looking for references, copying everything else */
  beg = 0;
  ib_data = blob_buffer(ib);
  while( beg<blob_size(ib) ){ /* iterating over lines */
    if( is_ref(ib_data, beg, blob_size(ib), &end, &rndr.refs) ){



      beg = end;
    }else{ /* skipping to the next line */
      end = beg;
      while( end<blob_size(ib) && ib_data[end]!='\n' && ib_data[end]!='\r' ){
        end += 1;
      }
      /* adding the line body if present */







>
>
>







2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
  rndr.active_char['&'] = char_entity;

  /* first pass: looking for references, copying everything else */
  beg = 0;
  ib_data = blob_buffer(ib);
  while( beg<blob_size(ib) ){ /* iterating over lines */
    if( is_ref(ib_data, beg, blob_size(ib), &end, &rndr.refs) ){
      beg = end;
    }else if( is_footnote(ib_data, beg, blob_size(ib), &end, &rndr.footnotes) ){
      /* FIXME: fossil_print("\nfootnote found at %i\n", beg); */
      beg = end;
    }else{ /* skipping to the next line */
      end = beg;
      while( end<blob_size(ib) && ib_data[end]!='\n' && ib_data[end]!='\r' ){
        end += 1;
      }
      /* adding the line body if present */
2256
2257
2258
2259
2260
2261
2262







2263
2264
2265
2266





















2267
2268
2269
2270
2271
2272
2273
  /* sorting the reference array */
  if( blob_size(&rndr.refs) ){
    qsort(blob_buffer(&rndr.refs),
          blob_size(&rndr.refs)/sizeof(struct link_ref),
          sizeof(struct link_ref),
          cmp_link_ref_sort);
  }








  /* second pass: actual rendering */
  if( rndr.make.prolog ) rndr.make.prolog(ob, rndr.make.opaque);
  parse_block(ob, &rndr, blob_buffer(&text), blob_size(&text));





















  if( rndr.make.epilog ) rndr.make.epilog(ob, rndr.make.opaque);

  /* clean-up */
  assert( rndr.iDepth==0 );
  blob_reset(&text);
  lr = (struct link_ref *)blob_buffer(&rndr.refs);
  end = blob_size(&rndr.refs)/sizeof(struct link_ref);







>
>
>
>
>
>
>




>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
  /* sorting the reference array */
  if( blob_size(&rndr.refs) ){
    qsort(blob_buffer(&rndr.refs),
          blob_size(&rndr.refs)/sizeof(struct link_ref),
          sizeof(struct link_ref),
          cmp_link_ref_sort);
  }
  /* sorting the footnotes array by id */
  if( blob_size(&rndr.footnotes) ){
    qsort(blob_buffer(&rndr.footnotes),
          blob_size(&rndr.footnotes)/sizeof(struct footnote),
          sizeof(struct footnote),
          cmp_link_ref_sort);
  }

  /* second pass: actual rendering */
  if( rndr.make.prolog ) rndr.make.prolog(ob, rndr.make.opaque);
  parse_block(ob, &rndr, blob_buffer(&text), blob_size(&text));

  /* sorting the footnotes array by index */
  if( blob_size(&rndr.footnotes) ){
    qsort(blob_buffer(&rndr.footnotes),
          blob_size(&rndr.footnotes)/sizeof(struct footnote),
          sizeof(struct footnote),
          cmp_footnote_sort);
  }
  /* FIXME: decouple parsing and HTML-specific rendering of footnotes */
  if( rndr.nLabels ){
    fn = (struct footnote *)blob_buffer(&rndr.footnotes);
    end = blob_size(&rndr.footnotes)/sizeof(struct footnote);
    blob_appendf(ob, "\n<ol class='footnotes'>\n");
    for(i=0; i<end; i++){
      if(fn[i].index == 0) continue;
      blob_appendf(ob, "<li id='footnote-%i'>\n  ", fn[i].index );
      parse_inline(ob,&rndr,blob_buffer(&fn[i].text),blob_size(&fn[i].text));
      blob_append(ob,"\n</li>\n",7);
    }
    blob_append(ob, "</ol>\n", 7);
  }
  if( rndr.make.epilog ) rndr.make.epilog(ob, rndr.make.opaque);

  /* clean-up */
  assert( rndr.iDepth==0 );
  blob_reset(&text);
  lr = (struct link_ref *)blob_buffer(&rndr.refs);
  end = blob_size(&rndr.refs)/sizeof(struct link_ref);