Index: Makefile.in ================================================================== --- Makefile.in +++ Makefile.in @@ -47,10 +47,17 @@ CFLAGS = @CFLAGS@ CFLAGS_INCLUDE = @CFLAGS_INCLUDE@ LIB = @LDFLAGS@ @EXTRA_LDFLAGS@ @LIBS@ BCCFLAGS = @CPPFLAGS@ $(CFLAGS) TCCFLAGS = @EXTRA_CFLAGS@ @CPPFLAGS@ $(CFLAGS) -DHAVE_AUTOCONFIG_H -D_HAVE_SQLITE_CONFIG_H +# +# Fuzzing may be enable by appending -fsanitize=fuzzer -DFOSSIL_FUZZ +# to the TCCFLAGS variable. +# For more thorouth (but also slower) investigation +# -fsanitize=fuzzer,undefined,address +# might be more useful. + INSTALLDIR = $(DESTDIR)@prefix@/bin USE_SYSTEM_SQLITE = @USE_SYSTEM_SQLITE@ SQLITE3_SRC.2 = @SQLITE3_SRC.2@ SQLITE3_OBJ.2 = @SQLITE3_OBJ.2@ SQLITE3_SHELL_SRC.2 = @SQLITE3_SHELL_SRC.2@ @@ -62,10 +69,13 @@ # 0=src/shell.c, 1=src/shell-see.c, 2=$(SQLITE3_SHELL_SRC.2) USE_LINENOISE = @USE_LINENOISE@ USE_MMAN_H = @USE_MMAN_H@ USE_SEE = @USE_SEE@ APPNAME = fossil +# +# APPNAME = fossil-fuzz +# may be more appropriate for fuzzing. .PHONY: all tags include $(SRCDIR)/main.mk Index: src/backlink.c ================================================================== --- src/backlink.c +++ src/backlink.c @@ -255,10 +255,12 @@ Backlink *p ){ struct mkd_renderer html_renderer = { /* prolog */ (void(*)(Blob*,void*))mkdn_noop0, /* epilog */ (void(*)(Blob*,void*))mkdn_noop0, + /* footnotes */ (void(*)(Blob*,const Blob*, void*))mkdn_noop0, + /* blockcode */ (void(*)(Blob*,Blob*,void*))mkdn_noop0, /* blockquote */ (void(*)(Blob*,Blob*,void*))mkdn_noop0, /* blockhtml */ (void(*)(Blob*,Blob*,void*))mkdn_noop0, /* header */ (void(*)(Blob*,Blob*,int,void*))mkdn_noop0, /* hrule */ (void(*)(Blob*,void*))mkdn_noop0, @@ -266,19 +268,23 @@ /* listitem */ (void(*)(Blob*,Blob*,int,void*))mkdn_noop0, /* paragraph */ (void(*)(Blob*,Blob*,void*))mkdn_noop0, /* table */ (void(*)(Blob*,Blob*,Blob*,void*))mkdn_noop0, /* table_cell */ (void(*)(Blob*,Blob*,int,void*))mkdn_noop0, /* table_row */ (void(*)(Blob*,Blob*,int,void*))mkdn_noop0, + /* footnoteitm*/ (void(*)(Blob*,const Blob*,int,int,void*))mkdn_noop0, + /* autolink */ (int(*)(Blob*,Blob*,enum mkd_autolink,void*))mkdn_noop1, /* codespan */ (int(*)(Blob*,Blob*,int,void*))mkdn_noop1, /* dbl_emphas */ (int(*)(Blob*,Blob*,char,void*))mkdn_noop1, /* emphasis */ (int(*)(Blob*,Blob*,char,void*))mkdn_noop1, /* image */ (int(*)(Blob*,Blob*,Blob*,Blob*,void*))mkdn_noop1, /* linebreak */ (int(*)(Blob*,void*))mkdn_noop1, /* link */ backlink_md_link, /* r_html_tag */ (int(*)(Blob*,Blob*,void*))mkdn_noop1, /* tri_emphas */ (int(*)(Blob*,Blob*,char,void*))mkdn_noop1, + /* footnoteref*/ (int(*)(Blob*,const Blob*,const Blob*,int,int,void*))mkdn_noop1, + 0, /* entity */ 0, /* normal_text */ "*_", /* emphasis characters */ 0 /* client data */ }; Index: src/blob.c ================================================================== --- src/blob.c +++ src/blob.c @@ -53,10 +53,33 @@ /* ** The buffer holding the blob data */ #define blob_buffer(X) ((X)->aData) +/* +** Append blob contents to another +*/ +#define blob_appendb(dest, src) \ + blob_append((dest), blob_buffer(src), blob_size(src)) + +/* +** Append a string literal to a blob +** TODO: Consider renaming to blob_appendl() +*/ +#define blob_append_literal(blob, literal) \ + blob_append((blob), "" literal, (sizeof literal)-1) + /* + * The empty string in the second argument leads to a syntax error + * when the macro is not used with a string literal. Unfortunately + * the error is not overly explicit. + */ + +/* +** TODO: Suggested for removal because the name seems misleading. +*/ +#define blob_append_string blob_append_literal + /* ** Seek whence parameter values */ #define BLOB_SEEK_SET 1 #define BLOB_SEEK_CUR 2 @@ -325,17 +348,10 @@ pBlob->nUsed += nData; pBlob->aData[pBlob->nUsed] = 0; memcpy(&pBlob->aData[nUsed], aData, nData); } -/* -** Append a string literal to a blob. -*/ -#if INTERFACE -#define blob_append_string(BLOB,STR) blob_append(BLOB,STR,sizeof(STR)-1) -#endif - /* ** Append a single character to the blob. If pBlob is zero then the ** character is written directly to stdout. */ void blob_append_char(Blob *pBlob, char c){ @@ -503,11 +519,11 @@ /* ** Compare two blobs. Return negative, zero, or positive if the first ** blob is less then, equal to, or greater than the second. */ -int blob_compare(Blob *pA, Blob *pB){ +int blob_compare(const Blob *pA, const Blob *pB){ int szA, szB, sz, rc; blob_is_init(pA); blob_is_init(pB); szA = blob_size(pA); szB = blob_size(pB); Index: src/default.css ================================================================== --- src/default.css +++ src/default.css @@ -1665,10 +1665,86 @@ } .monospace { font-family: monospace; } + +div.markdown > ol.footnotes { + font-size: 90%; +} +div.markdown > ol.footnotes > li { + margin-bottom: 0.5em; +} +div.markdown ol.footnotes > li.fn-joined > sup.fn-joined { + color: gray; + font-family: monospace; +} +div.markdown ol.footnotes > li.fn-joined > sup.fn-joined::after { + content: "(joined from multiple locations) "; +} +div.markdown ol.footnotes > li.fn-misreference { + margin-top: 0.75em; + margin-bottom: 0.75em; +} +div.markdown ol.footnotes > li.fn-toodeep > i, +div.markdown ol.footnotes > li.fn-misreference, +div.markdown ol.footnotes > li.fn-unreferenced { + color: gray; +} +div.markdown ol.footnotes > li.fn-misreference > span { + color: red; +} +div.markdown ol.footnotes > li.fn-misreference > span::after { + content: " (use of undefined label)."; +} +div.markdown ol.footnotes > li.fn-unreferenced { + padding-left: 0.5em; +} +div.markdown ol.footnotes > li.fn-unreferenced > code { + color: red; +} +div.markdown ol.footnotes > li.fn-unreferenced > i::after { + content: " was defined but is not referenced"; +} +div.markdown ol.footnotes > li.fn-toodeep > i::after { + content: " depth of nesting of inline footnotes exceeded the limit"; +} +div.markdown ol.footnotes > li.fn-toodeep > pre, +div.markdown ol.footnotes > li.fn-unreferenced > pre { + color: gray; + font-size: 85%; + padding-left: 0.5em; + margin-top: 0.25em; + border-left: 2px solid red; +} +div.markdown ol.footnotes > li.fn-toodeep > pre { + margin-left: 0.5em; +} +div.markdown > ol.footnotes > li > .fn-backrefs { + margin-right: 0.5em; + font-weight: bold; +} +div.markdown > ol.footnotes > li > .fn-backrefs > a, +div.markdown sup.noteref > a { + padding-left: 2px; + padding-right: 2px; +} +div.markdown sup.noteref.misref, +div.markdown sup.noteref.misref > a { + color: red; + font-size: 90%; +} +div.markdown sup.noteref > a:target, +div.markdown span.notescope:target > sup.noteref > a, +div.markdown span.notescope:hover > sup.noteref > a, +div.markdown > ol.footnotes > li > .fn-backrefs > a:target { + background: gold; +} +div.markdown span.notescope:hover, +div.markdown span.notescope:target { + border-bottom: 2px solid gold; +} /* Objects in the "desktoponly" class are invisible on mobile */ @media screen and (max-width: 600px) { .desktoponly { display: none; Index: src/encode.c ================================================================== --- src/encode.c +++ src/encode.c @@ -205,10 +205,40 @@ ** by this routine. */ char *urlize(const char *z, int n){ return EncodeHttp(z, n, 0); } + +/* +** If input string does not contain quotes (niether ' nor ") +** then return the argument itself. Otherwise return a newly allocated +** copy of input with all quotes %-escaped. +*/ +const char* escape_quotes(const char *zIn){ + char *zRet, *zOut; + size_t i, n = 0; + for(i=0; zIn[i]; i++){ + if( zIn[i]== '"' || zIn[i]== '\'' ) n++; + } + if( !n ) return zIn; + zRet = zOut = fossil_malloc( i + 2*n + 1 ); + for(i=0; zIn[i]; i++){ + if( zIn[i]=='"' ){ + *(zOut++) = '%'; + *(zOut++) = '2'; + *(zOut++) = '2'; + }else if( zIn[i]=='\'' ){ + *(zOut++) = '%'; + *(zOut++) = '2'; + *(zOut++) = '7'; + }else{ + *(zOut++) = zIn[i]; + } + } + *zOut = 0; + return zRet; +} /* ** Convert a single HEX digit to an integer */ static int AsciiToHex(int c){ Index: src/main.c ================================================================== --- src/main.c +++ src/main.c @@ -324,10 +324,11 @@ } reqPayload; /* request payload object (if any) */ cson_array *warnings; /* response warnings */ int timerId; /* fetched from fossil_timer_start() */ } json; #endif /* FOSSIL_ENABLE_JSON */ + int ftntsIssues[4]; /* Counts for misref, strayed, joined, overnested */ int diffCnt[3]; /* Counts for DIFF_NUMSTAT: files, ins, del */ }; /* ** Macro for debugging: Index: src/markdown.c ================================================================== --- src/markdown.c +++ src/markdown.c @@ -45,10 +45,11 @@ /* 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); @@ -63,10 +64,12 @@ 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); @@ -79,10 +82,12 @@ 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); @@ -113,31 +118,54 @@ /********************** * EXPORTED FUNCTIONS * **********************/ -/* markdown -- parses the input buffer and renders it into the output buffer */ +/* +** markdown -- parses the input buffer and renders it into the output buffer. +*/ void markdown( struct Blob *ob, - struct Blob *ib, + const struct Blob *ib, const struct mkd_renderer *rndr); #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 */ +/* +** link_ref -- reference to a link. +*/ struct link_ref { - struct Blob id; + 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 */ @@ -156,12 +184,19 @@ 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; @@ -183,16 +218,18 @@ { "html", 4 }, { "pre", 3 }, { "script", 6 }, }; - /*************************** * STATIC HELPER FUNCTIONS * ***************************/ -/* build_ref_id -- collapse whitespace from input text to make it a ref id */ +/* +** 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 */ @@ -247,10 +284,58 @@ 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; @@ -579,12 +664,14 @@ 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 */ +/* +** 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, @@ -624,12 +711,13 @@ } } return 0; } - -/* parse_emph2 -- parsing single emphasis */ +/* +** parse_emph2 -- parsing single emphasis. +*/ static size_t parse_emph2( struct Blob *ob, struct render *rndr, char *data, size_t size, @@ -663,13 +751,14 @@ i++; } return 0; } - -/* parse_emph3 -- parsing single emphasis */ -/* finds the first closing tag, and delegates to the other emph */ +/* +** 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, @@ -711,12 +800,13 @@ } } return 0; } - -/* char_emphasis -- single and double emphasis parsing */ +/* +** char_emphasis -- single and double emphasis parsing. +*/ static size_t char_emphasis( struct Blob *ob, struct render *rndr, char *data, size_t offset, @@ -756,12 +846,13 @@ return ret+3; } return 0; } - -/* char_linebreak -- '\n' preceded by two spaces (assuming linebreak != 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, @@ -771,12 +862,13 @@ /* 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) */ +/* +** 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, @@ -813,11 +905,13 @@ } return end; } -/* char_escape -- '\\' backslash escape */ +/* +** char_escape -- '\\' backslash escape. +*/ static size_t char_escape( struct Blob *ob, struct render *rndr, char *data, size_t offset, @@ -833,13 +927,14 @@ } } 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]+; */ +/* +** 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, @@ -869,12 +964,13 @@ blob_append(ob, data, end); } return end; } - -/* char_langle_tag -- '<' when tags or autolinks are allowed */ +/* +** 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, @@ -899,12 +995,12 @@ }else{ return end; } } - -/* get_link_inline -- extract inline-style link and title from +/* +** get_link_inline -- extract inline-style link and title from ** parenthesed data */ static int get_link_inline( struct Blob *link, struct Blob *title, @@ -954,11 +1050,11 @@ link_e--; } /* remove optional angle brackets around the link */ if( data[link_b]=='<' ) link_b += 1; - if( data[link_e-1]=='>' ) link_e -= 1; + if( link_e && data[link_e-1]=='>' ) link_e -= 1; /* escape backslashed character from link */ blob_reset(link); i = link_b; while( irefs); /* find the link from its id (stored temporarily in link) */ blob_reset(link); - if( build_ref_id(link, data, size)<0 ) return -1; + if( !sz || build_ref_id(link, data, size)<0 ) return -1; lr = bsearch(link, blob_buffer(&rndr->refs), - blob_size(&rndr->refs)/sizeof(struct link_ref), + 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_append(link, blob_buffer(&lr->link), blob_size(&lr->link)); - blob_append(title, blob_buffer(&lr->title), blob_size(&lr->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; -/* char_link -- '[': parsing a link or an image */ + 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 + size_t size /* parse_inline() ensures that size > 0 */ ){ - int is_img = (offset && data[-1] == '!'), level; + const int is_img = (offset && data[-1] == '!'); size_t i = 1, txt_e; struct Blob *content = 0; struct Blob *link = 0; struct Blob *title = 0; + struct footnote fn; int ret; /* checking whether the correct renderer exists */ if( (is_img && !rndr->make.image) || (!is_img && !rndr->make.link) ){ return 0; } /* looking for the matching closing bracket */ - for(level=1; i=size ) return 0; - txt_e = i; - i++; - - /* skip any amount of whitespace or newline */ - /* (this is much more laxist 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 */ - }else if( i=size ) goto char_link_cleanup; - - if( i+1==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( 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; - } - + fn.nUsed = 0; + + /* free-standing footnote refernece */ + if(!is_img && size>3 && data[1]=='^'){ + fn = get_footnote(rndr, data+2, txt_e-2); + }else{ + + /* 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 ){ + if( txt_e>1 && content ){ 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--; + 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 */ @@ -1121,11 +1425,10 @@ release_work_buffer(rndr, title); release_work_buffer(rndr, link); release_work_buffer(rndr, content); return ret ? i : 0; } - /********************************* * BLOCK-LEVEL PARSING FUNCTIONS * *********************************/ @@ -1806,11 +2109,12 @@ /* 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); - blob_init(&work, data+k, i-(j+k)); + 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); @@ -2013,11 +2317,13 @@ char *data, /* input text */ size_t size /* input text size */ ){ size_t beg, end, i; char *txt_data; - int has_table = (rndr->make.table + 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; @@ -2063,11 +2369,11 @@ * REFERENCE PARSING * *********************/ /* is_ref -- returns whether a line is a reference or not */ static int is_ref( - char *data, /* input text */ + 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 *refs /* array of link references */ ){ @@ -2097,10 +2403,12 @@ 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; @@ -2125,10 +2433,11 @@ && data[i]!='\n' && data[i]!='\r' ){ i += 1; } + /* TODO: maybe require both data[i-1]=='>' && data[link_offset-1]=='<' ? */ if( data[i-1]=='>' ) link_end = i-1; else link_end = i; /* optional spacer: (space | tab)* (newline | '\'' | '"' | '(' ) */ while( i=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, ib_data + beg, end - beg); - while( endbeg ) 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; 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( 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; ibRndred ? 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 < 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); @@ -2275,9 +2866,17 @@ 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 ~~~ pikchr oval "Start" fit; arrow; box "Hello, World!" fit; arrow; oval "Done" fit ~~~ + +## Footnotes ## + +> Footnotes (or "endnotes") is a Fossil's extention of classical Markdown. +> Fossil's syntax for footnotes is similar to links and +> is distinguished by the use of character **^** +> that *immediately* follows an opening bracket. + +> 1. **\(^** footnote's text **)** +> 2. **\[** fragment of text **]\(^** a comment about that fragment **\)** +> 3. **\[^** label **\]** +> 4. **\[** fragment of text **\]\[^** label **\]** +> 5. **\[** fragment of text **\]\[^\]** + +> With formats 1 and 2 ("inline footnotes") text of a footnote is provided +> in the place where the corresponding numeric mark will be rendered. +> With formats 3, 4, and 5 ("reference footnotes") text of a footnote +> is supplied elsewhere in the document, as shown below. +> Formats 2, 4 and 5 ("span-specific footnotes") mark a specific fragment +> that is being commented in the footnote. +> Format 5 reuses a fragment of text as a label. +> Labels are case-insensitive. + +> ``` +> [^label]: Footnote definition must start on the first column. +> The second line (if any) must be indented by two or more spaces. +> Definition continues until indentation drops below that of the 2nd line. +>``` +> Character **^** is not part of a label, it is part of the syntax. +> Both a footnote's text and a fragment to which a footnote applies +> are subject to further interpretation as Markdown sources. + ## Miscellaneous ## > * In-line images are made using **\!\[alt-text\]\(image-URL\)**. > * Use HTML for advanced formatting such as forms. > * **\** are supported. Index: src/markdown_html.c ================================================================== --- src/markdown_html.c +++ src/markdown_html.c @@ -29,37 +29,72 @@ struct Blob *output_title, struct Blob *output_body); #endif /* INTERFACE */ +/* +** Markdown-internal helper for generating unique link reference IDs. +** Fields provide typed interpretation of the underline memory buffer. +*/ +typedef union bitfield64_t bitfield64_t; +union bitfield64_t{ + char c[8]; /* interpret as the array of signed characters */ + unsigned char b[8]; /* interpret as the array of unsigned characters */ +}; + /* ** An instance of the following structure is passed through the ** "opaque" pointer. */ typedef struct MarkdownToHtml MarkdownToHtml; struct MarkdownToHtml { Blob *output_title; /* Store the title here */ + bitfield64_t unique; /* Enables construction of unique #id elements */ + + #ifndef FOOTNOTES_WITHOUT_URI + Blob reqURI; /* REQUEST_URI with escaped quotes */ + #endif }; /* INTER_BLOCK -- skip a line between block level elements */ #define INTER_BLOCK(ob) \ do { if( blob_size(ob)>0 ) blob_append_char(ob, '\n'); } while (0) -/* BLOB_APPEND_LITERAL -- append a string literal to a blob */ -#define BLOB_APPEND_LITERAL(blob, literal) \ - blob_append((blob), "" literal, (sizeof literal)-1) - /* - * The empty string in the second argument leads to a syntax error - * when the macro is not used with a string literal. Unfortunately - * the error is not overly explicit. - */ - -/* BLOB_APPEND_BLOB -- append blob contents to another */ -#define BLOB_APPEND_BLOB(dest, src) \ - blob_append((dest), blob_buffer(src), blob_size(src)) - +/* +** FOOTNOTES_WITHOUT_URI macro was introduced by [2c1f8f3592ef00e0] +** to enable flexibility in rendering of footnote-specific hyperlinks. +** It may be defined for a particular build in order to omit +** full REQUEST_URIs within footnote-specific (and page-local) hyperlinks. +** This *is* used for the builds that incorporate 'base-href-fix' branch +** (which in turn fixes footnotes on the preview tab of /wikiedit page). +*/ +#ifndef FOOTNOTES_WITHOUT_URI + #define BLOB_APPEND_URI(dest,ctx) blob_appendb(dest,&((ctx)->reqURI)) +#else + #define BLOB_APPEND_URI(dest,ctx) +#endif + +/* Converts an integer to a textual base26 representation +** with proper null-termination. + * Return empty string if that integer is negative. */ +static bitfield64_t to_base26(int i, int uppercase){ + bitfield64_t x; + int j; + memset( &x, 0, sizeof(x) ); + if( i >= 0 ){ + for(j=7; j >= 0; j--){ + x.b[j] = (unsigned char)(uppercase?'A':'a') + i%26; + if( (i /= 26) == 0 ) break; + } + assert( j > 0 ); /* because 2^32 < 26^7 */ + for(i=0; i<8-j; i++) x.b[i] = x.b[i+j]; + for( ; i<8 ; i++) x.b[i] = 0; + } + assert( x.c[7] == 0 ); + return x; +} /* HTML escapes ** ** html_escape() converts < to <, > to >, and & to &. ** html_quote() goes further and converts " into " and ' in '. @@ -78,19 +113,19 @@ i++; } blob_append(ob, data+beg, i-beg); while( i' ){ - BLOB_APPEND_LITERAL(ob, ">"); + blob_append_literal(ob, ">"); }else if( data[i]=='&' ){ - BLOB_APPEND_LITERAL(ob, "&"); + blob_append_literal(ob, "&"); }else if( data[i]=='"' ){ - BLOB_APPEND_LITERAL(ob, """); + blob_append_literal(ob, """); }else if( data[i]=='\'' ){ - BLOB_APPEND_LITERAL(ob, "'"); + blob_append_literal(ob, "'"); }else{ break; } i++; } @@ -108,15 +143,15 @@ i++; } blob_append(ob, data+beg, i-beg); while( i' ){ - BLOB_APPEND_LITERAL(ob, ">"); + blob_append_literal(ob, ">"); }else if( data[i]=='&' ){ - BLOB_APPEND_LITERAL(ob, "&"); + blob_append_literal(ob, "&"); }else{ break; } i++; } @@ -129,17 +164,17 @@ /* Size of the prolog: "
\n" */ #define PROLOG_SIZE 23 static void html_prolog(struct Blob *ob, void *opaque){ INTER_BLOCK(ob); - BLOB_APPEND_LITERAL(ob, "
\n"); + blob_append_literal(ob, "
\n"); assert( blob_size(ob)==PROLOG_SIZE ); } static void html_epilog(struct Blob *ob, void *opaque){ INTER_BLOCK(ob); - BLOB_APPEND_LITERAL(ob, "
\n"); + blob_append_literal(ob, "
\n"); } static void html_blockhtml(struct Blob *ob, struct Blob *text, void *opaque){ char *data = blob_buffer(text); size_t size = blob_size(text); @@ -157,25 +192,25 @@ blob_append(title, data+nTag, size - nTag - 5); return; } INTER_BLOCK(ob); blob_append(ob, data, size); - BLOB_APPEND_LITERAL(ob, "\n"); + blob_append_literal(ob, "\n"); } static void html_blockcode(struct Blob *ob, struct Blob *text, void *opaque){ INTER_BLOCK(ob); - BLOB_APPEND_LITERAL(ob, "
");
+  blob_append_literal(ob, "
");
   html_escape(ob, blob_buffer(text), blob_size(text));
-  BLOB_APPEND_LITERAL(ob, "
\n"); + blob_append_literal(ob, "
\n"); } static void html_blockquote(struct Blob *ob, struct Blob *text, void *opaque){ INTER_BLOCK(ob); - BLOB_APPEND_LITERAL(ob, "
\n"); - BLOB_APPEND_BLOB(ob, text); - BLOB_APPEND_LITERAL(ob, "
\n"); + blob_append_literal(ob, "
\n"); + blob_appendb(ob, text); + blob_append_literal(ob, "
\n"); } static void html_header( struct Blob *ob, struct Blob *text, @@ -184,22 +219,22 @@ ){ struct Blob *title = ((MarkdownToHtml*)opaque)->output_title; /* The first header at the beginning of a text is considered as * a title and not output. */ if( blob_size(ob)<=PROLOG_SIZE && title!=0 && blob_size(title)==0 ){ - BLOB_APPEND_BLOB(title, text); + blob_appendb(title, text); return; } INTER_BLOCK(ob); blob_appendf(ob, "", level); - BLOB_APPEND_BLOB(ob, text); + blob_appendb(ob, text); blob_appendf(ob, "", level); } static void html_hrule(struct Blob *ob, void *opaque){ INTER_BLOCK(ob); - BLOB_APPEND_LITERAL(ob, "
\n"); + blob_append_literal(ob, "
\n"); } static void html_list( struct Blob *ob, @@ -210,11 +245,11 @@ char ol[] = "ol"; char ul[] = "ul"; char *tag = (flags & MKD_LIST_ORDERED) ? ol : ul; INTER_BLOCK(ob); blob_appendf(ob, "<%s>\n", tag); - BLOB_APPEND_BLOB(ob, text); + blob_appendb(ob, text); blob_appendf(ob, "\n", tag); } static void html_list_item( struct Blob *ob, @@ -223,20 +258,20 @@ void *opaque ){ char *text_data = blob_buffer(text); size_t text_size = blob_size(text); while( text_size>0 && text_data[text_size-1]=='\n' ) text_size--; - BLOB_APPEND_LITERAL(ob, "
  • "); + blob_append_literal(ob, "
  • "); blob_append(ob, text_data, text_size); - BLOB_APPEND_LITERAL(ob, "
  • \n"); + blob_append_literal(ob, "\n"); } static void html_paragraph(struct Blob *ob, struct Blob *text, void *opaque){ INTER_BLOCK(ob); - BLOB_APPEND_LITERAL(ob, "

    "); - BLOB_APPEND_BLOB(ob, text); - BLOB_APPEND_LITERAL(ob, "

    \n"); + blob_append_literal(ob, "

    "); + blob_appendb(ob, text); + blob_append_literal(ob, "

    \n"); } static void html_table( struct Blob *ob, @@ -243,71 +278,301 @@ struct Blob *head_row, struct Blob *rows, void *opaque ){ INTER_BLOCK(ob); - BLOB_APPEND_LITERAL(ob, "\n"); + blob_append_literal(ob, "
    \n"); if( head_row && blob_size(head_row)>0 ){ - BLOB_APPEND_LITERAL(ob, "\n"); - BLOB_APPEND_BLOB(ob, head_row); - BLOB_APPEND_LITERAL(ob, "\n\n"); + blob_append_literal(ob, "\n"); + blob_appendb(ob, head_row); + blob_append_literal(ob, "\n\n"); } if( rows ){ - BLOB_APPEND_BLOB(ob, rows); + blob_appendb(ob, rows); } if( head_row && blob_size(head_row)>0 ){ - BLOB_APPEND_LITERAL(ob, "\n"); + blob_append_literal(ob, "\n"); } - BLOB_APPEND_LITERAL(ob, "
    \n"); + blob_append_literal(ob, "\n"); } static void html_table_cell( struct Blob *ob, struct Blob *text, int flags, void *opaque ){ if( flags & MKD_CELL_HEAD ){ - BLOB_APPEND_LITERAL(ob, " "); - BLOB_APPEND_BLOB(ob, text); + blob_append_literal(ob, ">"); + blob_appendb(ob, text); if( flags & MKD_CELL_HEAD ){ - BLOB_APPEND_LITERAL(ob, "\n"); + blob_append_literal(ob, "\n"); }else{ - BLOB_APPEND_LITERAL(ob, "\n"); + blob_append_literal(ob, "\n"); } } static void html_table_row( struct Blob *ob, struct Blob *cells, int flags, void *opaque ){ - BLOB_APPEND_LITERAL(ob, " \n"); - BLOB_APPEND_BLOB(ob, cells); - BLOB_APPEND_LITERAL(ob, " \n"); + blob_append_literal(ob, " \n"); + blob_appendb(ob, cells); + blob_append_literal(ob, " \n"); +} + +/* +** Render a token of user provided classes. +** If bHTML is true then render HTML for (presumably) visible text, +** otherwise just a space-separated list of the derived classes. +*/ +static void append_footnote_upc( + struct Blob *ob, + const struct Blob *upc, /* token of user-provided classes */ + int bHTML +){ + const char *z = blob_buffer(upc); + int i, n = blob_size(upc); + + if( n<3 ) return; + assert( z[0]=='.' && z[n-1] == ':' ); + if( bHTML ){ + blob_append_literal(ob, "" + "."); + } + n = 0; + do{ + z++; + if( *z!='.' && *z!=':' ){ + assert( fossil_isalnum(*z) || *z=='-' ); + n++; + continue; + } + assert( n ); + if( bHTML ) blob_append_literal(ob, ""); + blob_append(ob, z-n, n); + blob_append_literal(ob, ""); + }else{ + blob_append_char(ob, ' '); + } + n = 0; + if( bHTML ){ + if( *z==':' ){ + blob_append_literal(ob,":"); + }else{ + blob_append_literal(ob,"."); + } + } + }while( *z != ':' ); + if( bHTML ) blob_append_literal(ob,"\n"); +} + +static int html_footnote_ref( + struct Blob *ob, const struct Blob *span, const struct Blob *upc, + int iMark, int locus, void *opaque +){ + const struct MarkdownToHtml* ctx = (struct MarkdownToHtml*)opaque; + const bitfield64_t l = to_base26(locus-1,0); + char pos[32]; + memset(pos,0,32); + assert( locus > 0 ); + /* expect BUGs if the following yields compiler warnings */ + if( iMark > 0 ){ /* a regular reference to a footnote */ + sprintf(pos, "%s-%d-%s", ctx->unique.c, iMark, l.c); + if(span && blob_size(span)) { + blob_append_literal(ob,"",pos); + blob_appendb(ob, span); + blob_trim(ob); + blob_append_literal(ob,"%d", pos, iMark); + }else{ + blob_trim(ob); + blob_append_literal(ob,"%d", + pos, pos, iMark); + } + }else{ /* misreference */ + assert( iMark == -1 ); + + sprintf(pos, "%s-%s", ctx->unique.c, l.c); + if(span && blob_size(span)) { + blob_appendf(ob, "", pos); + blob_appendb(ob, span); + blob_trim(ob); + blob_append_literal(ob, "misref", pos); + }else{ + blob_trim(ob); + blob_append_literal(ob, "", pos, pos); + blob_append_literal(ob, "misref"); + } + } + return 1; +} + +/* Render a single item of the footnotes list. + * Each backref gets a unique id to enable dynamic styling. */ +static void html_footnote_item( + struct Blob *ob, const struct Blob *text, int iMark, int nUsed, void *opaque +){ + const struct MarkdownToHtml* ctx = (struct MarkdownToHtml*)opaque; + const char * const unique = ctx->unique.c; + assert( nUsed >= 0 ); + /* expect BUGs if the following yields compiler warnings */ + + if( iMark < 0 ){ /* misreferences */ + assert( iMark == -1 ); + assert( nUsed ); + blob_append_literal(ob,"
  • " + ""); + if( nUsed == 1 ){ + blob_appendf(ob,"^", unique); + }else{ + int i; + blob_append_char(ob, '^'); + for(i=0; i%c", unique,c, c); + } + if( i < nUsed ) blob_append_literal(ob," …"); + } + blob_append_literal(ob,"\nMisreference"); + }else if( iMark > 0 ){ /* regular, joined and overnested footnotes */ + char pos[24]; + int bJoin = 0; + #define _joined_footnote_indicator "
      " + #define _jfi_sz (sizeof(_joined_footnote_indicator)-1) + /* make.footnote_item() invocations should pass args accordingly */ + const struct Blob *upc = text+1; + assert( text ); + /* allow blob_size(text)==0 for constructs like [...](^ [] ()) */ + memset(pos,0,24); + sprintf(pos, "%s-%d", unique, iMark); + blob_appendf(ob, "
    • "); + blob_appendf(ob,"^", pos); + }else{ + int i; + blob_append_literal(ob, "fn-polyref'>^"); + for(i=0; i%c", pos,c, c); + } + /* It's unlikely that so many backrefs will be usefull */ + /* but maybe for some machine generated documents... */ + for(; i%s", pos,l.c, l.c); + } + if( i < nUsed ) blob_append_literal(ob," …"); + } + blob_append_literal(ob,"\n"); + if( bJoin ){ + blob_append_literal(ob,"
        "); + blob_append(ob,blob_buffer(text)+_jfi_sz,blob_size(text)-_jfi_sz); + }else if( nUsed ){ + append_footnote_upc(ob, upc, 1); + blob_appendb(ob, text); + }else{ + blob_append_literal(ob,"\n" + "
        ");
        +      if( blob_size(upc) ){
        +        blob_appendb(ob, upc);
        +      }
        +      html_escape(ob, blob_buffer(text), blob_size(text));
        +      blob_append_literal(ob,"
        "); + } + #undef _joined_footnote_indicator + #undef _jfi_sz + }else{ /* a footnote was defined but wasn't referenced */ + /* make.footnote_item() invocations should pass args accordingly */ + const struct Blob *id = text-1, *upc = text+1; + assert( !nUsed ); + assert( text ); + assert( blob_size(text) ); + assert( blob_size(id) ); + blob_append_literal(ob,"
      • \n[^ "); + html_escape(ob, blob_buffer(id), blob_size(id)); + blob_append_literal(ob, " ]\n" + "
        ");
        +    if( blob_size(upc) ){
        +      blob_appendb(ob, upc);
        +    }
        +    html_escape(ob, blob_buffer(text), blob_size(text));
        +    blob_append_literal(ob,"
        "); + } + blob_append_literal(ob, "\n
      • \n"); } - +static void html_footnotes( + struct Blob *ob, const struct Blob *items, void *opaque +){ + if( items && blob_size(items) ){ + blob_append_literal(ob, + "\n
        \n
          \n"); + blob_appendb(ob, items); + blob_append_literal(ob, "
        \n"); + } +} /* HTML span tags */ static int html_raw_html_tag(struct Blob *ob, struct Blob *text, void *opaque){ blob_append(ob, blob_buffer(text), blob_size(text)); @@ -319,21 +584,21 @@ struct Blob *link, enum mkd_autolink type, void *opaque ){ if( !link || blob_size(link)<=0 ) return 0; - BLOB_APPEND_LITERAL(ob, ""); + blob_append_literal(ob, "\">"); if( type==MKDA_EXPLICIT_EMAIL && blob_size(link)>7 ){ /* remove "mailto:" from displayed text */ html_escape(ob, blob_buffer(link)+7, blob_size(link)-7); }else{ html_escape(ob, blob_buffer(link), blob_size(link)); } - BLOB_APPEND_LITERAL(ob, ""); + blob_append_literal(ob, ""); return 1; } /* ** The nSrc bytes at zSrc[] are Pikchr input text (allegedly). Process that @@ -422,13 +687,13 @@ ){ if( text==0 ){ /* no-op */ }else if( nSep<=2 ){ /* One or two graves: an in-line code span */ - BLOB_APPEND_LITERAL(ob, ""); + blob_append_literal(ob, ""); html_escape(ob, blob_buffer(text), blob_size(text)); - BLOB_APPEND_LITERAL(ob, ""); + blob_append_literal(ob, ""); }else{ /* Three or more graves: a fenced code block */ int n = blob_size(text); const char *z = blob_buffer(text); int i; @@ -460,25 +725,25 @@ struct Blob *ob, struct Blob *text, char c, void *opaque ){ - BLOB_APPEND_LITERAL(ob, ""); - BLOB_APPEND_BLOB(ob, text); - BLOB_APPEND_LITERAL(ob, ""); + blob_append_literal(ob, ""); + blob_appendb(ob, text); + blob_append_literal(ob, ""); return 1; } static int html_emphasis( struct Blob *ob, struct Blob *text, char c, void *opaque ){ - BLOB_APPEND_LITERAL(ob, ""); - BLOB_APPEND_BLOB(ob, text); - BLOB_APPEND_LITERAL(ob, ""); + blob_append_literal(ob, ""); + blob_appendb(ob, text); + blob_append_literal(ob, ""); return 1; } static int html_image( struct Blob *ob, @@ -485,24 +750,24 @@ struct Blob *link, struct Blob *title, struct Blob *alt, void *opaque ){ - BLOB_APPEND_LITERAL(ob, "\"");0 ){ - BLOB_APPEND_LITERAL(ob, "\" title=\""); + blob_append_literal(ob, "\" title=\""); html_quote(ob, blob_buffer(title), blob_size(title)); } - BLOB_APPEND_LITERAL(ob, "\" />"); + blob_append_literal(ob, "\" />"); return 1; } static int html_linebreak(struct Blob *ob, void *opaque){ - BLOB_APPEND_LITERAL(ob, "
        \n"); + blob_append_literal(ob, "
        \n"); return 1; } static int html_link( struct Blob *ob, @@ -523,13 +788,13 @@ WIKI_MARKDOWNLINKS ; wiki_resolve_hyperlink(ob, flags, zLink, zClose, sizeof(zClose), 0, zTitle); } if( blob_size(content)==0 ){ - if( link ) BLOB_APPEND_BLOB(ob, link); + if( link ) blob_appendb(ob, link); }else{ - BLOB_APPEND_BLOB(ob, content); + blob_appendb(ob, content); } blob_append(ob, zClose, -1); return 1; } @@ -537,13 +802,13 @@ struct Blob *ob, struct Blob *text, char c, void *opaque ){ - BLOB_APPEND_LITERAL(ob, ""); - BLOB_APPEND_BLOB(ob, text); - BLOB_APPEND_LITERAL(ob, ""); + blob_append_literal(ob, ""); + blob_appendb(ob, text); + blob_append_literal(ob, ""); return 1; } static void html_normal_text(struct Blob *ob, struct Blob *text, void *opaque){ @@ -563,10 +828,11 @@ ){ struct mkd_renderer html_renderer = { /* prolog and epilog */ html_prolog, html_epilog, + html_footnotes, /* block level elements */ html_blockcode, html_blockquote, html_blockhtml, @@ -576,10 +842,11 @@ html_list_item, html_paragraph, html_table, html_table_cell, html_table_row, + html_footnote_item, /* span level elements */ html_autolink, html_codespan, html_double_emphasis, @@ -587,22 +854,30 @@ html_image, html_linebreak, html_link, html_raw_html_tag, html_triple_emphasis, + html_footnote_ref, /* low level elements */ 0, /* entity */ html_normal_text, /* misc. parameters */ "*_", /* emph_chars */ 0 /* opaque */ }; + static int invocation = -1; /* no marker for the first document */ + static const char* zRU = 0; /* REQUEST_URI with escaped quotes */ MarkdownToHtml context; memset(&context, 0, sizeof(context)); context.output_title = output_title; + context.unique = to_base26(invocation++,1); + if( !zRU ) zRU = escape_quotes(PD("REQUEST_URI","")); + #ifndef FOOTNOTES_WITHOUT_URI + blob_set( &context.reqURI, zRU ); + #endif html_renderer.opaque = &context; if( output_title ) blob_reset(output_title); blob_reset(output_body); markdown(output_body, input_markdown, &html_renderer); } Index: src/style.c ================================================================== --- src/style.c +++ src/style.c @@ -782,10 +782,17 @@ image_url_var("background"); if( !login_is_nobody() ){ Th_Store("login", g.zLogin); } Th_MaybeStore("current_feature", feature_from_page_path(local_zCurrentPage) ); + if( g.ftntsIssues[0] || g.ftntsIssues[1] || + g.ftntsIssues[2] || g.ftntsIssues[3] ){ + char buf[80]; + sprintf(&buf[0],"%i %i %i %i",g.ftntsIssues[0],g.ftntsIssues[1], + g.ftntsIssues[2],g.ftntsIssues[3]); + Th_Store("footnotes_issues_counters", buf); + } } /* ** Draw the header. */ Index: src/wikiformat.c ================================================================== --- src/wikiformat.c +++ src/wikiformat.c @@ -1893,18 +1893,20 @@ ** Usage: %fossil test-markdown-render FILE ... ** ** Render markdown in FILE as HTML on stdout. ** Options: ** -** --safe Restrict the output to use only "safe" HTML +** --safe Restrict the output to use only "safe" HTML +** --lint-footnotes Print stats for footnotes-related issues */ void test_markdown_render(void){ Blob in, out; int i; - int bSafe = 0; + int bSafe = 0, bFnLint = 0; db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE,0); bSafe = find_option("safe",0,0)!=0; + bFnLint = find_option("lint-footnotes",0,0)!=0; verify_all_options(); for(i=2; i3 ){ @@ -1915,10 +1917,20 @@ safe_html(&out); blob_write_to_file(&out, "-"); blob_reset(&in); blob_reset(&out); } + if( bFnLint && (g.ftntsIssues[0] || g.ftntsIssues[1] + || g.ftntsIssues[2] || g.ftntsIssues[3] )){ + fossil_fatal("There were issues with footnotes:\n" + " %8d misreference%s\n" + " %8d unreferenced\n" + " %8d splitted\n" + " %8d overnested", + g.ftntsIssues[0], g.ftntsIssues[0]==1?"":"s", + g.ftntsIssues[1], g.ftntsIssues[2], g.ftntsIssues[3]); + } } /* ** Search for a ... at the beginning of a wiki page. ** Return true (nonzero) if a title is found. Return zero if there is ADDED test/markdown-test3.md Index: test/markdown-test3.md ================================================================== --- /dev/null +++ test/markdown-test3.md @@ -0,0 +1,250 @@ + +Markdown Footnotes Test Document +================================ + +**This document** should help with testing of footnotes support that +is introduced by the ["`markdown-footnotes`"][branch] branch. +It **might look pretty misformatted unless rendered by the proper Fossil +executable** that incorporates the abovementioned branch.[^1] +That is also a humble attempt to explore the robustness of the Markdown parser. +So please excuse for the mess in the [source code of this document][src]. +By no means the normal use of footnotes should look that scarry. + +Developers are invited to add test cases here[^here]. +It is suggested that the more simple is a test case the earlier it should +appear in this document.[^ if glitch occurs ] + + +[^lost3]: This note was defined at the begining of the document. + +[^duplicate]: This came from the begining of the document. + +A footnote's label should be case insensitive[^ case INSENSITIVE ], +it is whitespace-savvy and can even contain newlines.[^ a +multiline +label] + +A labeled footnote may be [referenced several times][^many-refs]. + +A footnote's text should support Markdown [markup][^]. +Markup within [a [text fragment](https://en.wikipedia.org/wiki/Lorem_ipsum) +of a *span-bounded footnote*][^markup] should also be rendered. + +Another reference[^many-refs] to the preveously used footnote. + +[^lost2]: This note was defined in the middle of the document. + It references [its previous][^lost3] + and [the forthcoming][^lost1] siblings. + +[^i am strayed]: + This should be presented **verbatim** (without any [markup][^]) + in the end of the footnotes. + + Default skin renders label in red font and the main text in gray. + Other styling may also apply. + +Inline footnotes are supported.(^These may be usefull for adding +small comments.) + +This is a corner case that is rendered as [an empty footnote](^ [] ()). + +If [undefined label is used][^] then red "`misref`" is emited instead of +a numeric marker.[^ see it yourself ] +This can be overridden by the skin though. + +The refenrence at the end of this sentence is the sole reason of +rendering of `lost1` and [lost2][^]. + +If several labeled footnote definitions have the same equal label then texts +from all these definitions are joined.[^duplicate] + +Several references should be recognized as several distinct numbers. +(^There should be an interval between numbers.) [^many-refs] + +If markup is ambigous between a span-bounded footnote and +a "free-standing" footnote followed by another footnote +then interpret as the later case. +This facilitates the usage in the usual case +when several footnotes are refenrenced at the end +of a phrase.[^scipub][^many-refs](^All these four should +be parsed as "free-standing" footnotes)[^Coelurosauria] + +A footnote may not be empty(^) +or consist just of blank characters.(^ + ) + +The same holds for labeled footnotes. If definition of a labeled footnote +is blank then it is not accepted by the first pass of the parser and +is recognized during the second pass as misreference. +[^ This definition consists of just blanks ]: + + + + +It is possible to provide a list of classes for a particular footnote and +all its references. This is achieved by prepending a footnote's text with +a special token that starts with dot and ends with colon. +(^ + .alpha-Numeric123.EXAMPLE: + This token defines a dot-separated list of CSS classes + which are added to that particular footnote and also to the + corresponding reference(s). Hypens ('-') are also allowed. + Classes from the token are tranformed to lowercase and are prepended + with `"fn-upc-"` to avoid collisions. +) +This feature is "*opt-in*": there is nothing wrong in starting a footnote's +text with a token of that form while not defining any corresponding classes +in the stylesheet.[^nostyle] +If a footnote consists just of a valid userclass token then this token +is not interpreted as such, instead it is emitted as plain text. +(^ + .bare.classlist.inside.inline.footnote: +)[^bare1] +[^bare2] + +[^duplicate]: .with.UPC.token: + When duplicates are joined their UPC tokens are treated as plain-text. + Blank characters between token and main text must be preserved. + + + Click + + here and + + here + to test escaping of REQUEST_URI in the generated footnote markers. + + +A depth of nesting must be limited. +(^ + .L.1: A long chain of nested inline footnotes... + (^ + .L.2: is a rather unusual thing... + (^ + .L.3: and requires extra CPU cycles for processing. + (^ + .L.4: Theoretically speaking O(n2). + (^ + .L.5: Thus it is worth dismissing those footnotes... + (^ + .L.6: that are nested deeper than on a certain level. + (^ + .L.7: A particular value for that limit... + (^ + is hard-coded in src/markdown.c ... + (^ + in function `markdown()` ... + (^ + in variable named `maxDepth`. + (^ + For the time being, its value is **5** + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) +) + +## Footnotes + +[branch]: /timeline?r=markdown-footnotes&nowiki + +[^ 1]: Footnotes is a Fossil' extention of + Markdown. Your other tools may have limited support for these. + +[^here]: [History of test/markdown-test3.md](/finfo/test/markdown-test3.md) + +[src]: /file/test/markdown-test3.md?ci=markdown-footnotes&txt&ln + +[^if glitch occurs]: + So that simple cases are processed even if + a glitch happens for more tricky cases. + +[^ CASE insensitive ]: And also tolerate whitespaces. + +[^ a multiline label ]: But at a footnote's definition it should still + be written within square brackets + on a single line. + +[^duplicate]: And that came from the end of the document. + +[^many-refs]: + Each letter on the left is a back-reference to the place of use. + Highlighted back-reference indicates a place from which navigation + occurred[^lost1]. + +[^lost1]: This note was defined at the end of the document. + It defines an inline note. + + (^This is inline note defined inside of [a labeled note][^lost1].) + +[^markup]: E.g. *emphasis*, and [so on](/md_rules). + BTW, this note may not have a backreference to the "stray". + +[^undefined label is used]: For example due to a typo. + +[^another stray]: Just to verify the correctness of ordering and styling. + +[^scipub]: Which is common in the scientific publications. + +[^bare1]: .at.the.1st.line.of.labeled.footnote.definition: + + +[^bare2]: + .at.the.2nd.line.of.labeled.footnote.definition: + +[^stray with UPC]: .UPC-token: + A token of user-provided classes must be rendered within strays. + Aslo: this and the previous line may not have extra indentation. + +[^nostyle]: + .unused.classes: + In that case text of the footnote just looks like as if + no special processing occured. + + +[^ ]: Labels are escaped + +[^ +