3 n__gui__md_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_gui_md.c

Markdown layout and drawing. More...

Structures

Name
struct md_line_ctx
Everything a line needs, carried through n_wrap to the emit callback, which is handed only an item range, a line number and a width.
struct md_hit
A hit-test threaded through the layout: when a link atom's cell contains the point, its url is copied out while the document is still live.
struct md_atom
One layout atom: a glyph (nbytes > 0) or a hard break.

Types

Name
enum MD_PASS { MD_MEASURE = 0, MD_HIT, MD_DRAW}
What one pass is doing.
enum MD_COLOUR { MD_COL_TEXT = 0, MD_COL_QUOTE, MD_COL_LINK, MD_COL_HEADING, MD_COL_CODE}
Which colour of the style an atom takes.
enum MD_ATOM_FLAGS { MD_AT_SPACE = 1, MD_AT_NEWLINE = 2}
Atom flags.

Functions

Name
void n_gui_md_style_default(N_GUI_MD_STYLE * out, ALLEGRO_COLOR text, ALLEGRO_COLOR background, float line_height)
fill a style from two colours and a line height
int n_gui_md_link_at(const N_GUI_MD_FONTS * fonts, const N_GUI_MD_STYLE * style, float x, float y, float width, const char * text, float px, float py, char * url, size_t url_cap)
the url of the link under (px, py), if there is one
float n_gui_md_height(const N_GUI_MD_FONTS * fonts, const N_GUI_MD_STYLE * style, float width, const char * text)
height of text laid out as markdown at width, in pixels
float n_gui_md_draw(const N_GUI_MD_FONTS * fonts, const N_GUI_MD_STYLE * style, float x, float y, float width, const char * text)
draw text as markdown at (x, y), wrapped to width
void md_wrap_of(const md_atom * a, n_wrap_item * w)
Project an atom onto the generic wrap's view of it: a width, and whether it is a break point.
void md_table_cols(const N_GUI_MD_FONTS * f, const N_GUI_MD_STYLE * st, const n_markdown_block * b, float avail, float * out)
Column widths for a table, including the cell padding.
float md_table(const N_GUI_MD_FONTS * f, const N_GUI_MD_STYLE * st, const n_markdown_block * b, float x, float y, float w, int mode, md_hit * hit)
One table.
float md_run(const N_GUI_MD_FONTS * f, const N_GUI_MD_STYLE * st, float x0, float y, float w0, const char * text, int mode, md_hit * hit)
Parse and lay out the whole text.
float md_lh(const N_GUI_MD_FONTS * f, ALLEGRO_FONT * font)
A font's line height, or the regular face's when it is NULL.
ALLEGRO_FONT * md_font_heading(const N_GUI_MD_FONTS * f, int level)
The face for a heading of this level, falling back to bold then regular.
ALLEGRO_FONT * md_font_for(const N_GUI_MD_FONTS * f, unsigned style)
The face for an inline style, falling back towards regular so that a caller with one font still gets its text.
void md_flush_line(const md_line_ctx * c, size_t from, size_t to, float y, float line_w)
Draw atoms [from, to) as one line: consecutive atoms sharing a font and a colour are batched into one al_draw_text.
float md_flow_width(const N_GUI_MD_FONTS * f, const n_markdown_span * spans, size_t nspans, unsigned style_or)
The width one flow would take on a single line: the same per-codepoint widths the atoms carry, summed without allocating.
float md_flow(const N_GUI_MD_FONTS * f, const N_GUI_MD_STYLE * st, const char * prefix, const n_markdown_span * spans, size_t nspans, float x, float y, float w, float lh, ALLEGRO_FONT * base_font, unsigned style_or, unsigned char colid, unsigned char align, int hard_nl, int mode, md_hit * hit)
One inline flow: an optional prefix, then the spans.
ALLEGRO_COLOR md_fade(ALLEGRO_COLOR c, float a)
A colour at a fraction of its opacity, premultiplied.
void md_emit_line(size_t first, size_t last, size_t line, double width, void * user)
int md_cp_len(const char * s, int i, int len)
Bytes in the UTF-8 codepoint at s[i], clamped to what is left.
ALLEGRO_COLOR md_col(const N_GUI_MD_STYLE * st, unsigned char colid)
int md_build_seg(md_atom * out, n_wrap_item * wrap, int cap, int na, const char * text, int len, ALLEGRO_FONT * font, unsigned char colid, const char * url, int hard_nl)
Append atoms for one styled segment.

Defines

Name
MD_COLS_MAX
Widest table this lays out.

Detailed Description

Markdown layout and drawing.

Author: Castagnier Mickael

Version: 1.0

