/* ** 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 callbacks for the markdown parser that generate ** XHTML output. */ #include "config.h" #include "markdown_html.h" #if INTERFACE void markdown_to_html( struct Blob *input_markdown, 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) /* ** 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 '. */ static void html_quote(struct Blob *ob, const char *data, size_t size){ size_t beg = 0, i = 0; while( i' ){ blob_append_literal(ob, ">"); }else if( data[i]=='&' ){ blob_append_literal(ob, "&"); }else if( data[i]=='"' ){ blob_append_literal(ob, """); }else if( data[i]=='\'' ){ blob_append_literal(ob, "'"); }else{ break; } i++; } } } static void html_escape(struct Blob *ob, const char *data, size_t size){ size_t beg = 0, i = 0; while( i' ){ blob_append_literal(ob, ">"); }else if( data[i]=='&' ){ blob_append_literal(ob, "&"); }else{ break; } i++; } } } /* HTML block tags */ /* 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"); assert( blob_size(ob)==PROLOG_SIZE ); } static void html_epilog(struct Blob *ob, void *opaque){ INTER_BLOCK(ob); 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); Blob *title = ((MarkdownToHtml*)opaque)->output_title; while( size>0 && fossil_isspace(data[0]) ){ data++; size--; } while( size>0 && fossil_isspace(data[size-1]) ){ size--; } /* If the first raw block is an

