/* markdown.c - generic markdown parser */

/*
 * Copyright (c) 2009, Natacha Porté
 * Copyright (c) 2011, Vicent Marti
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include "markdown.h"
#include "stack.h"

#include <assert.h>
#include <string.h>
#include <ctype.h>
#include <stdio.h>

#if defined(_WIN32)
#define strncasecmp _strnicmp
#endif

#define REF_TABLE_SIZE  8

#define BUFFER_BLOCK    0
#define BUFFER_SPAN     1

#define MKD_LI_END      8 /* internal list flag */

#define gperf_case_strncmp( s1, s2, n ) strncasecmp( s1, s2, n )
#define GPERF_DOWNCASE      1
#define GPERF_CASE_STRNCMP  1
#include "html_blocks.h"

/***************
* LOCAL TYPES *
***************/

/* link_ref: reference to a link */
struct link_ref
{
    unsigned int id;

    struct buf* link;
    struct buf* title;

    struct link_ref* next;
};

/* char_trigger: function pointer to render active chars */
/*   returns the number of chars taken care of */
/*   data is the pointer of the beginning of the span */
/*   offset is the number of valid chars before data */
struct sd_markdown;
typedef size_t (* char_trigger)( struct buf* ob, struct sd_markdown* rndr, uint8_t* data, size_t offset,
                size_t size );

static size_t char_emphasis( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t offset,
        size_t size );
static size_t char_linebreak( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t offset,
        size_t size );
static size_t char_codespan( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t offset,
        size_t size );
static size_t char_escape( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t offset,
        size_t size );
static size_t char_entity( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t offset,
        size_t size );
static size_t char_langle_tag( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t offset,
        size_t size );
static size_t char_autolink_url( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t offset,
        size_t size );
static size_t char_autolink_email( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t offset,
        size_t size );
static size_t char_autolink_www( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t offset,
        size_t size );
static size_t char_link( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t offset,
        size_t size );
static size_t char_superscript( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t offset,
        size_t size );

enum markdown_char_t
{
    MD_CHAR_NONE = 0,
    MD_CHAR_EMPHASIS,
    MD_CHAR_CODESPAN,
    MD_CHAR_LINEBREAK,
    MD_CHAR_LINK,
    MD_CHAR_LANGLE,
    MD_CHAR_ESCAPE,
    MD_CHAR_ENTITITY,
    MD_CHAR_AUTOLINK_URL,
    MD_CHAR_AUTOLINK_EMAIL,
    MD_CHAR_AUTOLINK_WWW,
    MD_CHAR_SUPERSCRIPT,
};

static char_trigger markdown_char_ptrs[] =
{
    NULL,
    &char_emphasis,
    &char_codespan,
    &char_linebreak,
    &char_link,
    &char_langle_tag,
    &char_escape,
    &char_entity,
    &char_autolink_url,
    &char_autolink_email,
    &char_autolink_www,
    &char_superscript,
};

/* render • structure containing one particular render */
struct sd_markdown
{
    struct sd_callbacks cb;
    void* opaque;

    struct link_ref* refs[REF_TABLE_SIZE];
    uint8_t active_char[256];
    struct stack work_bufs[2];
    unsigned int ext_flags;
    size_t max_nesting;
    int in_link_body;
};

/***************************
* HELPER FUNCTIONS *
***************************/

static inline struct buf* rndr_newbuf( struct sd_markdown* rndr, int type )
{
    static const size_t buf_size[2] = { 256, 64 };
    struct buf* work = NULL;
    struct stack* pool = &rndr->work_bufs[type];

    if( pool->size < pool->asize
        && pool->item[pool->size] != NULL )
    {
        work = pool->item[pool->size++];
        work->size = 0;
    }
    else
    {
        work = bufnew( buf_size[type] );
        stack_push( pool, work );
    }

    return work;
}


static inline void rndr_popbuf( struct sd_markdown* rndr, int type )
{
    rndr->work_bufs[type].size--;
}


static void unscape_text( struct buf* ob, struct buf* src )
{
    size_t i = 0, org;

    while( i < src->size )
    {
        org = i;

        while( i < src->size && src->data[i] != '\\' )
            i++;

        if( i > org )
            bufput( ob, src->data + org, i - org );

        if( i + 1 >= src->size )
            break;

        bufputc( ob, src->data[i + 1] );
        i += 2;
    }
}


static unsigned int hash_link_ref( const uint8_t* link_ref, size_t length )
{
    size_t i;
    unsigned int hash = 0;

    for( i = 0; i < length; ++i )
        hash = tolower( link_ref[i] ) + (hash << 6) + (hash << 16) - hash;

    return hash;
}


static struct link_ref* add_link_ref( struct link_ref** references,
        const uint8_t* name, size_t name_size )
{
    struct link_ref* ref = calloc( 1, sizeof(struct link_ref) );

    if( !ref )
        return NULL;

    ref->id = hash_link_ref( name, name_size );
    ref->next = references[ref->id % REF_TABLE_SIZE];

    references[ref->id % REF_TABLE_SIZE] = ref;
    return ref;
}


static struct link_ref* find_link_ref( struct link_ref** references, uint8_t* name, size_t length )
{
    unsigned int hash = hash_link_ref( name, length );
    struct link_ref* ref = NULL;

    ref = references[hash % REF_TABLE_SIZE];

    while( ref != NULL )
    {
        if( ref->id == hash )
            return ref;

        ref = ref->next;
    }

    return NULL;
}


static void free_link_refs( struct link_ref** references )
{
    size_t i;

    for( i = 0; i < REF_TABLE_SIZE; ++i )
    {
        struct link_ref* r = references[i];
        struct link_ref* next;

        while( r )
        {
            next = r->next;
            bufrelease( r->link );
            bufrelease( r->title );
            free( r );
            r = next;
        }
    }
}


/*
 * Check whether a char is a Markdown space.
 *
 * Right now we only consider spaces the actual
 * space and a newline: tabs and carriage returns
 * are filtered out during the preprocessing phase.
 *
 * If we wanted to actually be UTF-8 compliant, we
 * should instead extract an Unicode codepoint from
 * this character and check for space properties.
 */
static inline int _isspace( int c )
{
    return c == ' ' || c == '\n';
}


/****************************
* INLINE PARSING FUNCTIONS *
****************************/

/* is_mail_autolink • looks for the address part of a mail autolink and '>' */
/* this is less strict than the original markdown e-mail address matching */
static size_t is_mail_autolink( uint8_t* data, size_t size )
{
    size_t i = 0, nb = 0;

    /* address is assumed to be: [-@._a-zA-Z0-9]+ with exactly one '@' */
    for( i = 0; i < size; ++i )
    {
        if( isalnum( data[i] ) )
            continue;

        switch( data[i] )
        {
        case '@':
            nb++;

        case '-':
        case '.':
        case '_':
            break;

        case '>':
            return (nb == 1) ? i + 1 : 0;

        default:
            return 0;
        }
    }

    return 0;
}


/* tag_length • returns the length of the given tag, or 0 is it's not valid */
static size_t tag_length( uint8_t* data, size_t size, enum mkd_autolink* autolink )
{
    size_t i, j;

    /* a valid tag can't be shorter than 3 chars */
    if( size < 3 )
        return 0;

    /* begins with a '<' optionally followed by '/', followed by letter or number */
    if( data[0] != '<' )
        return 0;

    i = (data[1] == '/') ? 2 : 1;

    if( !isalnum( data[i] ) )
        return 0;

    /* scheme test */
    *autolink = MKDA_NOT_AUTOLINK;

    /* try to find the beginning of an URI */
    while( i < size && (isalnum( data[i] ) || data[i] == '.' || data[i] == '+' || data[i] == '-') )
        i++;

    if( i > 1 && data[i] == '@' )
    {
        if( ( j = is_mail_autolink( data + i, size - i ) ) != 0 )
        {
            *autolink = MKDA_EMAIL;
            return i + j;
        }
    }

    if( i > 2 && data[i] == ':' )
    {
        *autolink = MKDA_NORMAL;
        i++;
    }

    /* completing autolink test: no whitespace or ' or " */
    if( i >= size )
        *autolink = MKDA_NOT_AUTOLINK;

    else if( *autolink )
    {
        j = i;

        while( i < size )
        {
            if( data[i] == '\\' )
                i += 2;
            else if( data[i] == '>' || data[i] == '\''
                     || data[i] == '"' || data[i] == ' ' || data[i] == '\n' )
                break;
            else
                i++;
        }

        if( i >= size )
            return 0;

        if( i > j && data[i] == '>' )
            return i + 1;

        /* one of the forbidden chars has been found */
        *autolink = MKDA_NOT_AUTOLINK;
    }

    /* looking for sometinhg looking like a tag end */
    while( i < size && data[i] != '>' )
        i++;

    if( i >= size )
        return 0;

    return i + 1;
}


