git-trace

romele / git-trace public

62 commits · 1 branch · 1 tag · 1 contributor (top: Romele) · first commit 3d ago · last push 11m ago

src/markdown.c

#include "markdown.h"

#include "util.h"

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

/* ---- small helpers -------------------------------------------------- */

static const char *find2(const char *s, size_t n, const char *nd, size_t nn)
{
    if (!nn || n < nn)
        return NULL;
    for (size_t i = 0; i + nn <= n; i++)
        if (memcmp(s + i, nd, nn) == 0)
            return s + i;
    return NULL;
}

/* prefix test on a bounded (non-NUL-terminated) run */
static int pfx(const char *s, size_t n, const char *p)
{
    size_t m = strlen(p);
    return n >= m && memcmp(s, p, m) == 0;
}

/* URLs may only carry inert schemes or be relative — anything else
 * renders as plain text, so the renderer can't mint javascript:/data: */
static int url_ok(const char *u, size_t n)
{
    if (pfx(u, n, "http://") || pfx(u, n, "https://") ||
        pfx(u, n, "mailto:"))
        return 1;
    if (n && (u[0] == '/' || u[0] == '#' || u[0] == '.'))
        return 1;
    return memchr(u, ':', n) == NULL;
}

/* ---- inline ----------------------------------------------------------
 * depth caps nested-marker recursion on adversarial input ("[[[[["). */

static void inl(struct buf *b, const char *s, size_t n, int depth)
{
    size_t i = 0;
    while (i < n) {
        char c = s[i];
        size_t adv = 0;

        if (c == '`') {
            const char *e = find2(s + i + 1, n - i - 1, "`", 1);
            if (e) {
                buf_puts(b, "<code>");
                buf_put_escaped(b, s + i + 1, (size_t)(e - s - i - 1));
                buf_puts(b, "</code>");
                adv = (size_t)(e - s - i) + 1;
            }
        } else if ((c == '*' || c == '_') && i + 1 < n && s[i + 1] == c &&
                   depth < 8) {
            char nd[2] = { c, c };
            const char *e = find2(s + i + 2, n - i - 2, nd, 2);
            if (e && e > s + i + 2) {
                buf_puts(b, "<strong>");
                inl(b, s + i + 2, (size_t)(e - s - i - 2), depth + 1);
                buf_puts(b, "</strong>");
                adv = (size_t)(e - s - i) + 2;
            }
        } else if ((c == '*' || c == '_') && depth < 8 &&
                   (i + 1 < n && !isspace((unsigned char)s[i + 1])) &&
                   (c == '*' || i == 0 || !isalnum((unsigned char)s[i - 1]))) {
            /* _ requires an opening word boundary (snake_case stays
             * literal); * just needs a non-space right neighbour */
            char nd[1] = { c };
            const char *e = find2(s + i + 1, n - i - 1, nd, 1);
            while (e && e + 1 < s + n && e[1] == c)   /* skip ** pair */
                e = find2(e + 2, (size_t)(s + n - e - 2), nd, 1);
            if (e && e > s + i + 1 &&
                !isspace((unsigned char)e[-1])) {
                buf_puts(b, "<em>");
                inl(b, s + i + 1, (size_t)(e - s - i - 1), depth + 1);
                buf_puts(b, "</em>");
                adv = (size_t)(e - s - i) + 1;
            }
        } else if ((c == '[' || (c == '!' && i + 1 < n && s[i + 1] == '['))
                   && depth < 8) {
            size_t t0 = i + (c == '!' ? 2 : 1);
            const char *cb = find2(s + t0, n - t0, "]", 1);
            if (cb && cb + 1 < s + n && cb[1] == '(') {
                const char *cp = find2(cb + 2, (size_t)(s + n - cb - 2),
                                       ")", 1);
                if (cp && url_ok(cb + 2, (size_t)(cp - cb - 2))) {
                    buf_puts(b, "<a href=\"");
                    buf_put_escaped(b, cb + 2, (size_t)(cp - cb - 2));
                    buf_puts(b, "\">");
                    inl(b, s + t0, (size_t)(cb - s - t0), depth + 1);
                    buf_puts(b, "</a>");
                    adv = (size_t)(cp - s - i) + 1;
                }
            }
        } else if ((c == 'h' || c == 'm') && depth < 8 &&
                   (i == 0 || isspace((unsigned char)s[i - 1])) &&
                   (pfx(s + i, n - i, "http://") ||
                    pfx(s + i, n - i, "https://") ||
                    pfx(s + i, n - i, "mailto:"))) {
            /* bare autolink: consume non-space, trim trailing punct */
            size_t j = i;
            while (j < n && !isspace((unsigned char)s[j]) && s[j] != '<')
                j++;
            while (j > i && strchr(".,;:!?'", s[j - 1]))
                j--;
            if (url_ok(s + i, j - i)) {
                buf_puts(b, "<a href=\"");
                buf_put_escaped(b, s + i, j - i);
                buf_puts(b, "\">");
                buf_put_escaped(b, s + i, j - i);
                buf_puts(b, "</a>");
                adv = j - i;
            }
        }

        if (adv) {
            i += adv;
            continue;
        }
        buf_put_escaped(b, s + i, 1);
        i++;
    }
}

