9 n__markdown_8c
gullradriel edited this page 2026-11-01 17:45:54 +01:00
This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

src/n_markdown.c

Safe-subset Markdown parser. More...

Structures

Name
struct tbuf
struct mline
struct md_cells

Types

Name
typedef size_t(*)(const char *s, size_t n) strip_fn

Functions

Name
size_t url_end(const char * s, size_t n, size_t i)
int try_strike(n_markdown_inline * a, tbuf * t, const char * s, size_t n, size_t i, unsigned inherited, int depth, unsigned flags, size_t * ni)
int try_link(n_markdown_inline * a, tbuf * t, const char * s, size_t n, size_t i, unsigned inherited, size_t * ni)
int try_emphasis(n_markdown_inline * a, tbuf * t, const char * s, size_t n, size_t i, unsigned inherited, int depth, unsigned flags, size_t * ni)
int try_code(n_markdown_inline * a, tbuf * t, const char * s, size_t n, size_t i, unsigned inherited, size_t * ni)
int try_autolink(n_markdown_inline * a, tbuf * t, const char * s, size_t n, size_t i, unsigned inherited, size_t * ni)
int tbuf_add(tbuf * t, char c)
void table_add_row(n_markdown_block * blk, const char * s, const md_cells * c, unsigned flags)
int table_add_cell(n_markdown_block * blk, const char * s, size_t a, size_t b, unsigned flags)
size_t strip_quote(const char * s, size_t n)
int starts_table(const mline * lines, size_t li, size_t nlines)
int starts_fence(const char * s, size_t n)
unsigned char rule_align(const char * s, size_t a, size_t b)
void row_cells(const char * s, size_t n, md_cells * out)
void parse_inline(n_markdown_inline * a, const char * s, size_t n, unsigned inherited, int depth, unsigned flags)
void n_markdown_parse_ex(const char * text, size_t len, unsigned flags, n_markdown_doc * out)
parse text into out, choosing which optional block kinds to recognise
void n_markdown_parse(const char * text, size_t len, n_markdown_doc * out)
parse text (len bytes, plain UTF-8) into out
int n_markdown_has_markup_ex(const char * text, size_t len, unsigned flags)
1 if text contains any markup the given flags would render, else 0
int n_markdown_has_markup(const char * text, size_t len)
1 if text contains any renderable markup, else 0
void n_markdown_free(n_markdown_doc * doc)
free everything a n_markdown_doc owns and zero it
int md_word_char(unsigned char c)
int md_us_opens(const char * s, size_t i)
int md_us_closes(const char * s, size_t n, size_t j, size_t len)
int match_ul(const char * s, size_t n, size_t * off)
int match_quote(const char * s, size_t n, size_t * off)
int match_ol(const char * s, size_t n, size_t * off)
int match_heading(const char * s, size_t n, int * level, size_t * off)
int list_add_wrapped(n_markdown_block * blk, const mline * lines, size_t a, size_t b, size_t off, unsigned flags)
int line_starts_block(const mline * lines, size_t li, size_t nlines, unsigned flags)
int line_indented(const char * s, size_t n)
int line_blank(const char * s, size_t n)
char * join_lines(const mline * lines, size_t a, size_t b, strip_fn strip, size_t * out_len)
int is_ws(char c)
int is_table_rule(const char * s, size_t n)
int is_md_punct(char c)
int is_http_url(const char * s, size_t n)
int is_digit(char c)
int inl_push(n_markdown_inline * a, char * text, char * url, unsigned style)
int has_pipe(const char * s, size_t n)
void free_inline(n_markdown_inline * inl)
void flush_text(n_markdown_inline * a, tbuf * t, unsigned style)
char * dup_range(const char * s, size_t n)
n_markdown_block * doc_push(n_markdown_doc * d, int kind)

Defines

Name
MD_MAX_SPANS
MD_MAX_ITEMS
MD_MAX_HEADING
MD_MAX_DEPTH
Most list levels N_MARKDOWN_NESTED tracks; deeper ones count as the last.
MD_MAX_DEPTH
Most list levels N_MARKDOWN_NESTED tracks; deeper ones count as the last.
MD_MAX_COLS
MD_MAX_BLOCKS

Detailed Description

Safe-subset Markdown parser.

Author: Castagnier Mickael

Version: 1.0

See n_markdown.h. Bold/italic nesting is flattened into per-span style flags. Bounded against hostile input – caps on blocks/spans/list-items and on bold/italic recursion depth; a cap overflow or an allocation failure truncates safely.

Types Documentation

typedef strip_fn

typedef size_t(* strip_fn) (const char *s, size_t n);

Functions Documentation

function url_end

static size_t url_end(
    const char * s,
    size_t n,
    size_t i
)

function try_strike

static int try_strike(
    n_markdown_inline * a,
    tbuf * t,
    const char * s,
    size_t n,
    size_t i,
    unsigned inherited,
    int depth,
    unsigned flags,
    size_t * ni
)
static int try_link(
    n_markdown_inline * a,
    tbuf * t,
    const char * s,
    size_t n,
    size_t i,
    unsigned inherited,
    size_t * ni
)

function try_emphasis

static int try_emphasis(
    n_markdown_inline * a,
    tbuf * t,
    const char * s,
    size_t n,
    size_t i,
    unsigned inherited,
    int depth,
    unsigned flags,
    size_t * ni
)

function try_code

static int try_code(
    n_markdown_inline * a,
    tbuf * t,
    const char * s,
    size_t n,
    size_t i,
    unsigned inherited,
    size_t * ni
)
static int try_autolink(
    n_markdown_inline * a,
    tbuf * t,
    const char * s,
    size_t n,
    size_t i,
    unsigned inherited,
    size_t * ni
)

function tbuf_add

static int tbuf_add(
    tbuf * t,
    char c
)

function table_add_row

static void table_add_row(
    n_markdown_block * blk,
    const char * s,
    const md_cells * c,
    unsigned flags
)

function table_add_cell

static int table_add_cell(
    n_markdown_block * blk,
    const char * s,
    size_t a,
    size_t b,
    unsigned flags
)

function strip_quote

static size_t strip_quote(
    const char * s,
    size_t n
)

function starts_table

static int starts_table(
    const mline * lines,
    size_t li,
    size_t nlines
)