/* parse_inline • parses inline markdown elements */
static void parse_inline( struct buf* ob, struct sd_markdown* rndr, uint8_t* data, size_t size )
{
    size_t i = 0, end = 0;
    uint8_t action  = 0;
    struct buf work = { 0, 0, 0, 0 };

    if( rndr->work_bufs[BUFFER_SPAN].size +
        rndr->work_bufs[BUFFER_BLOCK].size > rndr->max_nesting )
        return;

    while( i < size )
    {
        /* copying inactive chars into the output */
        while( end < size && (action = rndr->active_char[data[end]]) == 0 )
        {
            end++;
        }

        if( rndr->cb.normal_text )
        {
            work.data = data + i;
            work.size = end - i;
            rndr->cb.normal_text( ob, &work, rndr->opaque );
        }
        else
            bufput( ob, data + i, end - i );

        if( end >= size )
            break;

        i = end;

        end = markdown_char_ptrs[(int) action]( ob, rndr, data + i, i, size - i );

        if( !end )    /* no action from the callback */
            end = i + 1;
        else
        {
            i += end;
            end = i;
        }
    }
}


/* find_emph_char • looks for the next emph uint8_t, skipping other constructs */
static size_t find_emph_char( uint8_t* data, size_t size, uint8_t c )
{
    size_t i = 1;

    while( i < size )
    {
        while( i < size && data[i] != c && data[i] != '`' && data[i] != '[' )
            i++;

        if( i == size )
            return 0;

        if( data[i] == c )
            return i;

        /* not counting escaped chars */
        if( i && data[i - 1] == '\\' )
        {
            i++; continue;
        }

        if( data[i] == '`' )
        {
            size_t span_nb = 0, bt;
            size_t tmp_i = 0;

            /* counting the number of opening backticks */
            while( i < size && data[i] == '`' )
            {
                i++; span_nb++;
            }

            if( i >= size )
                return 0;

            /* finding the matching closing sequence */
            bt = 0;

            while( i < size && bt < span_nb )
            {
                if( !tmp_i && data[i] == c )
                    tmp_i = i;

                if( data[i] == '`' )
                    bt++;
                else
                    bt = 0;

                i++;
            }

            if( i >= size )
                return tmp_i;
        }
        /* skipping a link */
        else if( data[i] == '[' )
        {
            size_t tmp_i = 0;
            uint8_t cc;

            i++;

            while( i < size && data[i] != ']' )
            {
                if( !tmp_i && data[i] == c )
                    tmp_i = i;

                i++;
            }

            i++;

            while( i < size && (data[i] == ' ' || data[i] == '\n') )
                i++;

            if( i >= size )
                return tmp_i;

            switch( data[i] )
            {
            case '[':
                cc = ']'; break;

            case '(':
                cc = ')'; break;

            default:

                if( tmp_i )
                    return tmp_i;
                else
                    continue;
            }

            i++;

            while( i < size && data[i] != cc )
            {
                if( !tmp_i && data[i] == c )
                    tmp_i = i;

                i++;
            }

            if( i >= size )
                return tmp_i;

            i++;
        }
    }

    return 0;
}


/* parse_emph1 • parsing single emphase */
/* closed by a symbol not preceded by whitespace and not followed by symbol */
static size_t parse_emph1( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t size,
        uint8_t c )
{
    size_t i = 0, len;
    struct buf* work = 0;
    int r;

    if( !rndr->cb.emphasis )
        return 0;

    /* skipping one symbol if coming from emph3 */
    if( size > 1 && data[0] == c && data[1] == c )
        i = 1;

    while( i < size )
    {
        len = find_emph_char( data + i, size - i, c );

        if( !len )
            return 0;

        i += len;

        if( i >= size )
            return 0;

        if( data[i] == c && !_isspace( data[i - 1] ) )
        {
            if( rndr->ext_flags & MKDEXT_NO_INTRA_EMPHASIS )
            {
                if( i + 1 < size && isalnum( data[i + 1] ) )
                    continue;
            }

            work = rndr_newbuf( rndr, BUFFER_SPAN );
            parse_inline( work, rndr, data, i );
            r = rndr->cb.emphasis( ob, work, rndr->opaque );
            rndr_popbuf( rndr, BUFFER_SPAN );
            return r ? i + 1 : 0;
        }
    }

    return 0;
}


/* parse_emph2 • parsing single emphase */
static size_t parse_emph2( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t size,
        uint8_t c )
{
    int (* render_method)( struct buf* ob, const struct buf* text, void* opaque );
    size_t i = 0, len;
    struct buf* work = 0;
    int r;

    render_method = (c == '~') ? rndr->cb.strikethrough : rndr->cb.double_emphasis;

    if( !render_method )
        return 0;

    while( i < size )
    {
        len = find_emph_char( data + i, size - i, c );

        if( !len )
            return 0;

        i += len;

        if( i + 1 < size && data[i] == c && data[i + 1] == c && i && !_isspace( data[i - 1] ) )
        {
            work = rndr_newbuf( rndr, BUFFER_SPAN );
            parse_inline( work, rndr, data, i );
            r = render_method( ob, work, rndr->opaque );
            rndr_popbuf( rndr, BUFFER_SPAN );
            return r ? i + 2 : 0;
        }

        i++;
    }

    return 0;
}


/* parse_emph3 • parsing single emphase */
/* finds the first closing tag, and delegates to the other emph */
static size_t parse_emph3( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t size,
        uint8_t c )
{
    size_t i = 0, len;
    int r;

    while( i < size )
    {
        len = find_emph_char( data + i, size - i, c );

        if( !len )
            return 0;

        i += len;

        /* skip whitespace preceded symbols */
        if( data[i] != c || _isspace( data[i - 1] ) )
            continue;

        if( i + 2 < size && data[i + 1] == c && data[i + 2] == c && rndr->cb.triple_emphasis )
        {
            /* triple symbol found */
            struct buf* work = rndr_newbuf( rndr, BUFFER_SPAN );

            parse_inline( work, rndr, data, i );
            r = rndr->cb.triple_emphasis( ob, work, rndr->opaque );
            rndr_popbuf( rndr, BUFFER_SPAN );
            return r ? i + 3 : 0;
        }
        else if( i + 1 < size && data[i + 1] == c )
        {
            /* double symbol found, handing over to emph1 */
            len = parse_emph1( ob, rndr, data - 2, size + 2, c );

            if( !len )
                return 0;
            else
                return len - 2;
        }
        else
        {
            /* single symbol found, handing over to emph2 */
            len = parse_emph2( ob, rndr, data - 1, size + 1, c );

            if( !len )
                return 0;
            else
                return len - 1;
        }
    }

    return 0;
}


/* char_emphasis • single and double emphasis parsing */
static size_t char_emphasis( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t offset,
        size_t size )
{
    uint8_t c = data[0];
    size_t  ret;

    if( rndr->ext_flags & MKDEXT_NO_INTRA_EMPHASIS )
    {
        if( offset > 0 && !_isspace( data[-1] ) && data[-1] != '>' )
            return 0;
    }

    if( size > 2 && data[1] != c )
    {
        /* whitespace cannot follow an opening emphasis;
         * strikethrough only takes two characters '~~' */
        if( c == '~' || _isspace( data[1] )
            || ( ret = parse_emph1( ob, rndr, data + 1, size - 1, c ) ) == 0 )
            return 0;

        return ret + 1;
    }

    if( size > 3 && data[1] == c && data[2] != c )
    {
        if( _isspace( data[2] ) || ( ret = parse_emph2( ob, rndr, data + 2, size - 2, c ) ) == 0 )
            return 0;

        return ret + 2;
    }

    if( size > 4 && data[1] == c && data[2] == c && data[3] != c )
    {
        if( c == '~' || _isspace( data[3] )
            || ( ret = parse_emph3( ob, rndr, data + 3, size - 3, c ) ) == 0 )
            return 0;

        return ret + 3;
    }

    return 0;
}


/* char_linebreak • '\n' preceded by two spaces (assuming linebreak != 0) */
static size_t char_linebreak( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t offset,
        size_t size )
{
    if( offset < 2 || data[-1] != ' ' || data[-2] != ' ' )
        return 0;

    /* removing the last space from ob and rendering */
    while( ob->size && ob->data[ob->size - 1] == ' ' )
        ob->size--;

    return rndr->cb.linebreak( ob, rndr->opaque ) ? 1 : 0;
}


/* char_codespan • '`' parsing a code span (assuming codespan != 0) */
static size_t char_codespan( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t offset,
        size_t size )
{
    size_t end, nb = 0, i, f_begin, f_end;

    /* counting the number of backticks in the delimiter */
    while( nb < size && data[nb] == '`' )
        nb++;

    /* finding the next delimiter */
    i = 0;

    for( end = nb; end < size && i < nb; end++ )
    {
        if( data[end] == '`' )
            i++;
        else
            i = 0;
    }

    if( i < nb && end >= size )
        return 0;    /* no matching delimiter */

    /* trimming outside whitespaces */
    f_begin = nb;

    while( f_begin < end && data[f_begin] == ' ' )
        f_begin++;

    f_end = end - nb;

    while( f_end > nb && data[f_end - 1] == ' ' )
        f_end--;

    /* real code span */
    if( f_begin < f_end )
    {
        struct buf work = { data + f_begin, f_end - f_begin, 0, 0 };

        if( !rndr->cb.codespan( ob, &work, rndr->opaque ) )
            end = 0;
    }
    else
    {
        if( !rndr->cb.codespan( ob, 0, rndr->opaque ) )
            end = 0;
    }

    return end;
}