/* ---- block level ----------------------------------------------------- */

/* leading spaces (up to 3) are insignificant for block starts */
static size_t skip_indent(const char *s, size_t n)
{
    size_t i = 0;
    while (i < n && s[i] == ' ' && i < 3)
        i++;
    return i;
}

/* opening fence: run >=3 of ` or ~ ; returns run length, 0 else.
 * The info string after the run is ignored. */
static size_t fence_run(const char *s, size_t n, char *fc)
{
    size_t i = skip_indent(s, n);
    if (i >= n || (s[i] != '`' && s[i] != '~'))
        return 0;
    size_t k = i;
    while (k < n && s[k] == s[i])
        k++;
    if (k - i < 3)
        return 0;
    *fc = s[i];
    return k - i;
}

/* closing fence per CommonMark: same char, run >= the opening run,
 * and nothing but spaces after it — so a ``` line inside a ````
 * block stays literal. */
static int fence_close(const char *s, size_t n, char fc, size_t flen)
{
    size_t i = skip_indent(s, n);
    if (i >= n || s[i] != fc)
        return 0;
    size_t k = i;
    while (k < n && s[k] == fc)
        k++;
    if (k - i < flen)
        return 0;
    for (; k < n; k++)
        if (!isspace((unsigned char)s[k]))
            return 0;
    return 1;
}

static int is_hr(const char *s, size_t n)
{
    size_t i = skip_indent(s, n);
    if (i >= n || (s[i] != '-' && s[i] != '*' && s[i] != '_'))
        return 0;
    char c = s[i];
    int cnt = 0;
    for (; i < n; i++) {
        if (s[i] == c)
            cnt++;
        else if (!isspace((unsigned char)s[i]))
            return 0;
    }
    return cnt >= 3;
}

/* run of one char (spaces allowed): "==="-style setext underlines */
static int is_run(const char *s, size_t n, char c, int min)
{
    int cnt = 0;
    for (size_t i = skip_indent(s, n); i < n; i++) {
        if (s[i] == c)
            cnt++;
        else if (!isspace((unsigned char)s[i]))
            return 0;
    }
    return cnt >= min;
}

/* "  - item" / "1. item" -> list kind: 1 ul, 2 ol, 0 not-a-list */
static int list_item(const char *s, size_t n, size_t *body)
{
    size_t i = skip_indent(s, n);
    if (i + 1 < n && (s[i] == '-' || s[i] == '*' || s[i] == '+') &&
        s[i + 1] == ' ') {
        *body = i + 2;
        return 1;
    }
    size_t k = i;
    while (k < n && isdigit((unsigned char)s[k]))
        k++;
    if (k > i && k + 1 < n && s[k] == '.' && s[k + 1] == ' ') {
        *body = k + 2;
        return 2;
    }
    return 0;
}