See n_gui_md.h. The text is parsed once per call, then each block is laid out as a flow of "atoms", one per codepoint, each carrying its own font and colour. A flow is word-wrapped by n_wrap, which never sees a glyph or a font: it is handed a width and a pair of flags per atom and hands back line ranges.

Measure, draw and hit-test are one function with a mode, so a height cannot disagree with what is painted. That is the whole reason for the shape of this file, and the reason the measure pass allocates and walks exactly as the draw does rather than taking a cheaper route to a number.

Types Documentation

enum MD_PASS

Enumerator Value Description
MD_MEASURE 0
MD_HIT
MD_DRAW

What one pass is doing.

A measure is the draw walk with the per-line callback left off, which is what keeps the two in step.

enum MD_COLOUR

Enumerator Value Description
MD_COL_TEXT 0
MD_COL_QUOTE
MD_COL_LINK
MD_COL_HEADING
MD_COL_CODE

Which colour of the style an atom takes.

enum MD_ATOM_FLAGS

Enumerator Value Description
MD_AT_SPACE 1
MD_AT_NEWLINE 2

Atom flags.

Functions Documentation

function n_gui_md_style_default

void n_gui_md_style_default(
    N_GUI_MD_STYLE * out,
    ALLEGRO_COLOR text,
    ALLEGRO_COLOR background,
    float line_height
)

fill a style from two colours and a line height

Parameters:

  • out the style to fill. Must not be NULL.
  • text body text colour
  • background what the text is drawn on, used to derive the code panel
  • line_height the regular face's line height, in pixels

The derived colours are the obvious ones: a heading and body text share a colour, a link is blue, code sits on a panel lifted or dropped from the background depending on which is darker. The metrics are fractions of line_height, so they follow the font.

int n_gui_md_link_at(
    const N_GUI_MD_FONTS * fonts,
    const N_GUI_MD_STYLE * style,
    float x,
    float y,
    float width,
    const char * text,
    float px,
    float py,
    char * url,
    size_t url_cap
)

the url of the link under (px, py), if there is one

Parameters:

  • fonts the faces. Must not be NULL, nor fonts->regular.
  • style colours and metrics. Must not be NULL.
  • x left edge the text would be drawn at
  • y top edge
  • width the width it would wrap to
  • text the markdown source. NULL is treated as empty.
  • px the point to test
  • py the point to test
  • url receives the url, NUL-terminated and bounded by url_cap
  • url_cap size of url

Return: 1 when a link was under the point and url was written, else 0

Lays the text out exactly as n_gui_md_draw() would at the same position and width, and reports the link whose glyph cell the point falls in.

function n_gui_md_height

float n_gui_md_height(
    const N_GUI_MD_FONTS * fonts,
    const N_GUI_MD_STYLE * style,
    float width,
    const char * text
)

height of text laid out as markdown at width, in pixels

Parameters:

  • fonts the faces. Must not be NULL, nor fonts->regular.
  • style colours and metrics. Must not be NULL.
  • width the width to wrap to, in pixels
  • text the markdown source. NULL is treated as empty.

Return: the height in pixels, never negative

The same walk n_gui_md_draw() makes, with nothing drawn, so the two agree to the pixel. Independent of any y: measuring at the origin gives the height a draw anywhere will use.

function n_gui_md_draw

float n_gui_md_draw(
    const N_GUI_MD_FONTS * fonts,
    const N_GUI_MD_STYLE * style,
    float x,
    float y,
    float width,
    const char * text
)

draw text as markdown at (x, y), wrapped to width

Parameters:

  • fonts the faces. Must not be NULL, nor fonts->regular.
  • style colours and metrics. Must not be NULL.
  • x left edge
  • y top edge
  • width the width to wrap to, in pixels
  • text the markdown source. NULL is treated as empty.

Return: the height used, equal to n_gui_md_height() for the same inputs

Draws into the current Allegro target with the current transform and clipping rectangle; a caller wanting the overflow clipped sets one.

function md_wrap_of

static void md_wrap_of(
    const md_atom * a,
    n_wrap_item * w
)

Project an atom onto the generic wrap's view of it: a width, and whether it is a break point.

n_wrap never sees a font, a glyph or a colour.

function md_table_cols

static void md_table_cols(
    const N_GUI_MD_FONTS * f,
    const N_GUI_MD_STYLE * st,
    const n_markdown_block * b,
    float avail,
    float * out
)

Column widths for a table, including the cell padding.