/* char_escape • '\\' backslash escape */
static size_t char_escape( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t offset,
        size_t size )
{
    static const char* escape_chars = "\\`*_{}[]()#+-.!:|&<>^~";
    struct buf work = { 0, 0, 0, 0 };

    if( size > 1 )
    {
        if( strchr( escape_chars, data[1] ) == NULL )
            return 0;

        if( rndr->cb.normal_text )
        {
            work.data = data + 1;
            work.size = 1;
            rndr->cb.normal_text( ob, &work, rndr->opaque );
        }
        else
            bufputc( ob, data[1] );
    }
    else if( size == 1 )
    {
        bufputc( ob, data[0] );
    }

    return 2;
}


/* char_entity • '&' escaped when it doesn't belong to an entity */
/* valid entities are assumed to be anything matching &#?[A-Za-z0-9]+; */
static size_t char_entity( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t offset,
        size_t size )
{
    size_t end = 1;
    struct buf work = { 0, 0, 0, 0 };

    if( end < size && data[end] == '#' )
        end++;

    while( end < size && isalnum( data[end] ) )
        end++;

    if( end < size && data[end] == ';' )
        end++;      /* real entity */
    else
        return 0;   /* lone '&' */

    if( rndr->cb.entity )
    {
        work.data = data;
        work.size = end;
        rndr->cb.entity( ob, &work, rndr->opaque );
    }
    else
        bufput( ob, data, end );

    return end;
}


/* char_langle_tag • '<' when tags or autolinks are allowed */
static size_t char_langle_tag( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t offset,
        size_t size )
{
    enum mkd_autolink altype = MKDA_NOT_AUTOLINK;
    size_t end = tag_length( data, size, &altype );
    struct buf work = { data, end, 0, 0 };
    int ret = 0;

    if( end > 2 )
    {
        if( rndr->cb.autolink && altype != MKDA_NOT_AUTOLINK )
        {
            struct buf* u_link = rndr_newbuf( rndr, BUFFER_SPAN );
            work.data = data + 1;
            work.size = end - 2;
            unscape_text( u_link, &work );
            ret = rndr->cb.autolink( ob, u_link, altype, rndr->opaque );
            rndr_popbuf( rndr, BUFFER_SPAN );
        }
        else if( rndr->cb.raw_html_tag )
            ret = rndr->cb.raw_html_tag( ob, &work, rndr->opaque );
    }

    if( !ret )
        return 0;
    else
        return end;
}


static size_t char_autolink_www( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t offset,
        size_t size )
{
    struct buf* link, * link_url, * link_text;
    size_t link_len, rewind;

    if( !rndr->cb.link || rndr->in_link_body )
        return 0;

    link = rndr_newbuf( rndr, BUFFER_SPAN );

    if( ( link_len = sd_autolink__www( &rewind, link, data, offset, size, 0 ) ) > 0 )
    {
        link_url = rndr_newbuf( rndr, BUFFER_SPAN );
        BUFPUTSL( link_url, "http://" );
        bufput( link_url, link->data, link->size );

        ob->size -= rewind;

        if( rndr->cb.normal_text )
        {
            link_text = rndr_newbuf( rndr, BUFFER_SPAN );
            rndr->cb.normal_text( link_text, link, rndr->opaque );
            rndr->cb.link( ob, link_url, NULL, link_text, rndr->opaque );
            rndr_popbuf( rndr, BUFFER_SPAN );
        }
        else
        {
            rndr->cb.link( ob, link_url, NULL, link, rndr->opaque );
        }

        rndr_popbuf( rndr, BUFFER_SPAN );
    }

    rndr_popbuf( rndr, BUFFER_SPAN );
    return link_len;
}


static size_t char_autolink_email( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t offset,
        size_t size )
{
    struct buf* link;
    size_t link_len, rewind;

    if( !rndr->cb.autolink || rndr->in_link_body )
        return 0;

    link = rndr_newbuf( rndr, BUFFER_SPAN );

    if( ( link_len = sd_autolink__email( &rewind, link, data, offset, size, 0 ) ) > 0 )
    {
        ob->size -= rewind;
        rndr->cb.autolink( ob, link, MKDA_EMAIL, rndr->opaque );
    }

    rndr_popbuf( rndr, BUFFER_SPAN );
    return link_len;
}


static size_t char_autolink_url( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t offset,
        size_t size )
{
    struct buf* link;
    size_t link_len, rewind;

    if( !rndr->cb.autolink || rndr->in_link_body )
        return 0;

    link = rndr_newbuf( rndr, BUFFER_SPAN );

    if( ( link_len = sd_autolink__url( &rewind, link, data, offset, size, 0 ) ) > 0 )
    {
        ob->size -= rewind;
        rndr->cb.autolink( ob, link, MKDA_NORMAL, rndr->opaque );
    }

    rndr_popbuf( rndr, BUFFER_SPAN );
    return link_len;
}


/* char_link • '[': parsing a link or an image */
static size_t char_link( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t offset,
        size_t size )
{
    int is_img = (offset && data[-1] == '!'), level;
    size_t i = 1, txt_e, link_b = 0, link_e = 0, title_b = 0, title_e = 0;
    struct buf* content     = 0;
    struct buf* link        = 0;
    struct buf* title       = 0;
    struct buf* u_link      = 0;
    size_t org_work_size    = rndr->work_bufs[BUFFER_SPAN].size;
    int text_has_nl = 0, ret = 0;
    int in_title = 0, qtype = 0;

    /* checking whether the correct renderer exists */
    if( (is_img && !rndr->cb.image) || (!is_img && !rndr->cb.link) )
        goto cleanup;

    /* looking for the matching closing bracket */
    for( level = 1; i < size; i++ )
    {
        if( data[i] == '\n' )
            text_has_nl = 1;

        else if( data[i - 1] == '\\' )
            continue;

        else if( data[i] == '[' )
            level++;

        else if( data[i] == ']' )
        {
            level--;

            if( level <= 0 )
                break;
        }
    }

    if( i >= size )
        goto cleanup;

    txt_e = i;
    i++;

    /* skip any amount of whitespace or newline */
    /* (this is much more laxist than original markdown syntax) */
    while( i < size && _isspace( data[i] ) )
        i++;

    /* inline style link */
    if( i < size && data[i] == '(' )
    {
        /* skipping initial whitespace */
        i++;

        while( i < size && _isspace( data[i] ) )
            i++;

        link_b = i;

        /* looking for link end: ' " ) */
        while( i < size )
        {
            if( data[i] == '\\' )
                i += 2;
            else if( data[i] == ')' )
                break;
            else if( i >= 1 && _isspace( data[i - 1] ) && (data[i] == '\'' || data[i] == '"') )
                break;
            else
                i++;
        }

        if( i >= size )
            goto cleanup;

        link_e = i;

        /* looking for title end if present */
        if( data[i] == '\'' || data[i] == '"' )
        {
            qtype = data[i];
            in_title = 1;
            i++;
            title_b = i;

            while( i < size )
            {
                if( data[i] == '\\' )
                    i += 2;
                else if( data[i] == qtype )
                {
                    in_title = 0; i++;
                }
                else if( (data[i] == ')') && !in_title )
                    break;
                else
                    i++;
            }

            if( i >= size )
                goto cleanup;

            /* skipping whitespaces after title */
            title_e = i - 1;

            while( title_e > title_b && _isspace( data[title_e] ) )
                title_e--;

            /* checking for closing quote presence */
            if( data[title_e] != '\'' &&  data[title_e] != '"' )
            {
                title_b = title_e = 0;
                link_e  = i;
            }
        }

        /* remove whitespace at the end of the link */
        while( link_e > link_b && _isspace( data[link_e - 1] ) )
            link_e--;

        /* remove optional angle brackets around the link */
        if( data[link_b] == '<' )
            link_b++;

        if( data[link_e - 1] == '>' )
            link_e--;

        /* building escaped link and title */
        if( link_e > link_b )
        {
            link = rndr_newbuf( rndr, BUFFER_SPAN );
            bufput( link, data + link_b, link_e - link_b );
        }

        if( title_e > title_b )
        {
            title = rndr_newbuf( rndr, BUFFER_SPAN );
            bufput( title, data + title_b, title_e - title_b );
        }

        i++;
    }
    /* reference style link */
    else if( i < size && data[i] == '[' )
    {
        struct buf id = { 0, 0, 0, 0 };
        struct link_ref* lr;

        /* looking for the id */
        i++;
        link_b = i;

        while( i < size && data[i] != ']' )
            i++;

        if( i >= size )
            goto cleanup;

        link_e = i;

        /* finding the link_ref */
        if( link_b == link_e )
        {
            if( text_has_nl )
            {
                struct buf* b = rndr_newbuf( rndr, BUFFER_SPAN );
                size_t j;

                for( j = 1; j < txt_e; j++ )
                {
                    if( data[j] != '\n' )
                        bufputc( b, data[j] );
                    else if( data[j - 1] != ' ' )
                        bufputc( b, ' ' );
                }

                id.data = b->data;
                id.size = b->size;
            }
            else
            {
                id.data = data + 1;
                id.size = txt_e - 1;
            }
        }
        else
        {
            id.data = data + link_b;
            id.size = link_e - link_b;
        }

        lr = find_link_ref( rndr->refs, id.data, id.size );

        if( !lr )
            goto cleanup;

        /* keeping link and title from link_ref */
        link = lr->link;
        title = lr->title;
        i++;
    }
    /* shortcut reference style link */
    else
    {
        struct buf id = { 0, 0, 0, 0 };
        struct link_ref* lr;

        /* crafting the id */
        if( text_has_nl )
        {
            struct buf* b = rndr_newbuf( rndr, BUFFER_SPAN );
            size_t j;

            for( j = 1; j < txt_e; j++ )
            {
                if( data[j] != '\n' )
                    bufputc( b, data[j] );
                else if( data[j - 1] != ' ' )
                    bufputc( b, ' ' );
            }

            id.data = b->data;
            id.size = b->size;
        }
        else
        {
            id.data = data + 1;
            id.size = txt_e - 1;
        }

        /* finding the link_ref */
        lr = find_link_ref( rndr->refs, id.data, id.size );

        if( !lr )
            goto cleanup;

        /* keeping link and title from link_ref */
        link = lr->link;
        title = lr->title;

        /* rewinding the whitespace */
        i = txt_e + 1;
    }

    /* building content: img alt is escaped, link content is parsed */
    if( txt_e > 1 )
    {
        content = rndr_newbuf( rndr, BUFFER_SPAN );

        if( is_img )
        {
            bufput( content, data + 1, txt_e - 1 );
        }
        else
        {
            /* disable autolinking when parsing inline the
             * content of a link */
            rndr->in_link_body = 1;
            parse_inline( content, rndr, data + 1, txt_e - 1 );
            rndr->in_link_body = 0;
        }
    }

    if( link )
    {
        u_link = rndr_newbuf( rndr, BUFFER_SPAN );
        unscape_text( u_link, link );
    }

    /* calling the relevant rendering function */
    if( is_img )
    {
        if( ob->size && ob->data[ob->size - 1] == '!' )
            ob->size -= 1;

        ret = rndr->cb.image( ob, u_link, title, content, rndr->opaque );
    }
    else
    {
        ret = rndr->cb.link( ob, u_link, title, content, rndr->opaque );
    }

    /* cleanup */
cleanup:
    rndr->work_bufs[BUFFER_SPAN].size = (int) org_work_size;
    return ret ? i : 0;
}