void md_emit(struct buf *b, const char *data, size_t len)
{
    struct buf para;                     /* pending paragraph lines   */
    int fence = 0, list = 0, quote = 0;  /* open block state          */
    char fc = '`';
    size_t flen = 0;                     /* opening fence run length  */
    buf_init(&para);

    size_t i = 0;
    while (i < len) {
        const char *ls = data + i;
        const char *le = memchr(ls, '\n', len - i);
        size_t ll = le ? (size_t)(le - ls) : len - i;
        i += ll + (le ? 1 : 0);
        while (ll && ls[ll - 1] == '\r')
            ll--;

        if (fence) {
            if (fence_close(ls, ll, fc, flen)) {
                buf_puts(b, "</code></pre>");
                fence = 0;
            } else {
                buf_put_escaped(b, ls, ll);
                buf_puts(b, "\n");
            }
            continue;
        }
        if ((flen = fence_run(ls, ll, &fc))) {
            if (list)  { buf_puts(b, list == 1 ? "</ul>" : "</ol>"); list = 0; }
            if (quote) { buf_puts(b, "</p></blockquote>"); quote = 0; }
            if (para.len) { buf_printf(b, "<p>%s</p>", para.data);
                            para.len = 0; }
            buf_puts(b, "<pre><code>");
            fence = 1;
            continue;
        }

        int blank = 1;
        for (size_t k = 0; k < ll; k++)
            if (!isspace((unsigned char)ls[k])) { blank = 0; break; }
        if (blank) {
            if (list)  { buf_puts(b, list == 1 ? "</ul>" : "</ol>"); list = 0; }
            if (quote) { buf_puts(b, "</p></blockquote>"); quote = 0; }
            if (para.len) { buf_printf(b, "<p>%s</p>", para.data);
                            para.len = 0; }
            continue;
        }

        size_t ind = skip_indent(ls, ll);
        if (is_run(ls, ll, '=', 2) && para.len) {
            buf_printf(b, "<h1>%s</h1>", para.data);
            para.len = 0;
            continue;
        }
        if (is_run(ls, ll, '-', 3) && para.len) {
            buf_printf(b, "<h2>%s</h2>", para.data);
            para.len = 0;
            continue;
        }

        /* any non-continuation block start closes paragraph/list state */
        int hd = 0;
        while (ind + (size_t)hd < ll && ls[ind + hd] == '#' && hd < 7)
            hd++;
        int is_h = hd >= 1 && hd <= 6 &&
                   (ind + (size_t)hd == ll || ls[ind + hd] == ' ');
        size_t ibody = 0;
        int lkind = list_item(ls, ll, &ibody);
        int is_q = ind < ll && ls[ind] == '>';

        if (is_h || is_hr(ls, ll) || lkind || is_q) {
            if (para.len) { buf_printf(b, "<p>%s</p>", para.data);
                            para.len = 0; }
        }

        if (is_h) {
            if (list)  { buf_puts(b, list == 1 ? "</ul>" : "</ol>"); list = 0; }
            if (quote) { buf_puts(b, "</p></blockquote>"); quote = 0; }
            buf_printf(b, "<h%d>", hd);
            size_t t0 = ind + hd;
            while (t0 < ll && ls[t0] == ' ')
                t0++;
            inl(b, ls + t0, ll - t0, 0);
            buf_printf(b, "</h%d>", hd);
        } else if (is_hr(ls, ll)) {
            if (list)  { buf_puts(b, list == 1 ? "</ul>" : "</ol>"); list = 0; }
            if (quote) { buf_puts(b, "</p></blockquote>"); quote = 0; }
            buf_puts(b, "<hr>");
        } else if (lkind) {
            if (quote) { buf_puts(b, "</p></blockquote>"); quote = 0; }
            if (list != lkind) {
                if (list)
                    buf_puts(b, list == 1 ? "</ul>" : "</ol>");
                buf_puts(b, lkind == 1 ? "<ul>" : "<ol>");
                list = lkind;
            }
            buf_puts(b, "<li>");
            inl(b, ls + ibody, ll - ibody, 0);
            buf_puts(b, "</li>");
        } else if (is_q) {
            if (list) { buf_puts(b, list == 1 ? "</ul>" : "</ol>"); list = 0; }
            if (!quote) {
                buf_puts(b, "<blockquote><p>");
                quote = 1;
            } else {
                buf_puts(b, "\n");
            }
            size_t t0 = ind + 1;
            if (t0 < ll && ls[t0] == ' ')
                t0++;
            inl(b, ls + t0, ll - t0, 0);
        } else {
            if (list)  { buf_puts(b, list == 1 ? "</ul>" : "</ol>"); list = 0; }
            if (quote) { buf_puts(b, "</p></blockquote>"); quote = 0; }
            if (para.len)
                buf_puts(&para, "\n");
            inl(&para, ls + ind, ll - ind, 0);
        }
    }

    if (fence)
        buf_puts(b, "</code></pre>");
    if (list)
        buf_puts(b, list == 1 ? "</ul>" : "</ol>");
    if (quote)
        buf_puts(b, "</p></blockquote>");
    if (para.len)
        buf_printf(b, "<p>%s</p>", para.data);
    buf_free(&para);
}