function starts_fence

static int starts_fence(
    const char * s,
    size_t n
)

function rule_align

static unsigned char rule_align(
    const char * s,
    size_t a,
    size_t b
)

function row_cells

static void row_cells(
    const char * s,
    size_t n,
    md_cells * out
)

function parse_inline

static void parse_inline(
    n_markdown_inline * a,
    const char * s,
    size_t n,
    unsigned inherited,
    int depth,
    unsigned flags
)

function n_markdown_parse_ex

void n_markdown_parse_ex(
    const char * text,
    size_t len,
    unsigned flags,
    n_markdown_doc * out
)

parse text into out, choosing which optional block kinds to recognise

Parameters:

  • text the UTF-8 source (may be NULL)
  • len number of bytes of text
  • flags any of N_MARKDOWN_HEADINGS, N_MARKDOWN_TABLES, N_MARKDOWN_AUTOLINKS, N_MARKDOWN_STRIKETHROUGH, N_MARKDOWN_ESCAPES, N_MARKDOWN_ALL, N_MARKDOWN_INLINE_ALL; unknown bits are ignored
  • out the document to fill (zeroed first)

n_markdown_parse() is this with flags of zero, and that is the whole difference: the default subset is unchanged, byte for byte, so a caller that never passes a flag can never be handed a block kind it does not know.

Same guarantees as n_markdown_parse(): out is zeroed first, an allocation failure or a bounds cap truncates safely rather than failing, and the result is always released with n_markdown_free().

function n_markdown_parse

void n_markdown_parse(
    const char * text,
    size_t len,
    n_markdown_doc * out
)

parse text (len bytes, plain UTF-8) into out

Parameters:

  • text the UTF-8 source (may be NULL)
  • len number of bytes of text
  • out the document to fill (zeroed first)

Never fails destructively: on allocation failure or a bounds cap it keeps what it has (safe truncation). out is zeroed first. Always pair with n_markdown_free.

function n_markdown_has_markup_ex

int n_markdown_has_markup_ex(
    const char * text,
    size_t len,
    unsigned flags
)

1 if text contains any markup the given flags would render, else 0

Parameters:

Return: 1 if there is renderable markup, 0 otherwise

n_markdown_has_markup() is this with flags of zero. A document that is nothing but a heading has no markup in the default subset and has markup with N_MARKDOWN_HEADINGS, which is the answer each caller wants.

function n_markdown_has_markup

int n_markdown_has_markup(
    const char * text,
    size_t len
)

1 if text contains any renderable markup, else 0

Parameters:

  • text the UTF-8 source
  • len number of bytes of text

Return: 1 if there is renderable markup, 0 otherwise

Lets a caller skip the styled path for a plain message.

function n_markdown_free

void n_markdown_free(
    n_markdown_doc * doc
)

free everything a n_markdown_doc owns and zero it

Parameters:

  • doc the document (safe on a zeroed doc and on NULL)

function md_word_char

static int md_word_char(
    unsigned char c
)

function md_us_opens

static int md_us_opens(
    const char * s,
    size_t i
)

function md_us_closes

static int md_us_closes(
    const char * s,
    size_t n,
    size_t j,
    size_t len
)

function match_ul

static int match_ul(
    const char * s,
    size_t n,
    size_t * off
)

function match_quote

static int match_quote(
    const char * s,
    size_t n,
    size_t * off
)

function match_ol

static int match_ol(
    const char * s,
    size_t n,
    size_t * off
)

function match_heading

static int match_heading(
    const char * s,
    size_t n,
    int * level,
    size_t * off
)

function list_add_wrapped

static int list_add_wrapped(
    n_markdown_block * blk,
    const mline * lines,
    size_t a,
    size_t b,
    size_t off,
    unsigned flags
)

function line_starts_block

static int line_starts_block(
    const mline * lines,
    size_t li,
    size_t nlines,
    unsigned flags
)

function line_indented

static int line_indented(
    const char * s,
    size_t n
)

function line_blank

static int line_blank(
    const char * s,
    size_t n
)

function join_lines

static char * join_lines(
    const mline * lines,
    size_t a,
    size_t b,
    strip_fn strip,
    size_t * out_len
)

function is_ws

static int is_ws(
    char c
)

function is_table_rule

static int is_table_rule(
    const char * s,
    size_t n
)

function is_md_punct

static int is_md_punct(
    char c
)

function is_http_url

static int is_http_url(
    const char * s,
    size_t n
)

function is_digit

static int is_digit(
    char c
)

function inl_push

static int inl_push(
    n_markdown_inline * a,
    char * text,
    char * url,
    unsigned style
)

function has_pipe

static int has_pipe(
    const char * s,
    size_t n
)

function free_inline

static void free_inline(
    n_markdown_inline * inl
)

function flush_text

static void flush_text(
    n_markdown_inline * a,
    tbuf * t,
    unsigned style
)

function dup_range

static char * dup_range(
    const char * s,
    size_t n
)

function doc_push

static n_markdown_block * doc_push(
    n_markdown_doc * d,
    int kind
)

Macros Documentation

define MD_MAX_SPANS

#define MD_MAX_SPANS 2048u  /* spans per inline flow    */

define MD_MAX_ITEMS

#define MD_MAX_ITEMS 1024u  /* rows per list, cells per table */

define MD_MAX_HEADING

#define MD_MAX_HEADING 6    /* deepest "######" heading */

define MD_MAX_DEPTH

#define MD_MAX_DEPTH 16

Most list levels N_MARKDOWN_NESTED tracks; deeper ones count as the last.

define MD_MAX_DEPTH

#define MD_MAX_DEPTH 16     /* bold/italic nesting      */

Most list levels N_MARKDOWN_NESTED tracks; deeper ones count as the last.

define MD_MAX_COLS

#define MD_MAX_COLS 32u     /* columns per table        */

define MD_MAX_BLOCKS

#define MD_MAX_BLOCKS 1024u /* blocks per document      */

Source code