static size_t char_superscript( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t offset,
        size_t size )
{
    size_t sup_start, sup_len;
    struct buf* sup;

    if( !rndr->cb.superscript )
        return 0;

    if( size < 2 )
        return 0;

    if( data[1] == '(' )
    {
        sup_start = sup_len = 2;

        while( sup_len < size && data[sup_len] != ')' && data[sup_len - 1] != '\\' )
            sup_len++;

        if( sup_len == size )
            return 0;
    }
    else
    {
        sup_start = sup_len = 1;

        while( sup_len < size && !_isspace( data[sup_len] ) )
            sup_len++;
    }

    if( sup_len - sup_start == 0 )
        return (sup_start == 2) ? 3 : 0;

    sup = rndr_newbuf( rndr, BUFFER_SPAN );
    parse_inline( sup, rndr, data + sup_start, sup_len - sup_start );
    rndr->cb.superscript( ob, sup, rndr->opaque );
    rndr_popbuf( rndr, BUFFER_SPAN );

    return (sup_start == 2) ? sup_len + 1 : sup_len;
}


/*********************************
* BLOCK-LEVEL PARSING FUNCTIONS *
*********************************/

/* is_empty • returns the line length when it is empty, 0 otherwise */
static size_t is_empty( uint8_t* data, size_t size )
{
    size_t i;

    for( i = 0; i < size && data[i] != '\n'; i++ )
        if( data[i] != ' ' )
            return 0;



    return i + 1;
}


/* is_hrule • returns whether a line is a horizontal rule */
static int is_hrule( uint8_t* data, size_t size )
{
    size_t i = 0, n = 0;
    uint8_t c;

    /* skipping initial spaces */
    if( size < 3 )
        return 0;

    if( data[0] == ' ' )
    {
        i++;

        if( data[1] == ' ' )
        {
            i++;

            if( data[2] == ' ' )
            {
                i++;
            }
        }
    }

    /* looking at the hrule uint8_t */
    if( i + 2 >= size
        || (data[i] != '*' && data[i] != '-' && data[i] != '_') )
        return 0;

    c = data[i];

    /* the whole line must be the char or whitespace */
    while( i < size && data[i] != '\n' )
    {
        if( data[i] == c )
            n++;
        else if( data[i] != ' ' )
            return 0;

        i++;
    }

    return n >= 3;
}


/* check if a line begins with a code fence; return the
 * width of the code fence */
static size_t prefix_codefence( uint8_t* data, size_t size )
{
    size_t i = 0, n = 0;
    uint8_t c;

    /* skipping initial spaces */
    if( size < 3 )
        return 0;

    if( data[0] == ' ' )
    {
        i++;

        if( data[1] == ' ' )
        {
            i++;

            if( data[2] == ' ' )
            {
                i++;
            }
        }
    }

    /* looking at the hrule uint8_t */
    if( i + 2 >= size || !(data[i] == '~' || data[i] == '`') )
        return 0;

    c = data[i];

    /* the whole line must be the uint8_t or whitespace */
    while( i < size && data[i] == c )
    {
        n++; i++;
    }

    if( n < 3 )
        return 0;

    return i;
}


/* check if a line is a code fence; return its size if it is */
static size_t is_codefence( uint8_t* data, size_t size, struct buf* syntax )
{
    size_t i = 0, syn_len = 0;
    uint8_t* syn_start;

    i = prefix_codefence( data, size );

    if( i == 0 )
        return 0;

    while( i < size && data[i] == ' ' )
        i++;

    syn_start = data + i;

    if( i < size && data[i] == '{' )
    {
        i++; syn_start++;

        while( i < size && data[i] != '}' && data[i] != '\n' )
        {
            syn_len++; i++;
        }

        if( i == size || data[i] != '}' )
            return 0;

        /* strip all whitespace at the beginning and the end
         * of the {} block */
        while( syn_len > 0 && _isspace( syn_start[0] ) )
        {
            syn_start++; syn_len--;
        }

        while( syn_len > 0 && _isspace( syn_start[syn_len - 1] ) )
            syn_len--;

        i++;
    }
    else
    {
        while( i < size && !_isspace( data[i] ) )
        {
            syn_len++; i++;
        }
    }

    if( syntax )
    {
        syntax->data = syn_start;
        syntax->size = syn_len;
    }

    while( i < size && data[i] != '\n' )
    {
        if( !_isspace( data[i] ) )
            return 0;

        i++;
    }

    return i + 1;
}


/* is_atxheader • returns whether the line is a hash-prefixed header */
static int is_atxheader( struct sd_markdown* rndr, uint8_t* data, size_t size )
{
    if( data[0] != '#' )
        return 0;

    if( rndr->ext_flags & MKDEXT_SPACE_HEADERS )
    {
        size_t level = 0;

        while( level < size && level < 6 && data[level] == '#' )
            level++;

        if( level < size && data[level] != ' ' )
            return 0;
    }

    return 1;
}


/* is_headerline • returns whether the line is a setext-style hdr underline */
static int is_headerline( uint8_t* data, size_t size )
{
    size_t i = 0;

    /* test of level 1 header */
    if( data[i] == '=' )
    {
        for( i = 1; i < size && data[i] == '='; i++ )
            ;

        while( i < size && data[i] == ' ' )
            i++;

        return (i >= size || data[i] == '\n') ? 1 : 0;
    }

    /* test of level 2 header */
    if( data[i] == '-' )
    {
        for( i = 1; i < size && data[i] == '-'; i++ )
            ;

        while( i < size && data[i] == ' ' )
            i++;

        return (i >= size || data[i] == '\n') ? 2 : 0;
    }

    return 0;
}


static int is_next_headerline( uint8_t* data, size_t size )
{
    size_t i = 0;

    while( i < size && data[i] != '\n' )
        i++;

    if( ++i >= size )
        return 0;

    return is_headerline( data + i, size - i );
}


/* prefix_quote • returns blockquote prefix length */
static size_t prefix_quote( uint8_t* data, size_t size )
{
    size_t i = 0;

    if( i < size && data[i] == ' ' )
        i++;

    if( i < size && data[i] == ' ' )
        i++;

    if( i < size && data[i] == ' ' )
        i++;

    if( i < size && data[i] == '>' )
    {
        if( i + 1 < size && data[i + 1] == ' ' )
            return i + 2;

        return i + 1;
    }

    return 0;
}


