/* ** Copyright (c) 2012 D. Richard Hipp ** ** This program is free software; you can redistribute it and/or ** modify it under the terms of the Simplified BSD License (also ** known as the "2-Clause License" or "FreeBSD License".) ** This program is distributed in the hope that it will be useful, ** but without any warranty; without even the implied warranty of ** merchantability or fitness for a particular purpose. ** ** Author contact information: ** drh@hwaci.com ** http://www.hwaci.com/drh/ ** ******************************************************************************* ** ** This file contains code to parse a blob containing markdown text, ** using an external renderer. */ #include "config.h" #include "markdown.h" #include #include #include #define MKD_LI_END 8 /* internal list flag */ /******************** * TYPE DEFINITIONS * ********************/ #if INTERFACE /* mkd_autolink -- type of autolink */ enum mkd_autolink { MKDA_NOT_AUTOLINK, /* used internally when it is not an autolink*/ MKDA_NORMAL, /* normal http/http/ftp link */ MKDA_EXPLICIT_EMAIL, /* e-mail link with explicit mailto: */ MKDA_IMPLICIT_EMAIL /* e-mail link without mailto: */ }; /* mkd_renderer -- functions for rendering parsed data */ struct mkd_renderer { /* document level callbacks */ void (*prolog)(struct Blob *ob, void *opaque); void (*epilog)(struct Blob *ob, void *opaque); void (*footnotes)(struct Blob *ob, const struct Blob *items, void *opaque); /* block level callbacks - NULL skips the block */ void (*blockcode)(struct Blob *ob, struct Blob *text, void *opaque); void (*blockquote)(struct Blob *ob, struct Blob *text, void *opaque); void (*blockhtml)(struct Blob *ob, struct Blob *text, void *opaque); void (*header)(struct Blob *ob, struct Blob *text, int level, void *opaque); void (*hrule)(struct Blob *ob, void *opaque); void (*list)(struct Blob *ob, struct Blob *text, int flags, void *opaque); void (*listitem)(struct Blob *ob, struct Blob *text, int flags, void *opaque); void (*paragraph)(struct Blob *ob, struct Blob *text, void *opaque); void (*table)(struct Blob *ob, struct Blob *head_row, struct Blob *rows, void *opaque); void (*table_cell)(struct Blob *ob, struct Blob *text, int flags, void *opaque); void (*table_row)(struct Blob *ob, struct Blob *cells, int flags, void *opaque); void (*footnote_item)(struct Blob *ob, const struct Blob *text, int index, int nUsed, void *opaque); /* span level callbacks - NULL or return 0 prints the span verbatim */ int (*autolink)(struct Blob *ob, struct Blob *link, enum mkd_autolink type, void *opaque); int (*codespan)(struct Blob *ob, struct Blob *text, int nSep, void *opaque); int (*double_emphasis)(struct Blob *ob, struct Blob *text, char c, void *opaque); int (*emphasis)(struct Blob *ob, struct Blob *text, char c,void*opaque); int (*image)(struct Blob *ob, struct Blob *link, struct Blob *title, struct Blob *alt, void *opaque); int (*linebreak)(struct Blob *ob, void *opaque); int (*link)(struct Blob *ob, struct Blob *link, struct Blob *title, struct Blob *content, void *opaque); int (*raw_html_tag)(struct Blob *ob, struct Blob *tag, void *opaque); int (*triple_emphasis)(struct Blob *ob, struct Blob *text, char c, void *opaque); int (*footnote_ref)(struct Blob *ob, const struct Blob *span, const struct Blob *upc, int index, int locus, void *opaque); /* low level callbacks - NULL copies input directly into the output */ void (*entity)(struct Blob *ob, struct Blob *entity, void *opaque); void (*normal_text)(struct Blob *ob, struct Blob *text, void *opaque); /* renderer data */ const char *emph_chars; /* chars that trigger emphasis rendering */ void *opaque; /* opaque data send to every rendering callback */ }; /********* * FLAGS * *********/ /* list/listitem flags */ #define MKD_LIST_ORDERED 1 #define MKD_LI_BLOCK 2 /*
  • containing block data */ /* table cell flags */ #define MKD_CELL_ALIGN_DEFAULT 0 #define MKD_CELL_ALIGN_LEFT 1 #define MKD_CELL_ALIGN_RIGHT 2 #define MKD_CELL_ALIGN_CENTER 3 /* LEFT | RIGHT */ #define MKD_CELL_ALIGN_MASK 3 #define MKD_CELL_HEAD 4 #endif /* INTERFACE */ #define BLOB_COUNT(pBlob,el_type) (blob_size(pBlob)/sizeof(el_type)) #define COUNT_FOOTNOTES(pBlob) BLOB_COUNT(pBlob,struct footnote) #define CAST_AS_FOOTNOTES(pBlob) ((struct footnote*)blob_buffer(pBlob)) /*************** * 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; }; /* ** A footnote's data. ** id, text, and upc fields must be in that particular order. */ 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 */ struct Blob upc; /* user-provided classes .ASCII-alnum.or-hypen: */ int bRndred; /* indicates if `text` holds a rendered content */ int defno; /* serial number of definition, set during the first pass */ int index; /* set to the index within array after ordering by id */ int iMark; /* user-visible numeric marker, assigned upon the first use*/ int nUsed; /* counts references to this note, increments upon each use*/ }; #define FOOTNOTE_INITIALIZER {empty_blob,empty_blob,empty_blob, 0,0,0,0,0} /* 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 */ struct { Blob all; /* Buffer that holds array of footnotes. Its underline memory may be reallocated when a new footnote is added. */ int nLbled; /* number of labeled footnotes found during the first pass */ int nMarks; /* counts distinct indices found during the second pass */ struct footnote misref; /* nUsed counts misreferences, iMark must be -1 */ } notes; }; /* html_tag -- structure for quick HTML tag search (inspired from discount) */ struct html_tag { const char *text; int size; }; /******************** * GLOBAL VARIABLES * ********************/ /* block_tags -- recognised block tags, sorted by cmp_html_tag. ** ** When these HTML tags are separated from other text by newlines ** then they are rendered verbatim. Their content is not interpreted ** in any way. */ static const struct html_tag block_tags[] = { { "html", 4 }, { "pre", 3 }, { "script", 6 }, }; /*************************** * STATIC HELPER FUNCTIONS * ***************************/ /* ** build_ref_id -- collapse whitespace from input text to make it a ref id. ** Potential TODO: maybe also handle CR+LF line endings? */ static int build_ref_id(struct Blob *id, const char *data, size_t size){ size_t beg, i; char *id_data; /* skip leading whitespace */ while( size>0 && (data[0]==' ' || data[0]=='\t' || data[0]=='\n') ){ data++; size--; } /* skip trailing whitespace */ while( size>0 && (data[size-1]==' ' || data[size-1]=='\t' || data[size-1]=='\n') ){ size--; } if( size==0 ) return -1; /* making the ref id */ i = 0; blob_reset(id); while( i='A' && id_data[i]<='Z' ) id_data[i] += 'a' - 'A'; } return 0; } /* cmp_link_ref -- comparison function for link_ref sorted arrays */ static int cmp_link_ref(const void *key, const void *array_entry){ struct link_ref *lr = (void *)array_entry; return blob_compare((void *)key, &lr->id); } /* 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_id -- comparison function for footnotes qsort. ** Empty IDs sort last (in undetermined order). ** Equal IDs are sorted in the order of definition in the source. */ static int cmp_footnote_id(const void *fna, const void *fnb){ const struct footnote *a = fna, *b = fnb; const int szA = blob_size(&a->id), szB = blob_size(&b->id); if( szA ){ if( szB ){ int cmp = blob_compare(&a->id, &b->id); if( cmp ) return cmp; }else return -1; }else return szB ? 1 : 0; /* IDs are equal and non-empty */ if( a->defno < b->defno ) return -1; if( a->defno > b->defno ) return 1; assert(!"reachable"); return 0; /* should never reach here */ } /* ** cmp_footnote_sort -- comparison function for footnotes qsort. ** Unreferenced footnotes (when nUsed == 0) sort last and ** are sorted in the order of definition in the source. */ static int cmp_footnote_sort(const void *fna, const void *fnb){ const struct footnote *a = fna, *b = fnb; int i, j; assert( a->nUsed >= 0 ); assert( b->nUsed >= 0 ); assert( a->defno >= 0 ); assert( b->defno >= 0 ); if( a->nUsed ){ assert( a->iMark > 0 ); if( !b->nUsed ) return -1; assert( b->iMark > 0 ); i = a->iMark; j = b->iMark; }else{ if( b->nUsed ) return 1; i = a->defno; j = b->defno; } if( i < j ) return -1; if( i > j ) return 1; return 0; } /* 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; if( htb->sizesize; c = fossil_strnicmp(hta->text, htb->text, sz); if( c==0 ) c = hta->size - htb->size; return c; } /* find_block_tag -- returns the current block tag */ static const struct html_tag *find_block_tag(const char *data, size_t size){ size_t i = 0; struct html_tag key; /* looking for the word end */ while( i='0' && data[i]<='9') || (data[i]>='A' && data[i]<='Z') || (data[i]>='a' && data[i]<='z')) ){ i++; } if( i>=size ) return 0; /* binary search of the tag */ key.text = data; key.size = i; return bsearch(&key, block_tags, count(block_tags), sizeof block_tags[0], cmp_html_tag); } /* return true if recursion has gone too deep */ static int too_deep(struct render *rndr){ return rndr->iDepth>200; } /* get a new working buffer from the cache or create one. return NULL ** if failIfDeep is true and the depth of recursion has gone too deep. */ static struct Blob *new_work_buffer(struct render *rndr){ struct Blob *ret; rndr->iDepth++; if( rndr->nBlobCache ){ ret = rndr->aBlobCache[--rndr->nBlobCache]; }else{ ret = fossil_malloc(sizeof(*ret)); } *ret = empty_blob; return ret; } /* release the given working buffer back to the cache */ static void release_work_buffer(struct render *rndr, struct Blob *buf){ if( !buf ) return; rndr->iDepth--; blob_reset(buf); if( rndr->nBlobCache < (int)(sizeof(rndr->aBlobCache)/sizeof(rndr->aBlobCache[0])) ){ rndr->aBlobCache[rndr->nBlobCache++] = buf; }else{ fossil_free(buf); } } /**************************** * INLINE PARSING FUNCTIONS * ****************************/ /* is_mail_autolink -- looks for the address part of a mail autolink and '>' */ /* this is less strict than the original markdown e-mail address matching */ static size_t is_mail_autolink(char *data, size_t size){ size_t i = 0, nb = 0; /* address is assumed to be: [-@._a-zA-Z0-9]+ with exactly one '@' */ while( i='a' && data[i]<='z') || (data[i]>='A' && data[i]<='Z') || (data[i]>='0' && data[i]<='9')) ){ if( data[i]=='@' ) nb++; i++; } if( i>=size || data[i]!='>' || nb!=1 ) return 0; return i+1; } /* tag_length -- returns the length of the given tag, or 0 if it's not valid */ static size_t tag_length(char *data, size_t size, enum mkd_autolink *autolink){ size_t i, j; /* a valid tag can't be shorter than 3 chars */ if( size<3 ) return 0; /* begins with a '<' optionally followed by '/', followed by letter */ if( data[0]!='<' ) return 0; i = (data[1]=='/') ? 2 : 1; if( (data[i]<'a' || data[i]>'z') && (data[i]<'A' || data[i]>'Z') ){ if( data[1]=='!' && size>=7 && data[2]=='-' && data[3]=='-' ){ for(i=6; i6 && fossil_strnicmp(data+1, "http", 4)==0 && (data[5]==':' || ((data[5]=='s' || data[5]=='S') && data[6]==':')) ){ i = (data[5]==':') ? 6 : 7; *autolink = MKDA_NORMAL; }else if( size>5 && fossil_strnicmp(data+1, "ftp:", 4)==0 ){ i = 5; *autolink = MKDA_NORMAL; }else if( size>7 && fossil_strnicmp(data+1, "mailto:", 7)==0 ){ i = 8; /* not changing *autolink to go to the address test */ } /* completing autolink test: no whitespace or ' or " */ if( i>=size || i=='>' ){ *autolink = MKDA_NOT_AUTOLINK; }else if( *autolink ){ j = i; while( i=size ) return 0; if( i>j && data[i]=='>' ) return i+1; /* one of the forbidden chars has been found */ *autolink = MKDA_NOT_AUTOLINK; }else if( (j = is_mail_autolink(data+i, size-i))!=0 ){ *autolink = (i==8) ? MKDA_EXPLICIT_EMAIL : MKDA_IMPLICIT_EMAIL; return i+j; } /* looking for something looking like a tag end */ while( i=size ) return 0; return i+1; } /* parse_inline -- parses inline markdown elements */ static void parse_inline( struct Blob *ob, struct render *rndr, char *data, size_t size ){ size_t i = 0, end = 0; char_trigger action = 0; struct Blob work = BLOB_INITIALIZER; if( too_deep(rndr) ){ blob_append(ob, data, size); return; } while( iactive_char[(unsigned char)data[end]])==0 ){ end++; } if( end>i ){ if( rndr->make.normal_text ){ blob_init(&work, data+i, end-i); rndr->make.normal_text(ob, &work, rndr->make.opaque); }else{ blob_append(ob, data+i, end-i); } } if( end>=size ) break; i = end; /* calling the trigger */ end = action(ob, rndr, data+i, i, size-i); if( !end ){ /* no action from the callback */ end = i+1; }else{ i += end; end = i; } } } /* ** data[*pI] should be a "`" character that introduces a code-span. ** The code-span boundry mark can be any number of one or more "`" ** characters. We do not know the size of the boundry marker, only ** that there is at least one "`" at data[*pI]. ** ** This routine increases *pI to move it past the code-span, including ** the closing boundary mark. Or, if the code-span is unterminated, ** this routine moves *pI past the opening boundary mark only. */ static void skip_codespan(const char *data, size_t size, size_t *pI){ size_t i = *pI; size_t span_nb; /* Number of "`" characters in the boundary mark */ size_t bt; assert( i=size ){ *pI += span_nb; return; } /* finding the matching closing sequence */ bt = 0; while( i=size ) return 0; /* not counting escaped chars */ if( i && data[i-1]=='\\' ){ i++; continue; } if( data[i]==c ) return i; if( data[i]=='`' ){ /* skip a code span */ skip_codespan(data, size, &i); }else if( data[i]=='[' ){ /* skip a link */ size_t tmp_i = 0; char cc; i++; while( i=size ) return tmp_i; if( data[i]!='[' && data[i]!='(' ){ /* not a link*/ if( tmp_i ) return tmp_i; else continue; } cc = data[i]; i++; while( i=size ) return tmp_i; i++; } } return 0; } /* CommonMark defines separate "right-flanking" and "left-flanking" ** deliminators for emphasis. Whether a deliminator is left- or ** right-flanking, or both, or neither depends on the characters ** immediately before and after. ** ** before after example left-flanking right-flanking ** ------ ----- ------- ------------- -------------- ** space space * no no ** space punct *) yes no ** space alnum *x yes no ** punct space (* no yes ** punct punct (*) yes yes ** punct alnum (*x yes no ** alnum space a* no yes ** alnum punct a*( no yes ** alnum alnum a*x yes yes ** ** The following routines determine whether a delimitor is left ** or right flanking. */ static int left_flanking(char before, char after){ if( fossil_isspace(after) ) return 0; if( fossil_isalnum(after) ) return 1; if( fossil_isalnum(before) ) return 0; return 1; } static int right_flanking(char before, char after){ if( fossil_isspace(before) ) return 0; if( fossil_isalnum(before) ) return 1; if( fossil_isalnum(after) ) return 0; return 1; } /* ** parse_emph1 -- parsing single emphasis. ** closed by a symbol not preceded by whitespace and not followed by symbol. */ static size_t parse_emph1( struct Blob *ob, struct render *rndr, char *data, size_t size, char c ){ size_t i = 0, len; struct Blob *work = 0; int r; char after; if( !rndr->make.emphasis ) return 0; /* skipping one symbol if coming from emph3 */ if( size>1 && data[0]==c && data[1]==c ) i = 1; while( i=size ) return 0; if( i+1make.emphasis(ob, work, c, rndr->make.opaque); release_work_buffer(rndr, work); return r ? i+1 : 0; } } return 0; } /* ** parse_emph2 -- parsing single emphasis. */ static size_t parse_emph2( struct Blob *ob, struct render *rndr, char *data, size_t size, char c ){ size_t i = 0, len; struct Blob *work = 0; int r; char after; if( !rndr->make.double_emphasis ) return 0; while( imake.double_emphasis(ob, work, c, rndr->make.opaque); release_work_buffer(rndr, work); return r ? i+2 : 0; } i++; } return 0; } /* ** parse_emph3 -- parsing single emphasis. ** finds the first closing tag, and delegates to the other emph. */ static size_t parse_emph3( struct Blob *ob, struct render *rndr, char *data, size_t size, char c ){ size_t i = 0, len; int r; while( imake.triple_emphasis && !too_deep(rndr) ){ /* triple symbol found */ struct Blob *work = new_work_buffer(rndr); parse_inline(work, rndr, data, i); r = rndr->make.triple_emphasis(ob, work, c, rndr->make.opaque); release_work_buffer(rndr, work); return r ? i+3 : 0; }else if( i+10 ? data[-1] : ' '; size_t ret; if( size>2 && data[1]!=c ){ if( !left_flanking(before, data[1]) || (c=='_' && fossil_isalnum(before)) || (ret = parse_emph1(ob, rndr, data+1, size-1, c))==0 ){ return 0; } return ret+1; } if( size>3 && data[1]==c && data[2]!=c ){ if( !left_flanking(before, data[2]) || (c=='_' && fossil_isalnum(before)) || (ret = parse_emph2(ob, rndr, data+2, size-2, c))==0 ){ return 0; } return ret+2; } if( size>4 && data[1]==c && data[2]==c && data[3]!=c ){ if( !left_flanking(before, data[3]) || (c=='_' && fossil_isalnum(before)) || (ret = parse_emph3(ob, rndr, data+3, size-3, c))==0 ){ return 0; } return ret+3; } return 0; } /* ** char_linebreak -- '\n' preceded by two spaces (assuming linebreak != 0). */ static size_t char_linebreak( struct Blob *ob, struct render *rndr, char *data, size_t offset, size_t size ){ if( offset<2 || data[-1]!=' ' || data[-2]!=' ' ) return 0; /* removing the last space from ob and rendering */ if( blob_size(ob)>0 && blob_buffer(ob)[blob_size(ob)-1]==' ' ) ob->nUsed--; return rndr->make.linebreak(ob, rndr->make.opaque) ? 1 : 0; } /* ** char_codespan -- '`' parsing a code span (assuming codespan != 0). */ static size_t char_codespan( struct Blob *ob, struct render *rndr, char *data, size_t offset, size_t size ){ size_t end, nb = 0, i, f_begin, f_end; char delim = data[0]; /* counting the number of backticks in the delimiter */ while( nb=size ) return 0; /* no matching delimiter */ /* trimming outside whitespaces */ f_begin = nb; while( f_beginnb && (data[f_end-1]==' ' || data[f_end-1]=='\t') ){ f_end--; } /* real code span */ if( f_beginmake.codespan(ob, &work, nb, rndr->make.opaque) ) end = 0; }else{ if( !rndr->make.codespan(ob, 0, nb, rndr->make.opaque) ) end = 0; } return end; } /* ** char_escape -- '\\' backslash escape. */ static size_t char_escape( struct Blob *ob, struct render *rndr, char *data, size_t offset, size_t size ){ struct Blob work = BLOB_INITIALIZER; if( size>1 ){ if( rndr->make.normal_text ){ blob_init(&work, data+1,1); rndr->make.normal_text(ob, &work, rndr->make.opaque); }else{ blob_append(ob, data+1, 1); } } return 2; } /* ** char_entity -- '&' escaped when it doesn't belong to an entity. ** valid entities are assumed to be anything matching &#?[A-Za-z0-9]+; */ static size_t char_entity( struct Blob *ob, struct render *rndr, char *data, size_t offset, size_t size ){ size_t end = 1; struct Blob work = BLOB_INITIALIZER; if( end='0' && data[end]<='9') || (data[end]>='a' && data[end]<='z') || (data[end]>='A' && data[end]<='Z')) ){ end++; } if( endmake.entity ){ blob_init(&work, data, end); rndr->make.entity(ob, &work, rndr->make.opaque); }else{ blob_append(ob, data, end); } return end; } /* ** char_langle_tag -- '<' when tags or autolinks are allowed. */ static size_t char_langle_tag( struct Blob *ob, struct render *rndr, char *data, size_t offset, size_t size ){ enum mkd_autolink altype = MKDA_NOT_AUTOLINK; size_t end = tag_length(data, size, &altype); struct Blob work = BLOB_INITIALIZER; int ret = 0; if( end ){ if( rndr->make.autolink && altype!=MKDA_NOT_AUTOLINK ){ blob_init(&work, data+1, end-2); ret = rndr->make.autolink(ob, &work, altype, rndr->make.opaque); }else if( rndr->make.raw_html_tag ){ blob_init(&work, data, end); ret = rndr->make.raw_html_tag(ob, &work, rndr->make.opaque); } } if( !ret ){ return 0; }else{ return end; } } /* ** get_link_inline -- extract inline-style link and title from ** parenthesed data */ static int get_link_inline( struct Blob *link, struct Blob *title, char *data, size_t size ){ size_t i = 0, mark; size_t link_b, link_e; size_t title_b = 0, title_e = 0; /* skipping initial whitespace */ while( ititle_b && (data[title_e]==' ' || data[title_e]=='\t' || data[title_e]=='\n') ){ title_e--; } /* checking for closing quote presence */ if (data[title_e] != '\'' && data[title_e] != '"') { title_b = title_e = 0; link_e = i; } } /* remove whitespace at the end of the link */ while( link_e>link_b && (data[link_e-1]==' ' || data[link_e-1]=='\t' || data[link_e-1]=='\n') ){ link_e--; } /* remove optional angle brackets around the link */ if( data[link_b]=='<' ) link_b += 1; if( link_e && data[link_e-1]=='>' ) link_e -= 1; /* escape backslashed character from link */ blob_reset(link); i = link_b; while( ititle_b ) blob_append(title, data+title_b, title_e-title_b); /* this function always succeed */ return 0; } /* ** get_link_ref -- extract referenced link and title from id. */ static int get_link_ref( struct render *rndr, struct Blob *link, struct Blob *title, char *data, size_t size ){ struct link_ref *lr; const size_t sz = blob_size(&rndr->refs); /* find the link from its id (stored temporarily in link) */ blob_reset(link); if( !sz || build_ref_id(link, data, size)<0 ) return -1; lr = bsearch(link, blob_buffer(&rndr->refs), sz/sizeof(struct link_ref), sizeof (struct link_ref), cmp_link_ref); if( !lr ) return -1; /* fill the output buffers */ blob_reset(link); blob_reset(title); blob_appendb(link, &lr->link); blob_appendb(title, &lr->title); return 0; } /* ** get_footnote() -- find a footnote by label, invoked during the 2nd pass. ** If found then return a shallow copy of the corresponding footnote; ** otherwise return a shallow copy of rndr->notes.misref. ** In both cases corresponding `nUsed` field is incremented before return. */ static struct footnote get_footnote( struct render *rndr, const char *data, size_t size ){ struct footnote *fn = 0; struct Blob *id; if( !rndr->notes.nLbled ) goto fallback; id = new_work_buffer(rndr); if( build_ref_id(id, data, size)<0 ) goto cleanup; fn = bsearch(id, blob_buffer(&rndr->notes.all), rndr->notes.nLbled, sizeof (struct footnote), cmp_link_ref); if( !fn ) goto cleanup; if( fn->nUsed == 0 ){ /* the first reference to the footnote */ assert( fn->iMark == 0 ); fn->iMark = ++(rndr->notes.nMarks); } assert( fn->iMark > 0 ); cleanup: release_work_buffer( rndr, id ); fallback: if( !fn ) fn = &rndr->notes.misref; fn->nUsed++; assert( fn->nUsed > 0 ); return *fn; } /* ** Counts characters in the blank prefix within at most nHalfLines. ** A sequence of spaces and tabs counts as odd halfline, ** a newline counts as even halfline. ** If nHalfLines < 0 then proceed without constraints. */ static inline size_t sizeof_blank_prefix( const char *data, size_t size, int nHalfLines ){ const char *p = data; const char * const end = data+size; if( nHalfLines < 0 ){ while( p!=end && fossil_isspace(*p) ){ p++; } }else while( nHalfLines > 0 ){ while( p!=end && (*p==' ' || *p=='\t' ) ){ p++; } if( p==end || --nHalfLines == 0 ) break; if( *p=='\n' || *p=='\r' ){ p++; if( p==end ) break; if( *p=='\n' && p[-1]=='\r' ){ p++; } } nHalfLines--; } return p-data; } /* ** Check if the data starts with a classlist token of the special form. ** If so then return the length of that token, otherwise return 0. ** ** The token must start with a dot and must end with a colon; ** in between of these it must be a dot-separated list of words; ** each word may contain only alphanumeric characters and hyphens. ** ** If `bBlank` is non-zero then a blank character must follow ** the token's ending colon: otherwise function returns 0 ** despite the well-formed token. */ static size_t is_footnote_classlist(const char * const data, size_t size, int bBlank){ const char *p; const char * const end = data+size; if( data==end || *data != '.' ) return 0; for(p=data+1; p!=end; p++){ if( fossil_isalnum(*p) || *p=='-' ) continue; if( p[-1]=='.' ) break; if( *p==':' ){ p++; if( bBlank ){ if( p==end || !fossil_isspace(*p) ) break; } return p-data; } if( *p!='.' ) break; } return 0; } /* ** Adds unlabeled footnote to the rndr->notes.all. ** On success puts a shallow copy of the constructed footnote into pFN ** and returns 1, otherwise pFN is unchanged and 0 is returned. */ static inline int add_inline_footnote( struct render *rndr, const char *text, size_t size, struct footnote* pFN ){ struct footnote fn = FOOTNOTE_INITIALIZER, *last; const char *zUPC = 0; size_t nUPC = 0, n = sizeof_blank_prefix(text, size, 3); if( n >= size ) return 0; text += n; size -= n; nUPC = is_footnote_classlist(text, size, 1); if( nUPC ){ assert( nUPCnotes.nMarks); fn.nUsed = 1; fn.index = COUNT_FOOTNOTES(&rndr->notes.all); assert( fn.iMark > 0 ); blob_append(&fn.text, text, size); if(nUPC) blob_append(&fn.upc, zUPC, nUPC); blob_append(&rndr->notes.all, (char *)&fn, sizeof fn); last = (struct footnote*)( blob_buffer(&rndr->notes.all) +( blob_size(&rndr->notes.all)-sizeof fn )); assert( pFN ); memcpy( pFN, last, sizeof fn ); return 1; } /* ** Return the byte offset of the matching closing bracket or 0 if not ** found. begin[0] must be either '[' or '('. ** ** TODO: It seems that things like "\\(" are not handled correctly. ** That is historical behavior for a corner-case, ** so it's left as it is until somebody complains. */ static inline size_t matching_bracket_offset( const char* begin, const char* end ){ const char *i; int level; const char bra = *begin; const char ket = bra=='[' ? ']' : ')'; assert( bra=='[' || bra=='(' ); for(i=begin+1,level=1; i!=end; i++){ if( *i=='\n' ) /* do nothing */; else if( i[-1]=='\\' ) continue; else if( *i==bra ) level++; else if( *i==ket ){ if( --level<=0 ) return i-begin; } } return 0; } /* ** char_footnote -- '(': parsing a standalone inline footnote. */ static size_t char_footnote( struct Blob *ob, struct render *rndr, char *data, size_t offset, size_t size ){ size_t end; struct footnote fn; if( size<4 || data[1]!='^' ) return 0; end = matching_bracket_offset(data, data+size); if( !end ) return 0; if( !add_inline_footnote(rndr, data+2, end-2, &fn) ) return 0; if( rndr->make.footnote_ref ){ rndr->make.footnote_ref(ob,0,&fn.upc,fn.iMark,1,rndr->make.opaque); } return end+1; } /* ** 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 /* parse_inline() ensures that size > 0 */ ){ const int bFsfn = (size>3 && data[1]=='^'); /*free-standing footnote ref*/ const int bImg = !bFsfn && (offset && data[-1] == '!'); size_t i, txt_e; struct Blob *content; struct Blob *link; struct Blob *title; struct footnote fn; int ret; /* checking whether the correct renderer exists */ if( !bFsfn ){ if( (bImg && !rndr->make.image) || (!bImg && !rndr->make.link) ){ return 0; } } /* looking for the matching closing bracket */ txt_e = matching_bracket_offset(data, data+size); if( !txt_e ) return 0; i = txt_e + 1; ret = 0; /* error if we don't get to the callback */ /* free-standing footnote reference */ if( bFsfn ){ fn = get_footnote(rndr, data+2, txt_e-2); content = link = title = 0; }else{ fn.nUsed = 0; /* skip "inter-bracket-whitespace" - any amount of whitespace or newline */ /* (this is much more lax than original markdown syntax) */ while( i=size || get_link_inline(link, title, data+i+1, span_end-(i+1))<0 ){ goto char_link_cleanup; } i = span_end+1; } /* reference style link or span-bounded footnote reference */ }else if( i=size ) goto char_link_cleanup; bFootnote = data[i+1]=='^'; if( i+1==id_end || (bFootnote && i+2==id_end) ){ /* implicit id - use the contents */ 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( bFootnote ){ id_data++; id_size--; } } if( bFootnote ){ fn = get_footnote(rndr, id_data, id_size); }else 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 an "inter-bracket-whitespace" */ i = txt_e+1; } } /* building content: img alt is escaped, link content is parsed */ if( txt_e>1 && content ){ if( bImg ) blob_append(content, data+1, txt_e-1); else parse_inline(content, rndr, data+1, txt_e-1); } /* calling the relevant rendering function */ if( bImg ){ 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 if( fn.nUsed ){ if( rndr->make.footnote_ref ){ ret = rndr->make.footnote_ref(ob, content, &fn.upc, fn.iMark, fn.nUsed, rndr->make.opaque); } }else{ ret = rndr->make.link(ob, link, title, content, rndr->make.opaque); } /* cleanup */ char_link_cleanup: release_work_buffer(rndr, title); release_work_buffer(rndr, link); release_work_buffer(rndr, content); return ret ? i : 0; } /********************************* * BLOCK-LEVEL PARSING FUNCTIONS * *********************************/ /* is_empty -- returns the line length when it is empty, 0 otherwise */ static size_t is_empty(const char *data, size_t size){ size_t i; for(i=0; i=size || (data[i]!='*' && data[i]!='-' && data[i]!='_') ) return 0; c = data[i]; /* the whole line must be the char or whitespace */ while (i < size && data[i] != '\n') { if( data[i]==c ){ n += 1; }else if( data[i]!=' ' && data[i]!='\t' ){ return 0; } i++; } return n>=3; } /* is_headerline -- returns whether the line is a setext-style hdr underline */ static int is_headerline(char *data, size_t size){ size_t i = 0; /* test of level 1 header */ if( data[i]=='=' ){ for(i=1; i=size || data[i]=='\n') ? 1 : 0; } /* test of level 2 header */ if( data[i]=='-' ){ for(i=1; i=size || data[i]=='\n') ? 2 : 0; } return 0; } /* is_table_sep -- returns whether there is a table separator at pos */ static int is_table_sep(char *data, size_t pos){ return data[pos]=='|' && (pos==0 || data[pos-1]!='\\'); } /* is_tableline -- returns the number of column tables in the given line */ static int is_tableline(char *data, size_t size){ size_t i = 0; int n_sep = 0, outer_sep = 0; /* skip initial blanks */ while( i0) ? (n_sep-outer_sep+1) : 0; } /* prefix_quote -- returns blockquote prefix length */ static size_t prefix_quote(char *data, size_t size){ size_t i = 0; if( i' ){ if( i+10 && data[0]=='\t' ) return 1; if( size>3 && data[0]==' ' && data[1]==' ' && data[2]==' ' && data[3]==' ' ){ return 4; } return 0; } /* Return the number of characters in the delimiter of a fenced code ** block. */ static size_t prefix_fencedcode(char *data, size_t size){ char c = data[0]; int nb; if( c!='`' && c!='~' ) return 0; for(nb=1; nb<(int)size-3 && data[nb]==c; nb++){} if( nb<3 ) return 0; if( nb>=(int)size-nb ) return 0; return nb; } /* prefix_oli -- returns ordered list item prefix */ static size_t prefix_oli(char *data, size_t size){ size_t i = 0; if( i=size || data[i]<'0' || data[i]>'9' ) return 0; while( i='0' && data[i]<='9' ){ i++; } if( i+1>=size || (data[i]!='.' && data[i]!=')') || (data[i+1]!=' ' && data[i+1]!='\t') ){ return 0; } i = i+2; while( i=size || (data[i]!='*' && data[i]!='+' && data[i]!='-') || (data[i+1]!=' ' && data[i+1]!='\t') ){ return 0; } i = i+2; while( i=size || (prefix_quote(data+end, size-end)==0 && !is_empty(data+end, size-end))) ){ /* empty line followed by non-quote line */ break; } if( begmake.blockquote ){ if( !too_deep(rndr) ){ parse_block(out, rndr, work_data, work_size); }else{ blob_append(out, work_data, work_size); } rndr->make.blockquote(ob, out, rndr->make.opaque); } release_work_buffer(rndr, out); return end; } /* parse_paragraph -- handles parsing of a regular paragraph */ static size_t parse_paragraph( struct Blob *ob, struct render *rndr, char *data, size_t size ){ size_t i = 0, end = 0; int level = 0; char *work_data = data; size_t work_size = 0; while( imake.paragraph ){ struct Blob *tmp = new_work_buffer(rndr); parse_inline(tmp, rndr, work_data, work_size); rndr->make.paragraph(ob, tmp, rndr->make.opaque); release_work_buffer(rndr, tmp); } }else{ if( work_size ){ size_t beg; i = work_size; work_size -= 1; while( work_size && data[work_size]!='\n' ){ work_size--; } beg = work_size+1; while( work_size && data[work_size-1]=='\n'){ work_size--; } if( work_size ){ struct Blob *tmp = new_work_buffer(rndr); parse_inline(tmp, rndr, work_data, work_size); if( rndr->make.paragraph ){ rndr->make.paragraph(ob, tmp, rndr->make.opaque); } release_work_buffer(rndr, tmp); work_data += beg; work_size = i - beg; }else{ work_size = i; } } if( rndr->make.header ){ struct Blob *span = new_work_buffer(rndr); parse_inline(span, rndr, work_data, work_size); rndr->make.header(ob, span, level, rndr->make.opaque); release_work_buffer(rndr, span); } } return end; } /* parse_blockcode -- handles parsing of a block-level code fragment */ static size_t parse_blockcode( struct Blob *ob, struct render *rndr, char *data, size_t size ){ size_t beg, end, pre; struct Blob *work = new_work_buffer(rndr); beg = 0; while( beg0 && blob_buffer(work)[end-1]=='\n' ){ end--; } work->nUsed = end; blob_append_char(work, '\n'); if( work!=ob ){ if( rndr->make.blockcode ){ rndr->make.blockcode(ob, work, rndr->make.opaque); } release_work_buffer(rndr, work); } return beg; } /* parse_listitem -- parsing of a single list item */ /* assuming initial prefix is already removed */ static size_t parse_listitem( struct Blob *ob, struct render *rndr, char *data, size_t size, int *flags ){ struct Blob *work = 0, *inter = 0; size_t beg = 0, end, pre, sublist = 0, orgpre = 0, i; int in_empty = 0, has_inside_empty = 0; /* keeping track of the first indentation prefix */ if( size>1 && data[0]==' ' ){ orgpre = 1; if( size>2 && data[1]==' ' ){ orgpre = 2; if( size>3 && data[2]==' ' ){ orgpre = 3; } } } beg = prefix_uli(data, size); if( !beg ) beg = prefix_oli(data, size); if( !beg ) return 0; /* skipping to the beginning of the following line */ end = beg; while( end1 && data[beg]==' ' ){ i = 1; if( end-beg>2 && data[beg+1]==' ' ){ i = 2; if( end-beg>3 && data[beg+2]==' ' ){ i = 3; if( end-beg>3 && data[beg+3]==' ' ){ i = 4; } } } } pre = i; if( data[beg]=='\t' ){ i = 1; pre = 8; } /* checking for a new item */ if( (prefix_uli(data+beg+i, end-beg-i) && !is_hrule(data+beg+i, end-beg-i)) || prefix_oli(data+beg+i, end-beg-i) ){ if( in_empty ) has_inside_empty = 1; if( pre == orgpre ){ /* the following item must have */ break; /* the same indentation */ } if( !sublist ) sublist = blob_size(work); /* joining only indented stuff after empty lines */ }else if( in_empty && i<4 && data[beg]!='\t' ){ *flags |= MKD_LI_END; break; }else if( in_empty ){ blob_append_char(work, '\n'); has_inside_empty = 1; } in_empty = 0; /* adding the line without prefix into the working buffer */ blob_append(work, data+beg+i, end-beg-i); beg = end; } /* non-recursive fallback when working buffer stack is full */ if( !inter ){ if( rndr->make.listitem ){ rndr->make.listitem(ob, work, *flags, rndr->make.opaque); } release_work_buffer(rndr, work); return beg; } /* render of li contents */ if( has_inside_empty ) *flags |= MKD_LI_BLOCK; if( *flags & MKD_LI_BLOCK ){ /* intermediate render of block li */ if( sublist && sublistmake.listitem ){ rndr->make.listitem(ob, inter, *flags, rndr->make.opaque); } release_work_buffer(rndr, inter); release_work_buffer(rndr, work); return beg; } /* parse_list -- parsing ordered or unordered list block */ static size_t parse_list( struct Blob *ob, struct render *rndr, char *data, size_t size, int flags ){ struct Blob *work = new_work_buffer(rndr); size_t i = 0, j; while( imake.list ) rndr->make.list(ob, work, flags, rndr->make.opaque); release_work_buffer(rndr, work); return i; } /* parse_atxheader -- parsing of atx-style headers */ static size_t parse_atxheader( struct Blob *ob, struct render *rndr, char *data, size_t size ){ int level = 0; size_t i, end, skip, span_beg, span_size; if( !size || data[0]!='#' ) return 0; while( level<(int)size && level<6 && data[level]=='#' ){ level++; } for(i=level; imake.header ){ struct Blob *span = new_work_buffer(rndr); parse_inline(span, rndr, data+span_beg, span_size); rndr->make.header(ob, span, level, rndr->make.opaque); release_work_buffer(rndr, span); } return skip; } /* htmlblock_end -- checking end of HTML block : [ \t]*\n[ \t*]\n */ /* returns the length on match, 0 otherwise */ static size_t htmlblock_end( const struct html_tag *tag, const char *data, size_t size ){ size_t i, w; /* assuming data[0]=='<' && data[1]=='/' already tested */ /* checking tag is a match */ if( (tag->size+3)>(int)size || fossil_strnicmp(data+2, tag->text, tag->size) || data[tag->size+2]!='>' ){ return 0; } /* checking white lines */ i = tag->size + 3; w = 0; if( i5 && data[1]=='!' && data[2]=='-' && data[3]=='-' ){ i = 5; while( i') ){ i++; } i++; if( imake.blockhtml ) return work_size; blob_init(&work, data, work_size); rndr->make.blockhtml(ob, &work, rndr->make.opaque); return work_size; } } } /* HR, which is the only self-closing block tag considered */ if( size>4 && (data[1]=='h' || data[1]=='H') && (data[2]=='r' || data[2]=='R') ){ i = 3; while( imake.blockhtml ) return work_size; blob_init(&work, data, work_size); rndr->make.blockhtml(ob, &work, rndr->make.opaque); return work_size; } } } /* no special case recognised */ return 0; } /* looking for an matching closing tag */ /* followed by a blank line */ i = 1; found = 0; while( isize)>size ) break; j = htmlblock_end(curtag, data+i-1, size-i+1); if (j) { i += j-1; found = 1; break; } } if( !found ) return 0; /* the end of the block has been found */ if( strcmp(curtag->text,"html")==0 ){ /* Omit tags */ enum mkd_autolink dummy; int k = tag_length(data, size, &dummy); int sz = i - (j+k); if( sz>0 ) blob_init(&work, data+k, sz); }else{ blob_init(&work, data, i); } if( rndr->make.blockhtml ){ rndr->make.blockhtml(ob, &work, rndr->make.opaque); } return i; } /* parse_table_cell -- parse a cell inside a table */ static void parse_table_cell( struct Blob *ob, /* output blob */ struct render *rndr, /* renderer description */ char *data, /* input text */ size_t size, /* input text size */ int flags /* table flags */ ){ struct Blob *span = new_work_buffer(rndr); parse_inline(span, rndr, data, size); rndr->make.table_cell(ob, span, flags, rndr->make.opaque); release_work_buffer(rndr, span); } /* parse_table_row -- parse an input line into a table row */ static size_t parse_table_row( struct Blob *ob, /* output blob for rendering */ struct render *rndr, /* renderer description */ char *data, /* input text */ size_t size, /* input text size */ int *aligns, /* array of default alignment for columns */ size_t align_size, /* number of columns with default alignment */ int flags /* table flags */ ){ size_t i = 0, col = 0; size_t beg, end, total = 0; struct Blob *cells = new_work_buffer(rndr); int align; /* skip leading blanks and separator */ while( ibeg && data[end-1]==':' ){ align |= MKD_CELL_ALIGN_RIGHT; end--; } /* remove trailing blanks */ while( end>beg && (data[end-1]==' ' || data[end-1]=='\t') ){ end--; } /* skip the last cell if it was only blanks */ /* (because it is only the optional end separator) */ if( total && end<=beg ) continue; /* fallback on default alignment if not explicit */ if( align==0 && aligns && col=beg ){ parse_table_cell(cells, rndr, data+beg, end-beg, align|flags); } col++; } /* render the whole row and clean up */ rndr->make.table_row(ob, cells, flags, rndr->make.opaque); release_work_buffer(rndr, cells); return total ? total : size; } /* parse_table -- parsing of a whole table */ static size_t parse_table( struct Blob *ob, struct render *rndr, char *data, size_t size ){ size_t i = 0, head_end, col; size_t align_size = 0; int *aligns = 0; struct Blob *head = 0; struct Blob *rows = new_work_buffer(rndr); /* skip the first (presumably header) line */ while( i=size ){ parse_table_row(rows, rndr, data, size, 0, 0, 0); rndr->make.table(ob, 0, rows, rndr->make.opaque); release_work_buffer(rndr, rows); return i; } /* attempt to parse a table rule, i.e. blanks, dash, colons and sep */ i++; col = 0; while( imake.table(ob, head, rows, rndr->make.opaque); /* cleanup */ release_work_buffer(rndr, head); release_work_buffer(rndr, rows); fossil_free(aligns); return i; } /* parse_block -- parsing of one block, returning next char to parse */ static void parse_block( struct Blob *ob, /* output blob */ struct render *rndr, /* renderer internal state */ char *data, /* input text */ size_t size /* input text size */ ){ size_t beg, end, i; char *txt_data; int has_table; if( !size ) return; has_table = (rndr->make.table && rndr->make.table_row && rndr->make.table_cell && memchr(data, '|', size)!=0); beg = 0; while( begmake.blockhtml && (i = parse_htmlblock(ob, rndr, txt_data, end))!=0 ){ beg += i; }else if( (i=is_empty(txt_data, end))!=0 ){ beg += i; }else if( is_hrule(txt_data, end) ){ if( rndr->make.hrule ) rndr->make.hrule(ob, rndr->make.opaque); while( beg=end ) return 0; if( data[beg]==' ' ){ i = 1; if( data[beg+1]==' ' ){ i = 2; if( data[beg+2]==' ' ){ i = 3; if( data[beg+3]==' ' ) return 0; } } } i += beg; /* id part: anything but a newline between brackets */ if( data[i]!='[' ) return 0; i++; if( i>=end || data[i]=='^' ) return 0; /* see is_footnote() */ id_offset = i; while( 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 ) return 0; /* link: whitespace-free sequence, optionally between angle brackets */ if( data[i]=='<' ) i++; link_offset = i; while( i' && data[link_offset-1]=='<' ? */ if( data[i-1]=='>' ) link_end = i-1; else link_end = i; /* optional spacer: (space | tab)* (newline | '\'' | '"' | '(' ) */ while( i=end || data[i]=='\r' || data[i]=='\n' ) line_end = i; if( i+1title_offset && (data[i]==' ' || data[i]=='\t') ){ i--; } if( i>title_offset && (data[i]=='\'' || data[i]=='"' || data[i]==')') ){ line_end = title_end; title_end = i; } } if( !line_end ) return 0; /* garbage after the link */ /* a valid ref has been found, filling-in return structures */ if( last ) *last = line_end; if( !refs ) return 1; if( build_ref_id(&lr.id, data+id_offset, id_end-id_offset)<0 ) return 0; blob_append(&lr.link, data+link_offset, link_end-link_offset); 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 -- check if data holds a definition of a labeled footnote. * If so then append the corresponding element to `footnotes` array */ static int is_footnote( const 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 ){ size_t i, id_offset, id_end, upc_offset, upc_size; struct footnote fn = FOOTNOTE_INITIALIZER; /* failfast if data is too short */ if( beg+5>=end ) return 0; i = beg; /* footnote definition must start at the begining of a line */ if( data[i]!='[' ) return 0; i++; if( data[i]!='^' ) return 0; id_offset = ++i; /* id part: anything but a newline between brackets */ while( i=end || data[i]!=']' ) return 0; id_end = i++; /* spacer: colon (space | tab)* */ if( i>=end || data[i]!=':' ) return 0; i++; while( i=end ) return 0; if( build_ref_id(&fn.id, data+id_offset, id_end-id_offset)<0 ) return 0; /* footnote's text may start on the same line after [^id]: */ upc_offset = upc_size = 0; if( data[i]!='\n' && data[i]!='\r' ){ size_t j; upc_size = is_footnote_classlist(data+i, end-i, 1); upc_offset = i; /* prevent further checks for a classlist */ i += upc_size; j = i; while( i=end ) goto footnote_finish; indent -= i; if( indent<2 ) goto footnote_finish; /* process the 2nd and subsequent lines */ while( i+indent=end ) break; blob_append_char(&fn.text, data[i]); i++; if( ibeg ) blob_append(&text, data + beg, end - beg); while( end 1 ){ int nDups = 0; fn = CAST_AS_FOOTNOTES( allNotes ); qsort(fn, rndr.notes.nLbled, sizeof(struct footnote), cmp_footnote_id); /* concatenate footnotes with equal labels */ for(i=0; itext) + 64 + blob_size(&x->upc); while(jid, &fn[j].id)){ k += blob_size(&fn[j].text) + 10 + blob_size(&fn[j].upc); j++; nDups++; } if( i+1\n"); for(k=i; (int)k"); if( blob_size(&y->upc) ){ blob_appendb(&list, &y->upc); blob_reset(&y->upc); } blob_appendb(&list, &y->text); blob_append_literal(&list, "
  • \n"); /* free memory buffer */ blob_reset(&y->text); if( (int)k!=i ) blob_reset(&y->id); } blob_append_literal(&list, "\n"); x->text = list; g.ftntsIssues[2]++; } i = j; } if( nDups ){ /* clean rndr.notes.all from invalidated footnotes */ const int n = rndr.notes.nLbled - nDups; struct Blob filtered = empty_blob; blob_reserve(&filtered, n*sizeof(struct footnote)); for(i=0; ibRndred || !x->nUsed ) continue; assert( x->iMark > 0 ); assert( blob_size(&x->text) ); blob_truncate(tmp,0); /* `allNotes` may be altered and extended through this call */ parse_inline(tmp, &rndr, blob_buffer(&x->text), blob_size(&x->text)); x = CAST_AS_FOOTNOTES(allNotes) + j; blob_truncate(&x->text,0); blob_appendb(&x->text, tmp); x->bRndred = 1; } } release_work_buffer(&rndr,tmp); } /* footnotes rendering */ if( rndr.make.footnote_item && rndr.make.footnotes ){ Blob *all_items = new_work_buffer(&rndr); int j = -1; /* Assert that the in-memory layout of id, text and upc within ** footnote struct matches the expectations of html_footnote_item() ** If it doesn't then a compiler has done something very weird. */ assert( &(rndr.notes.misref.id) == &(rndr.notes.misref.text) - 1 ); assert( &(rndr.notes.misref.upc) == &(rndr.notes.misref.text) + 1 ); for(i=0; i<(int)(COUNT_FOOTNOTES(notes)); i++){ const struct footnote* x = CAST_AS_FOOTNOTES(notes) + i; const int xUsed = x->bRndred ? x->nUsed : 0; if( !x->iMark ) break; assert( x->nUsed ); rndr.make.footnote_item(all_items, &x->text, x->iMark, xUsed, rndr.make.opaque); if( !xUsed ) g.ftntsIssues[3]++; /* an overnested footnote */ j = i; } if( rndr.notes.misref.nUsed ){ rndr.make.footnote_item(all_items, 0, -1, rndr.notes.misref.nUsed, rndr.make.opaque); g.ftntsIssues[0] += rndr.notes.misref.nUsed; } while( ++j < (int)(COUNT_FOOTNOTES(notes)) ){ const struct footnote* x = CAST_AS_FOOTNOTES(notes) + j; assert( !x->iMark ); assert( !x->nUsed ); assert( !x->bRndred ); rndr.make.footnote_item(all_items,&x->text,0,0,rndr.make.opaque); g.ftntsIssues[1]++; } rndr.make.footnotes(ob, all_items, rndr.make.opaque); release_work_buffer(&rndr, all_items); } release_work_buffer(&rndr, notes); } 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); for(i=0; i<(int)end; i++){ blob_reset(&lr[i].id); blob_reset(&lr[i].link); blob_reset(&lr[i].title); } blob_reset(&rndr.refs); fn = CAST_AS_FOOTNOTES( allNotes ); end = COUNT_FOOTNOTES( allNotes ); for(i=0; i<(int)end; i++){ if(blob_size(&fn[i].id)) blob_reset(&fn[i].id); if(blob_size(&fn[i].upc)) blob_reset(&fn[i].upc); blob_reset(&fn[i].text); } blob_reset(&rndr.notes.all); for(i=0; i