/*
 * Nilorea Library
 * Copyright (C) 2005-2026 Castagnier Mickael
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * permissions and limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include "nilorea/n_markdown.h"

#include <stdlib.h>
#include <string.h>

#define MD_MAX_BLOCKS 1024u /* blocks per document      */
#define MD_MAX_SPANS 2048u  /* spans per inline flow    */
#define MD_MAX_ITEMS 1024u  /* rows per list, cells per table */
#define MD_MAX_DEPTH 16     /* bold/italic nesting      */
#define MD_MAX_COLS 32u     /* columns per table        */
#define MD_MAX_HEADING 6    /* deepest "######" heading */

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

static int is_ws(char c) {
    return c == ' ' || c == '\t' || c == '\r' || c == '\f' || c == '\v';
}

static int is_digit(char c) {
    return c >= '0' && c <= '9';
}

/* Own a NUL-terminated copy of s[0..n); NULL on OOM (treated as truncation). */
static char* dup_range(const char* s, size_t n) {
    char* out = malloc(n + 1);
    if (!out)
        return NULL;
    if (n)
        memcpy(out, s, n);
    out[n] = '\0';
    return out;
}

/* A growable byte buffer for the pending plain-text run. */
typedef struct {
    char* b;
    size_t len, cap;
} tbuf;

static int tbuf_add(tbuf* t, char c) {
    if (t->len + 1 > t->cap) {
        size_t nc = t->cap ? t->cap * 2 : 32;
        char* nb = realloc(t->b, nc);
        if (!nb)
            return 0;
        t->b = nb;
        t->cap = nc;
    }
    t->b[t->len++] = c;
    return 1;
}

/* ---- inline parsing ----------------------------------------------------- */

/* Append an owned span (takes ownership of text/url). Drops it (freeing) on a
 * cap overflow or a NULL text. Returns 1 if stored. */
static int inl_push(n_markdown_inline* a, char* text, char* url, unsigned style) {
    n_markdown_span* ns;
    if (!text) {
        free(url);
        return 0;
    }
    if (a->n_spans >= MD_MAX_SPANS) {
        free(text);
        free(url);
        return 0;
    }
    ns = realloc(a->spans, (a->n_spans + 1) * sizeof(*ns));
    if (!ns) {
        free(text);
        free(url);
        return 0;
    }
    a->spans = ns;
    ns[a->n_spans].text = text;
    ns[a->n_spans].url = url;
    ns[a->n_spans].style = style;
    a->n_spans++;
    return 1;
}

/* Flush the pending plain text (if any) as one span with \p style. */
static void flush_text(n_markdown_inline* a, tbuf* t, unsigned style) {
    if (t->len == 0)
        return;
    (void)inl_push(a, dup_range(t->b, t->len), NULL, style);
    t->len = 0;
}

/* Case-insensitive "http://" / "https://" prefix test on s[0..n). */
static int is_http_url(const char* s, size_t n) {
    static const char* h = "http";
    size_t i = 0;
    for (; i < 4; i++)
        if (i >= n || (s[i] | 0x20) != h[i])
            return 0;
    if (i < n && (s[i] | 0x20) == 's')
        i++;
    return i + 3 <= n && s[i] == ':' && s[i + 1] == '/' && s[i + 2] == '/';
}

static void parse_inline(n_markdown_inline* a, const char* s, size_t n, unsigned inherited, int depth, unsigned flags);

/* Try to match `code` at s[i]; on success emit the span and set *ni past it. */
static int try_code(n_markdown_inline* a, tbuf* t, const char* s, size_t n, size_t i, unsigned inherited, size_t* ni) {
    size_t j;
    if (s[i] != '`')
        return 0;
    for (j = i + 1; j < n && s[j] != '`'; j++);
    if (j >= n || j == i + 1) /* no closing backtick, or "``": literal */
        return 0;
    flush_text(a, t, inherited);
    (void)inl_push(a, dup_range(s + i + 1, j - (i + 1)), NULL,
                   inherited | N_MARKDOWN_CODE);
    *ni = j + 1;
    return 1;
}

/* Try to match [text](url) at s[i]. */
static int try_link(n_markdown_inline* a, tbuf* t, const char* s, size_t n, size_t i, unsigned inherited, size_t* ni) {
    size_t close, url0, k;
    if (s[i] != '[')
        return 0;
    for (close = i + 1; close < n && s[close] != ']'; close++);
    if (close >= n || close + 1 >= n || s[close + 1] != '(')
        return 0;
    url0 = close + 2;
    for (k = url0; k < n && s[k] != ')' && !is_ws(s[k]); k++);
    if (k >= n || s[k] != ')' || k == url0) /* empty url or ws/EOF: no match */
        return 0;
    flush_text(a, t, inherited);
    if (is_http_url(s + url0, k - url0)) {
        (void)inl_push(a, dup_range(s + i + 1, close - (i + 1)),
                       dup_range(s + url0, k - url0),
                       inherited | N_MARKDOWN_LINK);
    } else { /* non-http scheme: render the whole [..](..) literally */
        (void)inl_push(a, dup_range(s + i, (k + 1) - i), NULL, inherited);
    }
    *ni = k + 1;
    return 1;
}

/* A character that makes an underscore intraword: an ASCII letter or digit,
 * or any byte of a multi-byte UTF-8 sequence (so "café_au_lait" counts). */
static int md_word_char(unsigned char c) {
    return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') ||
           (c >= 'A' && c <= 'Z') || c >= 0x80;
}

/* Underscore emphasis does not open or close INSIDE a word (CommonMark):
 * "snake_case_name" and ":throwing_kiss: :wrapped_present:" stay literal
 * instead of italicising what lies between two of them. An opener at s[i]
 * needs no word character before its run of underscores, a closer of length
 * len at s[j] none after its run (so "a__b__c" is literal too). Asterisks
 * are unaffected. */
static int md_us_opens(const char* s, size_t i) {
    while (i > 0 && s[i - 1] == '_')
        i--;
    return i == 0 || !md_word_char((unsigned char)s[i - 1]);
}
static int md_us_closes(const char* s, size_t n, size_t j, size_t len) {
    size_t k = j + len;
    while (k < n && s[k] == '_')
        k++;
    return k >= n || !md_word_char((unsigned char)s[k]);
}