/* prefix_code • returns prefix length for block code*/
static size_t prefix_code( uint8_t* data, size_t size )
{
    if( size > 3 && data[0] == ' ' && data[1] == ' '
        && data[2] == ' ' && data[3] == ' ' )
        return 4;

    return 0;
}


/* prefix_oli • returns ordered list item prefix */
static size_t prefix_oli( uint8_t* data, size_t size )
{
    size_t i = 0;

    if( i < size && data[i] == ' ' )
        i++;

    if( i < size && data[i] == ' ' )
        i++;

    if( i < size && data[i] == ' ' )
        i++;

    if( i >= size || data[i] < '0' || data[i] > '9' )
        return 0;

    while( i < size && data[i] >= '0' && data[i] <= '9' )
        i++;

    if( i + 1 >= size || data[i] != '.' || data[i + 1] != ' ' )
        return 0;

    if( is_next_headerline( data + i, size - i ) )
        return 0;

    return i + 2;
}


/* prefix_uli • returns ordered list item prefix */
static size_t prefix_uli( uint8_t* data, size_t size )
{
    size_t i = 0;

    if( i < size && data[i] == ' ' )
        i++;

    if( i < size && data[i] == ' ' )
        i++;

    if( i < size && data[i] == ' ' )
        i++;

    if( i + 1 >= size
        || (data[i] != '*' && data[i] != '+' && data[i] != '-')
        || data[i + 1] != ' ' )
        return 0;

    if( is_next_headerline( data + i, size - i ) )
        return 0;

    return i + 2;
}


/* parse_block • parsing of one block, returning next uint8_t to parse */
static void parse_block( struct buf* ob, struct sd_markdown* rndr,
        uint8_t* data, size_t size );


/* parse_blockquote • handles parsing of a blockquote fragment */
static size_t parse_blockquote( struct buf* ob, struct sd_markdown* rndr, uint8_t* data,
        size_t size )
{
    size_t beg, end = 0, pre, work_size = 0;
    uint8_t* work_data = 0;
    struct buf* out = 0;

    out = rndr_newbuf( rndr, BUFFER_BLOCK );
    beg = 0;

    while( beg < size )
    {
        for( end = beg + 1; end < size && data[end - 1] != '\n'; end++ )
            ;

        pre = prefix_quote( data + beg, end - beg );

        if( pre )
            beg += pre;    /* skipping prefix */

        /* empty line followed by non-quote line */
        else if( is_empty( data + beg, end - beg )
                 && ( end >= size || ( prefix_quote( data + end, size - end ) == 0
                                       && !is_empty( data + end, size - end ) ) ) )
            break;

        if( beg < end )    /* copy into the in-place working buffer */
        {
            /* bufput(work, data + beg, end - beg); */
            if( !work_data )
                work_data = data + beg;
            else if( data + beg != work_data + work_size )
                memmove( work_data + work_size, data + beg, end - beg );

            work_size += end - beg;
        }

        beg = end;
    }

    parse_block( out, rndr, work_data, work_size );

    if( rndr->cb.blockquote )
        rndr->cb.blockquote( ob, out, rndr->opaque );

    rndr_popbuf( rndr, BUFFER_BLOCK );
    return end;
}


static size_t parse_htmlblock( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t size,
        int do_render );

/* parse_blockquote • handles parsing of a regular paragraph */
static size_t parse_paragraph( struct buf* ob, struct sd_markdown* rndr, uint8_t* data,
        size_t size )
{
    size_t i = 0, end = 0;
    int level = 0;
    struct buf work = { data, 0, 0, 0 };

    while( i < size )
    {
        for( end = i + 1; end < size && data[end - 1] != '\n'; end++ )    /* empty */
            ;

        if( is_empty( data + i, size - i ) )
            break;

        if( ( level = is_headerline( data + i, size - i ) ) != 0 )
            break;

        if( is_atxheader( rndr, data + i, size - i )
            || is_hrule( data + i, size - i )
            || prefix_quote( data + i, size - i ) )
        {
            end = i;
            break;
        }

        /*
         * Early termination of a paragraph with the same logic
         * as Markdown 1.0.0. If this logic is applied, the
         * Markdown 1.0.3 test suite won't pass cleanly
         *
         * :: If the first character in a new line is not a letter,
         * let's check to see if there's some kind of block starting
         * here
         */
        if( (rndr->ext_flags & MKDEXT_LAX_SPACING) && !isalnum( data[i] ) )
        {
            if( prefix_oli( data + i, size - i )
                || prefix_uli( data + i, size - i ) )
            {
                end = i;
                break;
            }

            /* see if an html block starts here */
            if( data[i] == '<' && rndr->cb.blockhtml
                && parse_htmlblock( ob, rndr, data + i, size - i, 0 ) )
            {
                end = i;
                break;
            }

            /* see if a code fence starts here */
            if( (rndr->ext_flags & MKDEXT_FENCED_CODE) != 0
                && is_codefence( data + i, size - i, NULL ) != 0 )
            {
                end = i;
                break;
            }
        }

        i = end;
    }

    work.size = i;

    while( work.size && data[work.size - 1] == '\n' )
        work.size--;

    if( !level )
    {
        struct buf* tmp = rndr_newbuf( rndr, BUFFER_BLOCK );
        parse_inline( tmp, rndr, work.data, work.size );

        if( rndr->cb.paragraph )
            rndr->cb.paragraph( ob, tmp, rndr->opaque );

        rndr_popbuf( rndr, BUFFER_BLOCK );
    }
    else
    {
        struct buf* header_work;

        if( work.size )
        {
            size_t beg;
            i = work.size;
            work.size -= 1;

            while( work.size && data[work.size] != '\n' )
                work.size -= 1;

            beg = work.size + 1;

            while( work.size && data[work.size - 1] == '\n' )
                work.size -= 1;

            if( work.size > 0 )
            {
                struct buf* tmp = rndr_newbuf( rndr, BUFFER_BLOCK );
                parse_inline( tmp, rndr, work.data, work.size );

                if( rndr->cb.paragraph )
                    rndr->cb.paragraph( ob, tmp, rndr->opaque );

                rndr_popbuf( rndr, BUFFER_BLOCK );
                work.data += beg;
                work.size = i - beg;
            }
            else
                work.size = i;
        }

        header_work = rndr_newbuf( rndr, BUFFER_SPAN );
        parse_inline( header_work, rndr, work.data, work.size );

        if( rndr->cb.header )
            rndr->cb.header( ob, header_work, (int) level, rndr->opaque );

        rndr_popbuf( rndr, BUFFER_SPAN );
    }

    return end;
}


/* parse_fencedcode • handles parsing of a block-level code fragment */
static size_t parse_fencedcode( struct buf* ob, struct sd_markdown* rndr, uint8_t* data,
        size_t size )
{
    size_t beg, end;
    struct buf* work = 0;
    struct buf  lang = { 0, 0, 0, 0 };

    beg = is_codefence( data, size, &lang );

    if( beg == 0 )
        return 0;

    work = rndr_newbuf( rndr, BUFFER_BLOCK );

    while( beg < size )
    {
        size_t fence_end;
        struct buf fence_trail = { 0, 0, 0, 0 };

        fence_end = is_codefence( data + beg, size - beg, &fence_trail );

        if( fence_end != 0 && fence_trail.size == 0 )
        {
            beg += fence_end;
            break;
        }

        for( end = beg + 1; end < size && data[end - 1] != '\n'; end++ )
            ;

        if( beg < end )
        {
            /* verbatim copy to the working buffer,
             *   escaping entities */
            if( is_empty( data + beg, end - beg ) )
                bufputc( work, '\n' );
            else
                bufput( work, data + beg, end - beg );
        }

        beg = end;
    }

    if( work->size && work->data[work->size - 1] != '\n' )
        bufputc( work, '\n' );

    if( rndr->cb.blockcode )
        rndr->cb.blockcode( ob, work, lang.size ? &lang : NULL, rndr->opaque );

    rndr_popbuf( rndr, BUFFER_BLOCK );
    return beg;
}


static size_t parse_blockcode( struct buf* ob, struct sd_markdown* rndr, uint8_t* data,
        size_t size )
{
    size_t beg, end, pre;
    struct buf* work = 0;

    work = rndr_newbuf( rndr, BUFFER_BLOCK );

    beg = 0;

    while( beg < size )
    {
        for( end = beg + 1; end < size && data[end - 1] != '\n'; end++ )
        {
        }

        ;
        pre = prefix_code( data + beg, end - beg );

        if( pre )
            beg += pre;    /* skipping prefix */
        else if( !is_empty( data + beg, end - beg ) )
            /* non-empty non-prefixed line breaks the pre */
            break;

        if( beg < end )
        {
            /* verbatim copy to the working buffer,
             *   escaping entities */
            if( is_empty( data + beg, end - beg ) )
                bufputc( work, '\n' );
            else
                bufput( work, data + beg, end - beg );
        }

        beg = end;
    }

    while( work->size && work->data[work->size - 1] == '\n' )
        work->size -= 1;

    bufputc( work, '\n' );

    if( rndr->cb.blockcode )
        rndr->cb.blockcode( ob, work, NULL, rndr->opaque );

    rndr_popbuf( rndr, BUFFER_BLOCK );
    return beg;
}