element, then use it as the title. */ if( blob_size(ob)<=PROLOG_SIZE && size>9 && title!=0 && sqlite3_strnicmp("", &data[size-5],5)==0 ){ int nTag = html_tag_length(data); blob_append(title, data+nTag, size - nTag - 5); return; } INTER_BLOCK(ob); blob_append(ob, data, size); blob_append_literal(ob, "\n"); } static void html_blockcode(struct Blob *ob, struct Blob *text, void *opaque){ INTER_BLOCK(ob); blob_append_literal(ob, "
");
  html_escape(ob, blob_buffer(text), blob_size(text));
  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_appendb(ob, text); blob_append_literal(ob, "
\n"); } static void html_header( struct Blob *ob, struct Blob *text, int level, void *opaque ){ 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_appendb(title, text); return; } INTER_BLOCK(ob); blob_appendf(ob, "", level); 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"); } static void html_list( struct Blob *ob, struct Blob *text, int flags, void *opaque ){ char ol[] = "ol"; char ul[] = "ul"; char *tag = (flags & MKD_LIST_ORDERED) ? ol : ul; INTER_BLOCK(ob); blob_appendf(ob, "<%s>\n", tag); blob_appendb(ob, text); blob_appendf(ob, "\n", tag); } static void html_list_item( struct Blob *ob, struct Blob *text, int flags, 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(ob, text_data, text_size); 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_appendb(ob, text); blob_append_literal(ob, "

    \n"); } static void html_table( struct Blob *ob, struct Blob *head_row, struct Blob *rows, void *opaque ){ INTER_BLOCK(ob); blob_append_literal(ob, "\n"); if( head_row && blob_size(head_row)>0 ){ blob_append_literal(ob, "\n"); blob_appendb(ob, head_row); blob_append_literal(ob, "\n\n"); } if( rows ){ blob_appendb(ob, rows); } if( head_row && blob_size(head_row)>0 ){ 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_appendb(ob, text); if( flags & MKD_CELL_HEAD ){ blob_append_literal(ob, "\n"); }else{ 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_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)); return 1; } static int html_autolink( struct Blob *ob, struct Blob *link, enum mkd_autolink type, void *opaque ){ if( !link || blob_size(link)<=0 ) return 0; 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, ""); return 1; } /* ** The nSrc bytes at zSrc[] are Pikchr input text (allegedly). Process that ** text and insert the result in place of the original. */ void pikchr_to_html( Blob *ob, /* Write the generated SVG here */ const char *zSrc, int nSrc, /* The Pikchr source text */ const char *zArg, int nArg /* Addition arguments */ ){ int pikFlags = PIKCHR_PROCESS_NONCE | PIKCHR_PROCESS_DIV | PIKCHR_PROCESS_SRC | PIKCHR_PROCESS_ERR_PRE; Blob bSrc = empty_blob; const char *zPikVar; double rPikVar; while( nArg>0 ){ int i; for(i=0; i=0.1 && rPikVar<10.0 ){ blob_appendf(&bSrc, "scale = %.13g\n", rPikVar); } zPikVar = skin_detail("pikchr-fontscale"); if( zPikVar && (rPikVar = atof(zPikVar))>=0.1 && rPikVar<10.0 ){ blob_appendf(&bSrc, "fontscale = %.13g\n", rPikVar); } blob_append(&bSrc, zSrc, nSrc) /*have to dup input to ensure a NUL-terminated source string */; pikchr_process(blob_str(&bSrc), pikFlags, 0, ob); blob_reset(&bSrc); } /* Invoked for `...` blocks where there are nSep grave accents in a ** row that serve as the delimiter. According to CommonMark: ** ** * https://spec.commonmark.org/0.29/#fenced-code-blocks ** * https://spec.commonmark.org/0.29/#code-spans ** ** If nSep is 1 or 2, then this is a code-span which is inline. ** If nSep is 3 or more, then this is a fenced code block */ static int html_codespan( struct Blob *ob, /* Write the output here */ struct Blob *text, /* The stuff in between the code span marks */ int nSep, /* Number of grave accents marks as delimiters */ void *opaque ){ if( text==0 ){ /* no-op */ }else if( nSep<=2 ){ /* One or two graves: an in-line code span */ blob_append_literal(ob, ""); html_escape(ob, blob_buffer(text), blob_size(text)); 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; for(i=0; i=n ){ blob_appendf(ob, "
        %#h
        ", n, z); }else{ int k, j; i++; for(k=0; k%#h", n-i, z+i); }else{ for(j=k+1; j%#h", j-k, z+k, n-i, z+i); } } } } return 1; } static int html_double_emphasis( struct Blob *ob, struct Blob *text, char c, void *opaque ){ 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_appendb(ob, text); blob_append_literal(ob, ""); return 1; } static int html_image( struct Blob *ob, struct Blob *link, struct Blob *title, struct Blob *alt, void *opaque ){ blob_append_literal(ob, "\"");0 ){ blob_append_literal(ob, "\" title=\""); html_quote(ob, blob_buffer(title), blob_size(title)); } blob_append_literal(ob, "\" />"); return 1; } static int html_linebreak(struct Blob *ob, void *opaque){ blob_append_literal(ob, "
        \n"); return 1; } static int html_link( struct Blob *ob, struct Blob *link, struct Blob *title, struct Blob *content, void *opaque ){ char *zLink = blob_buffer(link); char *zTitle = title!=0 && blob_size(title)>0 ? blob_str(title) : 0; char zClose[20]; if( zLink==0 || zLink[0]==0 ){ zClose[0] = 0; }else{ static const int flags = WIKI_NOBADLINKS | WIKI_MARKDOWNLINKS ; wiki_resolve_hyperlink(ob, flags, zLink, zClose, sizeof(zClose), 0, zTitle); } if( blob_size(content)==0 ){ if( link ) blob_appendb(ob, link); }else{ blob_appendb(ob, content); } blob_append(ob, zClose, -1); return 1; } static int html_triple_emphasis( struct Blob *ob, struct Blob *text, char c, void *opaque ){ 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){ html_escape(ob, blob_buffer(text), blob_size(text)); } /* ** Convert markdown into HTML. ** ** The document title is placed in output_title if not NULL. Or if ** output_title is NULL, the document title appears in the body. */ void markdown_to_html( struct Blob *input_markdown, /* Markdown content to be rendered */ struct Blob *output_title, /* Put title here. May be NULL */ struct Blob *output_body /* Put document body here. */ ){ struct mkd_renderer html_renderer = { /* prolog and epilog */ html_prolog, html_epilog, html_footnotes, /* block level elements */ html_blockcode, html_blockquote, html_blockhtml, html_header, html_hrule, html_list, 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, html_emphasis, 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); }