Every column asks for its widest cell on one line, and if the table then fits, that is what it gets: a two-column table of short keys does not stretch across the pane. When the asks add up to more than there is, they are scaled by the same factor with a floor of style->col_min, so a long neighbour cannot squeeze a column to nothing – and if even the floors do not fit, the width is shared equally, because at that point nothing will look right and equal is at least predictable.

Cells wrap to the width they are given; the alternative, panning the table sideways, hides the right-hand column of exactly the tables people read.

function md_table

static float md_table(
    const N_GUI_MD_FONTS * f,
    const N_GUI_MD_STYLE * st,
    const n_markdown_block * b,
    float x,
    float y,
    float w,
    int mode,
    md_hit * hit
)

One table.

Cells wrap inside their column, a row is as tall as its tallest cell, and a rule goes under the header row. Returns the height.

function md_run

static float md_run(
    const N_GUI_MD_FONTS * f,
    const N_GUI_MD_STYLE * st,
    float x0,
    float y,
    float w0,
    const char * text,
    int mode,
    md_hit * hit
)

Parse and lay out the whole text.

The one walk behind all three entry points: [mode](ex__gui__network_8c#user-content-variable-mode) says whether it draws, and used accumulates from zero so the height it returns is independent of y.

function md_lh

static float md_lh(
    const N_GUI_MD_FONTS * f,
    ALLEGRO_FONT * font
)

A font's line height, or the regular face's when it is NULL.

function md_font_heading

static ALLEGRO_FONT * md_font_heading(
    const N_GUI_MD_FONTS * f,
    int level
)

The face for a heading of this level, falling back to bold then regular.

function md_font_for

static ALLEGRO_FONT * md_font_for(
    const N_GUI_MD_FONTS * f,
    unsigned style
)

The face for an inline style, falling back towards regular so that a caller with one font still gets its text.

function md_flush_line

static void md_flush_line(
    const md_line_ctx * c,
    size_t from,
    size_t to,
    float y,
    float line_w
)

Draw atoms [from, to) as one line: consecutive atoms sharing a font and a colour are batched into one al_draw_text.

On a hit-test pass nothing is drawn and a link atom under the point records its url instead.

function md_flow_width

static float md_flow_width(
    const N_GUI_MD_FONTS * f,
    const n_markdown_span * spans,
    size_t nspans,
    unsigned style_or
)

The width one flow would take on a single line: the same per-codepoint widths the atoms carry, summed without allocating.

Used to size table columns, where a whole-string measure would disagree with the wrap.

function md_flow

static float md_flow(
    const N_GUI_MD_FONTS * f,
    const N_GUI_MD_STYLE * st,
    const char * prefix,
    const n_markdown_span * spans,
    size_t nspans,
    float x,
    float y,
    float w,
    float lh,
    ALLEGRO_FONT * base_font,
    unsigned style_or,
    unsigned char colid,
    unsigned char align,
    int hard_nl,
    int mode,
    md_hit * hit
)

One inline flow: an optional prefix, then the spans.

base_font, when given, is the face every atom takes whatever its span's style says, bar a code span which keeps the mono face: that is how a heading is drawn in the heading face rather than in a bold body face, and there is no bold-heading face to switch to inside one. NULL picks the face from each span's own style, which is what body text wants.

style_or is ORed into every span's style, which is how a table's header row is bold without the parser having to say so. colid is the colour every non-link, non-code atom takes. Returns the height.

function md_fade

static ALLEGRO_COLOR md_fade(
    ALLEGRO_COLOR c,
    float a
)

A colour at a fraction of its opacity, premultiplied.

Allegro's default blender adds the source and keeps (1 - alpha) of the destination, so the rgb has to come down with the alpha or the colour draws at very nearly full strength over a hole it has punched in what was there. Setting .a alone, which is the obvious thing to write, gives a rule that looks solid and a panel that looks white.

function md_emit_line

static void md_emit_line(
    size_t first,
    size_t last,
    size_t line,
    double width,
    void * user
)

function md_cp_len

static int md_cp_len(
    const char * s,
    int i,
    int len
)

Bytes in the UTF-8 codepoint at s[i], clamped to what is left.

function md_col

static ALLEGRO_COLOR md_col(
    const N_GUI_MD_STYLE * st,
    unsigned char colid
)

function md_build_seg

static int md_build_seg(
    md_atom * out,
    n_wrap_item * wrap,
    int cap,
    int na,
    const char * text,
    int len,
    ALLEGRO_FONT * font,
    unsigned char colid,
    const char * url,
    int hard_nl
)

Append atoms for one styled segment.

hard_nl decides what a newline in the text becomes: a line break, or a space for the wrap to break at if it needs to. Returns the new atom count.

Macros Documentation

define MD_COLS_MAX

#define MD_COLS_MAX 32

Widest table this lays out.

Past it the remaining columns are dropped, which is also n_markdown's own cap, so it cannot be reached from a parsed document; it bounds the stack arrays below.

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_gui_md.h"

#include "nilorea/n_wrap.h"

#include <allegro5/allegro_primitives.h>

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

#define MD_COLS_MAX 32

enum MD_ATOM_FLAGS { MD_AT_SPACE = 1,
                     MD_AT_NEWLINE = 2 };

typedef struct {
    ALLEGRO_FONT* font;
    const char* url;
    float w;
    unsigned char nbytes;
    unsigned char colid;
    unsigned char flags;
    char bytes[4];
} md_atom;

enum MD_COLOUR { MD_COL_TEXT = 0,
                 MD_COL_LINK,
                 MD_COL_CODE,
                 MD_COL_HEADING,
                 MD_COL_QUOTE };

typedef struct {
    float px, py;
    char* out;
    size_t cap;
    int found;
} md_hit;

enum MD_PASS { MD_MEASURE = 0,
               MD_DRAW,
               MD_HIT };

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

static int md_cp_len(const char* s, int i, int len) {
    int n = 1;
    if (i >= len)
        return 0;
    while (i + n < len && ((unsigned char)s[i + n] & 0xC0) == 0x80)
        n++;
    return n;
}

static ALLEGRO_FONT* md_font_for(const N_GUI_MD_FONTS* f, unsigned style) {
    ALLEGRO_FONT* r = f->regular;
    if (style & N_MARKDOWN_CODE)
        return f->mono ? f->mono : r;
    if ((style & N_MARKDOWN_BOLD) && (style & N_MARKDOWN_ITALIC))
        return f->bolditalic ? f->bolditalic : (f->bold ? f->bold : (f->italic ? f->italic : r));
    if (style & N_MARKDOWN_BOLD)
        return f->bold ? f->bold : r;
    if (style & N_MARKDOWN_ITALIC)
        return f->italic ? f->italic : r;
    return r;
}

static ALLEGRO_FONT* md_font_heading(const N_GUI_MD_FONTS* f, int level) {
    ALLEGRO_FONT* want = NULL;
    if (level <= 1)
        want = f->h1;
    else if (level == 2)
        want = f->h2;
    else
        want = f->h3;
    if (want)
        return want;
    return f->bold ? f->bold : f->regular;
}

static ALLEGRO_COLOR md_col(const N_GUI_MD_STYLE* st, unsigned char colid) {
    switch (colid) {
        case MD_COL_LINK:
            return st->link;
        case MD_COL_CODE:
            return st->code;
        case MD_COL_HEADING:
            return st->heading;
        case MD_COL_QUOTE:
            return st->quote;
        default:
            break;
    }
    return st->text;
}

static ALLEGRO_COLOR md_fade(ALLEGRO_COLOR c, float a) {
    c.r *= a;
    c.g *= a;
    c.b *= a;
    c.a *= a;
    return c;
}

static float md_lh(const N_GUI_MD_FONTS* f, ALLEGRO_FONT* font) {
    return (float)al_get_font_line_height(font ? font : f->regular);
}

/* ---- one flow of inline spans -------------------------------------------- */

static void md_wrap_of(const md_atom* a, n_wrap_item* w) {
    w->width = (double)a->w;
    w->flags = ((a->flags & MD_AT_SPACE) ? N_WRAP_SPACE : 0u) |
               ((a->flags & MD_AT_NEWLINE) ? N_WRAP_NEWLINE : 0u);
}

static int md_build_seg(md_atom* out, n_wrap_item* wrap, int cap, int na, const char* text, int len, ALLEGRO_FONT* font, unsigned char colid, const char* url, int hard_nl) {
    int i = 0;
    while (i < len && na < cap) {
        md_atom* a = &out[na];
        a->font = font;
        a->colid = colid;
        a->url = url;
        a->flags = 0;
        a->nbytes = 0;
        a->bytes[0] = '\0';
        if (text[i] == '\n' && !hard_nl) {
            /* A soft newline is a space: one the wrap may break at, and one
               that is drawn if it does not. */
            a->flags = MD_AT_SPACE;
            a->nbytes = 1;
            a->bytes[0] = ' ';
            a->bytes[1] = '\0';
            a->w = (float)al_get_text_width(font, " ");
            md_wrap_of(a, &wrap[na]);
            na++;
            i++;
            continue;
        }
        if (text[i] == '\n') {
            a->flags = MD_AT_NEWLINE;
            a->w = 0.0f;
            md_wrap_of(a, &wrap[na]);
            na++;
            i++;
            continue;
        }
        {
            int bl = md_cp_len(text, i, len);
            char g[8];
            if (bl > 4)
                bl = 4;
            memcpy(g, text + i, (size_t)bl);
            g[bl] = '\0';
            a->w = (float)al_get_text_width(font, g);
            a->nbytes = (unsigned char)bl;
            memcpy(a->bytes, g, (size_t)bl);
            if (text[i] == ' ' || text[i] == '\t')
                a->flags = MD_AT_SPACE;
            md_wrap_of(a, &wrap[na]);
            na++;
            i += bl;
        }
    }
    return na;
}

typedef struct {
    const md_atom* a;
    const N_GUI_MD_STYLE* st;
    float x, y, lh, avail;
    int mode;
    unsigned char align;
    md_hit* hit;
} md_line_ctx;

static void md_flush_line(const md_line_ctx* c, size_t from, size_t to, float y, float line_w) {
    char buf[1024];
    size_t k;
    int blen = 0;
    ALLEGRO_FONT* bfont = NULL;
    unsigned char bcol = 0;
    float x0 = c->x, rx, bx;

    /* Alignment is applied here rather than in the wrap, which reports the
     * line's own width precisely so that this can. */
    if (c->align == N_MARKDOWN_ALIGN_CENTER)
        x0 += (c->avail - line_w) * 0.5f;
    else if (c->align == N_MARKDOWN_ALIGN_RIGHT)
        x0 += c->avail - line_w;
    if (x0 < c->x)
        x0 = c->x;
    rx = x0;
    bx = x0;

    if (c->mode == MD_HIT) {
        for (k = from; k < to; k++) {
            const md_atom* at = &c->a[k];
            if (at->url && c->hit->px >= rx && c->hit->px < rx + at->w &&
                c->hit->py >= y && c->hit->py < y + c->lh) {
                if (c->hit->cap)
                    (void)snprintf(c->hit->out, c->hit->cap, "%s", at->url);
                c->hit->found = 1;
            }
            rx += at->w;
        }
        return;
    }
    for (k = from; k < to; k++) {
        const md_atom* at = &c->a[k];
        if (!at->nbytes)
            continue;
        if (blen && (at->font != bfont || at->colid != bcol ||
                     blen + at->nbytes >= (int)sizeof(buf))) {
            al_draw_text(bfont, md_col(c->st, bcol), bx, y, 0, buf);
            blen = 0;
        }
        if (blen == 0) {
            bfont = at->font;
            bcol = at->colid;
            bx = rx;
        }
        memcpy(buf + blen, at->bytes, at->nbytes);
        blen += at->nbytes;
        buf[blen] = '\0';
        rx += at->w;
    }
    if (blen)
        al_draw_text(bfont, md_col(c->st, bcol), bx, y, 0, buf);
}

// cppcheck-suppress constParameterCallback ; callback signature must match n_wrap_emit
static void md_emit_line(size_t first, size_t last, size_t line, double width, void* user) {
    const md_line_ctx* c = (const md_line_ctx*)user;
    md_flush_line(c, first, last, c->y + (float)line * c->lh, (float)width);
}

static float md_flow(const N_GUI_MD_FONTS* f, const N_GUI_MD_STYLE* st, const char* prefix, const n_markdown_span* spans, size_t nspans, float x, float y, float w, float lh, ALLEGRO_FONT* base_font, unsigned style_or, unsigned char colid, unsigned char align, int hard_nl, int mode, md_hit* hit) {
    size_t total = prefix ? strlen(prefix) : 0, i;
    md_atom* atoms;
    n_wrap_item* wrap;
    md_line_ctx ctx;
    int na = 0;
    size_t lines;

    for (i = 0; i < nspans; i++)
        total += spans[i].text ? strlen(spans[i].text) : 0;
    if (total == 0)
        return lh; /* an empty flow still occupies its line */
    if (w < 1.0f)
        w = 1.0f;
    /* One atom per codepoint at most, so the byte count bounds both arrays;
     * one allocation for the pair keeps the failure path single. */
    atoms = malloc(total * (sizeof(*atoms) + sizeof(*wrap)));
    if (!atoms)
        return lh;
    wrap = (n_wrap_item*)(void*)(atoms + total);
    if (prefix)
        na = md_build_seg(atoms, wrap, (int)total, na, prefix, (int)strlen(prefix),
                          base_font ? base_font : f->regular, colid, NULL, hard_nl);
    for (i = 0; i < nspans; i++) {
        unsigned sty = spans[i].style | style_or;
        int is_link = (sty & N_MARKDOWN_LINK) ? 1 : 0;
        int is_code = (sty & N_MARKDOWN_CODE) ? 1 : 0;
        unsigned char cid = colid;
        ALLEGRO_FONT* font;
        if (is_link)
            cid = MD_COL_LINK;
        else if (is_code)
            cid = MD_COL_CODE;
        if (!spans[i].text)
            continue;
        if (base_font)
            font = (is_code && f->mono) ? f->mono : base_font;
        else
            font = md_font_for(f, sty);
        na = md_build_seg(atoms, wrap, (int)total, na, spans[i].text,
                          (int)strlen(spans[i].text), font, cid,
                          is_link ? spans[i].url : NULL,
                          /* a code span keeps its own breaks whatever the
                             document asks for: it is preformatted text */
                          hard_nl || (sty & N_MARKDOWN_CODE) ? 1 : 0);
    }

    ctx.a = atoms;
    ctx.st = st;
    ctx.x = x;
    ctx.y = y;
    ctx.lh = lh;
    ctx.avail = w;
    ctx.mode = mode;
    ctx.align = align;
    ctx.hit = hit;
    lines = n_wrap_layout(wrap, (size_t)na, (double)w,
                          mode == MD_MEASURE ? NULL : md_emit_line, &ctx);
    free(atoms);
    return (float)lines * lh;
}

static float md_flow_width(const N_GUI_MD_FONTS* f, const n_markdown_span* spans, size_t nspans, unsigned style_or) {
    float sum = 0.0f;
    size_t i;
    for (i = 0; i < nspans; i++) {
        ALLEGRO_FONT* font = md_font_for(f, spans[i].style | style_or);
        const char* t = spans[i].text;
        int len = t ? (int)strlen(t) : 0;
        int j = 0;
        while (j < len) {
            int bl = md_cp_len(t, j, len);
            char g[8];
            if (bl > 4)
                bl = 4;
            memcpy(g, t + j, (size_t)bl);
            g[bl] = '\0';
            sum += (float)al_get_text_width(font, g);
            j += bl;
        }
    }
    return sum;
}

/* ---- tables -------------------------------------------------------------- */

static void md_table_cols(const N_GUI_MD_FONTS* f, const N_GUI_MD_STYLE* st, const n_markdown_block* b, float avail, float* out) {
    size_t cols = b->n_cols, rows = b->n_cols ? b->n_items / b->n_cols : 0;
    size_t c, r;
    float pad2 = st->cell_pad * 2.0f, sum = 0.0f;

    if (cols > MD_COLS_MAX)
        cols = MD_COLS_MAX;
    for (c = 0; c < cols; c++) {
        float want = 0.0f;
        for (r = 0; r < rows; r++) {
            const n_markdown_inline* cell = &b->items[r * b->n_cols + c];
            float cw = md_flow_width(f, cell->spans, cell->n_spans,
                                     r == 0 ? N_MARKDOWN_BOLD : 0u);
            if (cw > want)
                want = cw;
        }
        out[c] = want + pad2;
        sum += out[c];
    }
    if (sum <= avail || sum <= 0.0f)
        return;
    {
        float scaled = 0.0f;
        float k = avail / sum;
        for (c = 0; c < cols; c++) {
            out[c] *= k;
            if (out[c] < st->col_min)
                out[c] = st->col_min;
            scaled += out[c];
        }
        if (scaled > avail) {
            float each = avail / (float)cols;
            for (c = 0; c < cols; c++)
                out[c] = each;
        }
    }
}

static float md_table(const N_GUI_MD_FONTS* f, const N_GUI_MD_STYLE* st, const n_markdown_block* b, float x, float y, float w, int mode, md_hit* hit) {
    float widths[MD_COLS_MAX];
    size_t cols = b->n_cols, rows = b->n_cols ? b->n_items / b->n_cols : 0;
    size_t c, r;
    float used = 0.0f;
    float lh = md_lh(f, f->regular);

    if (!cols || !rows)
        return 0.0f;
    if (cols > MD_COLS_MAX)
        cols = MD_COLS_MAX;
    md_table_cols(f, st, b, w, widths);

    for (r = 0; r < rows; r++) {
        float cx = x, row_h = 0.0f;
        unsigned style_or = (r == 0) ? N_MARKDOWN_BOLD : 0u;

        /* The row's height comes from a measure pass over every cell, because
         * a cell cannot be drawn until the row's height is known and a cell's
         * own height is not known until it is laid out. */
        for (c = 0; c < cols; c++) {
            const n_markdown_inline* cell = &b->items[r * b->n_cols + c];
            float h = md_flow(f, st, NULL, cell->spans, cell->n_spans, 0.0f, 0.0f,
                              widths[c] - st->cell_pad * 2.0f, lh, NULL, style_or,
                              MD_COL_TEXT, N_MARKDOWN_ALIGN_LEFT, 0, MD_MEASURE, NULL);
            if (h > row_h)
                row_h = h;
        }
        if (mode != MD_MEASURE) {
            for (c = 0; c < cols; c++) {
                const n_markdown_inline* cell = &b->items[r * b->n_cols + c];
                unsigned char al = b->align && c < b->n_cols
                                       ? b->align[c]
                                       : (unsigned char)N_MARKDOWN_ALIGN_LEFT;
                (void)md_flow(f, st, NULL, cell->spans, cell->n_spans,
                              cx + st->cell_pad, y + used,
                              widths[c] - st->cell_pad * 2.0f, lh, NULL, style_or,
                              MD_COL_TEXT, al, 0, mode, hit);
                cx += widths[c];
            }
        }
        used += row_h;
        if (r == 0) {
            /* the rule under the header, which is what makes the rest read as
             * rows of a table rather than as three columns of loose words */
            float total = 0.0f;
            for (c = 0; c < cols; c++)
                total += widths[c];
            if (mode == MD_DRAW)
                al_draw_line(x, y + used + 1.0f, x + total, y + used + 1.0f, st->rule, 1.0f);
            used += 3.0f;
        }
    }
    return used;
}

/* ---- the document ------------------------------------------------------- */

static float md_run(const N_GUI_MD_FONTS* f, const N_GUI_MD_STYLE* st, float x0, float y, float w0, const char* text, int mode, md_hit* hit) {
    n_markdown_doc doc;
    float lh = md_lh(f, f->regular);
    float used = 0.0f;
    size_t bi, ii;
    int any = 0;

    if (!text)
        text = "";
    n_markdown_parse_ex(text, strlen(text), st->parse_flags, &doc);
    for (bi = 0; bi < doc.n_blocks; bi++) {
        const n_markdown_block* blk = &doc.blocks[bi];
        float by = y + used;
        /* a nested block (N_MARKDOWN_NESTED) sits one indent per level to the right */
        float dx = (float)blk->depth * st->indent;
        float x = x0 + (dx < w0 * 0.5f ? dx : w0 * 0.5f);
        float w = w0 - (x - x0);
        any = 1;
        switch (blk->kind) {
            case N_MARKDOWN_HEADING: {
                ALLEGRO_FONT* hf = md_font_heading(f, blk->level);
                float hlh = md_lh(f, hf);
                float h;
                /* The heading face is imposed on every atom: md_font_heading
                 * has already fallen back to bold, or to regular, so what
                 * comes out is the most heading-like face there is. */
                h = md_flow(f, st, NULL, blk->inl.spans, blk->inl.n_spans, x, by, w, hlh,
                            hf, 0u, MD_COL_HEADING, N_MARKDOWN_ALIGN_LEFT, 0, mode, hit);
                used += h;
                if (blk->level <= 2) {
                    if (mode == MD_DRAW)
                        al_draw_line(x, by + h + 1.0f, x + w, by + h + 1.0f, st->rule, 1.0f);
                    used += 3.0f;
                }
                break;
            }
            case N_MARKDOWN_TABLE:
                used += md_table(f, st, blk, x, by, w, mode, hit);
                break;
            case N_MARKDOWN_QUOTE: {
                float h = md_flow(f, st, NULL, blk->inl.spans, blk->inl.n_spans,
                                  x + st->indent, by, w - st->indent, lh, NULL, 0u,
                                  MD_COL_QUOTE, N_MARKDOWN_ALIGN_LEFT,
                                  st->hard_breaks, MD_MEASURE, NULL);
                if (mode == MD_DRAW)
                    al_draw_filled_rectangle(x, by, x + 2.0f, by + h, st->quote);
                if (mode != MD_MEASURE)
                    (void)md_flow(f, st, NULL, blk->inl.spans, blk->inl.n_spans,
                                  x + st->indent, by, w - st->indent, lh, NULL, 0u,
                                  MD_COL_QUOTE, N_MARKDOWN_ALIGN_LEFT,
                                  st->hard_breaks, mode, hit);
                used += h;
                break;
            }
            case N_MARKDOWN_CODE_BLOCK: {
                n_markdown_span cs;
                float mlh = md_lh(f, f->mono ? f->mono : f->regular);
                float h;
                cs.text = blk->code ? blk->code : (char*)"";
                cs.url = NULL;
                cs.style = N_MARKDOWN_CODE;
                h = md_flow(f, st, NULL, &cs, 1, x + st->indent, by,
                            w - st->indent * 2.0f, mlh, NULL, 0u, MD_COL_CODE,
                            N_MARKDOWN_ALIGN_LEFT, 1, MD_MEASURE, NULL);
                if (mode == MD_DRAW)
                    al_draw_filled_rectangle(x, by - st->cell_pad, x + w,
                                             by + h + st->cell_pad, st->code_bg);
                if (mode != MD_MEASURE)
                    (void)md_flow(f, st, NULL, &cs, 1, x + st->indent, by,
                                  w - st->indent * 2.0f, mlh, NULL, 0u, MD_COL_CODE,
                                  N_MARKDOWN_ALIGN_LEFT, 1, mode, hit);
                used += h + st->cell_pad * 2.0f;
                break;
            }
            case N_MARKDOWN_UL:
            case N_MARKDOWN_OL:
                for (ii = 0; ii < blk->n_items; ii++) {
                    char bullet[32];
                    if (blk->kind == N_MARKDOWN_UL)
                        (void)snprintf(bullet, sizeof(bullet), "-  ");
                    else
                        (void)snprintf(bullet, sizeof(bullet), "%ld. ", (blk->start > 0 ? blk->start : 1) + (long)ii);
                    used += md_flow(f, st, bullet, blk->items[ii].spans,
                                    blk->items[ii].n_spans, x + st->indent, y + used,
                                    w - st->indent, lh, NULL, 0u, MD_COL_TEXT,
                                    N_MARKDOWN_ALIGN_LEFT, st->hard_breaks, mode, hit);
                }
                break;
            case N_MARKDOWN_PARA:
            default:
                used += md_flow(f, st, NULL, blk->inl.spans, blk->inl.n_spans, x, by, w,
                                lh, NULL, 0u, MD_COL_TEXT, N_MARKDOWN_ALIGN_LEFT, st->hard_breaks, mode,
                                hit);
                break;
        }
        used += st->block_gap;
    }
    n_markdown_free(&doc);
    if (!any)
        return 0.0f;
    return used - st->block_gap; /* trim the gap after the last block */
}

/* ---- public ------------------------------------------------------------- */

void n_gui_md_style_default(N_GUI_MD_STYLE* out, ALLEGRO_COLOR text, ALLEGRO_COLOR background, float line_height) {
    float level;
    if (!out)
        return;
    if (line_height < 1.0f)
        line_height = 16.0f;
    out->text = text;
    out->heading = text;
    out->link = al_map_rgb(120, 170, 245);
    out->code = text;
    out->quote = text;
    out->rule = text;
    /* The code panel is lifted off a dark background and dropped into a light
     * one, so it reads as a panel either way rather than vanishing into one of
     * the two. A faint wash rather than a mixed colour, because what it sits
     * on is the caller's business. */
    level = (background.r + background.g + background.b) / 3.0f;
    out->code_bg = level > 0.5f ? al_premul_rgba_f(0.0f, 0.0f, 0.0f, 0.06f)
                                : al_premul_rgba_f(1.0f, 1.0f, 1.0f, 0.10f);
    out->rule = md_fade(out->rule, 0.35f);
    out->quote = md_fade(out->quote, 0.80f);
    out->block_gap = line_height * 0.45f;
    out->indent = line_height * 0.8f;
    out->cell_pad = line_height * 0.35f;
    out->col_min = line_height * 2.5f;
    out->parse_flags = N_MARKDOWN_ALL;
    out->hard_breaks = 0;
}

float n_gui_md_height(const N_GUI_MD_FONTS* fonts, const N_GUI_MD_STYLE* style, float width, const char* text) {
    if (!fonts || !fonts->regular || !style)
        return 0.0f;
    return md_run(fonts, style, 0.0f, 0.0f, width, text, MD_MEASURE, NULL);
}

float n_gui_md_draw(const N_GUI_MD_FONTS* fonts, const N_GUI_MD_STYLE* style, float x, float y, float width, const char* text) {
    if (!fonts || !fonts->regular || !style)
        return 0.0f;
    return md_run(fonts, style, x, y, width, text, MD_DRAW, NULL);
}

int n_gui_md_link_at(const N_GUI_MD_FONTS* fonts, const N_GUI_MD_STYLE* style, float x, float y, float width, const char* text, float px, float py, char* url, size_t url_cap) {
    md_hit hit;
    if (!fonts || !fonts->regular || !style)
        return 0;
    hit.px = px;
    hit.py = py;
    hit.out = url;
    hit.cap = url_cap;
    hit.found = 0;
    if (url && url_cap)
        url[0] = '\0';
    (void)md_run(fonts, style, x, y, width, text, MD_HIT, &hit);
    return hit.found;
}