/* parse_listitem • parsing of a single list item */
/*	assuming initial prefix is already removed */
static size_t parse_listitem( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t size,
        int* flags )
{
    struct buf* work = 0, * inter = 0;
    size_t beg = 0, end, pre, sublist = 0, orgpre = 0, i;
    int in_empty = 0, has_inside_empty = 0, in_fence = 0;

    /* keeping track of the first indentation prefix */
    while( orgpre < 3 && orgpre < size && data[orgpre] == ' ' )
        orgpre++;

    beg = prefix_uli( data, size );

    if( !beg )
        beg = prefix_oli( data, size );

    if( !beg )
        return 0;

    /* skipping to the beginning of the following line */
    end = beg;

    while( end < size && data[end - 1] != '\n' )
        end++;

    /* getting working buffers */
    work = rndr_newbuf( rndr, BUFFER_SPAN );
    inter = rndr_newbuf( rndr, BUFFER_SPAN );

    /* putting the first line into the working buffer */
    bufput( work, data + beg, end - beg );
    beg = end;

    /* process the following lines */
    while( beg < size )
    {
        size_t has_next_uli = 0, has_next_oli = 0;

        end++;

        while( end < size && data[end - 1] != '\n' )
            end++;

        /* process an empty line */
        if( is_empty( data + beg, end - beg ) )
        {
            in_empty = 1;
            beg = end;
            continue;
        }

        /* calculating the indentation */
        i = 0;

        while( i < 4 && beg + i < end && data[beg + i] == ' ' )
            i++;

        pre = i;

        if( rndr->ext_flags & MKDEXT_FENCED_CODE )
        {
            if( is_codefence( data + beg + i, end - beg - i, NULL ) != 0 )
                in_fence = !in_fence;
        }

        /* Only check for new list items if we are **not** inside
         * a fenced code block */
        if( !in_fence )
        {
            has_next_uli = prefix_uli( data + beg + i, end - beg - i );
            has_next_oli = prefix_oli( data + beg + i, end - beg - i );
        }

        /* checking for ul/ol switch */
        if( in_empty && (
                ( (*flags & MKD_LIST_ORDERED) && has_next_uli )
                || (!(*flags & MKD_LIST_ORDERED) && has_next_oli) ) )
        {
            *flags |= MKD_LI_END;
            break;    /* the following item must have same list type */
        }

        /* checking for a new item */
        if( ( has_next_uli && !is_hrule( data + beg + i, end - beg - i ) ) || has_next_oli )
        {
            if( in_empty )
                has_inside_empty = 1;

            if( pre == orgpre )     /* the following item must have */
                break;              /* the same indentation */

            if( !sublist )
                sublist = work->size;
        }
        /* joining only indented stuff after empty lines;
         * note that now we only require 1 space of indentation
         * to continue a list */
        else if( in_empty && pre == 0 )
        {
            *flags |= MKD_LI_END;
            break;
        }
        else if( in_empty )
        {
            bufputc( work, '\n' );
            has_inside_empty = 1;
        }

        in_empty = 0;

        /* adding the line without prefix into the working buffer */
        bufput( work, data + beg + i, end - beg - i );
        beg = end;
    }

    /* render of li contents */
    if( has_inside_empty )
        *flags |= MKD_LI_BLOCK;

    if( *flags & MKD_LI_BLOCK )
    {
        /* intermediate render of block li */
        if( sublist && sublist < work->size )
        {
            parse_block( inter, rndr, work->data, sublist );
            parse_block( inter, rndr, work->data + sublist, work->size - sublist );
        }
        else
            parse_block( inter, rndr, work->data, work->size );
    }
    else
    {
        /* intermediate render of inline li */
        if( sublist && sublist < work->size )
        {
            parse_inline( inter, rndr, work->data, sublist );
            parse_block( inter, rndr, work->data + sublist, work->size - sublist );
        }
        else
            parse_inline( inter, rndr, work->data, work->size );
    }

    /* render of li itself */
    if( rndr->cb.listitem )
        rndr->cb.listitem( ob, inter, *flags, rndr->opaque );

    rndr_popbuf( rndr, BUFFER_SPAN );
    rndr_popbuf( rndr, BUFFER_SPAN );
    return beg;
}


/* parse_list • parsing ordered or unordered list block */
static size_t parse_list( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t size,
        int flags )
{
    struct buf* work = 0;
    size_t i = 0, j;

    work = rndr_newbuf( rndr, BUFFER_BLOCK );

    while( i < size )
    {
        j = parse_listitem( work, rndr, data + i, size - i, &flags );
        i += j;

        if( !j || (flags & MKD_LI_END) )
            break;
    }

    if( rndr->cb.list )
        rndr->cb.list( ob, work, flags, rndr->opaque );

    rndr_popbuf( rndr, BUFFER_BLOCK );
    return i;
}


/* parse_atxheader • parsing of atx-style headers */
static size_t parse_atxheader( struct buf* ob, struct sd_markdown* rndr, uint8_t* data,
        size_t size )
{
    size_t level = 0;
    size_t i, end, skip;

    while( level < size && level < 6 && data[level] == '#' )
        level++;

    for( i = level; i < size && data[i] == ' '; i++ )
        ;

    for( end = i; end < size && data[end] != '\n'; end++ )
        ;

    skip = end;

    while( end && data[end - 1] == '#' )
        end--;

    while( end && data[end - 1] == ' ' )
        end--;

    if( end > i )
    {
        struct buf* work = rndr_newbuf( rndr, BUFFER_SPAN );

        parse_inline( work, rndr, data + i, end - i );

        if( rndr->cb.header )
            rndr->cb.header( ob, work, (int) level, rndr->opaque );

        rndr_popbuf( rndr, BUFFER_SPAN );
    }

    return skip;
}


/* htmlblock_end • checking end of HTML block : </tag>[ \t]*\n[ \t*]\n */
/*	returns the length on match, 0 otherwise */
static size_t htmlblock_end_tag( const char* tag,
        size_t tag_len,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t size )
{
    size_t i, w;

    /* checking if tag is a match */
    if( tag_len + 3 >= size
        || strncasecmp( (char*) data + 2, tag, tag_len ) != 0
        || data[tag_len + 2] != '>' )
        return 0;

    /* checking white lines */
    i = tag_len + 3;
    w = 0;

    if( i < size && ( w = is_empty( data + i, size - i ) ) == 0 )
        return 0;    /* non-blank after tag */

    i += w;
    w = 0;

    if( i < size )
        w = is_empty( data + i, size - i );

    return i + w;
}


static size_t htmlblock_end( const char* curtag,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t size,
        int start_of_line )
{
    size_t tag_size = strlen( curtag );
    size_t i = 1, end_tag;
    int block_lines = 0;

    while( i < size )
    {
        i++;

        while( i < size && !(data[i - 1] == '<' && data[i] == '/') )
        {
            if( data[i] == '\n' )
                block_lines++;

            i++;
        }

        /* If we are only looking for unindented tags, skip the tag
         * if it doesn't follow a newline.
         *
         * The only exception to this is if the tag is still on the
         * initial line; in that case it still counts as a closing
         * tag
         */
        if( start_of_line && block_lines > 0 && data[i - 2] != '\n' )
            continue;

        if( i + 2 + tag_size >= size )
            break;

        end_tag = htmlblock_end_tag( curtag, tag_size, rndr, data + i - 1, size - i + 1 );

        if( end_tag )
            return i + end_tag - 1;
    }

    return 0;
}


/* parse_htmlblock • parsing of inline HTML block */
static size_t parse_htmlblock( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t size,
        int do_render )
{
    size_t i, j = 0, tag_end;
    const char* curtag = NULL;
    struct buf  work = { data, 0, 0, 0 };

    /* identification of the opening tag */
    if( size < 2 || data[0] != '<' )
        return 0;

    i = 1;

    while( i < size && data[i] != '>' && data[i] != ' ' )
        i++;

    if( i < size )
        curtag = find_block_tag( (char*) data + 1, (int) i - 1 );

    /* handling of special cases */
    if( !curtag )
    {
        /* HTML comment, laxist form */
        if( size > 5 && data[1] == '!' && data[2] == '-' && data[3] == '-' )
        {
            i = 5;

            while( i < size && !(data[i - 2] == '-' && data[i - 1] == '-' && data[i] == '>') )
                i++;

            i++;

            if( i < size )
                j = is_empty( data + i, size - i );

            if( j )
            {
                work.size = i + j;

                if( do_render && rndr->cb.blockhtml )
                    rndr->cb.blockhtml( ob, &work, rndr->opaque );

                return work.size;
            }
        }

        /* HR, which is the only self-closing block tag considered */
        if( size > 4 && (data[1] == 'h' || data[1] == 'H') && (data[2] == 'r' || data[2] == 'R') )
        {
            i = 3;

            while( i < size && data[i] != '>' )
                i++;

            if( i + 1 < size )
            {
                i++;
                j = is_empty( data + i, size - i );

                if( j )
                {
                    work.size = i + j;

                    if( do_render && rndr->cb.blockhtml )
                        rndr->cb.blockhtml( ob, &work, rndr->opaque );

                    return work.size;
                }
            }
        }

        /* no special case recognised */
        return 0;
    }

    /* looking for an unindented matching closing tag */
    /*	followed by a blank line */
    tag_end = htmlblock_end( curtag, rndr, data, size, 1 );

    /* if not found, trying a second pass looking for indented match */
    /* but not if tag is "ins" or "del" (following original Markdown.pl) */
    if( !tag_end && strcmp( curtag, "ins" ) != 0 && strcmp( curtag, "del" ) != 0 )
    {
        tag_end = htmlblock_end( curtag, rndr, data, size, 0 );
    }

    if( !tag_end )
        return 0;

    /* the end of the block has been found */
    work.size = tag_end;

    if( do_render && rndr->cb.blockhtml )
        rndr->cb.blockhtml( ob, &work, rndr->opaque );

    return tag_end;
}