/* Try bold or italic emphasis at s[i]; recurses on the inner span. */
static int try_emphasis(n_markdown_inline* a, tbuf* t, const char* s, size_t n, size_t i, unsigned inherited, int depth, unsigned flags, size_t* ni) {
    char c = s[i];
    size_t j;
    if (c != '*' && c != '_')
        return 0;
    if (depth >= MD_MAX_DEPTH) /* too deep: the marker is literal text */
        return 0;
    if (c == '_' && !md_us_opens(s, i)) /* intraword: literal */
        return 0;
    if (i + 1 < n && s[i + 1] == c) { /* bold: find the next "cc" */
        for (j = i + 2; j + 1 < n; j++)
            if (s[j] == c && s[j + 1] == c) {
                if (c == '_' && !md_us_closes(s, n, j, 2))
                    continue;
                if (j == i + 2) /* "****" is not an empty bold */
                    break;
                flush_text(a, t, inherited);
                parse_inline(a, s + i + 2, j - (i + 2),
                             inherited | N_MARKDOWN_BOLD, depth + 1, flags);
                *ni = j + 2;
                return 1;
            }
    }
    /* italic: find the next single c. An empty one ("**" left over from a
     * bold that never closed) is not italic: it used to swallow both
     * markers, so "an **unclosed run" lost its asterisks. */
    for (j = i + 1; j < n; j++)
        if (s[j] == c) {
            if (c == '_' && j > i + 1 && !md_us_closes(s, n, j, 1))
                continue;
            if (j == i + 1)
                return 0;
            flush_text(a, t, inherited);
            parse_inline(a, s + i + 1, j - (i + 1),
                         inherited | N_MARKDOWN_ITALIC, depth + 1, flags);
            *ni = j + 1;
            return 1;
        }
    return 0;
}

/* ~~strike~~ (N_MARKDOWN_STRIKETHROUGH): two tildes, then the next "~~".
 * Recurses like bold, so ~~**both**~~ carries both styles. */
static int try_strike(n_markdown_inline* a, tbuf* t, const char* s, size_t n, size_t i, unsigned inherited, int depth, unsigned flags, size_t* ni) {
    size_t j;
    if (!(flags & N_MARKDOWN_STRIKETHROUGH) || s[i] != '~' || i + 1 >= n ||
        s[i + 1] != '~' || depth >= MD_MAX_DEPTH)
        return 0;
    for (j = i + 2; j + 1 < n; j++)
        if (s[j] == '~' && s[j + 1] == '~') {
            if (j == i + 2) /* "~~~~" is not an empty strike */
                return 0;
            flush_text(a, t, inherited);
            parse_inline(a, s + i + 2, j - (i + 2),
                         inherited | N_MARKDOWN_STRIKE, depth + 1, flags);
            *ni = j + 2;
            return 1;
        }
    return 0;
}

/* Where a bare URL starting at s[i] ends: at white space or a character a URL
 * in running text does not carry, then less the trailing punctuation that
 * belongs to the sentence ("see https://x.org." ends at the g) and a closing
 * parenthesis with no opening one inside the URL ("(https://x.org)"). */
static size_t url_end(const char* s, size_t n, size_t i) {
    size_t e = i, k;
    long depth = 0;
    while (e < n && !is_ws(s[e]) && s[e] != '\n' && s[e] != '<' &&
           s[e] != '>' && s[e] != '"' && s[e] != '`')
        e++;
    for (;;) {
        char c;
        if (e <= i)
            return e;
        c = s[e - 1];
        if (c == '.' || c == ',' || c == ';' || c == ':' || c == '!' ||
            c == '?' || c == '\'' || c == '"' || c == '*' || c == '_' ||
            c == '~') {
            e--;
            continue;
        }
        if (c == ')') {
            depth = 0;
            for (k = i; k < e; k++)
                depth += s[k] == '(' ? 1 : s[k] == ')' ? -1
                                                       : 0;
            if (depth < 0) {
                e--;
                continue;
            }
        }
        return e;
    }
}

/* Bare http(s) URLs and <http(s)://...> (N_MARKDOWN_AUTOLINKS): the URL is
 * both the text and the link. A bare one starts only at a word boundary, so
 * "xhttp://" is text, and needs something after "://". */
static int try_autolink(n_markdown_inline* a, tbuf* t, const char* s, size_t n, size_t i, unsigned inherited, size_t* ni) {
    size_t e;
    if (s[i] == '<') {
        size_t k, u0;
        if (!is_http_url(s + i + 1, n - (i + 1)))
            return 0;
        for (k = i + 1; k < n && s[k] != '>' && !is_ws(s[k]) && s[k] != '\n'; k++);
        if (k >= n || s[k] != '>')
            return 0;
        u0 = i + 1;
        e = k;
        if (e - u0 <= (((s[u0 + 4] | 0x20) == 's') ? 8u : 7u))
            return 0; /* nothing after the "://" */
        flush_text(a, t, inherited);
        (void)inl_push(a, dup_range(s + u0, e - u0), dup_range(s + u0, e - u0),
                       inherited | N_MARKDOWN_LINK);
        *ni = k + 1;
        return 1;
    }
    if ((s[i] | 0x20) != 'h' || !is_http_url(s + i, n - i))
        return 0;
    if (i > 0 && (is_digit(s[i - 1]) || ((s[i - 1] | 0x20) >= 'a' && (s[i - 1] | 0x20) <= 'z') ||
                  s[i - 1] == '/' || (unsigned char)s[i - 1] >= 0x80))
        return 0;
    e = url_end(s, n, i);
    {
        size_t scheme = ((s[i + 4] | 0x20) == 's') ? 8 : 7; /* https:// */
        if (e <= i + scheme)
            return 0; /* nothing after the "://" */
    }
    flush_text(a, t, inherited);
    (void)inl_push(a, dup_range(s + i, e - i), dup_range(s + i, e - i),
                   inherited | N_MARKDOWN_LINK);
    *ni = e;
    return 1;
}

/* A backslash before ASCII punctuation (N_MARKDOWN_ESCAPES) is that character,
 * literally: "\\*not italic\\*". A backslash before anything else is a
 * backslash. */
static int is_md_punct(char c) {
    return (c >= '!' && c <= '/') || (c >= ':' && c <= '@') ||
           (c >= '[' && c <= '`') || (c >= '{' && c <= '~');
}

