Files
Martin Vogel 94d940ff24 fix(doclinks): never qsort an empty rst token vector
doclink_rst.c sorted d.toks unconditionally; the vector stays NULL until
the first token, and qsort's base is declared nonnull even for zero
elements. GCC UBSan on Linux amd64 reported it in rst_section_text_cut and
diagrams_rst ("null pointer passed as argument 1, which is declared to
never be null", doclink_rst.c:1767) -- the same class as the k8s pass fix.
Sort only when there are at least two tokens, as the file already does for
its section titles.

Proof: doc_links_rst on Linux amd64 (GCC, ASan+UBSan) reports the
violation without the change and none with it; doc_links_rst + diagrams on
macOS: 27 passed.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-13 18:33:40 +02:00

1791 lines
59 KiB
C

/*
* doclink_rst.c — reStructuredText documents: their sections, and the code
* their text names (the extraction half; src/pipeline/doc_links_rst.c
* resolves).
*
* Structure: the field test's line model (H3 RstDoc) with CPython's str
* semantics -- tabs expanded to 8 columns, str.strip / str.split whitespace
* and re's \s and \w from the Unicode tables the PDF scanner uses. Every line
* gets a kind: title, adornment, prose, directive head / argument / option,
* literal block (after `::` or in a code directive), comment, hyperlink
* target. Titles are found by adornment style (over+under or under only; the
* level is the style's first appearance). Each title becomes a Section
* definition: name the title, docstring the text up to the next title, lines
* from its title to the line before the next one. Text before the first title
* belongs to the file.
*
* References become tokens whose `raw` is the reference AS WRITTEN followed by
* TAB-separated fields the resolver reads (a doc_link_unresolved row shows the
* written form first; whitespace inside a field is collapsed, so no field
* holds a TAB):
* role :py:class:`x`, :func:`~a.b`, :c:type:`t`, :source:`p`
* written, role, content, module, class, C namespace
* object .. py:class:: sig, .. function:: sig (one per signature)
* written, directive, signature, module, class, C namespace,
* the Python full name in its class ("" when Sphinx's
* signature grammar does not read it)
* autodoc .. autoclass:: a.b
* written, directive, argument, module, class
* include .. include:: / .. kernel-include:: of a code file
* written, directive, path
* literalinclude .. literalinclude:: path
* written, path, :lines:, :pyobject:, "1" when a
* :start-after: / :end-before: / :start-at: / :end-at:
* bounds it by text (the resolver binds the file then)
* kernel_doc .. kernel-doc:: path, and each name of :identifiers: /
* :functions: -- written, path, name ("" for the file)
* code_path, an inline literal ``...`` that names a path or a qualified
* code_name name (the Markdown code-span classifier); raw is its text
* A role or directive without a domain is taken for Python's when this file
* sets contexts (`module`, `currentmodule`, class directives: Sphinx's
* default domain); which domain it belongs to is the resolver's call (the
* documentation set's conf.py). Roles of no code domain are tokens only when
* their content is a path (an extlink such as :source:`django/db/x.py`).
*/
#include "doclink.h"
#include "arena.h"
#include "cbm.h"
#include "helpers.h"
#include "pdf/pdf_unicode.h"
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
enum {
RST_TAB = 8,
RST_SHORT_ADORN = 4, /* an underline this long fits any title; a transition */
RST_BODY_MAX = 500, /* a section's docstring (Markdown's MAX_COMMENT_LEN) */
RST_SPAN_MAX = 150, /* an inline literal the span classifier reads (MD_SPAN_MAX) */
RST_INIT_CAP = 16,
RST_REPLACEMENT = 0xFFFD,
RST_ASCII_END = 0x80,
RST_LEAD2_MIN = 0xC2, /* UTF-8 lead bytes (no overlong two-byte form) */
RST_LEAD3_MIN = 0xE0,
RST_LEAD3_MAX = 0xEF,
RST_LEAD4_MIN = 0xF0,
RST_LEAD4_MAX = 0xF4,
RST_NEED3 = 2,
RST_NEED4 = 3,
RST_BULLET = 0x2022,
RST_SIG_ARROW = 2, /* "->" */
};
#define RST_SEP '\t'
typedef enum {
K_BLANK = 0,
K_TITLE,
K_ADORN,
K_DHEAD,
K_DHEAD_PROSE,
K_DARG,
K_DARG_PROSE,
K_DOPT,
K_LITERAL,
K_PLIT,
K_SKIP,
K_TARGET,
K_COMMENT,
K_PROSE,
} rst_kind_t;
typedef struct {
char *s; /* the line, tabs expanded, NUL-terminated */
int len; /* bytes */
int indent; /* leading spaces (H3 indent_of) */
bool blank; /* nothing but whitespace */
} rst_line_t;
typedef struct {
const char *key;
const char *val;
} rst_opt_t;
typedef struct {
int line;
int indent;
const char *name; /* as written, lower-cased, with its domain */
const char *dom; /* "py", "c", "std" or the written domain */
const char *dname; /* without the domain */
const char **args;
int nargs;
rst_opt_t *opts;
int nopts;
int cs; /* content lines [cs, ce) */
int ce;
} rst_dir_t;
typedef struct {
int first; /* the title block's first line (an overline, or the title) */
int idx; /* the title text's line */
int next; /* the line after the title block */
const char *text;
const char *qn;
bool shared_qn; /* another title of the file has the same QN: the file is the source */
} rst_title_t;
typedef struct {
int line; /* 0-based */
int col; /* byte offset in its line; directives use their indent */
int syntax;
const char *raw;
} rst_tok_t;
typedef struct {
CBMExtractCtx *ctx;
CBMArena *scratch;
rst_line_t *L;
int n;
unsigned char *K;
rst_dir_t *dirs;
int ndirs;
int cap_dirs;
rst_title_t *titles;
int ntitles;
int cap_titles;
char styles[2 * RST_INIT_CAP * RST_INIT_CAP]; /* (char, overline) pairs in first-seen order */
int nstyles;
const char **mod_at;
const char **cls_at;
const char **ns_at;
rst_tok_t *toks;
int ntoks;
int cap_toks;
char *span_buf;
bool failed;
} rst_doc_t;
/* ── Unicode, as CPython's str and re see it ─────────────────────── */
/* The code point at s[i] and its length; a malformed byte is U+FFFD of one
* byte (decode(errors='replace')). */
static uint32_t rst_cp(const char *s, int len, int i, int *n) {
unsigned char c = (unsigned char)s[i];
if (c < RST_ASCII_END) {
*n = 1;
return c;
}
int need = 0; /* continuation bytes of a well-formed lead byte */
if (c >= RST_LEAD4_MIN && c <= RST_LEAD4_MAX) {
need = RST_NEED4;
} else if (c >= RST_LEAD3_MIN && c <= RST_LEAD3_MAX) {
need = RST_NEED3;
} else if (c >= RST_LEAD2_MIN && c < RST_LEAD3_MIN) {
need = 1;
}
if (need == 0 || i + need >= len) {
*n = 1;
return RST_REPLACEMENT;
}
uint32_t cp = c & (0x3F >> need);
for (int k = 1; k <= need; k++) {
unsigned char cc = (unsigned char)s[i + k];
if ((cc & 0xC0) != 0x80) {
*n = 1;
return RST_REPLACEMENT;
}
cp = (cp << 6) | (cc & 0x3F);
}
static const uint32_t min_cp[] = {0, 0x80, 0x800, 0x10000};
if (cp < min_cp[need] || cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) {
*n = 1;
return RST_REPLACEMENT;
}
*n = need + 1;
return cp;
}
static bool rst_space_cp(uint32_t cp) {
if (cp < RST_ASCII_END) {
return cp == ' ' || (cp >= '\t' && cp <= '\r') || (cp >= 0x1C && cp <= 0x1F);
}
return pdf_u_in(PDF_U_SPACE, PDF_U_SPACE_COUNT, cp);
}
static bool rst_word_cp(uint32_t cp) {
if (cp < RST_ASCII_END) {
return (cp >= 'a' && cp <= 'z') || (cp >= 'A' && cp <= 'Z') || (cp >= '0' && cp <= '9') ||
cp == '_';
}
return pdf_u_in(PDF_U_WORD, PDF_U_WORD_COUNT, cp);
}
static bool rst_alpha_ascii(unsigned char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
}
static bool rst_digit(unsigned char c) {
return c >= '0' && c <= '9';
}
/* Whitespace code points from s[i]: the index after them. */
static int rst_skip_ws(const char *s, int len, int i) {
while (i < len) {
int n;
if (!rst_space_cp(rst_cp(s, len, i, &n))) {
break;
}
i += n;
}
return i;
}
/* str.strip(): [*a, *b) without leading and trailing whitespace. */
static void rst_strip(const char *s, int len, int *a, int *b) {
int i = rst_skip_ws(s, len, 0);
int end = i;
int k = i;
while (k < len) {
int n;
uint32_t cp = rst_cp(s, len, k, &n);
k += n;
if (!rst_space_cp(cp)) {
end = k;
}
}
*a = i;
*b = end;
}
/* Code points in s[a, b). */
static int rst_cps(const char *s, int a, int b) {
int count = 0;
while (a < b) {
int n;
(void)rst_cp(s, b, a, &n);
a += n;
count++;
}
return count;
}
/* ' '.join(s.split()), into the arena. */
static char *rst_collapse(CBMArena *a, const char *s, int len) {
char *out = (char *)cbm_arena_alloc(a, (size_t)len + 1);
if (!out) {
return NULL;
}
int w = 0;
int i = 0;
while (i < len) {
int n;
uint32_t cp = rst_cp(s, len, i, &n);
if (rst_space_cp(cp)) {
i += n;
continue;
}
if (w > 0) {
out[w++] = ' ';
}
while (i < len) {
cp = rst_cp(s, len, i, &n);
if (rst_space_cp(cp)) {
break;
}
memcpy(out + w, s + i, (size_t)n);
w += n;
i += n;
}
}
out[w] = '\0';
return out;
}
/* ── Growing arrays in the scratch arena ─────────────────────────── */
static void *rst_grow(rst_doc_t *d, void *items, int count, int *cap, size_t size) {
if (count < *cap) {
return items;
}
int ncap = *cap ? *cap * 2 : RST_INIT_CAP;
void *neu = cbm_arena_alloc(d->scratch, (size_t)ncap * size);
if (!neu) {
d->failed = true;
return NULL;
}
if (items && count) {
memcpy(neu, items, (size_t)count * size);
}
*cap = ncap;
return neu;
}
/* ── Lines ───────────────────────────────────────────────────────── */
/* text.expandtabs(8).split('\n'): the column restarts after '\r' and '\n'. */
static bool rst_lines(rst_doc_t *d, const char *src, int n) {
int count = 1;
for (int i = 0; i < n; i++) {
count += src[i] == '\n';
}
d->L = (rst_line_t *)cbm_arena_alloc(d->scratch, (size_t)count * sizeof(*d->L));
d->K = (unsigned char *)cbm_arena_alloc(d->scratch, (size_t)count);
if (!d->L || !d->K) {
return false;
}
memset(d->K, 0, (size_t)count);
int pos = 0;
for (int li = 0; li < count; li++) {
const char *nl = memchr(src + pos, '\n', (size_t)(n - pos));
int end = nl ? (int)(nl - src) : n;
int tabs = 0;
for (int i = pos; i < end; i++) {
tabs += src[i] == '\t';
}
int cap = (end - pos) + tabs * RST_TAB + 1;
char *out = (char *)cbm_arena_alloc(d->scratch, (size_t)cap);
if (!out) {
return false;
}
int w = 0;
int col = 0;
int i = pos;
while (i < end) {
int cn;
uint32_t cp = rst_cp(src, end, i, &cn);
if (cp == '\t') {
int fill = RST_TAB - (col % RST_TAB);
memset(out + w, ' ', (size_t)fill);
w += fill;
col += fill;
} else {
memcpy(out + w, src + i, (size_t)cn);
w += cn;
col = cp == '\r' ? 0 : col + 1;
}
i += cn;
}
out[w] = '\0';
rst_line_t *ln = &d->L[li];
ln->s = out;
ln->len = w;
int sp = 0;
while (sp < w && out[sp] == ' ') {
sp++;
}
ln->indent = sp;
int a;
int b;
rst_strip(out, w, &a, &b);
ln->blank = a >= b;
pos = end + 1;
}
d->n = count;
return true;
}
/* ── Line patterns (H3's regexes) ────────────────────────────────── */
static bool rst_punct(unsigned char c) {
return (c >= '!' && c <= '/') || (c >= ':' && c <= '@') || (c >= '[' && c <= '`') ||
(c >= '{' && c <= '~');
}
/* ADORN_RE ^([!-/:-@\[-`{-~])\1*\s*$ */
static bool rst_adorn(const char *s, int len) {
if (len == 0 || !rst_punct((unsigned char)s[0])) {
return false;
}
int i = 1;
while (i < len && s[i] == s[0]) {
i++;
}
return rst_skip_ws(s, len, i) == len;
}
/* s.rstrip() length. */
static int rst_rlen(const char *s, int len) {
int a;
int b;
rst_strip(s, len, &a, &b);
return a >= b ? 0 : b;
}
static bool rst_style_index(rst_doc_t *d, char ch, bool over, int *level) {
for (int i = 0; i < d->nstyles; i++) {
if (d->styles[2 * i] == ch && d->styles[(2 * i) + 1] == (char)over) {
*level = i;
return true;
}
}
if ((size_t)(2 * (d->nstyles + 1)) > sizeof(d->styles)) {
return false;
}
d->styles[2 * d->nstyles] = ch;
d->styles[(2 * d->nstyles) + 1] = (char)over;
*level = d->nstyles++;
return true;
}
/* H3 _title_at: the title's line, its style and the line after the block. */
static bool rst_title_at(const rst_doc_t *d, int i, int *tidx, char *ch, bool *over, int *next) {
const rst_line_t *L = d->L;
int n = d->n;
int sl = rst_rlen(L[i].s, L[i].len);
int sa;
int sb;
rst_strip(L[i].s, sl, &sa, &sb);
if (rst_adorn(L[i].s, sl)) {
if (sb - sa == 2 && L[i].s[sa] == ':' && L[i].s[sa + 1] == ':') {
return false;
}
if (i + 2 < n) {
const rst_line_t *t = &L[i + 1];
int ul = rst_rlen(L[i + 2].s, L[i + 2].len);
const char *u = L[i + 2].s;
if (!t->blank && !rst_adorn(t->s, t->len) && rst_adorn(u, ul) && u[0] == L[i].s[sa]) {
*tidx = i + 1;
*ch = L[i].s[sa];
*over = true;
*next = i + 3;
return true;
}
}
return false;
}
if (i + 1 < n && !(sl >= 2 && L[i].s[0] == '.' && L[i].s[1] == '.')) {
const char *u = L[i + 1].s;
int ul = rst_rlen(u, L[i + 1].len);
if (ul > 0 && !rst_space_cp((unsigned char)u[0]) && rst_adorn(u, ul)) {
int ua;
int ub;
rst_strip(u, ul, &ua, &ub);
bool dcolon = ub - ua == 2 && u[ua] == ':' && u[ua + 1] == ':';
int ulen = ub - ua; /* ASCII punctuation: bytes are code points */
if (!dcolon && (ulen >= rst_cps(L[i].s, sa, sb) || ulen >= RST_SHORT_ADORN)) {
*tidx = i;
*ch = u[ua];
*over = false;
*next = i + 2;
return true;
}
}
}
return false;
}
static void rst_mark(rst_doc_t *d, int a, int b, rst_kind_t kind) {
for (int k = a; k < b; k++) {
d->K[k] = (unsigned char)(d->L[k].blank ? K_BLANK : kind);
}
}
static int rst_block_end(const rst_doc_t *d, int j, int ind) {
int k = j;
while (k < d->n && (d->L[k].blank || d->L[k].indent > ind)) {
k++;
}
return k;
}
static bool rst_name_char(const char *s, int len, int i, int *n) {
uint32_t cp = rst_cp(s, len, i, n);
return cp == ':' || cp == '.' || cp == '+' || cp == '-' || rst_word_cp(cp);
}
/* DIRECTIVE_RE: ^(\s*)\.\.\s+(?:\|[^|]+\|\s+)?([A-Za-z0-9][\w:.+-]*?)::(?:\s+(.*?))?\s*$
* Fills the indent (code points), the name span and the first argument
* span (stripped). */
static bool rst_directive_match(const char *s, int len, int *ind, int *na, int *nb, int *aa,
int *ab) {
int p = rst_skip_ws(s, len, 0);
*ind = rst_cps(s, 0, p);
if (p + 2 > len || s[p] != '.' || s[p + 1] != '.') {
return false;
}
int q = rst_skip_ws(s, len, p + 2);
if (q == p + 2) {
return false;
}
p = q;
if (p < len && s[p] == '|') {
const char *bar = memchr(s + p + 1, '|', (size_t)(len - p - 1));
if (bar && bar > s + p + 1) {
int after = (int)(bar - s) + 1;
int w = rst_skip_ws(s, len, after);
if (w > after) {
p = w;
}
}
}
if (p >= len || !(rst_alpha_ascii((unsigned char)s[p]) || rst_digit((unsigned char)s[p]))) {
return false;
}
int e = p + 1;
for (;;) {
if (e + 2 <= len && s[e] == ':' && s[e + 1] == ':') {
int t = e + 2;
int w = rst_skip_ws(s, len, t);
if (t == len || w > t) {
*na = p;
*nb = e;
int a;
int b;
rst_strip(s + w, len - w, &a, &b);
*aa = w + a;
*ab = w + (b > a ? b : a);
return true;
}
}
int cn;
if (e >= len || !rst_name_char(s, len, e, &cn)) {
return false;
}
e += cn;
}
}
/* EXPLICIT_RE ^(\s*)\.\.(?:\s|$) */
static bool rst_explicit_match(const char *s, int len) {
int p = rst_skip_ws(s, len, 0);
if (p + 2 > len || s[p] != '.' || s[p + 1] != '.') {
return false;
}
if (p + 2 == len) {
return true;
}
int n;
return rst_space_cp(rst_cp(s, len, p + 2, &n));
}
/* OPTION_LINE_RE ^(\s*):([A-Za-z][\w-]*):(?:\s+(.*?))?\s*$ */
static bool rst_option_match(const char *s, int len, int *ka, int *kb, int *va, int *vb) {
int p = rst_skip_ws(s, len, 0);
if (p >= len || s[p] != ':' || p + 1 >= len || !rst_alpha_ascii((unsigned char)s[p + 1])) {
return false;
}
int k = p + 2;
while (k < len) {
int n;
uint32_t cp = rst_cp(s, len, k, &n);
if (cp != '-' && !rst_word_cp(cp)) {
break;
}
k += n;
}
if (k >= len || s[k] != ':') {
return false;
}
int t = k + 1;
int w = rst_skip_ws(s, len, t);
if (t != len && w == t) {
return false;
}
*ka = p + 1;
*kb = k;
int a;
int b;
rst_strip(s + w, len - w, &a, &b);
*va = w + a;
*vb = w + (b > a ? b : a);
return true;
}
static bool rst_roman(unsigned char c) {
return strchr("ivxlcdmIVXLCDM", c) != NULL && c != '\0';
}
/* LIST_MARKER_RE: the match length in code points, or -1. */
static int rst_list_marker(const char *s, int len) {
int p = rst_skip_ws(s, len, 0);
if (p >= len) {
return -1;
}
int n;
uint32_t cp = rst_cp(s, len, p, &n);
int q = -1;
if (cp == '-' || cp == '*' || cp == '+' || cp == RST_BULLET) {
q = p + n;
} else {
int r = p + (s[p] == '(');
int e = -1;
if (r < len && rst_digit((unsigned char)s[r])) {
e = r;
while (e < len && rst_digit((unsigned char)s[e])) {
e++;
}
if (!(e < len && (s[e] == '.' || s[e] == ')'))) {
e = -1;
}
}
if (e < 0 && r + 1 < len && rst_alpha_ascii((unsigned char)s[r]) &&
(s[r + 1] == '.' || s[r + 1] == ')')) {
e = r + 1;
}
if (e < 0 && r + 1 < len && s[r] == '#' && (s[r + 1] == '.' || s[r + 1] == ')')) {
e = r + 1;
}
if (e < 0 && r < len && rst_roman((unsigned char)s[r])) {
e = r;
while (e < len && rst_roman((unsigned char)s[e])) {
e++;
}
if (!(e < len && (s[e] == '.' || s[e] == ')'))) {
e = -1;
}
}
if (e >= 0) {
q = e + 1;
}
}
if (q < 0) {
return -1;
}
int w = rst_skip_ws(s, len, q);
if (w == q || w >= len) {
return -1;
}
return rst_cps(s, 0, w);
}
/* ── Block structure (H3 _scan) ──────────────────────────────────── */
static const char *const PY_DOMAIN_OBJ[] = {
"class", "exception", "method", "classmethod", "staticmethod",
"attribute", "property", "function", "decorator", "decoratormethod",
"data", "module", "currentmodule", NULL};
static const char *const LITERAL_CONTENT[] = {
"code-block", "code", "sourcecode", "literalinclude", "raw", "math",
"graphviz", "digraph", "graph", "productionlist", "console", "doctest",
"testcode", "testoutput", "testsetup", "testcleanup", "highlight", "kernel-render",
"prompt", "kernel-include", "include", "kernel-abi", "kernel-feat", "uml",
"ipython", "mermaid", NULL};
static const char *const SKIP_CONTENT[] = {"toctree", "index", "tabularcolumns", NULL};
static const char *const ARG_PROSE[] = {"note", "warning", "tip",
"hint", "important", "caution",
"attention", "danger", "error",
"seealso", "admonition", "versionadded",
"versionchanged", "deprecated", "versionremoved",
"rubric", "topic", "sidebar",
"replace", "centered", NULL};
static const char *const PY_OBJ[] = {
"class", "exception", "method", "classmethod", "staticmethod",
"attribute", "property", "function", "decorator", "decoratormethod",
"data", "module", NULL};
static const char *const C_OBJ[] = {"function", "macro", "type", "struct", "union",
"enum", "enumerator", "member", "var", NULL};
static const char *const AUTODOC[] = {
"automodule", "autoclass", "autofunction", "automethod", "autoattribute",
"autoexception", "autodata", "autodecorator", "autoproperty", NULL};
static const char *const PY_ROLES[] = {"func", "class", "meth", "attr", "mod", "exc",
"data", "obj", "const", "deco", NULL};
static const char *const C_ROLES[] = {"func", "macro", "struct", "union", "enum", "enumerator",
"type", "member", "data", "var", NULL};
/* include / kernel-include of these is code (H3 CODE_EXT) */
static const char *const CODE_EXT[] = {".c", ".h", ".s", ".py", ".sh", ".rs", ".pl",
".awk", ".cpp", ".cc", ".hpp", ".go", ".js", ".ts",
".lds", ".dts", ".dtsi", ".yaml", ".yml", ".json", ".toml",
".mk", ".cmake", ".bpf", ".tc", NULL};
static bool rst_in(const char *s, const char *const *list) {
for (int i = 0; s && list[i]; i++) {
if (strcmp(s, list[i]) == 0) {
return true;
}
}
return false;
}
/* H3 split_domain with Python as the primary domain. */
static void rst_split_domain(rst_doc_t *d, const char *name, const char *const *py_members,
const char **dom, const char **rest) {
const char *colon = strchr(name, ':');
if (colon) {
*dom = cbm_arena_strndup(d->scratch, name, (size_t)(colon - name));
*rest = colon + 1;
if (!*dom) {
d->failed = true;
*dom = "";
}
return;
}
*dom = rst_in(name, py_members) ? "py" : "std";
*rest = name;
}
static char *rst_dup_stripped(rst_doc_t *d, const char *s, int len) {
int a;
int b;
rst_strip(s, len, &a, &b);
char *out = cbm_arena_strndup(d->scratch, s + a, (size_t)(b > a ? b - a : 0));
if (!out) {
d->failed = true;
}
return out;
}
static int rst_directive(rst_doc_t *d, int i, int ind, int na, int nb, int aa, int ab) {
rst_line_t *L = d->L;
int n = d->n;
char *name = cbm_arena_strndup(d->scratch, L[i].s + na, (size_t)(nb - na));
if (!name) {
d->failed = true;
return n;
}
for (char *c = name; *c; c++) {
if (*c >= 'A' && *c <= 'Z') {
*c = (char)(*c + ('a' - 'A'));
}
}
rst_dir_t dir = {.line = i, .indent = ind, .name = name};
rst_split_domain(d, name, PY_DOMAIN_OBJ, &dir.dom, &dir.dname);
bool prose_arg = rst_in(dir.dname, ARG_PROSE);
d->K[i] = prose_arg ? K_DHEAD_PROSE : K_DHEAD;
int cap_args = 0;
if (ab > aa) {
dir.args = (const char **)rst_grow(d, NULL, 0, &cap_args, sizeof(char *));
if (!dir.args) {
return n;
}
dir.args[dir.nargs] = rst_dup_stripped(d, L[i].s + aa, ab - aa);
dir.nargs++;
}
int j = i + 1;
int ka;
int kb;
int va;
int vb;
while (j < n && !L[j].blank && L[j].indent > ind &&
!rst_option_match(L[j].s, L[j].len, &ka, &kb, &va, &vb)) {
dir.args = (const char **)rst_grow(d, dir.args, dir.nargs, &cap_args, sizeof(char *));
if (!dir.args) {
return n;
}
dir.args[dir.nargs++] = rst_dup_stripped(d, L[j].s, L[j].len);
d->K[j] = prose_arg ? K_DARG_PROSE : K_DARG;
j++;
}
int cap_opts = 0;
while (j < n && !L[j].blank && L[j].indent > ind &&
rst_option_match(L[j].s, L[j].len, &ka, &kb, &va, &vb)) {
char *key = cbm_arena_strndup(d->scratch, L[j].s + ka, (size_t)(kb - ka));
char *val = cbm_arena_strndup(d->scratch, L[j].s + va, (size_t)(vb - va));
int oind = L[j].indent;
d->K[j] = K_DOPT;
j++;
while (j < n && !L[j].blank && L[j].indent > oind &&
!rst_option_match(L[j].s, L[j].len, &ka, &kb, &va, &vb)) {
char *more = rst_dup_stripped(d, L[j].s, L[j].len);
if (val && more) {
val = cbm_arena_sprintf(d->scratch, "%s%s%s", val, val[0] && more[0] ? " " : "",
more);
}
d->K[j] = K_DOPT;
j++;
}
dir.opts = (rst_opt_t *)rst_grow(d, dir.opts, dir.nopts, &cap_opts, sizeof(rst_opt_t));
if (!key || !val || !dir.opts) {
d->failed = true;
return n;
}
/* options[key] = val: a repeated key keeps its first place, the last value */
bool found = false;
for (int k = 0; k < dir.nopts; k++) {
if (strcmp(dir.opts[k].key, key) == 0) {
dir.opts[k].val = val;
found = true;
}
}
if (!found) {
dir.opts[dir.nopts++] = (rst_opt_t){key, val};
}
}
dir.cs = j;
dir.ce = rst_block_end(d, j, ind);
d->dirs = (rst_dir_t *)rst_grow(d, d->dirs, d->ndirs, &d->cap_dirs, sizeof(rst_dir_t));
if (!d->dirs) {
return n;
}
d->dirs[d->ndirs++] = dir;
if (rst_in(dir.dname, LITERAL_CONTENT)) {
rst_mark(d, dir.cs, dir.ce, K_LITERAL);
return dir.ce;
}
if (strcmp(dir.dname, "parsed-literal") == 0) {
rst_mark(d, dir.cs, dir.ce, K_PLIT);
return dir.ce;
}
if (rst_in(dir.dname, SKIP_CONTENT)) {
rst_mark(d, dir.cs, dir.ce, K_SKIP);
return dir.ce;
}
return dir.cs;
}
static int rst_explicit(rst_doc_t *d, int i, int ind) {
rst_line_t *L = d->L;
int a;
int b;
rst_strip(L[i].s, L[i].len, &a, &b);
const char *s = L[i].s + a;
int sl = b - a;
if ((sl >= 4 && strncmp(s, ".. _", 4) == 0) || (sl >= 3 && strncmp(s, "__ ", 3) == 0)) {
d->K[i] = K_TARGET;
int j = i + 1;
while (j < d->n && !L[j].blank && L[j].indent > ind) {
d->K[j] = K_TARGET;
j++;
}
return j;
}
/* \.\.\s+\[[^\]]+\] : a footnote or citation */
int p = rst_skip_ws(s, sl, 2);
if (p > 2 && p < sl && s[p] == '[') {
const char *close = memchr(s + p + 1, ']', (size_t)(sl - p - 1));
if (close && close > s + p + 1) {
d->K[i] = K_PROSE;
return i + 1;
}
}
if (sl == 2 && (i + 1 >= d->n || L[i + 1].blank)) {
d->K[i] = K_COMMENT;
return i + 1;
}
int j = rst_block_end(d, i + 1, ind);
rst_mark(d, i, j, K_COMMENT);
return j;
}
static void rst_scan(rst_doc_t *d) {
rst_line_t *L = d->L;
int n = d->n;
int i = 0;
int para_indent = 0;
int literal_pending = -1;
while (i < n && !d->failed) {
if (L[i].blank) {
d->K[i] = K_BLANK;
i++;
continue;
}
bool prev_blank = i == 0 || L[i - 1].blank;
int ind = L[i].indent;
if (literal_pending >= 0) {
int lp = literal_pending;
literal_pending = -1;
if (prev_blank && ind > lp) {
int j = rst_block_end(d, i, lp);
rst_mark(d, i, j, K_LITERAL);
i = j;
continue;
}
}
if (ind == 0 && prev_blank) {
int tidx;
char ch;
bool over;
int next;
if (rst_title_at(d, i, &tidx, &ch, &over, &next)) {
int level;
d->titles = (rst_title_t *)rst_grow(d, d->titles, d->ntitles, &d->cap_titles,
sizeof(rst_title_t));
char *text = rst_collapse(d->scratch, L[tidx].s, L[tidx].len);
if (!d->titles || !text || !rst_style_index(d, ch, over, &level)) {
d->failed = true;
return;
}
(void)level;
d->titles[d->ntitles++] =
(rst_title_t){.first = i, .idx = tidx, .next = next, .text = text};
rst_mark(d, i, next, K_ADORN);
d->K[tidx] = K_TITLE;
i = next;
continue;
}
int sa;
int sb;
rst_strip(L[i].s, L[i].len, &sa, &sb);
if (rst_adorn(L[i].s, L[i].len) && rst_cps(L[i].s, sa, sb) >= RST_SHORT_ADORN &&
(i + 1 >= n || L[i + 1].blank)) {
d->K[i] = K_ADORN;
i++;
continue;
}
}
int dind;
int na;
int nb;
int aa;
int ab;
if (rst_directive_match(L[i].s, L[i].len, &dind, &na, &nb, &aa, &ab)) {
i = rst_directive(d, i, dind, na, nb, aa, ab);
continue;
}
if (rst_explicit_match(L[i].s, L[i].len)) {
i = rst_explicit(d, i, ind);
continue;
}
d->K[i] = K_PROSE;
if (prev_blank) {
int mm = rst_list_marker(L[i].s, L[i].len);
para_indent = mm >= 0 ? mm : ind;
}
int sa;
int sb;
rst_strip(L[i].s, L[i].len, &sa, &sb);
if (sb - sa >= 2 && L[i].s[sb - 1] == ':' && L[i].s[sb - 2] == ':') {
literal_pending = para_indent;
}
i++;
}
}
/* ── Python signatures (Sphinx py_sig_re, H3 join_sig_lines / py_fullname) ── */
/* One signature per argument line; a trailing backslash continues it. */
static int rst_join_sigs(rst_doc_t *d, const rst_dir_t *dir, const char ***out) {
const char **sigs =
(const char **)cbm_arena_alloc(d->scratch, (size_t)(dir->nargs + 1) * sizeof(char *));
if (!sigs) {
d->failed = true;
return 0;
}
int ns = 0;
const char *cur = "";
bool open = false;
for (int k = 0; k < dir->nargs; k++) {
const char *a = dir->args[k];
size_t al = strlen(a);
if (al > 0 && a[al - 1] == '\\') {
cur = cbm_arena_sprintf(d->scratch, "%s%.*s ", cur, (int)(al - 1), a);
open = true;
} else {
sigs[ns++] = cbm_arena_sprintf(d->scratch, "%s%s", cur, a);
cur = "";
open = false;
}
if (!cur || (ns > 0 && !sigs[ns - 1])) {
d->failed = true;
return 0;
}
}
if (open) {
sigs[ns++] = cur;
}
*out = sigs;
return ns;
}
/* After the name: \s* (\[ .* \])? \s* ( \( .* \) (\s* -> .*)? )? $ */
static bool rst_sig_paren_ok(const char *s, int len, int i) {
if (i == len) {
return true;
}
if (s[i] != '(') {
return false;
}
for (int q = len - 1; q > i; q--) {
if (s[q] != ')') {
continue;
}
if (q + 1 == len) {
return true;
}
int w = rst_skip_ws(s, len, q + 1);
if (w + RST_SIG_ARROW <= len && s[w] == '-' && s[w + 1] == '>') {
return true;
}
}
return false;
}
static bool rst_sig_rest_ok(const char *s, int len, int i) {
i = rst_skip_ws(s, len, i);
if (i == len) {
return true;
}
if (s[i] == '[') {
for (int q = len - 1; q > i; q--) {
if (s[q] == ']' && rst_sig_paren_ok(s, len, rst_skip_ws(s, len, q + 1))) {
return true;
}
}
return false;
}
return rst_sig_paren_ok(s, len, i);
}
/* PY_SIG_RE on sig.strip(): the prefix ("a.b." or "") and the name. */
static bool rst_py_sig(rst_doc_t *d, const char *sig, const char **prefix, const char **name) {
int a;
int b;
rst_strip(sig, (int)strlen(sig), &a, &b);
const char *s = sig + a;
int len = b > a ? b - a : 0;
int r = 0;
int last_dot = -1;
while (r < len) {
int n;
uint32_t cp = rst_cp(s, len, r, &n);
if (cp == '.') {
last_dot = r;
} else if (!rst_word_cp(cp)) {
break;
}
r += n;
}
int ns = last_dot + 1;
if (ns >= r || !rst_sig_rest_ok(s, len, r)) {
return false;
}
*prefix = cbm_arena_strndup(d->scratch, s, (size_t)ns);
*name = cbm_arena_strndup(d->scratch, s + ns, (size_t)(r - ns));
if (!*prefix || !*name) {
d->failed = true;
return false;
}
return true;
}
/* Sphinx PyObject.handle_signature's fullname, and the remaining prefix. */
static void rst_py_fullname(rst_doc_t *d, const char *prefix, const char *name, const char *cls,
const char **full, const char **rem) {
if (cls && cls[0]) {
size_t cl = strlen(cls);
if (prefix[0] &&
(strcmp(prefix, cls) == 0 || (strncmp(prefix, cls, cl) == 0 && prefix[cl] == '.'))) {
const char *r = prefix + cl;
while (*r == '.') {
r++;
}
*full = cbm_arena_sprintf(d->scratch, "%s%s", prefix, name);
*rem = r;
} else if (prefix[0]) {
*full = cbm_arena_sprintf(d->scratch, "%s.%s%s", cls, prefix, name);
*rem = prefix;
} else {
*full = cbm_arena_sprintf(d->scratch, "%s.%s", cls, name);
*rem = "";
}
} else {
*full = cbm_arena_sprintf(d->scratch, "%s%s", prefix, name);
*rem = prefix;
}
if (!*full) {
d->failed = true;
*full = "";
}
}
static const char *rst_opt(const rst_dir_t *dir, const char *key) {
for (int k = 0; k < dir->nopts; k++) {
if (strcmp(dir->opts[k].key, key) == 0) {
return dir->opts[k].val;
}
}
return NULL;
}
/* H3 _contexts: the module, C namespace and class in force on every line. */
static void rst_contexts(rst_doc_t *d) {
int n = d->n;
d->mod_at = (const char **)cbm_arena_alloc(d->scratch, (size_t)n * sizeof(char *));
d->ns_at = (const char **)cbm_arena_alloc(d->scratch, (size_t)n * sizeof(char *));
d->cls_at = (const char **)cbm_arena_alloc(d->scratch, (size_t)n * sizeof(char *));
if (!d->mod_at || !d->ns_at || !d->cls_at) {
d->failed = true;
return;
}
memset(d->cls_at, 0, (size_t)n * sizeof(char *));
const char *cm = NULL;
const char *cn = NULL;
int k = 0;
/* directives are in line order (the scan appends them so) */
for (int di = 0; di <= d->ndirs; di++) {
int upto = di < d->ndirs ? d->dirs[di].line : n;
for (; k < upto; k++) {
d->mod_at[k] = cm;
d->ns_at[k] = cn;
}
if (di == d->ndirs) {
break;
}
const rst_dir_t *dir = &d->dirs[di];
const char *v = dir->nargs ? dir->args[0] : NULL;
if (strcmp(dir->dom, "py") == 0 &&
(strcmp(dir->dname, "module") == 0 || strcmp(dir->dname, "currentmodule") == 0)) {
cm = (!v || !v[0] || strcmp(v, "None") == 0) ? NULL : v;
}
if (strcmp(dir->dom, "c") == 0 &&
(strcmp(dir->dname, "namespace") == 0 || strcmp(dir->dname, "namespace-push") == 0)) {
cn = (!v || !v[0] || strcmp(v, "NULL") == 0) ? NULL : v;
}
if (strcmp(dir->dom, "c") == 0 && strcmp(dir->dname, "namespace-pop") == 0) {
cn = NULL;
}
}
for (int di = 0; di < d->ndirs && !d->failed; di++) {
const rst_dir_t *dir = &d->dirs[di];
if (strcmp(dir->dom, "py") != 0 || !rst_in(dir->dname, PY_OBJ) ||
strcmp(dir->dname, "module") == 0 || dir->cs >= dir->ce) {
continue;
}
const char *outer = d->cls_at[dir->line];
const char **sigs = NULL;
int ns = rst_join_sigs(d, dir, &sigs);
const char *content_cls = outer;
const char *prefix;
const char *name;
if (ns > 0 && rst_py_sig(d, sigs[ns - 1], &prefix, &name)) {
const char *full;
const char *rem;
rst_py_fullname(d, prefix, name, outer, &full, &rem);
if (strcmp(dir->dname, "class") == 0 || strcmp(dir->dname, "exception") == 0) {
content_cls = full;
} else if (rem[0]) {
size_t rl = strlen(rem);
const char *r = rem;
while (*r == '.') {
r++;
rl--;
}
while (rl > 0 && r[rl - 1] == '.') {
rl--;
}
content_cls = cbm_arena_strndup(d->scratch, r, rl);
}
}
const char *mod = rst_opt(dir, "module");
if (mod) {
for (int j = dir->cs; j < dir->ce; j++) {
d->mod_at[j] = mod;
}
}
for (int j = dir->cs; j < dir->ce; j++) {
d->cls_at[j] = content_cls;
}
}
}
/* ── Tokens ──────────────────────────────────────────────────────── */
static void rst_tok(rst_doc_t *d, int line, int col, int syntax, const char *raw) {
if (!raw) {
d->failed = true;
return;
}
d->toks = (rst_tok_t *)rst_grow(d, d->toks, d->ntoks, &d->cap_toks, sizeof(rst_tok_t));
if (!d->toks) {
return;
}
d->toks[d->ntoks++] = (rst_tok_t){.line = line, .col = col, .syntax = syntax, .raw = raw};
}
static const char *rst_s(const char *s) {
return s ? s : "";
}
/* The written form of a directive: its head line, whitespace collapsed. */
static const char *rst_written_dir(rst_doc_t *d, const rst_dir_t *dir) {
const rst_line_t *ln = &d->L[dir->line];
return rst_collapse(d->ctx->arena, ln->s, ln->len);
}
static bool rst_code_ext(const char *path) {
const char *base = strrchr(path, '/');
base = base ? base + 1 : path;
if (strncmp(base, "Makefile", strlen("Makefile")) == 0 ||
strncmp(base, "Kconfig", strlen("Kconfig")) == 0) {
return true;
}
const char *dot = strrchr(base, '.');
if (!dot) {
return false;
}
char ext[16];
size_t el = strlen(dot);
if (el >= sizeof(ext)) {
return false;
}
for (size_t i = 0; i <= el; i++) {
char c = dot[i];
ext[i] = (char)(c >= 'A' && c <= 'Z' ? c + ('a' - 'A') : c);
}
return rst_in(ext, CODE_EXT);
}
static void rst_directive_tokens(rst_doc_t *d, const rst_dir_t *dir) {
CBMArena *a = d->ctx->arena;
const char *dn = dir->dname;
const char *dom = dir->dom;
bool py_obj = (strcmp(dom, "py") == 0 || strcmp(dom, "std") == 0) && rst_in(dn, PY_OBJ);
bool c_obj = (strcmp(dom, "c") == 0 || strcmp(dom, "std") == 0 || strcmp(dom, "py") == 0) &&
rst_in(dn, C_OBJ);
if (strcmp(dom, "py") != 0 && strcmp(dom, "c") != 0 && strcmp(dom, "std") != 0) {
py_obj = false;
c_obj = false;
}
if (strcmp(dom, "c") == 0) {
py_obj = false;
}
int line = dir->line;
const char *written = NULL;
if (py_obj || c_obj) {
written = rst_written_dir(d, dir);
const char *mod = rst_opt(dir, "module");
const char **sigs = NULL;
int ns = rst_join_sigs(d, dir, &sigs);
if (ns == 0 && !d->failed) {
sigs = (const char **)cbm_arena_alloc(d->scratch, sizeof(char *));
if (!sigs) {
d->failed = true;
return;
}
sigs[0] = "";
ns = 1;
}
bool attr_like =
strcmp(dn, "attribute") == 0 || strcmp(dn, "data") == 0 || strcmp(dn, "property") == 0;
for (int k = 0; k < ns && !d->failed; k++) {
char *sig = rst_collapse(d->scratch, sigs[k], (int)strlen(sigs[k]));
if (!sig) {
d->failed = true;
return;
}
/* Python's reading (H3 py_directive): an attribute's type or
* value is no part of its name; the full name in the class */
char *py_sig = sig;
if (attr_like) {
size_t cut = strcspn(sig, ":=");
while (cut > 0 && sig[cut - 1] == ' ') {
cut--;
}
py_sig = cbm_arena_strndup(d->scratch, sig, cut);
}
const char *prefix;
const char *name;
const char *full = "";
const char *rem;
if (py_sig && strcmp(dn, "module") != 0 && rst_py_sig(d, py_sig, &prefix, &name)) {
rst_py_fullname(d, prefix, name, d->cls_at[line], &full, &rem);
}
rst_tok(d, line, dir->indent, CBM_DOCLINK_RST_OBJECT,
cbm_arena_sprintf(a, "%s\t%s\t%s\t%s\t%s\t%s\t%s", rst_s(written), dir->name,
sig, rst_s(mod ? mod : d->mod_at[line]),
rst_s(d->cls_at[line]), rst_s(d->ns_at[line]), full));
}
return;
}
const char *arg0 = dir->nargs ? dir->args[0] : "";
if (rst_in(dn, AUTODOC)) {
written = rst_written_dir(d, dir);
char *arg = rst_collapse(d->scratch, arg0, (int)strlen(arg0));
rst_tok(d, line, dir->indent, CBM_DOCLINK_RST_AUTODOC,
cbm_arena_sprintf(a, "%s\t%s\t%s\t%s\t%s", rst_s(written), dn, rst_s(arg),
rst_s(d->mod_at[line]), rst_s(d->cls_at[line])));
return;
}
if (strcmp(dn, "literalinclude") == 0) {
written = rst_written_dir(d, dir);
const char *lines = rst_opt(dir, "lines");
const char *pyobject = rst_opt(dir, "pyobject");
bool bounded = rst_opt(dir, "start-after") || rst_opt(dir, "end-before") ||
rst_opt(dir, "start-at") || rst_opt(dir, "end-at");
rst_tok(d, line, dir->indent, CBM_DOCLINK_RST_LITERALINCLUDE,
cbm_arena_sprintf(a, "%s\t%s\t%s\t%s\t%s", rst_s(written), arg0, rst_s(lines),
rst_s(pyobject), bounded ? "1" : ""));
return;
}
if (strcmp(dn, "include") == 0 || strcmp(dn, "kernel-include") == 0) {
if (!arg0[0] || arg0[0] == '<' || !rst_code_ext(arg0)) {
return; /* a document or text include: not code */
}
written = rst_written_dir(d, dir);
rst_tok(d, line, dir->indent, CBM_DOCLINK_RST_INCLUDE,
cbm_arena_sprintf(a, "%s\t%s\t%s", rst_s(written), dn, arg0));
return;
}
if (strcmp(dn, "kernel-doc") == 0) {
written = rst_written_dir(d, dir);
rst_tok(d, line, dir->indent, CBM_DOCLINK_RST_KERNEL_DOC,
cbm_arena_sprintf(a, "%s\t%s\t", rst_s(written), arg0));
const char *ids = rst_opt(dir, "identifiers");
if (!ids) {
ids = rst_opt(dir, "functions");
}
if (!ids || rst_opt(dir, "export") || rst_opt(dir, "internal") || rst_opt(dir, "doc")) {
return; /* the names come from the source file's own kernel-doc comments */
}
/* each name, a trailing backslash dropped (kerneldoc.py) */
const char *p = ids;
while (*p && !d->failed) {
while (*p == ' ') {
p++;
}
const char *e = p;
while (*e && *e != ' ') {
e++;
}
size_t nl = (size_t)(e - p);
while (nl > 0 && p[nl - 1] == '\\') {
nl--;
}
if (nl > 0) {
rst_tok(d, line, dir->indent, CBM_DOCLINK_RST_KERNEL_DOC,
cbm_arena_sprintf(a, "%s\t%s\t%.*s", rst_s(written), arg0, (int)nl, p));
}
p = e;
}
}
}
/* ── Inline text: roles and inline literals ──────────────────────── */
static bool rst_inline_kind(unsigned char k) {
return k == K_PROSE || k == K_TITLE || k == K_DHEAD_PROSE || k == K_DARG_PROSE || k == K_PLIT;
}
typedef struct {
char *text; /* the run's lines joined by '\n' */
char *mask; /* inline literals blanked (H3 mask) */
int len;
int a; /* first line */
int *offs;
int nlines;
} rst_run_t;
static int rst_run_line(const rst_run_t *r, int pos) {
int lo = 0;
int hi = r->nlines;
while (lo < hi) {
int mid = lo + ((hi - lo) / 2);
if (r->offs[mid] <= pos) {
lo = mid + 1;
} else {
hi = mid;
}
}
return lo - 1;
}
/* Is the code point before byte i a role's left neighbour that blocks it
* ((?<![\w`\\]))? */
static bool rst_role_blocked(const char *s, int i) {
if (i == 0) {
return false;
}
int k = i - 1;
while (k > 0 && ((unsigned char)s[k] & 0xC0) == 0x80) {
k--;
}
int n;
uint32_t cp = rst_cp(s, i, k, &n);
return cp == '`' || cp == '\\' || rst_word_cp(cp);
}
/* [A-Za-z][\w.+-]* at s[i]: its end, or -1. */
static int rst_role_name(const char *s, int len, int i) {
if (i >= len || !rst_alpha_ascii((unsigned char)s[i])) {
return -1;
}
i++;
while (i < len) {
int n;
uint32_t cp = rst_cp(s, len, i, &n);
if (cp != '.' && cp != '+' && cp != '-' && !rst_word_cp(cp)) {
break;
}
i += n;
}
return i;
}
static bool rst_role_end_ok(const char *s, int len, int i) {
if (i >= len) {
return true;
}
int n;
uint32_t cp = rst_cp(s, len, i, &n);
return cp != '`' && !rst_word_cp(cp);
}
static void rst_emit_role(rst_doc_t *d, const rst_run_t *r, int start, int role_a, int role_b,
int ca, int cb) {
CBMArena *a = d->ctx->arena;
char *role = cbm_arena_strndup(d->scratch, r->text + role_a, (size_t)(role_b - role_a));
if (!role) {
d->failed = true;
return;
}
const char *dom;
const char *rname;
rst_split_domain(d, role, PY_ROLES, &dom, &rname);
bool code = ((strcmp(dom, "py") == 0 || strcmp(dom, "std") == 0) && rst_in(rname, PY_ROLES)) ||
((strcmp(dom, "c") == 0 || strcmp(dom, "std") == 0) && rst_in(rname, C_ROLES));
char *content = rst_collapse(d->scratch, r->text + ca, cb - ca);
if (!content) {
d->failed = true;
return;
}
if (!code) {
/* an extlink to a repository path (:source:`django/db/x.py`, or with a
* title, :source:`x <django/db/x.py>`): only a role without a domain
* whose target is a path */
const char *target = content;
size_t tl = strlen(content);
const char *lt = strrchr(content, '<');
if (tl > 0 && content[tl - 1] == '>' && lt && lt > content) {
target = lt + 1;
tl = (size_t)(content + tl - 1 - target);
}
CBMDocLinkMdPath p;
if (strchr(role, ':') ||
!cbm_doclink_md_classify_span(target, tl, d->span_buf, RST_SPAN_MAX + 1, &p) ||
p.shape == CBM_DOCLINK_MD_QUALIFIED) {
return;
}
}
int li = rst_run_line(r, start);
int line = r->a + li;
char *written = rst_collapse(a, r->text + start, cb + 1 - start);
rst_tok(d, line, start - r->offs[li], CBM_DOCLINK_RST_ROLE,
written ? cbm_arena_sprintf(a, "%s\t%s\t%s\t%s\t%s\t%s", written, role, content,
rst_s(d->mod_at[line]), rst_s(d->cls_at[line]),
rst_s(d->ns_at[line]))
: NULL);
}
/* ROLE_RE over the masked text. */
static void rst_roles(rst_doc_t *d, const rst_run_t *r) {
const char *m = r->mask;
int len = r->len;
int i = 0;
while (i < len && !d->failed) {
const char *colon = memchr(m + i, ':', (size_t)(len - i));
if (!colon) {
break;
}
i = (int)(colon - m);
if (rst_role_blocked(m, i)) {
i++;
continue;
}
int e1 = rst_role_name(m, len, i + 1);
int role_end = -1;
if (e1 > 0 && e1 < len && m[e1] == ':') {
int e2 = rst_role_name(m, len, e1 + 1);
if (e2 > 0 && e2 + 1 < len && m[e2] == ':' && m[e2 + 1] == '`') {
role_end = e2;
} else if (e1 + 1 < len && m[e1 + 1] == '`') {
role_end = e1;
}
}
if (role_end < 0) {
i++;
continue;
}
int open = role_end + 1;
const char *close = memchr(m + open + 1, '`', (size_t)(len - open - 1));
if (!close || close == m + open + 1 || !rst_role_end_ok(m, len, (int)(close - m) + 1)) {
i++;
continue;
}
int cb = (int)(close - m);
rst_emit_role(d, r, i, i + 1, role_end, open + 1, cb);
i = cb + 1;
}
}
static void rst_literal_token(rst_doc_t *d, const rst_run_t *r, int i, int a, int b) {
if (b <= a || b - a > RST_SPAN_MAX || memchr(r->text + a, '\n', (size_t)(b - a))) {
return;
}
CBMDocLinkMdPath p;
if (!cbm_doclink_md_classify_span(r->text + a, (size_t)(b - a), d->span_buf, RST_SPAN_MAX + 1,
&p)) {
return;
}
int li = rst_run_line(r, i);
rst_tok(d, r->a + li, i - r->offs[li],
p.shape == CBM_DOCLINK_MD_QUALIFIED ? CBM_DOCLINK_RST_CODE_NAME
: CBM_DOCLINK_RST_CODE_PATH,
cbm_arena_strndup(d->ctx->arena, r->text + a, (size_t)(b - a)));
}
/* INLINE_LIT_RE ``.+?`` (DOTALL): blank them in the mask, and take each as a
* possible code_path / code_name. */
static void rst_literals(rst_doc_t *d, rst_run_t *r) {
const char *t = r->text;
int len = r->len;
int i = 0;
while (i + 1 < len && !d->failed) {
if (t[i] != '`' || t[i + 1] != '`') {
i++;
continue;
}
int j = i + 3;
while (j + 1 < len && !(t[j] == '`' && t[j + 1] == '`')) {
j++;
}
if (j + 1 >= len) {
i++;
continue;
}
for (int k = i; k < j + 2; k++) {
if (r->mask[k] != '\n') {
r->mask[k] = ' ';
}
}
rst_literal_token(d, r, i, i + 2, j);
i = j + 2;
}
}
static void rst_inline(rst_doc_t *d) {
int i = 0;
while (i < d->n && !d->failed) {
if (!rst_inline_kind(d->K[i])) {
i++;
continue;
}
int j = i;
int len = 0;
while (j < d->n && rst_inline_kind(d->K[j])) {
len += d->L[j].len + 1;
j++;
}
rst_run_t r = {.a = i, .nlines = j - i};
r.text = (char *)cbm_arena_alloc(d->scratch, (size_t)len + 1);
r.mask = (char *)cbm_arena_alloc(d->scratch, (size_t)len + 1);
r.offs = (int *)cbm_arena_alloc(d->scratch, (size_t)(j - i) * sizeof(int));
if (!r.text || !r.mask || !r.offs) {
d->failed = true;
return;
}
int pos = 0;
for (int k = i; k < j; k++) {
r.offs[k - i] = pos;
memcpy(r.text + pos, d->L[k].s, (size_t)d->L[k].len);
pos += d->L[k].len;
if (k + 1 < j) {
r.text[pos++] = '\n';
}
}
r.text[pos] = '\0';
r.len = pos;
memcpy(r.mask, r.text, (size_t)pos + 1);
rst_literals(d, &r);
rst_roles(d, &r);
i = j;
}
}
/* ── Sections ────────────────────────────────────────────────────── */
/* qn_safe_segment (extract_defs.c): whitespace runs become '-'. */
static const char *rst_qn_segment(CBMArena *a, const char *name) {
char *out = cbm_arena_strdup(a, name);
if (!out) {
return NULL;
}
size_t w = 0;
bool in_ws = false;
for (const char *p = name; *p; p++) {
if (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') {
in_ws = true;
continue;
}
if (in_ws && w > 0) {
out[w++] = '-';
}
in_ws = false;
out[w++] = *p;
}
out[w] = '\0';
return out;
}
/* The section's own text (title block to the next title), whitespace
* collapsed, at most RST_BODY_MAX bytes and never a cut character. */
static const char *rst_body(rst_doc_t *d, int from, int to) {
char *out = (char *)cbm_arena_alloc(d->ctx->arena, RST_BODY_MAX + 1);
if (!out) {
return NULL;
}
int w = 0;
bool full = false; /* the next character did not fit: the text ends before it */
for (int k = from; k < to && !full; k++) {
const char *s = d->L[k].s;
int len = d->L[k].len;
int i = 0;
while (i < len && !full) {
int n;
uint32_t cp = rst_cp(s, len, i, &n);
if (rst_space_cp(cp)) {
i += n;
continue;
}
if (w > 0 && w < RST_BODY_MAX) {
out[w++] = ' ';
}
while (i < len) {
cp = rst_cp(s, len, i, &n);
if (rst_space_cp(cp)) {
break;
}
if (w + n > RST_BODY_MAX) {
full = true; /* only whole characters are written: none is cut */
break;
}
memcpy(out + w, s + i, (size_t)n);
w += n;
i += n;
}
}
}
while (w > 0 && out[w - 1] == ' ') {
w--;
}
out[w] = '\0';
return w > 0 ? out : NULL;
}
static int rst_title_qn_cmp(const void *a, const void *b) {
const rst_title_t *x = *(const rst_title_t *const *)a;
const rst_title_t *y = *(const rst_title_t *const *)b;
return strcmp(x->qn, y->qn);
}
static void rst_sections(rst_doc_t *d) {
CBMExtractCtx *ctx = d->ctx;
CBMArena *a = ctx->arena;
for (int t = 0; t < d->ntitles; t++) {
rst_title_t *ti = &d->titles[t];
const char *seg = rst_qn_segment(a, ti->text);
ti->qn = seg ? cbm_fqn_compute(a, ctx->project, ctx->rel_path, seg) : NULL;
if (!ti->qn) {
d->failed = true;
return;
}
}
if (d->ntitles > 1) {
rst_title_t **by_qn =
(rst_title_t **)cbm_arena_alloc(d->scratch, (size_t)d->ntitles * sizeof(*by_qn));
if (!by_qn) {
d->failed = true;
return;
}
for (int t = 0; t < d->ntitles; t++) {
by_qn[t] = &d->titles[t];
}
qsort(by_qn, (size_t)d->ntitles, sizeof(*by_qn), rst_title_qn_cmp);
for (int t = 1; t < d->ntitles; t++) {
if (strcmp(by_qn[t]->qn, by_qn[t - 1]->qn) == 0) {
by_qn[t]->shared_qn = true;
by_qn[t - 1]->shared_qn = true;
}
}
}
for (int t = 0; t < d->ntitles; t++) {
const rst_title_t *ti = &d->titles[t];
int end = t + 1 < d->ntitles ? d->titles[t + 1].first : d->n;
int last = end - 1;
while (last > ti->idx && d->L[last].blank) {
last--;
}
char *name = cbm_arena_strdup(a, ti->text);
if (!name) {
d->failed = true;
return;
}
CBMDefinition def;
memset(&def, 0, sizeof(def));
def.name = name;
def.qualified_name = ti->qn;
def.label = "Section";
def.file_path = ctx->rel_path;
def.start_line = (uint32_t)ti->idx + 1;
def.end_line = (uint32_t)last + 1;
def.is_exported = true;
def.docstring = rst_body(d, ti->next, end);
cbm_defs_push(&ctx->result->defs, a, def);
}
}
/* ── Driver ──────────────────────────────────────────────────────── */
static int rst_tok_cmp(const void *a, const void *b) {
const rst_tok_t *x = (const rst_tok_t *)a;
const rst_tok_t *y = (const rst_tok_t *)b;
if (x->line != y->line) {
return x->line < y->line ? -1 : 1;
}
if (x->col != y->col) {
return x->col < y->col ? -1 : 1;
}
if (x->syntax != y->syntax) {
return x->syntax < y->syntax ? -1 : 1;
}
return strcmp(x->raw, y->raw);
}
/* The section a 0-based line is written in (by its title block's first line). */
static const rst_title_t *rst_section_of(const rst_doc_t *d, int line) {
int lo = 0;
int hi = d->ntitles;
while (lo < hi) {
int mid = lo + ((hi - lo) / 2);
if (d->titles[mid].first <= line) {
lo = mid + 1;
} else {
hi = mid;
}
}
return lo > 0 ? &d->titles[lo - 1] : NULL;
}
void cbm_doclink_rst_scan_file(CBMExtractCtx *ctx) {
if (!ctx || !ctx->result || !ctx->source || ctx->source_len <= 0) {
return;
}
rst_doc_t d = {.ctx = ctx, .scratch = ctx->scratch ? ctx->scratch : ctx->arena};
d.span_buf = (char *)cbm_arena_alloc(d.scratch, (size_t)(RST_SPAN_MAX + 1) * 2);
if (!d.span_buf || !rst_lines(&d, ctx->source, ctx->source_len)) {
ctx->result->doc_links.failed = true;
return;
}
rst_scan(&d);
if (!d.failed) {
rst_contexts(&d);
}
if (!d.failed) {
rst_sections(&d);
}
for (int i = 0; i < d.ndirs && !d.failed; i++) {
rst_directive_tokens(&d, &d.dirs[i]);
}
if (!d.failed) {
rst_inline(&d);
}
if (d.failed) {
ctx->result->doc_links.failed = true;
return;
}
/* d.toks stays NULL until the first token: qsort's base is nonnull even
* for zero elements (glibc attribute; UBSan on Linux amd64, 2026-10-13). */
if (d.ntoks > 1) {
qsort(d.toks, (size_t)d.ntoks, sizeof(*d.toks), rst_tok_cmp);
}
for (int i = 0; i < d.ntoks; i++) {
const rst_tok_t *t = &d.toks[i];
const rst_title_t *sec = rst_section_of(&d, t->line);
if (sec && sec->shared_qn) {
sec = NULL; /* the file, not the node another title owns */
}
CBMDocLink link = {
.source_qn = sec ? sec->qn : (ctx->module_qn ? ctx->module_qn : ""),
.raw = t->raw,
.line = (uint32_t)t->line + 1,
.def_line = sec ? (uint32_t)sec->idx + 1 : 1,
.syntax = (uint16_t)t->syntax,
.flags = sec ? 0 : CBM_DOCLINK_FLAG_FILE,
};
cbm_doclinks_push(&ctx->result->doc_links, ctx->arena, link);
}
/* an architecture decision record in reST: its node, facts and supersedes */
cbm_adr_extract(ctx);
}