static void parse_table_row( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t size,
        size_t columns,
        int* col_data,
        int header_flag )
{
    size_t i = 0, col;
    struct buf* row_work = 0;

    if( !rndr->cb.table_cell || !rndr->cb.table_row )
        return;

    row_work = rndr_newbuf( rndr, BUFFER_SPAN );

    if( i < size && data[i] == '|' )
        i++;

    for( col = 0; col < columns && i < size; ++col )
    {
        size_t cell_start, cell_end;
        struct buf* cell_work;

        cell_work = rndr_newbuf( rndr, BUFFER_SPAN );

        while( i < size && _isspace( data[i] ) )
            i++;

        cell_start = i;

        while( i < size && data[i] != '|' )
            i++;

        cell_end = i - 1;

        while( cell_end > cell_start && _isspace( data[cell_end] ) )
            cell_end--;

        parse_inline( cell_work, rndr, data + cell_start, 1 + cell_end - cell_start );
        rndr->cb.table_cell( row_work, cell_work, col_data[col] | header_flag, rndr->opaque );

        rndr_popbuf( rndr, BUFFER_SPAN );
        i++;
    }

    for( ; col < columns; ++col )
    {
        struct buf empty_cell = { 0, 0, 0, 0 };
        rndr->cb.table_cell( row_work, &empty_cell, col_data[col] | header_flag, rndr->opaque );
    }

    rndr->cb.table_row( ob, row_work, rndr->opaque );

    rndr_popbuf( rndr, BUFFER_SPAN );
}


static size_t parse_table_header( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t size,
        size_t* columns,
        int** column_data )
{
    int pipes;
    size_t i = 0, col, header_end, under_end;

    pipes = 0;

    while( i < size && data[i] != '\n' )
        if( data[i++] == '|' )
            pipes++;



    if( i == size || pipes == 0 )
        return 0;

    header_end = i;

    while( header_end > 0 && _isspace( data[header_end - 1] ) )
        header_end--;

    if( data[0] == '|' )
        pipes--;

    if( header_end && data[header_end - 1] == '|' )
        pipes--;

    *columns = pipes + 1;
    *column_data = calloc( *columns, sizeof(int) );

    /* Parse the header underline */
    i++;

    if( i < size && data[i] == '|' )
        i++;

    under_end = i;

    while( under_end < size && data[under_end] != '\n' )
        under_end++;

    for( col = 0; col < *columns && i < under_end; ++col )
    {
        size_t dashes = 0;

        while( i < under_end && data[i] == ' ' )
            i++;

        if( data[i] == ':' )
        {
            i++; (*column_data)[col] |= MKD_TABLE_ALIGN_L;
            dashes++;
        }

        while( i < under_end && data[i] == '-' )
        {
            i++; dashes++;
        }

        if( i < under_end && data[i] == ':' )
        {
            i++; (*column_data)[col] |= MKD_TABLE_ALIGN_R;
            dashes++;
        }

        while( i < under_end && data[i] == ' ' )
            i++;

        if( i < under_end && data[i] != '|' )
            break;

        if( dashes < 3 )
            break;

        i++;
    }

    if( col < *columns )
        return 0;

    parse_table_row(
            ob, rndr, data,
            header_end,
            *columns,
            *column_data,
            MKD_TABLE_HEADER
            );

    return under_end + 1;
}


static size_t parse_table( struct buf* ob,
        struct sd_markdown* rndr,
        uint8_t* data,
        size_t size )
{
    size_t i;

    struct buf* header_work = 0;
    struct buf* body_work = 0;

    size_t columns;
    int* col_data = NULL;

    header_work = rndr_newbuf( rndr, BUFFER_SPAN );
    body_work = rndr_newbuf( rndr, BUFFER_BLOCK );

    i = parse_table_header( header_work, rndr, data, size, &columns, &col_data );

    if( i > 0 )
    {
        while( i < size )
        {
            size_t row_start;
            int pipes = 0;

            row_start = i;

            while( i < size && data[i] != '\n' )
                if( data[i++] == '|' )
                    pipes++;



            if( pipes == 0 || i == size )
            {
                i = row_start;
                break;
            }

            parse_table_row(
                    body_work,
                    rndr,
                    data + row_start,
                    i - row_start,
                    columns,
                    col_data, 0
                    );

            i++;
        }

        if( rndr->cb.table )
            rndr->cb.table( ob, header_work, body_work, rndr->opaque );
    }

    free( col_data );
    rndr_popbuf( rndr, BUFFER_SPAN );
    rndr_popbuf( rndr, BUFFER_BLOCK );
    return i;
}


/* parse_block • parsing of one block, returning next uint8_t to parse */
static void parse_block( struct buf* ob, struct sd_markdown* rndr, uint8_t* data, size_t size )
{
    size_t beg, end, i;
    uint8_t* txt_data;

    beg = 0;

    if( rndr->work_bufs[BUFFER_SPAN].size +
        rndr->work_bufs[BUFFER_BLOCK].size > rndr->max_nesting )
        return;

    while( beg < size )
    {
        txt_data = data + beg;
        end = size - beg;

        if( is_atxheader( rndr, txt_data, end ) )
            beg += parse_atxheader( ob, rndr, txt_data, end );

        else if( data[beg] == '<' && rndr->cb.blockhtml
                 && ( i = parse_htmlblock( ob, rndr, txt_data, end, 1 ) ) != 0 )
            beg += i;

        else if( ( i = is_empty( txt_data, end ) ) != 0 )
            beg += i;

        else if( is_hrule( txt_data, end ) )
        {
            if( rndr->cb.hrule )
                rndr->cb.hrule( ob, rndr->opaque );

            while( beg < size && data[beg] != '\n' )
                beg++;

            beg++;
        }
        else if( (rndr->ext_flags & MKDEXT_FENCED_CODE) != 0
                 && ( i = parse_fencedcode( ob, rndr, txt_data, end ) ) != 0 )
            beg += i;

        else if( (rndr->ext_flags & MKDEXT_TABLES) != 0
                 && ( i = parse_table( ob, rndr, txt_data, end ) ) != 0 )
            beg += i;

        else if( prefix_quote( txt_data, end ) )
            beg += parse_blockquote( ob, rndr, txt_data, end );

        else if( prefix_code( txt_data, end ) )
            beg += parse_blockcode( ob, rndr, txt_data, end );

        else if( prefix_uli( txt_data, end ) )
            beg += parse_list( ob, rndr, txt_data, end, 0 );

        else if( prefix_oli( txt_data, end ) )
            beg += parse_list( ob, rndr, txt_data, end, MKD_LIST_ORDERED );

        else
            beg += parse_paragraph( ob, rndr, txt_data, end );
    }
}


/*********************
* REFERENCE PARSING *
*********************/

/* is_ref • returns whether a line is a reference or not */
static int is_ref( const uint8_t* data, size_t beg, size_t end, size_t* last,
        struct link_ref** refs )
{
/*	int n; */
    size_t i = 0;
    size_t id_offset, id_end;
    size_t link_offset, link_end;
    size_t title_offset, title_end;
    size_t line_end;

    /* up to 3 optional leading spaces */
    if( beg + 3 >= end )
        return 0;

    if( data[beg] == ' ' )
    {
        i = 1;

        if( data[beg + 1] == ' ' )
        {
            i = 2;

            if( data[beg + 2] == ' ' )
            {
                i = 3;

                if( data[beg + 3] == ' ' )
                    return 0;
            }
        }
    }

    i += beg;

    /* id part: anything but a newline between brackets */
    if( data[i] != '[' )
        return 0;

    i++;
    id_offset = i;

    while( i < end && data[i] != '\n' && data[i] != '\r' && data[i] != ']' )
        i++;

    if( i >= end || data[i] != ']' )
        return 0;

    id_end = i;

    /* spacer: colon (space | tab)* newline? (space | tab)* */
    i++;