/* Parse s[0..n) into inline spans appended to \p a, each carrying \p inherited
 * plus whatever emphasis/code/link applies. \p flags are the parse_ex flags;
 * only the inline ones matter here. */
static void parse_inline(n_markdown_inline* a, const char* s, size_t n, unsigned inherited, int depth, unsigned flags) {
    tbuf t = {0};
    size_t i = 0, ni;
    while (i < n) {
        if ((flags & N_MARKDOWN_ESCAPES) && s[i] == '\\' && i + 1 < n &&
            is_md_punct(s[i + 1])) {
            if (!tbuf_add(&t, s[i + 1]))
                break;
            i += 2;
            continue;
        }
        if (try_code(a, &t, s, n, i, inherited, &ni) ||
            try_link(a, &t, s, n, i, inherited, &ni) ||
            ((flags & N_MARKDOWN_AUTOLINKS) &&
             try_autolink(a, &t, s, n, i, inherited, &ni)) ||
            try_strike(a, &t, s, n, i, inherited, depth, flags, &ni) ||
            try_emphasis(a, &t, s, n, i, inherited, depth, flags, &ni)) {
            i = ni;
            continue;
        }
        if (!tbuf_add(&t, s[i])) /* OOM: stop, keep what we have */
            break;
        i++;
    }
    flush_text(a, &t, inherited);
    free(t.b);
}

/* ---- block parsing ------------------------------------------------------ */

/* One line = {ptr, len} into the normalized buffer. */
typedef struct {
    const char* p;
    size_t n;
} mline;

static int line_blank(const char* s, size_t n) {
    size_t i;
    for (i = 0; i < n; i++)
        if (!is_ws(s[i]) && s[i] != '\n')
            return 0;
    return 1;
}

static int starts_fence(const char* s, size_t n) {
    return n >= 3 && s[0] == '`' && s[1] == '`' && s[2] == '`';
}

/* > blockquote: on match, set *off to the start of the content (past "> "). */
static int match_quote(const char* s, size_t n, size_t* off) {
    if (n == 0 || s[0] != '>')
        return 0;
    *off = 1;
    if (*off < n && is_ws(s[*off]))
        (*off)++;
    return 1;
}

/* "- " / "* " list row: set *off past the marker and its following space(s). */
static int match_ul(const char* s, size_t n, size_t* off) {
    if (n < 2 || (s[0] != '-' && s[0] != '*') || !is_ws(s[1]))
        return 0;
    *off = 1;
    while (*off < n && is_ws(s[*off]))
        (*off)++;
    return 1;
}

/* "1. " list row: set *off past the digits, dot and following space(s). */
static int match_ol(const char* s, size_t n, size_t* off) {
    size_t i = 0;
    while (i < n && is_digit(s[i]))
        i++;
    if (i == 0 || i >= n || s[i] != '.')
        return 0;
    i++;
    if (i >= n || !is_ws(s[i]))
        return 0;
    while (i < n && is_ws(s[i]))
        i++;
    *off = i;
    return 1;
}

/* "## " heading: one to six hashes, then a space, then the text. Without the
 * space it is not a heading, which is what keeps a hash tag out. */
static int match_heading(const char* s, size_t n, int* level, size_t* off) {
    size_t i = 0, h = 0;
    while (i + h < n && s[i + h] == '#')
        h++;
    if (h == 0 || h > (size_t)MD_MAX_HEADING)
        return 0;
    i += h;
    if (i >= n || s[i] != ' ')
        return 0;
    while (i < n && is_ws(s[i]))
        i++;
    *level = (int)h;
    *off = i;
    return 1;
}

/* The cells of one table row, as [a, b) bounds into the line. */
typedef struct {
    size_t a[MD_MAX_COLS];
    size_t b[MD_MAX_COLS];
    size_t n;
} md_cells;

/* Split a table row on its pipes. A leading and a trailing pipe are both
 * optional, which is what GitHub accepts and what the documentation in this
 * tree is written with; dropping the trailing one here is what stops the blank
 * after it counting as an extra column. There is no escape for a pipe inside a
 * cell, because the subset has no backslash escapes at all and inventing one
 * here alone would be a rule nothing else in the parser follows. */
static void row_cells(const char* s, size_t n, md_cells* out) {
    size_t i = 0;
    out->n = 0;
    while (i < n && is_ws(s[i]))
        i++;
    if (i < n && s[i] == '|')
        i++;
    while (n > i && is_ws(s[n - 1]))
        n--;
    if (n > i && s[n - 1] == '|')
        n--;
    for (;;) {
        size_t j = i;
        while (j < n && s[j] != '|')
            j++;
        if (out->n >= MD_MAX_COLS)
            return;
        out->a[out->n] = i;
        out->b[out->n] = j;
        out->n++;
        if (j >= n)
            return;
        i = j + 1;
    }
}

/* Is this the "|---|:-:|" row under a table's header? Every cell has to be
 * dashes, with an optional colon at either end and at least one dash. That is
 * the whole test, and it is what tells a table apart from a paragraph that
 * happens to contain pipes. Split by row_cells, so the rule row and the header
 * row can never disagree about how many columns there are. */
static int is_table_rule(const char* s, size_t n) {
    md_cells c;
    size_t k;
    row_cells(s, n, &c);
    if (c.n == 0)
        return 0;
    for (k = 0; k < c.n; k++) {
        size_t a = c.a[k], b = c.b[k];
        while (a < b && is_ws(s[a]))
            a++;
        while (b > a && is_ws(s[b - 1]))
            b--;
        if (a < b && s[a] == ':')
            a++;
        if (b > a && s[b - 1] == ':')
            b--;
        if (a >= b)
            return 0;
        while (a < b) {
            if (s[a] != '-')
                return 0;
            a++;
        }
    }
    return 1;
}

/* Alignment asked for by one cell of the rule row. */
static unsigned char rule_align(const char* s, size_t a, size_t b) {
    int left, right;
    while (a < b && is_ws(s[a]))
        a++;
    while (b > a && is_ws(s[b - 1]))
        b--;
    left = (a < b && s[a] == ':');
    right = (b > a && s[b - 1] == ':');
    if (left && right)
        return N_MARKDOWN_ALIGN_CENTER;
    if (right)
        return N_MARKDOWN_ALIGN_RIGHT;
    return N_MARKDOWN_ALIGN_LEFT;
}

