diff options
| author | cashmere <cashmere@autistici.org> | 2026-09-04 09:05:24 +0200 |
|---|---|---|
| committer | cashmere <cashmere@autistici.org> | 2026-09-04 09:05:24 +0200 |
| commit | 4f49b16079b27d7ce633619e4cddfdf2bffbc952 (patch) | |
| tree | c296c1e7f3c3e2f6859c6d3a1b1b208779b89679 /src/markdown_html.c | |
| download | relic-4f49b16079b27d7ce633619e4cddfdf2bffbc952.tar.gz | |
Diffstat (limited to 'src/markdown_html.c')
| -rw-r--r-- | src/markdown_html.c | 1013 |
1 files changed, 1013 insertions, 0 deletions
diff --git a/src/markdown_html.c b/src/markdown_html.c new file mode 100644 index 0000000..d71f5a0 --- /dev/null +++ b/src/markdown_html.c @@ -0,0 +1,1013 @@ +/* +** 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<size ){ + beg = i; + while( i<size + && data[i]!='<' + && data[i]!='>' + && data[i]!='"' + && data[i]!='&' + && data[i]!='\'' + ){ + i++; + } + blob_append(ob, data+beg, i-beg); + while( i<size ){ + if( data[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 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<size ){ + beg = i; + while( i<size + && data[i]!='<' + && data[i]!='>' + && data[i]!='&' + ){ + i++; + } + blob_append(ob, data+beg, i-beg); + while( i<size ){ + 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++; + } + } +} + + +/* HTML block tags */ + +/* Size of the prolog: "<div class='markdown'>\n" */ +#define PROLOG_SIZE 23 + +static void html_prolog(struct Blob *ob, void *opaque){ + INTER_BLOCK(ob); + blob_append_literal(ob, "<div class=\"markdown\">\n"); + assert( blob_size(ob)==PROLOG_SIZE ); +} + +static void html_epilog(struct Blob *ob, void *opaque){ + INTER_BLOCK(ob); + blob_append_literal(ob, "</div>\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 <h1> element, then use it as the title. */ + if( blob_size(ob)<=PROLOG_SIZE + && size>9 + && title!=0 + && sqlite3_strnicmp("<h1",data,3)==0 + && sqlite3_strnicmp("</h1>", &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, "<pre><code>"); + html_escape(ob, blob_buffer(text), blob_size(text)); + blob_append_literal(ob, "</code></pre>\n"); +} + +static void html_blockquote(struct Blob *ob, struct Blob *text, void *opaque){ + INTER_BLOCK(ob); + blob_append_literal(ob, "<blockquote>\n"); + blob_appendb(ob, text); + blob_append_literal(ob, "</blockquote>\n"); +} + +static void html_header( + struct Blob *ob, + struct Blob *text, + int level, + void *opaque +){ + struct Blob *title = ((MarkdownToHtml*)opaque)->output_title; + char *z = 0; + int i,j; + /* 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); + z = fossil_strdup(blob_buffer(text)); + if( z==0 ){ + j = 0; + }else{ + /* + ** The GitHub "slugify" algorithm converts the text of a markdown header + ** into a ID for that header. The algorithm is: + ** + ** 1. ASCII alphanumerics -> convert to lower case + ** 2. Spaces, hyphens, underscores -> convert to '-' + ** 3. Non-ASCII -> preserve as-is + ** 4. Other punctuation -> remove + ** 5. Multiple consecutive dashes -> collapse to one + ** 6. Leading and trailing dashes -> remove + ** 7. Markup <...> and &...; -> remove + ** + ** This implementation does the conversion in-place. + */ + for(i=j=0; z[i]; i++){ + if( fossil_isalnum(z[i]) ){ + z[j++] = fossil_tolower(z[i]); + }else if( fossil_isspace(z[i]) || z[i]=='-' || z[i]=='_' ){ + if( j>0 && z[j-1]!='-' ) z[j++] = '-'; + }else if( z[i]=='<' ){ + do{ i++; }while( z[i]!=0 && z[i]!='>' ); + }else if( z[i]=='&' ){ + do{ i++; }while( z[i]!=0 && z[i]!=';' ); + }else if( (z[i]&0x80)!=0 ){ + z[j++] = z[i]; + } + } + if( j>0 && z[j-1]=='-' ) j--; + z[j] = 0; + } + if( j>0 ){ + blob_appendf(ob, "<h%d id=\"%s\">", level, z); + }else{ + blob_appendf(ob, "<h%d>", level); + } + blob_appendb(ob, text); + blob_appendf(ob, "</h%d>", level); + fossil_free(z); +} + +static void html_hrule(struct Blob *ob, void *opaque){ + INTER_BLOCK(ob); + blob_append_literal(ob, "<hr>\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, "</%s>\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, "<li>"); + blob_append(ob, text_data, text_size); + blob_append_literal(ob, "</li>\n"); +} + +static void html_paragraph(struct Blob *ob, struct Blob *text, void *opaque){ + INTER_BLOCK(ob); + blob_append_literal(ob, "<p>"); + blob_appendb(ob, text); + blob_append_literal(ob, "</p>\n"); +} + + +static void html_table( + struct Blob *ob, + struct Blob *head_row, + struct Blob *rows, + void *opaque +){ + INTER_BLOCK(ob); + blob_append_literal(ob, "<table class='md-table'>\n"); + if( head_row && blob_size(head_row)>0 ){ + blob_append_literal(ob, "<thead>\n"); + blob_appendb(ob, head_row); + blob_append_literal(ob, "</thead>\n<tbody>\n"); + } + if( rows ){ + blob_appendb(ob, rows); + } + if( head_row && blob_size(head_row)>0 ){ + blob_append_literal(ob, "</tbody>\n"); + } + blob_append_literal(ob, "</table>\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, " <th"); + }else{ + blob_append_literal(ob, " <td"); + } + switch( flags & MKD_CELL_ALIGN_MASK ){ + case MKD_CELL_ALIGN_LEFT: { + blob_append_literal(ob, " style=\"text-align:left\""); + break; + } + case MKD_CELL_ALIGN_RIGHT: { + blob_append_literal(ob, " style=\"text-align:right\""); + break; + } + case MKD_CELL_ALIGN_CENTER: { + blob_append_literal(ob, " style=\"text-align:center\""); + break; + } + } + blob_append_literal(ob, ">"); + blob_appendb(ob, text); + if( flags & MKD_CELL_HEAD ){ + blob_append_literal(ob, "</th>\n"); + }else{ + blob_append_literal(ob, "</td>\n"); + } +} + +static void html_table_row( + struct Blob *ob, + struct Blob *cells, + int flags, + void *opaque +){ + blob_append_literal(ob, " <tr>\n"); + blob_appendb(ob, cells); + blob_append_literal(ob, " </tr>\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, "<span class='fn-upc'>" + "<span class='fn-upcDot'>.</span>"); + } + n = 0; + do{ + z++; + if( *z!='.' && *z!=':' ){ + assert( fossil_isalnum(*z) || *z=='-' ); + n++; + continue; + } + assert( n ); + if( bHTML ) blob_append_literal(ob, "<span class='"); + blob_append_literal(ob, "fn-upc-"); + + for(i=-n; i<0; i++){ + blob_append_char(ob, fossil_tolower(z[i]) ); + } + if( bHTML ){ + blob_append_literal(ob, "'>"); + blob_append(ob, z-n, n); + blob_append_literal(ob, "</span>"); + }else{ + blob_append_char(ob, ' '); + } + n = 0; + if( bHTML ){ + if( *z==':' ){ + blob_append_literal(ob,"<span class='fn-upcColon'>:</span>"); + }else{ + blob_append_literal(ob,"<span class='fn-upcDot'>.</span>"); + } + } + }while( *z != ':' ); + if( bHTML ) blob_append_literal(ob,"</span>\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 */ + sqlite3_snprintf(sizeof(pos), pos, "%s-%d-%s", ctx->unique.c, iMark, l.c); + if(span && blob_size(span)) { + blob_append_literal(ob,"<span class='"); + append_footnote_upc(ob, upc, 0); + blob_append_literal(ob,"notescope' id='noteref"); + blob_appendf(ob,"%s'>",pos); + blob_appendb(ob, span); + blob_trim(ob); + blob_append_literal(ob,"<sup class='noteref'><a href='"); + BLOB_APPEND_URI(ob, ctx); + blob_appendf(ob,"#footnote%s'>%d</a></sup></span>", pos, iMark); + }else{ + blob_trim(ob); + blob_append_literal(ob,"<sup class='"); + append_footnote_upc(ob, upc, 0); + blob_append_literal(ob,"noteref'><a href='"); + BLOB_APPEND_URI(ob, ctx); + blob_appendf(ob,"#footnote%s' id='noteref%s'>%d</a></sup>", + pos, pos, iMark); + } + }else{ /* misreference */ + assert( iMark == -1 ); + + sqlite3_snprintf(sizeof(pos), pos, "%s-%s", ctx->unique.c, l.c); + if(span && blob_size(span)) { + blob_appendf(ob, "<span class='notescope' id='misref%s'>", pos); + blob_appendb(ob, span); + blob_trim(ob); + blob_append_literal(ob, "<sup class='noteref misref'><a href='"); + BLOB_APPEND_URI(ob, ctx); + blob_appendf(ob, "#misreference%s'>misref</a></sup></span>", pos); + }else{ + blob_trim(ob); + blob_append_literal(ob, "<sup class='noteref misref'><a href='"); + BLOB_APPEND_URI(ob, ctx); + blob_appendf(ob, "#misreference%s' id='misref%s'>", pos, pos); + blob_append_literal(ob, "misref</a></sup>"); + } + } + 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,"<li class='fn-misreference'>" + "<sup class='fn-backrefs'>"); + if( nUsed == 1 ){ + blob_appendf(ob,"<a id='misreference%s-a' href='", unique); + BLOB_APPEND_URI(ob, ctx); + blob_appendf(ob,"#misref%s-a'>^</a>", unique); + }else{ + int i; + blob_append_char(ob, '^'); + for(i=0; i<nUsed && i<26; i++){ + const int c = i + (unsigned)'a'; + blob_appendf(ob," <a id='misreference%s-%c' href='", unique,c); + BLOB_APPEND_URI(ob, ctx); + blob_appendf(ob,"#misref%s-%c'>%c</a>", unique,c, c); + } + if( i < nUsed ) blob_append_literal(ob," …"); + } + blob_append_literal(ob,"</sup>\n<span>Misreference</span>"); + }else if( iMark > 0 ){ /* regular, joined and overnested footnotes */ + char pos[24]; + int bJoin = 0; + #define _joined_footnote_indicator "<ul class='fn-joined'>" + #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); + sqlite3_snprintf(sizeof(pos), pos, "%s-%d", unique, iMark); + blob_appendf(ob, "<li id='footnote%s' class='", pos); + if( nUsed ){ + if( blob_size(text)>=_jfi_sz && + !memcmp(blob_buffer(text),_joined_footnote_indicator,_jfi_sz)){ + bJoin = 1; + blob_append_literal(ob, "fn-joined "); + } + append_footnote_upc(ob, upc, 0); + }else{ + blob_append_literal(ob, "fn-toodeep "); + } + if( nUsed <= 1 ){ + blob_append_literal(ob, "fn-monoref'><sup class='fn-backrefs'>"); + blob_appendf(ob,"<a id='footnote%s-a' href='", pos); + BLOB_APPEND_URI(ob, ctx); + blob_appendf(ob,"#noteref%s-a'>^</a>", pos); + }else{ + int i; + blob_append_literal(ob, "fn-polyref'><sup class='fn-backrefs'>^"); + for(i=0; i<nUsed && i<26; i++){ + const int c = i + (unsigned)'a'; + blob_appendf(ob," <a id='footnote%s-%c' href='", pos,c); + BLOB_APPEND_URI(ob, ctx); + blob_appendf(ob,"#noteref%s-%c'>%c</a>", pos,c, c); + } + /* It's unlikely that so many backrefs will be usefull */ + /* but maybe for some machine generated documents... */ + for(; i<nUsed && i<676; i++){ + const bitfield64_t l = to_base26(i,0); + blob_appendf(ob," <a id='footnote%s-%s' href='", pos, l.c); + BLOB_APPEND_URI(ob, ctx); + blob_appendf(ob,"#noteref%s-%s'>%s</a>", pos,l.c, l.c); + } + if( i < nUsed ) blob_append_literal(ob," …"); + } + blob_append_literal(ob,"</sup>\n"); + if( bJoin ){ + blob_append_literal(ob,"<sup class='fn-joined'></sup><ul>"); + 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,"<i></i>\n" + "<pre><code class='language-markdown'>"); + if( blob_size(upc) ){ + blob_appendb(ob, upc); + } + html_escape(ob, blob_buffer(text), blob_size(text)); + blob_append_literal(ob,"</code></pre>"); + } + #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,"<li class='fn-unreferenced'>\n[^ <code>"); + html_escape(ob, blob_buffer(id), blob_size(id)); + blob_append_literal(ob, "</code> ]<i></i>\n" + "<pre><code class='language-markdown'>"); + if( blob_size(upc) ){ + blob_appendb(ob, upc); + } + html_escape(ob, blob_buffer(text), blob_size(text)); + blob_append_literal(ob,"</code></pre>"); + } + blob_append_literal(ob, "\n</li>\n"); +} + +static void html_footnotes( + struct Blob *ob, const struct Blob *items, void *opaque +){ + if( items && blob_size(items) ){ + blob_append_literal(ob, + "\n<hr class='footnotes-separator'/>\n<ol class='footnotes'>\n"); + blob_appendb(ob, items); + blob_append_literal(ob, "</ol>\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, "<a href=\""); + if( type==MKDA_IMPLICIT_EMAIL ) blob_append_literal(ob, "mailto:"); + html_quote(ob, blob_buffer(link), blob_size(link)); + 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, "</a>"); + return 1; +} + +/* +** Flags for use with/via pikchr_to_html_add_flags(). +*/ +static int pikchrToHtmlFlags = 0; +/* +** Sets additional pikchr_process() flags to use for all future calls +** to pikch_to_html(). This is intended to be used by commands such as +** test-wiki-render and test-markdown-render to set the +** PIKCHR_PROCESS_DARK_MODE flag for all embedded pikchr elements. +** +** Not all PIKCHR_PROCESS flags are legal, as pikchr_to_html() +** hard-codes a subset of flags and passing arbitrary flags here may +** interfere with that. +** +** The only tested/intended use of this function is to pass it either +** 0 or PIKCHR_PROCESS_DARK_MODE. +** +** Design note: this is not implemented as an additional argument to +** pikchr_to_html() because the commands for which dark-mode rendering +** are now supported (test-wiki-render and test-markdown-render) are +** far removed from their corresponding pikchr_to_html() calls and +** there is no direct path from those commands to those calls. A +** cleaner, but much more invasive, approach would be to add a flag to +** markdown_to_html(), extend the WIKI_... flags with +** WIKI_DARK_PIKCHR, and extend both wiki.c:Renderer and +** markdown_html.c:MarkdownToHtml to contain and pass on that flag. +*/ +void pikchr_to_html_add_flags( int f ){ + pikchrToHtmlFlags = f; +} + +/* +** 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 + | pikchrToHtmlFlags; + Blob bSrc = empty_blob; + const char *zPikVar; + double rPikVar; + + while( nArg>0 ){ + int i; + for(i=0; i<nArg && !fossil_isspace(zArg[i]); i++){} + if( i==6 && strncmp(zArg, "center", 6)==0 ){ + pikFlags |= PIKCHR_PROCESS_DIV_CENTER; + }else if( i==6 && strncmp(zArg, "indent", 6)==0 ){ + pikFlags |= PIKCHR_PROCESS_DIV_INDENT; + }else if( i==10 && strncmp(zArg, "float-left", 10)==0 ){ + pikFlags |= PIKCHR_PROCESS_DIV_FLOAT_LEFT; + }else if( i==11 && strncmp(zArg, "float-right", 11)==0 ){ + pikFlags |= PIKCHR_PROCESS_DIV_FLOAT_RIGHT; + }else if( i==6 && strncmp(zArg, "toggle", 6)==0 ){ + pikFlags |= PIKCHR_PROCESS_DIV_TOGGLE; + }else if( i==6 && strncmp(zArg, "source", 6)==0 ){ + pikFlags |= PIKCHR_PROCESS_DIV_SOURCE; + }else if( i==13 && strncmp(zArg, "source-inline", 13)==0 ){ + pikFlags |= PIKCHR_PROCESS_DIV_SOURCE_INLINE; + } + while( i<nArg && fossil_isspace(zArg[i]) ){ i++; } + zArg += i; + nArg -= i; + } + if( skin_detail_boolean("white-foreground") ){ + pikFlags |= 0x02; /* PIKCHR_DARK_MODE */ + } + zPikVar = skin_detail("pikchr-foreground"); + if( zPikVar && zPikVar[0] ){ + blob_appendf(&bSrc, "fgcolor = %s\n", zPikVar); + } + zPikVar = skin_detail("pikchr-background"); + if( zPikVar && zPikVar[0] ){ + blob_appendf(&bSrc, "bgcolor = %s\n", zPikVar); + } + zPikVar = skin_detail("pikchr-scale"); + if( zPikVar + && (rPikVar = atof(zPikVar))>=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, 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, "<code>"); + html_escape(ob, blob_buffer(text), blob_size(text)); + blob_append_literal(ob, "</code>"); + }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 && z[i]!='\n'; i++){} + if( i>=n ){ + blob_appendf(ob, "<pre><code>%#h</code></pre>", n, z); + }else{ + int k, j; + i++; + for(k=0; k<i && fossil_isspace(z[k]); k++){} + if( k==i ){ + blob_appendf(ob, "<pre><code>%#h</code></pre>", n-i, z+i); + }else{ + for(j=k+1; j<i && !fossil_isspace(z[j]); j++){} + if( j-k==6 && strncmp(z+k,"pikchr",6)==0 ){ + while( j<i && fossil_isspace(z[j]) ){ j++; } + pikchr_to_html(ob, z+i, n-i, z+j, i-j); + }else{ + blob_appendf(ob, "<pre><code class='language-%#h'>%#h</code></pre>", + 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, "<strong>"); + blob_appendb(ob, text); + blob_append_literal(ob, "</strong>"); + return 1; +} + +static int html_emphasis( + struct Blob *ob, + struct Blob *text, + char c, + void *opaque +){ + blob_append_literal(ob, "<em>"); + blob_appendb(ob, text); + blob_append_literal(ob, "</em>"); + return 1; +} + +static int html_image( + struct Blob *ob, + struct Blob *link, + struct Blob *title, + struct Blob *alt, + void *opaque +){ + blob_append_literal(ob, "<img src=\""); + html_quote(ob, blob_buffer(link), blob_size(link)); + blob_append_literal(ob, "\" alt=\""); + html_quote(ob, blob_buffer(alt), blob_size(alt)); + if( title && blob_size(title)>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, "<br>\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, "<strong><em>"); + blob_appendb(ob, text); + blob_append_literal(ob, "</em></strong>"); + 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); +} + +/* +** Undo HTML escapes in Blob p. In other words convert: +** +** & -> & +** < -> < +** > -> > +** " -> " +** &#NNN; -> ascii character NNN +*/ +void markdown_dehtmlize_blob(Blob *p){ + char *z; + unsigned int j, k; + + z = p->aData; + for(j=k=0; j<p->nUsed; j++){ + char c = z[j]; + if( c=='&' ){ + if( z[j+1]=='#' && fossil_isdigit(z[j+2]) ){ + int n = 3; + int x = z[j+2] - '0'; + if( fossil_isdigit(z[j+3]) ){ + x = x*10 + z[j+3] - '0'; + n++; + if( fossil_isdigit(z[j+4]) ){ + x = x*10 + z[j+4] - '0'; + n++; + } + } + if( z[j+n]==';' ){ + z[k++] = (char)x; + j += n; + }else{ + z[k++] = c; + } + }else if( memcmp(&z[j],"<",4)==0 ){ + z[k++] = '<'; + j += 3; + }else if( memcmp(&z[j],">",4)==0 ){ + z[k++] = '>'; + j += 3; + }else if( memcmp(&z[j],""",6)==0 ){ + z[k++] = '"'; + j += 5; + }else if( memcmp(&z[j],"&",5)==0 ){ + z[k++] = '&'; + j += 4; + }else{ + z[k++] = c; + } + }else{ + z[k++] = c; + } + } + z[k] = 0; + p->nUsed = k; +} |