    if( i >= end || data[i] != ':' )
        return 0;

    i++;

    while( i < end && data[i] == ' ' )
        i++;

    if( i < end && (data[i] == '\n' || data[i] == '\r') )
    {
        i++;

        if( i < end && data[i] == '\r' && data[i - 1] == '\n' )
            i++;
    }

    while( i < end && data[i] == ' ' )
        i++;

    if( i >= end )
        return 0;

    /* link: whitespace-free sequence, optionally between angle brackets */
    if( data[i] == '<' )
        i++;

    link_offset = i;

    while( i < end && data[i] != ' ' && data[i] != '\n' && data[i] != '\r' )
        i++;

    if( data[i - 1] == '>' )
        link_end = i - 1;
    else
        link_end = i;

    /* optional spacer: (space | tab)* (newline | '\'' | '"' | '(' ) */
    while( i < end && data[i] == ' ' )
        i++;

    if( i < end && data[i] != '\n' && data[i] != '\r'
        && data[i] != '\'' && data[i] != '"' && data[i] != '(' )
        return 0;

    line_end = 0;

    /* computing end-of-line */
    if( i >= end || data[i] == '\r' || data[i] == '\n' )
        line_end = i;

    if( i + 1 < end && data[i] == '\n' && data[i + 1] == '\r' )
        line_end = i + 1;

    /* optional (space|tab)* spacer after a newline */
    if( line_end )
    {
        i = line_end + 1;

        while( i < end && data[i] == ' ' )
            i++;
    }

    /* optional title: any non-newline sequence enclosed in '"()
     *               alone on its line */
    title_offset = title_end = 0;

    if( i + 1 < end
        && (data[i] == '\'' || data[i] == '"' || data[i] == '(') )
    {
        i++;
        title_offset = i;

        /* looking for EOL */
        while( i < end && data[i] != '\n' && data[i] != '\r' )
            i++;

        if( i + 1 < end && data[i] == '\n' && data[i + 1] == '\r' )
            title_end = i + 1;
        else
            title_end = i;

        /* stepping back */
        i -= 1;

        while( i > title_offset && data[i] == ' ' )
            i -= 1;

        if( i > title_offset
            && (data[i] == '\'' || data[i] == '"' || data[i] == ')') )
        {
            line_end = title_end;
            title_end = i;
        }
    }

    if( !line_end || link_end == link_offset )
        return 0;    /* garbage after the link empty link */

    /* a valid ref has been found, filling-in return structures */
    if( last )
        *last = line_end;

    if( refs )
    {
        struct link_ref* ref;

        ref = add_link_ref( refs, data + id_offset, id_end - id_offset );

        if( !ref )
            return 0;

        ref->link = bufnew( link_end - link_offset );
        bufput( ref->link, data + link_offset, link_end - link_offset );

        if( title_end > title_offset )
        {
            ref->title = bufnew( title_end - title_offset );
            bufput( ref->title, data + title_offset, title_end - title_offset );
        }
    }

    return 1;
}


static void expand_tabs( struct buf* ob, const uint8_t* line, size_t size )
{
    size_t i = 0, tab = 0;

    while( i < size )
    {
        size_t org = i;

        while( i < size && line[i] != '\t' )
        {
            i++; tab++;
        }

        if( i > org )
            bufput( ob, line + org, i - org );

        if( i >= size )
            break;

        do {
            bufputc( ob, ' ' ); tab++;
        } while( tab % 4 );

        i++;
    }
}


/**********************
* EXPORTED FUNCTIONS *
**********************/

struct sd_markdown* sd_markdown_new( unsigned int extensions,
        size_t max_nesting,
        const struct sd_callbacks* callbacks,
        void* opaque )
{
    struct sd_markdown* md = NULL;

    assert( max_nesting > 0 && callbacks );

    md = malloc( sizeof(struct sd_markdown) );

    if( !md )
        return NULL;

    memcpy( &md->cb, callbacks, sizeof(struct sd_callbacks) );

    stack_init( &md->work_bufs[BUFFER_BLOCK], 4 );
    stack_init( &md->work_bufs[BUFFER_SPAN], 8 );

    memset( md->active_char, 0x0, 256 );

    if( md->cb.emphasis || md->cb.double_emphasis || md->cb.triple_emphasis )
    {
        md->active_char['*'] = MD_CHAR_EMPHASIS;
        md->active_char['_'] = MD_CHAR_EMPHASIS;

        if( extensions & MKDEXT_STRIKETHROUGH )
            md->active_char['~'] = MD_CHAR_EMPHASIS;
    }

    if( md->cb.codespan )
        md->active_char['`'] = MD_CHAR_CODESPAN;

    if( md->cb.linebreak )
        md->active_char['\n'] = MD_CHAR_LINEBREAK;

    if( md->cb.image || md->cb.link )
        md->active_char['['] = MD_CHAR_LINK;

    md->active_char['<'] = MD_CHAR_LANGLE;
    md->active_char['\\'] = MD_CHAR_ESCAPE;
    md->active_char['&'] = MD_CHAR_ENTITITY;

    if( extensions & MKDEXT_AUTOLINK )
    {
        md->active_char[':'] = MD_CHAR_AUTOLINK_URL;
        md->active_char['@'] = MD_CHAR_AUTOLINK_EMAIL;
        md->active_char['w'] = MD_CHAR_AUTOLINK_WWW;
    }

    if( extensions & MKDEXT_SUPERSCRIPT )
        md->active_char['^'] = MD_CHAR_SUPERSCRIPT;

    /* Extension data */
    md->ext_flags = extensions;
    md->opaque = opaque;
    md->max_nesting = max_nesting;
    md->in_link_body = 0;

    return md;
}


void sd_markdown_render( struct buf* ob,
        const uint8_t* document,
        size_t doc_size,
        struct sd_markdown* md )
{
#define MARKDOWN_GROW( x ) ( (x) + ( (x) >> 1 ) )
    static const char UTF8_BOM[] = { 0xEF, 0xBB, 0xBF };

    struct buf* text;
    size_t beg, end;

    text = bufnew( 64 );

    if( !text )
        return;

    /* Preallocate enough space for our buffer to avoid expanding while copying */
    bufgrow( text, doc_size );

    /* reset the references table */
    memset( &md->refs, 0x0, REF_TABLE_SIZE * sizeof(void*) );

    /* first pass: looking for references, copying everything else */
    beg = 0;

    /* Skip a possible UTF-8 BOM, even though the Unicode standard
     * discourages having these in UTF-8 documents */
    if( doc_size >= 3 && memcmp( document, UTF8_BOM, 3 ) == 0 )
        beg += 3;

    while( beg < doc_size )    /* iterating over lines */
        if( is_ref( document, beg, doc_size, &end, md->refs ) )
            beg = end;
        else    /* skipping to the next line */
        {
            end = beg;

            while( end < doc_size && document[end] != '\n' && document[end] != '\r' )
                end++;

            /* adding the line body if present */
            if( end > beg )
                expand_tabs( text, document + beg, end - beg );

            while( end < doc_size && (document[end] == '\n' || document[end] == '\r') )
            {
                /* add one \n per newline */
                if( document[end] == '\n' || (end + 1 < doc_size && document[end + 1] != '\n') )
                    bufputc( text, '\n' );

                end++;
            }

            beg = end;
        }



    /* pre-grow the output buffer to minimize allocations */
    bufgrow( ob, MARKDOWN_GROW( text->size ) );

    /* second pass: actual rendering */
    if( md->cb.doc_header )
        md->cb.doc_header( ob, md->opaque );

    if( text->size )
    {
        /* adding a final newline if not already present */
        if( text->data[text->size - 1] != '\n' &&  text->data[text->size - 1] != '\r' )
            bufputc( text, '\n' );

        parse_block( ob, md, text->data, text->size );
    }

    if( md->cb.doc_footer )
        md->cb.doc_footer( ob, md->opaque );

    /* clean-up */
    bufrelease( text );
    free_link_refs( md->refs );

    assert( md->work_bufs[BUFFER_SPAN].size == 0 );
    assert( md->work_bufs[BUFFER_BLOCK].size == 0 );
}


void sd_markdown_free( struct sd_markdown* md )
{
    size_t i;

    for( i = 0; i < (size_t) md->work_bufs[BUFFER_SPAN].asize; ++i )
        bufrelease( md->work_bufs[BUFFER_SPAN].item[i] );

    for( i = 0; i < (size_t) md->work_bufs[BUFFER_BLOCK].asize; ++i )
        bufrelease( md->work_bufs[BUFFER_BLOCK].item[i] );

    stack_free( &md->work_bufs[BUFFER_SPAN] );
    stack_free( &md->work_bufs[BUFFER_BLOCK] );

    free( md );
}


void sd_version( int* ver_major, int* ver_minor, int* ver_revision )
{
    *ver_major  = SUNDOWN_VER_MAJOR;
    *ver_minor  = SUNDOWN_VER_MINOR;
    *ver_revision = SUNDOWN_VER_REVISION;
}


/* vim: set filetype=c: */