/* Does a line hold a cell separator at all: the cheap half of "is this a
 * table", asked of the header row before the rule row below it is looked at. */
static int has_pipe(const char* s, size_t n) {
    size_t i;
    for (i = 0; i < n; i++)
        if (s[i] == '|')
            return 1;
    return 0;
}

/* Does a table start at line @p li: a row with a pipe in it, and the rule row
 * underneath. Both halves are needed. A paragraph may well contain a pipe, and
 * a line that has one decides nothing on its own. */
static int starts_table(const mline* lines, size_t li, size_t nlines) {
    return has_pipe(lines[li].p, lines[li].n) && li + 1 < nlines &&
           is_table_rule(lines[li + 1].p, lines[li + 1].n);
}

/* Would line @p li start a block of its own rather than continue a paragraph?
 *
 * Takes the whole array because a table cannot be recognised from one line,
 * and @p flags because a heading or a table is only a block when it was asked
 * for: without the flag those lines are ordinary text and have to keep being
 * swallowed by the paragraph around them, exactly as they were before the
 * flags existed. */
static int line_starts_block(const mline* lines, size_t li, size_t nlines, unsigned flags) {
    const char* s = lines[li].p;
    size_t n = lines[li].n, off;
    int level;
    if ((flags & N_MARKDOWN_HEADINGS) && match_heading(s, n, &level, &off))
        return 1;
    if ((flags & N_MARKDOWN_TABLES) && starts_table(lines, li, nlines))
        return 1;
    return starts_fence(s, n) || match_quote(s, n, &off) ||
           match_ul(s, n, &off) || match_ol(s, n, &off);
}

/* Append a block; returns a pointer to it, or NULL on a cap/OOM. */
static n_markdown_block* doc_push(n_markdown_doc* d, int kind) {
    n_markdown_block* nb;
    if (d->n_blocks >= MD_MAX_BLOCKS)
        return NULL;
    nb = realloc(d->blocks, (d->n_blocks + 1) * sizeof(*nb));
    if (!nb)
        return NULL;
    d->blocks = nb;
    memset(&nb[d->n_blocks], 0, sizeof(nb[d->n_blocks]));
    nb[d->n_blocks].kind = kind;
    return &d->blocks[d->n_blocks++];
}

/* Join lines [a..b) with '\n' into an owned buffer, applying \p strip to each
 * line to skip a prefix (quote/list marker). \p strip may be NULL. */
typedef size_t (*strip_fn)(const char* s, size_t n);

static char* join_lines(const mline* lines, size_t a, size_t b, strip_fn strip, size_t* out_len) {
    size_t total = 0, i, w = 0;
    char* buf;
    for (i = a; i < b; i++) {
        size_t off = strip ? strip(lines[i].p, lines[i].n) : 0;
        total += lines[i].n - off + (i + 1 < b ? 1 : 0);
    }
    buf = malloc(total + 1);
    if (!buf) {
        *out_len = 0;
        return NULL;
    }
    for (i = a; i < b; i++) {
        size_t off = strip ? strip(lines[i].p, lines[i].n) : 0;
        size_t seg = lines[i].n - off;
        memcpy(buf + w, lines[i].p + off, seg);
        w += seg;
        if (i + 1 < b)
            buf[w++] = '\n';
    }
    buf[w] = '\0';
    *out_len = w;
    return buf;
}

static size_t strip_quote(const char* s, size_t n) {
    size_t off;
    return match_quote(s, n, &off) ? off : 0;
}

/* Is this line indented under the marker above it? A wrapped list item is
 * written that way, and GitHub calls taking it as part of the item above a
 * lazy continuation. Without it a list item long enough to wrap ends its own
 * list and the rest of the sentence becomes a paragraph. */
static int line_indented(const char* s, size_t n) {
    return n > 0 && is_ws(s[0]) && !line_blank(s, n);
}

/* Add one table cell (parse its inline spans). Cells are appended row by row,
 * so the array stays row-major with blk->n_cols to a row. */
static int table_add_cell(n_markdown_block* blk, const char* s, size_t a, size_t b, unsigned flags) {
    n_markdown_inline* ni;
    if (blk->n_items >= MD_MAX_ITEMS)
        return 0;
    while (a < b && is_ws(s[a]))
        a++;
    while (b > a && is_ws(s[b - 1]))
        b--;
    ni = realloc(blk->items, (blk->n_items + 1) * sizeof(*ni));
    if (!ni)
        return 0;
    blk->items = ni;
    memset(&ni[blk->n_items], 0, sizeof(ni[blk->n_items]));
    parse_inline(&blk->items[blk->n_items], s + a, b - a, 0, 0, flags);
    blk->n_items++;
    return 1;
}

/* One row of cells, padded or truncated to blk->n_cols so that every row is
 * the same length and cell (r, c) is always items[r * n_cols + c]. A row of a
 * hand-written table is short or long often enough that a renderer should not
 * have to ask. */
static void table_add_row(n_markdown_block* blk, const char* s, const md_cells* c, unsigned flags) {
    size_t k;
    for (k = 0; k < blk->n_cols; k++) {
        if (k < c->n) {
            if (!table_add_cell(blk, s, c->a[k], c->b[k], flags))
                return;
        } else if (!table_add_cell(blk, s, 0, 0, flags)) {
            return;
        }
    }
}

/* Add one list item that may run over several lines: the marker is stripped
 * from the first and the indent from the rest, and the lines are joined with a
 * space rather than a newline, because they are one sentence that the renderer
 * will wrap to whatever width it has. */
