/* markdown: a C implementation of John Gruber's Markdown markup language.
*
* Copyright (C) 2007 David L Parsons.
* The redistribution terms are provided in the COPYRIGHT file that must
* be distributed with this source code.
*/
#include ", " " };
static char *End[] = { "", "", MKD_NOIMAGE|MKD_TAGTEXT, IS_URL };
static linkytype linkt = { 0, 0, "", "", MKD_NOLINKS, IS_URL };
/*
* pseudo-protocols for [][];
*
* id: generates tag
* class: generates tag
* raw: just dump the link without any processing
*/
static linkytype specials[] = {
{ "id:", 3, "", "", 0, 0 },
{ "raw:", 4, 0, 0, 0, 0, 0, MKD_NOHTML, 0 },
{ "lang:", 5, "", "", 0, 0 },
{ "abbr:", 5, "", "", 0, 0 },
{ "class:", 6, "", "", 0, 0 },
} ;
#define NR(x) (sizeof x / sizeof x[0])
/* see if t contains one of our pseudo-protocols.
*/
static linkytype *
pseudo(Cstring t)
{
int i;
linkytype *r;
for ( i=0, r=specials; i < NR(specials); i++,r++ ) {
if ( (S(t) > r->szpat) && (strncasecmp(T(t), r->pat, r->szpat) == 0) )
return r;
}
return 0;
}
/* print out the start of an `img' or `a' tag, applying callbacks as needed.
*/
static void
printlinkyref(MMIOT *f, linkytype *tag, char *link, int size)
{
char *edit;
if ( f->flags & IS_LABEL )
return;
Qstring(tag->link_pfx, f);
if ( tag->kind & IS_URL ) {
if ( f->cb && f->cb->e_url && (edit = (*f->cb->e_url)(link, size, f->cb->e_data)) ) {
puturl(edit, strlen(edit), f, 0);
if ( f->cb->e_free ) (*f->cb->e_free)(edit, f->cb->e_data);
}
else
puturl(link + tag->szpat, size - tag->szpat, f, 0);
}
else
___mkd_reparse(link + tag->szpat, size - tag->szpat, MKD_TAGTEXT, f, 0);
Qstring(tag->link_sfx, f);
if ( f->cb && f->cb->e_flags && (edit = (*f->cb->e_flags)(link, size, f->cb->e_data)) ) {
Qchar(' ', f);
Qstring(edit, f);
if ( f->cb->e_free ) (*f->cb->e_free)(edit, f->cb->e_data);
}
} /* printlinkyref */
/* helper function for php markdown extra footnotes; allow the user to
* define a prefix tag instead of just `fn`
*/
static char *
p_or_nothing(p)
MMIOT *p;
{
return p->ref_prefix ? p->ref_prefix : "fn";
}
/* php markdown extra/daring fireball style print footnotes
*/
static int
extra_linky(MMIOT *f, Cstring text, Footnote *ref)
{
if ( ref->flags & REFERENCED )
return 0;
if ( f->flags & IS_LABEL )
___mkd_reparse(T(text), S(text), linkt.flags, f, 0);
else {
ref->flags |= REFERENCED;
ref->refnumber = ++ f->footnotes->reference;
Qprintf(f, "%d",
p_or_nothing(f), ref->refnumber,
p_or_nothing(f), ref->refnumber, ref->refnumber);
}
return 1;
} /* extra_linky */
/* print out a linky (or fail if it's Not Allowed)
*/
static int
linkyformat(MMIOT *f, Cstring text, int image, Footnote *ref)
{
linkytype *tag;
if ( image )
tag = &imaget;
else if ( tag = pseudo(ref->link) ) {
if ( f->flags & (MKD_NO_EXT|MKD_SAFELINK) )
return 0;
}
else if ( (f->flags & MKD_SAFELINK) && T(ref->link)
&& (T(ref->link)[0] != '/')
&& !isautoprefix(T(ref->link), S(ref->link)) )
/* if MKD_SAFELINK, only accept links that are local or
* a well-known protocol
*/
return 0;
else
tag = &linkt;
if ( f->flags & tag->flags )
return 0;
if ( f->flags & IS_LABEL )
___mkd_reparse(T(text), S(text), tag->flags, f, 0);
else if ( tag->link_pfx ) {
printlinkyref(f, tag, T(ref->link), S(ref->link));
if ( tag->WxH ) {
if ( ref->height ) Qprintf(f," height=\"%d\"", ref->height);
if ( ref->width ) Qprintf(f, " width=\"%d\"", ref->width);
}
if ( S(ref->title) ) {
Qstring(" title=\"", f);
___mkd_reparse(T(ref->title), S(ref->title), MKD_TAGTEXT, f, 0);
Qchar('"', f);
}
Qstring(tag->text_pfx, f);
___mkd_reparse(T(text), S(text), tag->flags, f, 0);
Qstring(tag->text_sfx, f);
}
else
Qwrite(T(ref->link) + tag->szpat, S(ref->link) - tag->szpat, f);
return 1;
} /* linkyformat */
/*
* process embedded links and images
*/
static int
linkylinky(int image, MMIOT *f)
{
int start = mmiottell(f);
Cstring name;
Footnote key, *ref;
int status = 0;
int extra_footnote = 0;
CREATE(name);
memset(&key, 0, sizeof key);
if ( linkylabel(f, &name) ) {
if ( peek(f,1) == '(' ) {
pull(f);
if ( linkyurl(f, image, &key) )
status = linkyformat(f, name, image, &key);
}
else {
int goodlink, implicit_mark = mmiottell(f);
if ( isspace(peek(f,1)) )
pull(f);
if ( peek(f,1) == '[' ) {
pull(f); /* consume leading '[' */
goodlink = linkylabel(f, &key.tag);
}
else {
/* new markdown implicit name syntax doesn't
* require a second []
*/
mmiotseek(f, implicit_mark);
goodlink = !(f->flags & MKD_1_COMPAT);
if ( (f->flags & MKD_EXTRA_FOOTNOTE) && (!image) && S(name) && T(name)[0] == '^' )
extra_footnote = 1;
}
if ( goodlink ) {
if ( !S(key.tag) ) {
DELETE(key.tag);
T(key.tag) = T(name);
S(key.tag) = S(name);
}
if ( ref = bsearch(&key, T(f->footnotes->note),
S(f->footnotes->note),
sizeof key, (stfu)__mkd_footsort) ) {
if ( extra_footnote )
status = extra_linky(f,name,ref);
else
status = linkyformat(f, name, image, ref);
}
}
}
}
DELETE(name);
___mkd_freefootnote(&key);
if ( status == 0 )
mmiotseek(f, start);
return status;
}
/* write a character to output, doing text escapes ( & -> &,
* > -> > < -> < )
*/
static void
cputc(int c, MMIOT *f)
{
switch (c) {
case '&': Qstring("&", f); break;
case '>': Qstring(">", f); break;
case '<': Qstring("<", f); break;
default : Qchar(c, f); break;
}
}
/*
* convert an email address to a string of nonsense
*/
static void
mangle(char *s, int len, MMIOT *f)
{
while ( len-- > 0 ) {
#if DEBIAN_GLITCH
Qprintf(f, "d;", *((unsigned char*)(s++)) );
#else
Qstring("", f);
Qprintf(f, COINTOSS() ? "x%02x;" : "%02d;", *((unsigned char*)(s++)) );
#endif
}
}
/* nrticks() -- count up a row of tick marks
*/
static int
nrticks(int offset, int tickchar, MMIOT *f)
{
int tick = 0;
while ( peek(f, offset+tick) == tickchar ) tick++;
return tick;
} /* nrticks */
/* matchticks() -- match a certain # of ticks, and if that fails
* match the largest subset of those ticks.
*
* if a subset was matched, return the # of ticks
* that were matched.
*/
static int
matchticks(MMIOT *f, int tickchar, int ticks, int *endticks)
{
int size, count, c;
int subsize=0, subtick=0;
*endticks = ticks;
for (size = 0; (c=peek(f,size+ticks)) != EOF; size ++) {
if ( (c == tickchar) && ( count = nrticks(size+ticks,tickchar,f)) ) {
if ( count == ticks )
return size;
else if ( count ) {
if ( (count > subtick) && (count < ticks) ) {
subsize = size;
subtick = count;
}
size += count;
}
}
}
if ( subsize ) {
*endticks = subtick;
return subsize;
}
return 0;
} /* matchticks */
/* code() -- write a string out as code. The only characters that have
* special meaning in a code block are * `<' and `&' , which
* are /always/ expanded to < and &
*/
static void
code(MMIOT *f, char *s, int length)
{
int i,c;
for ( i=0; i < length; i++ )
if ( (c = s[i]) == MKD_EOLN) /* ^C: expand back to 2 spaces */
Qstring(" ", f);
else if ( c == '\\' && (i < length-1) && escaped(f, s[i+1]) )
cputc(s[++i], f);
else
cputc(c, f);
} /* code */
/* delspan() -- write out a chunk of text, blocking with
...
*/
static void
delspan(MMIOT *f, int size)
{
Qstring("", f);
___mkd_reparse(cursor(f)-1, size, 0, f, 0);
Qstring("", f);
}
/* codespan() -- write out a chunk of text as code, trimming one
* space off the front and/or back as appropriate.
*/
static void
codespan(MMIOT *f, int size)
{
int i=0;
if ( size > 1 && peek(f, size-1) == ' ' ) --size;
if ( peek(f,i) == ' ' ) ++i, --size;
Qstring("", f);
code(f, cursor(f)+(i-1), size);
Qstring("
", f);
} /* codespan */
/* before letting a tag through, validate against
* MKD_NOLINKS and MKD_NOIMAGE
*/
static int
forbidden_tag(MMIOT *f)
{
int c = toupper(peek(f, 1));
if ( f->flags & MKD_NOHTML )
return 1;
if ( c == 'A' && (f->flags & MKD_NOLINKS) && !isthisalnum(f,2) )
return 1;
if ( c == 'I' && (f->flags & MKD_NOIMAGE)
&& strncasecmp(cursor(f)+1, "MG", 2) == 0
&& !isthisalnum(f,4) )
return 1;
return 0;
}
/* Check a string to see if it looks like a mail address
* "looks like a mail address" means alphanumeric + some
* specials, then a `@`, then alphanumeric + some specials,
* but with a `.`
*/
static int
maybe_address(char *p, int size)
{
int ok = 0;
for ( ;size && (isalnum(*p) || strchr("._-+*", *p)); ++p, --size)
;
if ( ! (size && *p == '@') )
return 0;
--size, ++p;
if ( size && *p == '.' ) return 0;
for ( ;size && (isalnum(*p) || strchr("._-+", *p)); ++p, --size )
if ( *p == '.' && size > 1 ) ok = 1;
return size ? 0 : ok;
}
/* The size-length token at cursor(f) is either a mailto:, an
* implicit mailto:, one of the approved url protocols, or just
* plain old text. If it's a mailto: or an approved protocol,
* linkify it, otherwise say "no"
*/
static int
process_possible_link(MMIOT *f, int size)
{
int address= 0;
int mailto = 0;
char *text = cursor(f);
if ( f->flags & MKD_NOLINKS ) return 0;
if ( (size > 7) && strncasecmp(text, "mailto:", 7) == 0 ) {
/* if it says it's a mailto, it's a mailto -- who am
* I to second-guess the user?
*/
address = 1;
mailto = 7; /* 7 is the length of "mailto:"; we need this */
}
else
address = maybe_address(text, size);
if ( address ) {
Qstring("", f);
mangle(text+mailto, size-mailto, f);
Qstring("", f);
return 1;
}
else if ( isautoprefix(text, size) ) {
printlinkyref(f, &linkt, text, size);
Qchar('>', f);
puturl(text,size,f, 1);
Qstring("", f);
return 1;
}
return 0;
} /* process_possible_link */
/* a < may be just a regular character, the start of an embedded html
* tag, or the start of an
", f);
break;
case '>': if ( tag_text(f) )
Qstring(">", f);
else
Qchar(c, f);
break;
case '"': if ( tag_text(f) )
Qstring(""", f);
else
Qchar(c, f);
break;
case '!': if ( peek(f,1) == '[' ) {
pull(f);
if ( tag_text(f) || !linkylinky(1, f) )
Qstring("![", f);
}
else
Qchar(c, f);
break;
case '[': if ( tag_text(f) || !linkylinky(0, f) )
Qchar(c, f);
break;
/* A^B -> AB */
case '^': if ( (f->flags & (MKD_NOSUPERSCRIPT|MKD_STRICT|MKD_TAGTEXT))
|| (isthisnonword(f,-1) && peek(f,-1) != ')')
|| isthisspace(f,1) )
Qchar(c,f);
else {
char *sup = cursor(f);
int len = 0;
if ( peek(f,1) == '(' ) {
int here = mmiottell(f);
pull(f);
if ( (len = parenthetical('(',')',f)) <= 0 ) {
mmiotseek(f,here);
Qchar(c, f);
break;
}
sup++;
}
else {
while ( isthisalnum(f,1+len) )
++len;
if ( !len ) {
Qchar(c,f);
break;
}
shift(f,len);
}
Qstring("",f);
___mkd_reparse(sup, len, 0, f, "()");
Qstring("", f);
}
break;
case '_':
/* Underscores don't count if they're in the middle of a word */
if ( !(f->flags & (MKD_NORELAXED|MKD_STRICT))
&& isthisalnum(f,-1)
&& isthisalnum(f,1) ) {
Qchar(c, f);
break;
}
case '*':
/* Underscores & stars don't count if they're out in the middle
* of whitespace */
if ( isthisspace(f,-1) && isthisspace(f,1) ) {
Qchar(c, f);
break;
}
/* else fall into the regular old emphasis case */
if ( tag_text(f) )
Qchar(c, f);
else {
for (rep = 1; peek(f,1) == c; pull(f) )
++rep;
Qem(f,c,rep);
}
break;
case '~': if ( (f->flags & (MKD_NOSTRIKETHROUGH|MKD_TAGTEXT|MKD_STRICT)) || ! tickhandler(f,c,2,0, delspan) )
Qchar(c, f);
break;
case '`': if ( tag_text(f) || !tickhandler(f,c,1,1,codespan) )
Qchar(c, f);
break;
case '\\': switch ( c = pull(f) ) {
case '&': Qstring("&", f);
break;
case '<': c = peek(f,1);
if ( (c == EOF) || isspace(c) )
Qstring("<", f);
else {
/* Markdown.pl does not escape <[nonwhite]
* sequences */
Qchar('\\', f);
shift(f, -1);
}
break;
case '^': if ( f->flags & (MKD_STRICT|MKD_NOSUPERSCRIPT) ) {
Qchar('\\', f);
shift(f,-1);
break;
}
Qchar(c, f);
break;
case ':': case '|':
if ( f->flags & MKD_NOTABLES ) {
Qchar('\\', f);
shift(f,-1);
break;
}
Qchar(c, f);
break;
case EOF: Qchar('\\', f);
break;
default: if ( escaped(f,c) ||
strchr(">#.-+{}]![*_\\()`", c) )
Qchar(c, f);
else {
Qchar('\\', f);
shift(f, -1);
}
break;
}
break;
case '<': if ( !maybe_tag_or_link(f) )
Qstring("<", f);
break;
case '&': j = (peek(f,1) == '#' ) ? 2 : 1;
while ( isthisalnum(f,j) )
++j;
if ( peek(f,j) != ';' )
Qstring("&", f);
else
Qchar(c, f);
break;
default: Qchar(c, f);
break;
}
}
/* truncate the input string after we've finished processing it */
S(f->in) = f->isp = 0;
} /* text */
/* print a header block
*/
static void
printheader(Paragraph *pp, MMIOT *f)
{
#if WITH_ID_ANCHOR
Qprintf(f, "\n", f);
while ( idx < S(p->text) ) {
first = idx;
if ( force && (colno >= S(align)-1) )
idx = S(p->text);
else
while ( (idx < S(p->text)) && (T(p->text)[idx] != '|') ) {
if ( T(p->text)[idx] == '\\' )
++idx;
++idx;
}
Qprintf(f, "<%s%s>",
block,
alignments[ (colno < S(align)) ? T(align)[colno] : a_NONE ]);
___mkd_reparse(T(p->text)+first, idx-first, 0, f, "|");
Qprintf(f, "%s>\n", block);
idx++;
colno++;
}
if ( force )
while (colno < S(align) ) {
Qprintf(f, "<%s>%s>\n", block, block);
++colno;
}
Qstring(" \n", f);
return colno;
}
static int
printtable(Paragraph *pp, MMIOT *f)
{
/* header, dashes, then lines of content */
Line *hdr, *dash, *body;
Istring align;
int hcols,start;
char *p;
enum e_alignments it;
hdr = pp->text;
dash= hdr->next;
body= dash->next;
if ( T(hdr->text)[hdr->dle] == '|' ) {
/* trim leading pipe off all lines
*/
Line *r;
for ( r = pp->text; r; r = r->next )
r->dle ++;
}
/* figure out cell alignments */
CREATE(align);
for (p=T(dash->text), start=dash->dle; start < S(dash->text); ) {
char first, last;
int end;
last=first=0;
for (end=start ; (end < S(dash->text)) && p[end] != '|'; ++ end ) {
if ( p[end] == '\\' )
++ end;
else if ( !isspace(p[end]) ) {
if ( !first) first = p[end];
last = p[end];
}
}
it = ( first == ':' ) ? (( last == ':') ? a_CENTER : a_LEFT)
: (( last == ':') ? a_RIGHT : a_NONE );
EXPAND(align) = it;
start = 1+end;
}
Qstring("\n", f);
Qstring("\n", f);
hcols = splat(hdr, "th", align, 0, f);
Qstring("\n", f);
if ( hcols < S(align) )
S(align) = hcols;
else
while ( hcols > S(align) )
EXPAND(align) = a_NONE;
Qstring("\n", f);
for ( ; body; body = body->next)
splat(body, "td", align, 1, f);
Qstring("\n", f);
Qstring("
\n", f);
DELETE(align);
return 1;
}
static int
printblock(Paragraph *pp, MMIOT *f)
{
Line *t = pp->text;
static char *Begin[] = { "", "
", f);
for ( blanks = 0; t ; t = t->next ) {
if ( S(t->text) > t->dle ) {
while ( blanks ) {
Qchar('\n', f);
--blanks;
}
code(f, T(t->text), S(t->text));
Qchar('\n', f);
}
else blanks++;
}
Qstring("
", f);
}
static void
printhtml(Line *t, MMIOT *f)
{
int blanks;
for ( blanks=0; t ; t = t->next )
if ( S(t->text) ) {
for ( ; blanks; --blanks )
Qchar('\n', f);
Qwrite(T(t->text), S(t->text), f);
Qchar('\n', f);
}
else
blanks++;
}
static void
htmlify(Paragraph *p, char *block, char *arguments, MMIOT *f)
{
___mkd_emblock(f);
if ( block )
Qprintf(f, arguments ? "<%s %s>" : "<%s>", block, arguments);
___mkd_emblock(f);
while (( p = display(p, f) )) {
___mkd_emblock(f);
Qstring("\n\n", f);
}
if ( block )
Qprintf(f, "%s>", block);
___mkd_emblock(f);
}
static void
definitionlist(Paragraph *p, MMIOT *f)
{
Line *tag;
if ( p ) {
Qstring("", p_or_nothing(m), t->refnumber); Csreparse(&m->out, T(t->title), S(t->title), 0); Csprintf(&m->out, "↩", p_or_nothing(m), t->refnumber); Csprintf(&m->out, "