static int list_add_wrapped(n_markdown_block* blk, const mline* lines, size_t a, size_t b, size_t off, unsigned flags) {
    n_markdown_inline* ni;
    char* buf;
    size_t total = 0, i, w = 0;

    if (blk->n_items >= MD_MAX_ITEMS)
        return 0;
    for (i = a; i < b; i++) {
        size_t skip = (i == a) ? off : 0;
        while (i != a && skip < lines[i].n && is_ws(lines[i].p[skip]))
            skip++;
        total += lines[i].n - skip + 1;
    }
    buf = malloc(total + 1);
    if (!buf)
        return 0;
    for (i = a; i < b; i++) {
        size_t skip = (i == a) ? off : 0;
        while (i != a && skip < lines[i].n && is_ws(lines[i].p[skip]))
            skip++;
        if (w && lines[i].n > skip)
            buf[w++] = ' ';
        memcpy(buf + w, lines[i].p + skip, lines[i].n - skip);
        w += lines[i].n - skip;
    }
    buf[w] = '\0';
    ni = realloc(blk->items, (blk->n_items + 1) * sizeof(*ni));
    if (!ni) {
        free(buf);
        return 0;
    }
    blk->items = ni;
    memset(&ni[blk->n_items], 0, sizeof(ni[blk->n_items]));
    parse_inline(&blk->items[blk->n_items], buf, w, 0, 0, flags);
    blk->n_items++;
    free(buf);
    return 1;
}

void n_markdown_parse(const char* text, size_t len, n_markdown_doc* out) {
    n_markdown_parse_ex(text, len, 0, out);
}

#define MD_MAX_DEPTH 16

void n_markdown_parse_ex(const char* text, size_t len, unsigned flags, n_markdown_doc* out) {
    char* norm;
    mline* lines = NULL;
    size_t nlines = 0, i, li, start;
    /* N_MARKDOWN_NESTED: each line's indentation, stripped from its view
       (columns, a tab counting 4, and bytes), and the open list levels */
    size_t* lead_col = NULL;
    size_t* lead_bytes = NULL;
    size_t marker[MD_MAX_DEPTH];
    int sp = 0;
    const int nested = (flags & N_MARKDOWN_NESTED) != 0;

    memset(out, 0, sizeof(*out));
    if (!text)
        return;

    /* Normalize CRLF / lone CR to LF. */
    norm = malloc(len + 1);
    if (!norm)
        return;
    {
        size_t w = 0;
        for (i = 0; i < len; i++) {
            if (text[i] == '\r') {
                norm[w++] = '\n';
                if (i + 1 < len && text[i + 1] == '\n')
                    i++;
            } else {
                norm[w++] = text[i];
            }
        }
        norm[w] = '\0';
        len = w;
    }

    /* Split into lines. */
    lines = malloc((len + 1) * sizeof(*lines));
    if (!lines) {
        free(norm);
        return;
    }
    start = 0;
    for (i = 0; i <= len; i++) {
        if (i == len || norm[i] == '\n') {
            lines[nlines].p = norm + start;
            lines[nlines].n = i - start;
            nlines++;
            start = i + 1;
        }
    }

    if (nested) {
        lead_col = malloc(nlines * sizeof(*lead_col));
        lead_bytes = malloc(nlines * sizeof(*lead_bytes));
        if (!lead_col || !lead_bytes) {
            free(lead_col);
            free(lead_bytes);
            free(lines);
            free(norm);
            return;
        }
        for (i = 0; i < nlines; i++) {
            size_t k = 0, col = 0;
            while (k < lines[i].n && is_ws(lines[i].p[k])) {
                col += lines[i].p[k] == '\t' ? 4 - col % 4 : 1;
                k++;
            }
            lead_col[i] = col;
            lead_bytes[i] = k;
            lines[i].p += k;
            lines[i].n -= k;
        }
    }

    li = 0;
    while (li < nlines) {
        const char* s = lines[li].p;
        size_t n = lines[li].n, off, nblocks = out->n_blocks;
        int depth = 0;
        n_markdown_block* blk;

        if (line_blank(s, n)) {
            li++;
            continue;
        }
        if (nested) {
            /* the list levels this line is not indented under end here */
            while (sp > 0 && lead_col[li] <= marker[sp - 1])
                sp--;
            depth = sp;
        }
        if (flags & N_MARKDOWN_HEADINGS) { /* "## " heading */
            int level = 0;
            if (match_heading(s, n, &level, &off)) {
                blk = doc_push(out, N_MARKDOWN_HEADING);
                if (blk) {
                    blk->level = level;
                    parse_inline(&blk->inl, s + off, n - off, 0, 0, flags);
                }
                li++;
                goto placed;
            }
        }
        if ((flags & N_MARKDOWN_TABLES) && starts_table(lines, li, nlines)) { /* pipe table */
            md_cells head, rule;

            row_cells(s, n, &head);
            row_cells(lines[li + 1].p, lines[li + 1].n, &rule);
            blk = doc_push(out, N_MARKDOWN_TABLE);
            if (!blk) {
                /* out of blocks: skip the whole table rather than leave its
                   rows to be read as paragraphs */
                li += 2;
                while (li < nlines && !line_blank(lines[li].p, lines[li].n) &&
                       has_pipe(lines[li].p, lines[li].n) &&
                       !line_starts_block(lines, li, nlines, flags))
                    li++;
                goto placed;
            }
            /* The header decides the width. A rule row of a different length
               is a malformed table, and taking the header's count keeps the
               cells and the alignments in step whichever way it differs. */
            blk->n_cols = head.n ? head.n : 1;
            blk->align = malloc(blk->n_cols);
            if (blk->align) {
                size_t k;
                for (k = 0; k < blk->n_cols; k++)
                    blk->align[k] = k < rule.n
                                        ? rule_align(lines[li + 1].p, rule.a[k], rule.b[k])
                                        : (unsigned char)N_MARKDOWN_ALIGN_LEFT;
            }
            table_add_row(blk, s, &head, flags);
            li += 2;
            /* a row is a non-blank line with a pipe that does not start a
               block of its own: "# x | y" or "- a|b" after a table used to be
               taken as rows */
            while (li < nlines && !line_blank(lines[li].p, lines[li].n) &&
                   has_pipe(lines[li].p, lines[li].n) &&
                   !line_starts_block(lines, li, nlines, flags)) {
                md_cells body;
                row_cells(lines[li].p, lines[li].n, &body);
                table_add_row(blk, lines[li].p, &body, flags);
                li++;
            }
            goto placed;
        }
        if (starts_fence(s, n)) { /* ``` fenced code block */
            size_t body_a, body_b, fence_lead = nested ? lead_bytes[li] : 0;
            char* code;
            size_t clen;
            li++;
            body_a = li;
            while (li < nlines && !starts_fence(lines[li].p, lines[li].n))
                li++;
            body_b = li;
            if (li < nlines)
                li++; /* closing fence */
            if (nested) {
                /* the code keeps the indentation it has beyond the fence's */
                size_t k;
                for (k = body_a; k < body_b; k++) {
                    size_t keep = lead_bytes[k] > fence_lead ? lead_bytes[k] - fence_lead : 0;
                    lines[k].p -= keep;
                    lines[k].n += keep;
                }
            }
            code = join_lines(lines, body_a, body_b, NULL, &clen);
            blk = doc_push(out, N_MARKDOWN_CODE_BLOCK);
            if (blk)
                blk->code = code ? code : dup_range("", 0);
            else
                free(code);
            goto placed;
        }
        if (match_quote(s, n, &off)) { /* > blockquote */
            size_t a = li, joined_len;
            char* joined;
            while (li < nlines && match_quote(lines[li].p, lines[li].n, &off))
                li++;
            joined = join_lines(lines, a, li, strip_quote, &joined_len);
            blk = doc_push(out, N_MARKDOWN_QUOTE);
            if (blk && joined)
                parse_inline(&blk->inl, joined, joined_len, 0, 0, flags);
            free(joined);
            goto placed;
        }
        if (match_ul(s, n, &off)) { /* unordered list */
            const size_t item_lead = nested ? lead_col[li] : 0;
            blk = doc_push(out, N_MARKDOWN_UL);
            while (li < nlines && match_ul(lines[li].p, lines[li].n, &off) && (!nested || lead_col[li] == item_lead)) {
                size_t item_a = li;
                /* off is the marker width of THIS item, set by the match in
                   the condition above; nothing below writes to it, so the
                   continuation scan can run before it is used. */
                li++;
                while (li < nlines &&
                       (nested ? lead_col[li] > item_lead && !line_blank(lines[li].p, lines[li].n)
                               : line_indented(lines[li].p, lines[li].n)) &&
                       !line_starts_block(lines, li, nlines, flags))
                    li++;
                if (blk)
                    (void)list_add_wrapped(blk, lines, item_a, li, off, flags);
            }
            if (nested && sp < MD_MAX_DEPTH)
                marker[sp++] = item_lead;
            goto placed;
        }
        if (match_ol(s, n, &off)) { /* ordered list */
            const size_t item_lead = nested ? lead_col[li] : 0;
            blk = doc_push(out, N_MARKDOWN_OL);
            if (blk && nested)
                blk->start = strtol(s, NULL, 10);
            while (li < nlines && match_ol(lines[li].p, lines[li].n, &off) && (!nested || lead_col[li] == item_lead)) {
                size_t item_a = li;
                /* off is the marker width of THIS item, set by the match in
                   the condition above; nothing below writes to it, so the
                   continuation scan can run before it is used. */
                li++;
                while (li < nlines &&
                       (nested ? lead_col[li] > item_lead && !line_blank(lines[li].p, lines[li].n)
                               : line_indented(lines[li].p, lines[li].n)) &&
                       !line_starts_block(lines, li, nlines, flags))
                    li++;
                if (blk)
                    (void)list_add_wrapped(blk, lines, item_a, li, off, flags);
            }
            if (nested && sp < MD_MAX_DEPTH)
                marker[sp++] = item_lead;
            goto placed;
        }
        { /* paragraph: consecutive non-blank, non-special lines */
            size_t a = li, joined_len;
            char* joined;
            /* The first line is taken whatever it is. Every block start is
               tested above, so a line reaching here is paragraph text by
               elimination -- and consuming it unconditionally is what
               guarantees this loop advances, whatever a future matcher
               decides. A predicate that says "block starts here" about a line
               the branches above declined would otherwise spin. */
            li++;
            while (li < nlines && !line_blank(lines[li].p, lines[li].n) &&
                   !line_starts_block(lines, li, nlines, flags))
                li++;
            joined = join_lines(lines, a, li, NULL, &joined_len);
            blk = doc_push(out, N_MARKDOWN_PARA);
            if (blk && joined)
                parse_inline(&blk->inl, joined, joined_len, 0, 0, flags);
            free(joined);
        }
    placed:
        /* the blocks this line started sit at its depth */
        for (; nblocks < out->n_blocks; nblocks++)
            out->blocks[nblocks].depth = depth;
    }

    free(lead_col);
    free(lead_bytes);
    free(lines);
    free(norm);
}

static void free_inline(n_markdown_inline* inl) {
    size_t i;
    for (i = 0; i < inl->n_spans; i++) {
        free(inl->spans[i].text);
        free(inl->spans[i].url);
    }
    free(inl->spans);
    inl->spans = NULL;
    inl->n_spans = 0;
}

void n_markdown_free(n_markdown_doc* doc) {
    size_t i, j;
    if (!doc || !doc->blocks)
        return;
    for (i = 0; i < doc->n_blocks; i++) {
        n_markdown_block* b = &doc->blocks[i];
        free_inline(&b->inl);
        free(b->code);
        for (j = 0; j < b->n_items; j++)
            free_inline(&b->items[j]);
        free(b->items);
        free(b->align);
    }
    free(doc->blocks);
    doc->blocks = NULL;
    doc->n_blocks = 0;
}

int n_markdown_has_markup(const char* text, size_t len) {
    return n_markdown_has_markup_ex(text, len, 0);
}

int n_markdown_has_markup_ex(const char* text, size_t len, unsigned flags) {
    n_markdown_doc d;
    int markup = 0;
    n_markdown_parse_ex(text, len, flags, &d);
    if (d.n_blocks > 1) {
        markup = 1;
    } else if (d.n_blocks == 1) {
        const n_markdown_block* b = &d.blocks[0];
        if (b->kind != N_MARKDOWN_PARA) {
            markup = 1;
        } else {
            size_t i;
            for (i = 0; i < b->inl.n_spans; i++)
                if (b->inl.spans[i].style != 0) {
                    markup = 1;
                    break;
                }
        }
    }
    n_markdown_free(&d);
    if (!markup && (flags & N_MARKDOWN_ESCAPES) && text) {
        size_t i;
        for (i = 0; i + 1 < len; i++)
            if (text[i] == '\\' && is_md_punct(text[i + 1]))
                return 1; /* the plain path would show the backslash */
    }
    return markup;
}