Files
codebase-memory-mcp/internal/cbm/diagram.c
Martin Vogel abf65583e3 feat(docs): a section's text keeps its diagram's labels, not its syntax
A Markdown Section's docstring is its body collapsed and cut at 500 bytes.
A diagram fence in that body put the format's syntax into it (`-->`,
`participant`, `classDiagram`, `subgraph`), which names no code, and pushed
the section's prose out of the cap. The doc -> code candidates are scored
on that text.

Each top-level diagram fence (mermaid, plantuml, puml, uml, dot, graphviz,
d2) is now replaced in the section's text by the labels its diagram parser
reads: the names of its elements, then its arrow labels, in source order.
The parse runs into a scratch result, so the file's own Diagram and
DiagramElement nodes are untouched. A section without such a fence keeps
its text byte for byte; other fences stay as written.

Measured before it was chosen, on held-out repositories (147 sections that
embed diagrams, 503 section/candidate pairs, three blind reading rounds):

  fence as written   277 stored  precision 0.45  42 sections with a correct candidate
  labels (this)      310 stored  precision 0.54  56 sections with a correct candidate
  fence dropped      127 stored  precision 0.54  29 sections with a correct candidate

Labels gave 20 sections a correct candidate that the fence as written did
not, and lost 6 (exact McNemar p = 0.009). Dropping the fence lost more
than it gained.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-11 12:00:31 +02:00

3623 lines
132 KiB
C

/*
* diagram.c — diagrams as graph nodes: a Diagram definition per diagram and a
* DiagramElement definition per element, with the arrows between elements
* (CBMDiagramArrow, written as DIAGRAM_ARROW edges by pass_diagrams.c).
*
* Where diagrams come from:
* - a fenced block of a Markdown file whose info string names a diagram
* language (mermaid; plantuml, puml, uml; dot, graphviz; d2): the Diagram
* hangs under the Section the block is written in (the last heading above
* it), or under the file before any heading;
* - a Mermaid file (.mmd, .mermaid): one Diagram, under the file.
*
* Qualified names: the parent's (a Section's, else the file's module QN) +
* "#diagram-<k>", k counting the diagrams of that parent from 1; an element's
* is the diagram's + "." + its id (dots and blanks as `_`).
*
* A Diagram's docstring is the text its elements and arrows carry (their
* labels), which is what a reader of the diagram reads; its properties hold
* the format and the diagram kind. An element's name is its label (its id
* when it has none); its properties hold the id and the element kind. An
* element named only by an arrow is an element too.
*
* Formats (one parser each, line by line, no grammar): Mermaid flowchart and
* graph, sequenceDiagram, classDiagram, erDiagram, stateDiagram, C4 (Context,
* Container, Component, Dynamic, Deployment) and architecture-beta. Every
* other Mermaid kind (gantt, pie, journey, mindmap, ...) is a Diagram without
* elements.
*/
#include "cbm.h"
#include "doclink.h"
#include "arena.h"
#include "helpers.h"
#include "foundation/constants.h"
#include "foundation/hash_table.h"
#include "foundation/mem_core.h"
#include "tree_sitter/api.h"
#include <yyjson/yyjson.h>
#include <zlib.h>
#include <ctype.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
enum {
DG_DOC_MAX = 2048, /* a Diagram's docstring */
DG_LABEL_MAX = 256, /* an element's or an arrow's label */
DG_ID_MAX = 128,
DG_ID_HEAD = DG_ID_MAX - 18, /* a long id's head: `~` and 16 hex digits follow */
DG_KIND_MAX = 32,
DG_ARGS_MAX = 8,
};
static const uint64_t DG_FNV_OFFSET = 0xcbf29ce484222325ULL; /* FNV-1a 64 */
static const uint64_t DG_FNV_PRIME = 0x100000001b3ULL;
enum { DG_UTF8_CONT_MASK = 0xC0, DG_UTF8_CONT = 0x80 }; /* a continuation byte: 10xxxxxx */
/* ── The builder ─────────────────────────────────────────────────── */
typedef struct {
CBMExtractCtx *ctx;
const char *qn; /* the diagram's */
const char *format;
char kind[DG_KIND_MAX];
const char *element_kind; /* what an element named only by an arrow is */
int def_index; /* the Diagram definition, in ctx->result->defs */
uint32_t line0; /* the source line of the diagram's text */
CBMHashTable *ids; /* element id -> its definition's index + 1 */
char doc[DG_DOC_MAX]; /* the labels, for the docstring */
size_t doc_len;
bool oom;
} dg_t;
typedef struct {
const char *p;
size_t n;
} dg_str_t;
static dg_str_t dg_s(const char *p, size_t n) {
return (dg_str_t){p, n};
}
static dg_str_t dg_trim(dg_str_t s) {
while (s.n && isspace((unsigned char)s.p[0])) {
s.p++;
s.n--;
}
while (s.n && isspace((unsigned char)s.p[s.n - SKIP_ONE])) {
s.n--;
}
return s;
}
/* Quotes and Markdown-string backticks around a label dropped. */
static dg_str_t dg_unquote(dg_str_t s) {
s = dg_trim(s);
if (s.n >= PAIR_LEN && (s.p[0] == '"' || s.p[0] == '\'' || s.p[0] == '`') &&
s.p[s.n - SKIP_ONE] == s.p[0]) {
s.p++;
s.n -= PAIR_LEN;
}
if (s.n >= PAIR_LEN && s.p[0] == '`' && s.p[s.n - SKIP_ONE] == '`') {
s.p++;
s.n -= PAIR_LEN;
}
return dg_trim(s);
}
static bool dg_eq(dg_str_t s, const char *lit) {
return strlen(lit) == s.n && memcmp(s.p, lit, s.n) == 0;
}
static bool dg_starts(dg_str_t s, const char *lit) {
size_t l = strlen(lit);
return s.n >= l && memcmp(s.p, lit, l) == 0;
}
/* A label's text: tags (`<br>`), the line breaks DOT, PlantUML and D2 write
* as `\n` (also `\l`, `\r`, `\t`) and blank runs as one space, at most cap-1
* bytes. */
static size_t dg_label_text(dg_str_t s, char *out, size_t cap) {
size_t w = 0;
bool space = false;
for (size_t i = 0; i < s.n && w + SKIP_ONE < cap; i++) {
char c = s.p[i];
if (c == '\\' && i + SKIP_ONE < s.n && strchr("nlrt", s.p[i + SKIP_ONE]) &&
s.p[i + SKIP_ONE]) {
i++;
space = true;
continue;
}
if (c == '<') {
const char *gt = memchr(s.p + i, '>', s.n - i);
if (gt) {
i = (size_t)(gt - s.p);
space = true;
continue;
}
}
if (isspace((unsigned char)c)) {
space = true;
continue;
}
if (space && w) {
out[w++] = ' ';
}
space = false;
out[w++] = c;
}
out[w] = '\0';
return w;
}
static void dg_doc_add(dg_t *dg, const char *text, size_t n) {
if (!n || dg->doc_len + n + PAIR_LEN >= sizeof(dg->doc)) {
return;
}
if (dg->doc_len) {
memcpy(dg->doc + dg->doc_len, "; ", PAIR_LEN);
dg->doc_len += PAIR_LEN;
}
memcpy(dg->doc + dg->doc_len, text, n);
dg->doc_len += n;
dg->doc[dg->doc_len] = '\0';
}
static const char *dg_dup(dg_t *dg, const char *s, size_t n) {
char *c = cbm_arena_strndup(dg->ctx->arena, s, n);
dg->oom = dg->oom || !c;
return c;
}
static const char **dg_props(dg_t *dg, const char *k1, const char *v1, const char *k2,
const char *v2) {
const char **kv = (const char **)cbm_arena_alloc(dg->ctx->arena, CBM_SZ_5 * sizeof(char *));
if (!kv) {
dg->oom = true;
return NULL;
}
kv[0] = k1;
kv[SKIP_ONE] = v1;
kv[PAIR_LEN] = k2;
kv[CBM_SZ_3] = v2;
kv[CBM_SZ_4] = NULL;
return kv;
}
/* An element id as a QN segment: dots, blanks, backslashes, quotes and `#`
* (QN suffix syntax) as `_`. */
static const char *dg_element_qn(dg_t *dg, dg_str_t id) {
char seg[DG_ID_MAX];
size_t n = id.n < sizeof(seg) - SKIP_ONE ? id.n : sizeof(seg) - SKIP_ONE;
for (size_t i = 0; i < n; i++) {
char c = id.p[i];
seg[i] =
(c == '.' || c == '\\' || c == '"' || c == '#' || isspace((unsigned char)c)) ? '_' : c;
}
seg[n] = '\0';
char *qn = cbm_arena_sprintf(dg->ctx->arena, "%s.%s", dg->qn, seg);
dg->oom = dg->oom || !qn;
return qn;
}
/* An element id as its table key and QN source. An id too long for one (a
* DOT node named by a sentence) keeps its head, cut at a character boundary,
* and the FNV-1a 64 of the whole: still one key per id. */
static size_t dg_id_key(dg_str_t id, char *key) {
if (id.n < DG_ID_MAX) {
memcpy(key, id.p, id.n);
key[id.n] = '\0';
return id.n;
}
uint64_t h = DG_FNV_OFFSET;
for (size_t i = 0; i < id.n; i++) {
h = (h ^ (unsigned char)id.p[i]) * DG_FNV_PRIME;
}
size_t head = DG_ID_HEAD;
while (head > 0 && ((unsigned char)id.p[head] & DG_UTF8_CONT_MASK) == DG_UTF8_CONT) {
head--; /* not inside a UTF-8 sequence */
}
int n = snprintf(key, DG_ID_MAX, "%.*s~%016llx", (int)head, id.p, (unsigned long long)h);
return n > 0 ? (size_t)n : 0;
}
/* An explicit link written on an element (or, for a PlantUML !include, on the
* diagram): a doc-link record the pipeline resolves as a Markdown link. An
* in-page anchor, a data URI or a script links nothing. */
static void dg_link(dg_t *dg, const char *source_qn, dg_str_t raw, int syntax, uint32_t line) {
raw = dg_unquote(raw);
if (!source_qn || !raw.n || raw.p[0] == '#' || dg_starts(raw, "data:") ||
dg_starts(raw, "javascript:")) {
return;
}
CBMDocLink link = {
.source_qn = source_qn,
.raw = dg_dup(dg, raw.p, raw.n),
.line = line,
.def_line = line,
.syntax = (uint16_t)syntax,
};
if (link.raw) {
cbm_doclinks_push(&dg->ctx->result->doc_links, dg->ctx->arena, link);
}
}
/* The element `id` (made on first sight), its label set when it has none. */
static const char *dg_element(dg_t *dg, dg_str_t id, dg_str_t label, const char *kind,
uint32_t line) {
id = dg_trim(id);
if (!id.n || dg->oom) {
return NULL;
}
char key[DG_ID_MAX];
size_t kn = dg_id_key(id, key);
char text[DG_LABEL_MAX];
size_t tl = dg_label_text(dg_unquote(label), text, sizeof(text));
CBMDefArray *defs = &dg->ctx->result->defs;
void *v = cbm_ht_get(dg->ids, key);
if (v) {
CBMDefinition *d = &defs->items[(uintptr_t)v - SKIP_ONE];
if (tl && d->name == d->extra_props[SKIP_ONE]) { /* still named by its id */
d->name = dg_dup(dg, text, tl);
dg_doc_add(dg, text, tl);
}
return d->qualified_name;
}
const char *qn = dg_element_qn(dg, dg_s(key, kn));
const char *idc = dg_dup(dg, id.p, id.n);
if (!qn || !idc) {
return NULL;
}
CBMDefinition def;
memset(&def, 0, sizeof(def));
def.name = tl ? dg_dup(dg, text, tl) : idc;
def.qualified_name = qn;
def.label = "DiagramElement";
def.file_path = dg->ctx->rel_path;
def.start_line = line;
def.end_line = line;
def.parent_class = dg->qn;
/* the kind may live in a parser buffer (a JSON value, a draw.io cell) */
const char *kindc = dg_dup(dg, kind, strlen(kind));
def.extra_props = dg_props(dg, "element_id", idc, "element_kind", kindc);
if (dg->oom || !def.name) {
return NULL;
}
cbm_defs_push(defs, dg->ctx->arena, def);
const char *k = dg_dup(dg, key, kn);
if (k) {
cbm_ht_set(dg->ids, k, (void *)(uintptr_t)defs->count);
}
if (tl) {
dg_doc_add(dg, text, tl);
}
return qn;
}
static void dg_arrow(dg_t *dg, dg_str_t from, dg_str_t to, dg_str_t label, const char *style,
uint32_t line) {
const char *kind = dg->element_kind ? dg->element_kind : "node";
const char *a = dg_element(dg, from, dg_s(NULL, 0), kind, line);
const char *b = dg_element(dg, to, dg_s(NULL, 0), kind, line);
if (!a || !b) {
return;
}
char text[DG_LABEL_MAX];
size_t tl = dg_label_text(dg_unquote(label), text, sizeof(text));
CBMDiagramArrow arrow = {.from_qn = a,
.to_qn = b,
.label = tl ? dg_dup(dg, text, tl) : NULL,
.style = style,
.line = line};
if (!dg->oom) {
cbm_diagram_arrows_push(&dg->ctx->result->diagram_arrows, dg->ctx->arena, arrow);
}
if (tl) {
dg_doc_add(dg, text, tl);
}
}
/* ── Lines ───────────────────────────────────────────────────────── */
typedef struct {
const char *p;
const char *end;
uint32_t line;
} dg_lines_t;
/* The next line (no newline), false at the end. */
static bool dg_next_line(dg_lines_t *it, dg_str_t *out, uint32_t *line) {
if (it->p >= it->end) {
return false;
}
const char *nl = memchr(it->p, '\n', (size_t)(it->end - it->p));
const char *le = nl ? nl : it->end;
*out = dg_s(it->p, (size_t)(le - it->p));
if (out->n && out->p[out->n - SKIP_ONE] == '\r') {
out->n--;
}
*line = it->line++;
it->p = nl ? nl + SKIP_ONE : it->end;
return true;
}
/* A Mermaid line without its `%%` comment. */
static dg_str_t dg_mermaid_strip(dg_str_t s) {
for (size_t i = 0; i + SKIP_ONE < s.n; i++) {
if (s.p[i] == '%' && s.p[i + SKIP_ONE] == '%') {
s.n = i;
break;
}
}
return dg_trim(s);
}
/* ── Mermaid: flowchart ──────────────────────────────────────────── */
static bool dg_id_char(char c) {
return isalnum((unsigned char)c) || c == '_' || c == '-' || (unsigned char)c >= 0x80;
}
/* The closing bracket of a node shape opened at s[i]: the shape's text in
* *label, the position after it. */
static size_t dg_shape(dg_str_t s, size_t i, dg_str_t *label) {
static const char *const opens[] = {"(((", "([", "[[", "[(", "((", "{{", "[/",
"[\\", "[", "(", "{", ">", NULL};
static const char *const closes[] = {")))", "])", "]]", ")]", "))", "}}", "/]",
"\\]", "]", ")", "}", "]", NULL};
for (int k = 0; opens[k]; k++) {
size_t ol = strlen(opens[k]);
if (i + ol > s.n || memcmp(s.p + i, opens[k], ol) != 0) {
continue;
}
size_t cl = strlen(closes[k]);
bool quoted = false;
for (size_t j = i + ol; j + cl <= s.n; j++) {
if (s.p[j] == '"') {
quoted = !quoted;
}
if (!quoted && memcmp(s.p + j, closes[k], cl) == 0) {
*label = dg_s(s.p + i + ol, j - i - ol);
return j + cl;
}
}
/* `[/text\]` and `[\text/]` close with the other slash */
if (k == CBM_SZ_6 || k == CBM_SZ_7) {
const char *alt = k == CBM_SZ_6 ? "\\]" : "/]";
for (size_t j = i + ol; j + PAIR_LEN <= s.n; j++) {
if (memcmp(s.p + j, alt, PAIR_LEN) == 0) {
*label = dg_s(s.p + i + ol, j - i - ol);
return j + PAIR_LEN;
}
}
}
return i;
}
return i;
}
/* A node at s[i]: its id and label; the position after it (i when none). */
static size_t dg_flow_node(dg_str_t s, size_t i, dg_str_t *id, dg_str_t *label) {
size_t j = i;
while (j < s.n && dg_id_char(s.p[j])) {
if (s.p[j] == '-' && j + SKIP_ONE < s.n &&
(s.p[j + SKIP_ONE] == '-' || s.p[j + SKIP_ONE] == '.' || s.p[j + SKIP_ONE] == '>')) {
break; /* an edge starts */
}
j++;
}
if (j == i) {
return i;
}
*id = dg_s(s.p + i, j - i);
*label = dg_s(NULL, 0);
if (j < s.n && s.p[j] == '@' && j + SKIP_ONE < s.n && s.p[j + SKIP_ONE] == '{') {
const char *close = memchr(s.p + j, '}', s.n - j);
if (close) {
dg_str_t body = dg_s(s.p + j + PAIR_LEN, (size_t)(close - s.p) - j - PAIR_LEN);
const char *lab = NULL;
for (size_t k = 0; k + CBM_SZ_6 <= body.n && !lab; k++) {
lab = memcmp(body.p + k, "label:", CBM_SZ_6) == 0 ? body.p + k + CBM_SZ_6 : NULL;
}
if (lab) {
const char *comma = memchr(lab, ',', (size_t)(body.p + body.n - lab));
*label = dg_s(lab, (size_t)((comma ? comma : body.p + body.n) - lab));
}
return (size_t)(close - s.p) + SKIP_ONE;
}
}
size_t k = dg_shape(s, j, label);
if (k > j) {
j = k;
}
if (j + CBM_SZ_3 <= s.n && memcmp(s.p + j, ":::", CBM_SZ_3) == 0) {
j += CBM_SZ_3;
while (j < s.n && dg_id_char(s.p[j])) {
j++;
}
}
return j;
}
static size_t dg_skip_ws(dg_str_t s, size_t i) {
while (i < s.n && isspace((unsigned char)s.p[i])) {
i++;
}
return i;
}
static bool dg_link_char(char c) {
return c == '-' || c == '=' || c == '.' || c == '>' || c == '<' || c == '~';
}
/* An edge at s[i]: its style and label; the position after it (i when none). */
static size_t dg_flow_edge(dg_str_t s, size_t i, const char **style, dg_str_t *label) {
size_t j = i;
if (j < s.n && (s.p[j] == '<' || s.p[j] == 'x' || s.p[j] == 'o') && j + SKIP_ONE < s.n &&
(s.p[j + SKIP_ONE] == '-' || s.p[j + SKIP_ONE] == '=')) {
j++;
}
size_t start = j;
while (j < s.n && dg_link_char(s.p[j])) {
j++;
}
size_t run = j - start;
if (run < PAIR_LEN) {
return i;
}
if (j < s.n && (s.p[j] == 'x' || s.p[j] == 'o') &&
(j + SKIP_ONE >= s.n || !dg_id_char(s.p[j + SKIP_ONE]))) {
j++; /* an arrowhead x or o */
}
dg_str_t op = dg_s(s.p + i, j - i);
*label = dg_s(NULL, 0);
bool head =
memchr(op.p, '>', op.n) || op.p[op.n - SKIP_ONE] == 'x' || op.p[op.n - SKIP_ONE] == 'o';
if (!head && (dg_eq(op, "--") || dg_eq(op, "==") || dg_eq(op, "-."))) {
/* `-- text -->`, `== text ==>`, `-. text .->`: the label runs to the
* closing link, which opens with `--`, `==` or `.-` */
char c0 = op.p[0] == '-' && op.p[SKIP_ONE] == '.' ? '.' : op.p[0];
char c1 = op.p[0] == '-' && op.p[SKIP_ONE] == '.' ? '-' : op.p[SKIP_ONE];
for (size_t k = j; k + SKIP_ONE < s.n; k++) {
if (s.p[k] == c0 && s.p[k + SKIP_ONE] == c1) {
size_t e = k;
while (e < s.n && dg_link_char(s.p[e])) {
e++;
}
*label = dg_s(s.p + j, k - j);
op = dg_s(s.p + i, e - i);
j = e;
break;
}
}
}
bool dotted = memchr(op.p, '.', op.n) != NULL;
bool thick = memchr(op.p, '=', op.n) != NULL;
bool both = op.p[0] == '<' && memchr(op.p + SKIP_ONE, '>', op.n - SKIP_ONE);
head = memchr(op.p, '>', op.n) != NULL;
*style = both ? "bidirectional" : dotted ? "dotted" : thick ? "thick" : head ? "arrow" : "line";
j = dg_skip_ws(s, j);
if (j < s.n && s.p[j] == '|') {
const char *bar = memchr(s.p + j + SKIP_ONE, '|', s.n - j - SKIP_ONE);
if (bar) {
*label = dg_s(s.p + j + SKIP_ONE, (size_t)(bar - s.p) - j - SKIP_ONE);
j = (size_t)(bar - s.p) + SKIP_ONE;
}
}
return j;
}
enum { DG_GROUP_MAX = 16 };
typedef struct {
dg_str_t ids[DG_GROUP_MAX];
int n;
} dg_group_t;
/* `A & B[label]`: the nodes of a group, made; the position after it. */
static size_t dg_flow_group(dg_t *dg, dg_str_t s, size_t i, uint32_t line, dg_group_t *g) {
g->n = 0;
for (;;) {
i = dg_skip_ws(s, i);
dg_str_t id;
dg_str_t label;
size_t j = dg_flow_node(s, i, &id, &label);
if (j == i) {
return i;
}
dg_element(dg, id, label, "node", line);
if (g->n < DG_GROUP_MAX) {
g->ids[g->n++] = id;
}
i = dg_skip_ws(s, j);
if (i < s.n && s.p[i] == '&') {
i++;
continue;
}
return i;
}
}
/* `click A "url"`, `click A href "url"` (a class diagram's `link A "url"`):
* the element's link; a callback (`click A call f()`) links nothing. */
static void dg_mermaid_click(dg_t *dg, dg_str_t s, uint32_t line) {
size_t kw = dg_starts(s, "click ") ? CBM_SZ_6 : CBM_SZ_5;
dg_str_t rest = dg_trim(dg_s(s.p + kw, s.n - kw));
size_t j = 0;
while (j < rest.n && !isspace((unsigned char)rest.p[j])) {
j++;
}
dg_str_t tail = dg_trim(dg_s(rest.p + j, rest.n - j));
if (dg_starts(tail, "href ")) {
tail = dg_trim(dg_s(tail.p + CBM_SZ_5, tail.n - CBM_SZ_5));
}
const char *close =
tail.n && tail.p[0] == '"' ? memchr(tail.p + SKIP_ONE, '"', tail.n - SKIP_ONE) : NULL;
if (!close) {
return;
}
const char *qn = dg_element(dg, dg_s(rest.p, j), dg_s(NULL, 0),
dg->element_kind ? dg->element_kind : "node", line);
dg_link(dg, qn, dg_s(tail.p + SKIP_ONE, (size_t)(close - tail.p) - SKIP_ONE),
CBM_DOCLINK_DG_MERMAID_CLICK, line);
}
static void dg_flow_statement(dg_t *dg, dg_str_t s, uint32_t line) {
if (dg_starts(s, "click ")) {
dg_mermaid_click(dg, s, line);
return;
}
static const char *const skip[] = {"classDef ", "class ", "style ", "linkStyle ",
"click ", "direction ", "end", NULL};
for (int k = 0; skip[k]; k++) {
if (dg_starts(s, skip[k])) {
return;
}
}
if (dg_starts(s, "subgraph")) {
/* `subgraph id [title]`, `subgraph id[title]`, or `subgraph Some title`
* (the title is its id too) */
dg_str_t rest = dg_trim(dg_s(s.p + CBM_SZ_8, s.n - CBM_SZ_8));
dg_str_t id;
dg_str_t label;
size_t j = dg_flow_node(rest, 0, &id, &label);
if (j == 0) {
return;
}
size_t k = dg_skip_ws(rest, j);
if (!label.n && k < rest.n && rest.p[k] == '[') {
dg_shape(rest, k, &label);
} else if (!label.n && k < rest.n) {
id = rest; /* a title of several words */
label = rest;
}
dg_element(dg, id, label, "subgraph", line);
return;
}
dg_group_t left;
size_t i = dg_flow_group(dg, s, 0, line, &left);
while (i < s.n && left.n) {
const char *style = NULL;
dg_str_t label;
size_t j = dg_flow_edge(s, i, &style, &label);
if (j == i) {
return;
}
dg_group_t right;
i = dg_flow_group(dg, s, j, line, &right);
for (int a = 0; a < left.n; a++) {
for (int b = 0; b < right.n; b++) {
dg_arrow(dg, left.ids[a], right.ids[b], label, style, line);
}
}
left = right;
}
}
static void dg_flowchart(dg_t *dg, dg_lines_t *it) {
dg_str_t ln;
uint32_t line = 0;
while (dg_next_line(it, &ln, &line)) {
ln = dg_mermaid_strip(ln);
size_t start = 0;
bool quoted = false;
int depth = 0;
for (size_t i = 0; i <= ln.n; i++) {
char c = i < ln.n ? ln.p[i] : ';';
quoted = c == '"' ? !quoted : quoted;
depth += !quoted && (c == '[' || c == '(' || c == '{');
depth -= !quoted && (c == ']' || c == ')' || c == '}') && depth > 0;
if (c == ';' && !quoted && depth == 0) {
dg_str_t st = dg_trim(dg_s(ln.p + start, i - start));
if (st.n) {
dg_flow_statement(dg, st, line);
}
start = i + SKIP_ONE;
}
}
}
}
/* ── Mermaid: sequence ───────────────────────────────────────────── */
/* A message line `A->>+B: text`: its arrow; false when it has none. */
static bool dg_sequence_message(dg_t *dg, dg_str_t ln, uint32_t line) {
static const char *const arrows[] = {"<<-->>", "<<->>", "-->>", "->>", "--x", "-x",
"--)", "-)", "-->", "->", NULL};
const char *colon = memchr(ln.p, ':', ln.n);
dg_str_t head = colon ? dg_s(ln.p, (size_t)(colon - ln.p)) : ln;
for (int k = 0; arrows[k]; k++) {
size_t al = strlen(arrows[k]);
for (size_t i = 0; i + al <= head.n; i++) {
if (memcmp(head.p + i, arrows[k], al) != 0) {
continue;
}
dg_str_t to = dg_trim(dg_s(head.p + i + al, head.n - i - al));
while (to.n && (to.p[0] == '+' || to.p[0] == '-')) {
to.p++;
to.n--;
}
dg_str_t label = colon
? dg_s(colon + SKIP_ONE, (size_t)(ln.p + ln.n - colon) - SKIP_ONE)
: dg_s(NULL, 0);
bool reply = arrows[k][0] == '-' && arrows[k][SKIP_ONE] == '-';
dg_arrow(dg, dg_s(head.p, i), to, label, reply ? "reply" : "message", line);
return true;
}
}
return false;
}
static void dg_sequence(dg_t *dg, dg_lines_t *it) {
static const char participant[] = "participant ";
dg_str_t ln;
uint32_t line = 0;
while (dg_next_line(it, &ln, &line)) {
ln = dg_mermaid_strip(ln);
if (dg_starts(ln, "create ")) {
ln = dg_trim(dg_s(ln.p + CBM_SZ_7, ln.n - CBM_SZ_7));
}
bool actor = dg_starts(ln, "actor ");
if (actor || dg_starts(ln, participant)) {
size_t kl = actor ? CBM_SZ_6 : sizeof(participant) - SKIP_ONE;
dg_str_t rest = dg_trim(dg_s(ln.p + kl, ln.n - kl));
dg_str_t id = rest;
dg_str_t label = dg_s(NULL, 0);
for (size_t i = 0; i + CBM_SZ_4 <= rest.n; i++) {
if (memcmp(rest.p + i, " as ", CBM_SZ_4) == 0) {
id = dg_s(rest.p, i);
label = dg_s(rest.p + i + CBM_SZ_4, rest.n - i - CBM_SZ_4);
break;
}
}
dg_element(dg, id, label, actor ? "actor" : "participant", line);
continue;
}
(void)dg_sequence_message(dg, ln, line);
}
}
/* ── Mermaid: class and ER ───────────────────────────────────────── */
typedef struct {
const char *op;
const char *style;
bool to_left; /* the arrow points at the left side (child -> parent) */
} dg_rel_t;
static const dg_rel_t DG_CLASS_RELS[] = {
{"<|--|>", "inheritance", false},
{"<|--", "inheritance", true},
{"--|>", "inheritance", false},
{"<|..", "realization", true},
{"..|>", "realization", false},
{"*--", "composition", false},
{"--*", "composition", true},
{"o--", "aggregation", false},
{"--o", "aggregation", true},
{"<--", "association", true},
{"-->", "association", false},
{"<..", "dependency", true},
{"..>", "dependency", false},
{"--", "link", false},
{"..", "link", false},
{NULL, NULL, false},
};
/* `A "1" op "*" B : label`: the relation of a class line, false when none. */
static bool dg_class_relation(dg_t *dg, dg_str_t ln, uint32_t line) {
const char *colon = NULL;
bool quoted = false;
for (size_t i = 0; i < ln.n && !colon; i++) {
quoted = ln.p[i] == '"' ? !quoted : quoted;
colon = !quoted && ln.p[i] == ':' ? ln.p + i : NULL;
}
dg_str_t head = colon ? dg_s(ln.p, (size_t)(colon - ln.p)) : ln;
for (int k = 0; DG_CLASS_RELS[k].op; k++) {
size_t ol = strlen(DG_CLASS_RELS[k].op);
for (size_t i = 0; i + ol <= head.n; i++) {
if (memcmp(head.p + i, DG_CLASS_RELS[k].op, ol) != 0) {
continue;
}
dg_str_t l = dg_trim(dg_s(head.p, i));
dg_str_t r = dg_trim(dg_s(head.p + i + ol, head.n - i - ol));
if (l.n && l.p[l.n - SKIP_ONE] == '"') { /* a cardinality */
const char *q = memchr(l.p, '"', l.n - SKIP_ONE);
l = q ? dg_trim(dg_s(l.p, (size_t)(q - l.p))) : l;
}
if (r.n && r.p[0] == '"') {
const char *q = memchr(r.p + SKIP_ONE, '"', r.n - SKIP_ONE);
r = q ? dg_trim(dg_s(q + SKIP_ONE, (size_t)(r.p + r.n - q) - SKIP_ONE)) : r;
}
if (!l.n || !r.n) {
return false;
}
dg_str_t label = colon
? dg_s(colon + SKIP_ONE, (size_t)(ln.p + ln.n - colon) - SKIP_ONE)
: dg_s(NULL, 0);
dg_element(dg, l, dg_s(NULL, 0), "class", line);
dg_element(dg, r, dg_s(NULL, 0), "class", line);
if (DG_CLASS_RELS[k].to_left) {
dg_arrow(dg, r, l, label, DG_CLASS_RELS[k].style, line);
} else {
dg_arrow(dg, l, r, label, DG_CLASS_RELS[k].style, line);
}
return true;
}
}
return false;
}
/* The name of `class Name~T~["Label"] {`, its label in *label. */
static dg_str_t dg_class_decl(dg_str_t rest, dg_str_t *label) {
size_t j = 0;
while (j < rest.n && (dg_id_char(rest.p[j]) || rest.p[j] == '.')) {
j++;
}
dg_str_t name = dg_s(rest.p, j);
*label = dg_s(NULL, 0);
const char *lb = memchr(rest.p + j, '[', rest.n - j);
const char *rb = lb ? memchr(lb, ']', (size_t)(rest.p + rest.n - lb)) : NULL;
if (lb && rb) {
*label = dg_s(lb + SKIP_ONE, (size_t)(rb - lb) - SKIP_ONE);
}
return name;
}
static void dg_classdiagram(dg_t *dg, dg_lines_t *it) {
dg_str_t ln;
uint32_t line = 0;
int body = 0;
while (dg_next_line(it, &ln, &line)) {
ln = dg_mermaid_strip(ln);
if (body > 0) {
body -= ln.n && ln.p[0] == '}';
continue;
}
if (dg_starts(ln, "class ")) {
dg_str_t label;
dg_str_t name = dg_class_decl(dg_trim(dg_s(ln.p + CBM_SZ_6, ln.n - CBM_SZ_6)), &label);
dg_element(dg, name, label, "class", line);
body += ln.n && ln.p[ln.n - SKIP_ONE] == '{';
continue;
}
if (dg_starts(ln, "click ") || dg_starts(ln, "link ")) {
dg_mermaid_click(dg, ln, line);
continue;
}
if (dg_starts(ln, "namespace ") || dg_starts(ln, "note") || dg_starts(ln, "<<") ||
dg_starts(ln, "style ") || dg_starts(ln, "cssClass") || dg_starts(ln, "classDef") ||
dg_starts(ln, "direction") || dg_starts(ln, "link ") || dg_starts(ln, "callback ") ||
dg_starts(ln, "click ") || dg_eq(ln, "}")) {
continue;
}
if (!dg_class_relation(dg, ln, line)) {
const char *colon = memchr(ln.p, ':', ln.n);
if (colon) { /* `Name : +member`: the class, no member */
dg_element(dg, dg_s(ln.p, (size_t)(colon - ln.p)), dg_s(NULL, 0), "class", line);
}
}
}
}
static bool dg_er_card(char c) {
return c == '|' || c == 'o' || c == '}' || c == '{';
}
static void dg_erdiagram(dg_t *dg, dg_lines_t *it) {
dg_str_t ln;
uint32_t line = 0;
int body = 0;
while (dg_next_line(it, &ln, &line)) {
ln = dg_mermaid_strip(ln);
if (body > 0) {
body -= ln.n && ln.p[0] == '}';
continue;
}
const char *colon = memchr(ln.p, ':', ln.n);
dg_str_t head = colon ? dg_s(ln.p, (size_t)(colon - ln.p)) : ln;
bool done = false;
for (size_t i = 0; i + PAIR_LEN <= head.n && !done; i++) {
bool solid = head.p[i] == '-' && head.p[i + SKIP_ONE] == '-';
bool dashed = head.p[i] == '.' && head.p[i + SKIP_ONE] == '.';
if (!(solid || dashed) || i == 0 || !dg_er_card(head.p[i - SKIP_ONE])) {
continue;
}
size_t a = i;
while (a > 0 && dg_er_card(head.p[a - SKIP_ONE])) {
a--;
}
size_t b = i + PAIR_LEN;
while (b < head.n && dg_er_card(head.p[b])) {
b++;
}
dg_str_t l = dg_trim(dg_s(head.p, a));
dg_str_t r = dg_trim(dg_s(head.p + b, head.n - b));
dg_str_t label = colon
? dg_s(colon + SKIP_ONE, (size_t)(ln.p + ln.n - colon) - SKIP_ONE)
: dg_s(NULL, 0);
dg_element(dg, l, dg_s(NULL, 0), "entity", line);
dg_element(dg, r, dg_s(NULL, 0), "entity", line);
dg_arrow(dg, l, r, label, solid ? "identifying" : "non_identifying", line);
done = true;
}
if (!done && ln.n && ln.p[ln.n - SKIP_ONE] == '{') {
dg_str_t name = dg_trim(dg_s(ln.p, ln.n - SKIP_ONE));
dg_str_t label = dg_s(NULL, 0);
const char *lb = memchr(name.p, '[', name.n);
if (lb && name.p[name.n - SKIP_ONE] == ']') {
label = dg_s(lb + SKIP_ONE, (size_t)(name.p + name.n - lb) - PAIR_LEN);
name = dg_trim(dg_s(name.p, (size_t)(lb - name.p)));
}
dg_element(dg, name, label, "entity", line);
body++;
}
}
}
/* ── Mermaid: state ──────────────────────────────────────────────── */
enum { DG_STATE_DEPTH = 8 };
/* A state of a transition: `[*]` is the start (left) or the end (right) of
* the composite state it is written in, `composite` (empty at the top). */
static dg_str_t dg_state_id(dg_str_t s, bool left, dg_str_t composite, char *buf, size_t cap) {
s = dg_trim(s);
if (!dg_eq(s, "[*]")) {
return s;
}
const char *end = left ? "start" : "end";
int n = composite.n ? snprintf(buf, cap, "%.*s_%s", (int)composite.n, composite.p, end)
: snprintf(buf, cap, "%s", end);
return n > 0 && (size_t)n < cap ? dg_s(buf, (size_t)n) : dg_s(end, strlen(end));
}
static void dg_statediagram(dg_t *dg, dg_lines_t *it) {
dg_str_t ln;
uint32_t line = 0;
dg_str_t stack[DG_STATE_DEPTH];
int depth = 0;
while (dg_next_line(it, &ln, &line)) {
ln = dg_mermaid_strip(ln);
if (dg_eq(ln, "}")) {
depth -= depth > 0;
continue;
}
if (dg_starts(ln, "state ")) {
dg_str_t rest = dg_trim(dg_s(ln.p + CBM_SZ_6, ln.n - CBM_SZ_6));
if (rest.n && rest.p[0] == '"') {
const char *q = memchr(rest.p + SKIP_ONE, '"', rest.n - SKIP_ONE);
if (q) {
dg_str_t label = dg_s(rest.p + SKIP_ONE, (size_t)(q - rest.p) - SKIP_ONE);
dg_str_t after =
dg_trim(dg_s(q + SKIP_ONE, (size_t)(rest.p + rest.n - q) - SKIP_ONE));
if (dg_starts(after, "as ")) {
dg_element(dg, dg_trim(dg_s(after.p + CBM_SZ_3, after.n - CBM_SZ_3)), label,
"state", line);
}
}
continue;
}
size_t j = 0;
while (j < rest.n && dg_id_char(rest.p[j])) {
j++;
}
dg_element(dg, dg_s(rest.p, j), dg_s(NULL, 0), "state", line);
if (rest.n && rest.p[rest.n - SKIP_ONE] == '{') {
if (depth < DG_STATE_DEPTH) {
stack[depth] = dg_s(rest.p, j);
}
depth++;
}
continue;
}
const char *arrow = NULL;
for (size_t i = 0; i + CBM_SZ_3 <= ln.n && !arrow; i++) {
arrow = memcmp(ln.p + i, "-->", CBM_SZ_3) == 0 ? ln.p + i : NULL;
}
if (!arrow) {
continue;
}
dg_str_t rest = dg_s(arrow + CBM_SZ_3, (size_t)(ln.p + ln.n - arrow) - CBM_SZ_3);
const char *colon = memchr(rest.p, ':', rest.n);
dg_str_t to = colon ? dg_s(rest.p, (size_t)(colon - rest.p)) : rest;
dg_str_t label = colon
? dg_s(colon + SKIP_ONE, (size_t)(rest.p + rest.n - colon) - SKIP_ONE)
: dg_s(NULL, 0);
dg_str_t composite =
depth > 0 && depth <= DG_STATE_DEPTH ? stack[depth - SKIP_ONE] : dg_s(NULL, 0);
char from_buf[DG_ID_MAX];
char to_buf[DG_ID_MAX];
dg_arrow(dg,
dg_state_id(dg_s(ln.p, (size_t)(arrow - ln.p)), true, composite, from_buf,
sizeof(from_buf)),
dg_state_id(to, false, composite, to_buf, sizeof(to_buf)), label, "transition",
line);
}
}
/* ── Mermaid: C4 ─────────────────────────────────────────────────── */
/* The arguments of `Name(a, "b", c)`: up to DG_ARGS_MAX, quotes dropped. */
static int dg_args(dg_str_t s, dg_str_t *args) {
const char *open = memchr(s.p, '(', s.n);
if (!open) {
return 0;
}
int n = 0;
bool quoted = false;
const char *start = open + SKIP_ONE;
for (const char *p = start; p < s.p + s.n && n < DG_ARGS_MAX; p++) {
quoted = *p == '"' ? !quoted : quoted;
if (!quoted && (*p == ',' || *p == ')')) {
args[n++] = dg_unquote(dg_s(start, (size_t)(p - start)));
start = p + SKIP_ONE;
if (*p == ')') {
break;
}
}
}
return n;
}
/* A C4 macro line `Person(id, "Label", ...)` or `Rel(a, b, "label")`: false
* when the line is none. */
static bool dg_c4_line(dg_t *dg, dg_str_t ln, uint32_t line) {
size_t j = 0;
while (j < ln.n && (isalnum((unsigned char)ln.p[j]) || ln.p[j] == '_')) {
j++;
}
if (j == 0 || j >= ln.n || ln.p[j] != '(') {
return false;
}
{
dg_str_t fn = dg_s(ln.p, j);
dg_str_t args[DG_ARGS_MAX];
int na = dg_args(ln, args);
if (dg_starts(fn, "Rel") || dg_starts(fn, "BiRel")) {
int base = dg_eq(fn, "RelIndex") ? SKIP_ONE : 0; /* RelIndex(i, from, to, ...) */
if (na >= base + PAIR_LEN) {
dg_str_t label = na > base + PAIR_LEN ? args[base + PAIR_LEN] : dg_s(NULL, 0);
dg_arrow(dg, args[base], args[base + SKIP_ONE], label,
dg_starts(fn, "BiRel") ? "bidirectional" : "relation", line);
}
return true;
}
if (dg_starts(fn, "Update") || dg_starts(fn, "Add") || dg_starts(fn, "Lay_") ||
dg_eq(fn, "title") || na < SKIP_ONE) {
return true;
}
char kind[DG_KIND_MAX];
size_t kl = fn.n < sizeof(kind) - SKIP_ONE ? fn.n : sizeof(kind) - SKIP_ONE;
for (size_t k = 0; k < kl; k++) {
kind[k] = (char)tolower((unsigned char)fn.p[k]);
}
kind[kl] = '\0';
const char *kc = dg_dup(dg, kind, kl);
dg_element(dg, args[0], na > SKIP_ONE ? args[SKIP_ONE] : dg_s(NULL, 0), kc ? kc : "c4",
line);
}
return true;
}
static void dg_c4(dg_t *dg, dg_lines_t *it) {
dg_str_t ln;
uint32_t line = 0;
while (dg_next_line(it, &ln, &line)) {
(void)dg_c4_line(dg, dg_mermaid_strip(ln), line);
}
}
/* ── Mermaid: architecture ───────────────────────────────────────── */
/* `id(icon)[Label] in group`: the id; the label in *label. */
static dg_str_t dg_arch_decl(dg_str_t rest, dg_str_t *label) {
size_t j = 0;
while (j < rest.n && dg_id_char(rest.p[j])) {
j++;
}
dg_str_t id = dg_s(rest.p, j);
*label = dg_s(NULL, 0);
const char *lb = memchr(rest.p + j, '[', rest.n - j);
const char *rb = lb ? memchr(lb, ']', (size_t)(rest.p + rest.n - lb)) : NULL;
if (lb && rb) {
*label = dg_s(lb + SKIP_ONE, (size_t)(rb - lb) - SKIP_ONE);
}
return id;
}
/* `id{group}:R` or `R:id`: the id. */
static dg_str_t dg_arch_end(dg_str_t s) {
s = dg_trim(s);
const char *colon = memchr(s.p, ':', s.n);
if (colon) {
dg_str_t a = dg_s(s.p, (size_t)(colon - s.p));
dg_str_t b = dg_s(colon + SKIP_ONE, (size_t)(s.p + s.n - colon) - SKIP_ONE);
s = a.n > SKIP_ONE ? a : b; /* the side is one letter */
}
const char *brace = memchr(s.p, '{', s.n);
return dg_trim(brace ? dg_s(s.p, (size_t)(brace - s.p)) : s);
}
static void dg_architecture(dg_t *dg, dg_lines_t *it) {
static const char *const decls[] = {"group ", "service ", "junction ", NULL};
dg_str_t ln;
uint32_t line = 0;
while (dg_next_line(it, &ln, &line)) {
ln = dg_mermaid_strip(ln);
bool decl = false;
for (int k = 0; decls[k] && !decl; k++) {
if (!dg_starts(ln, decls[k])) {
continue;
}
size_t kl = strlen(decls[k]);
dg_str_t label;
dg_str_t id = dg_arch_decl(dg_trim(dg_s(ln.p + kl, ln.n - kl)), &label);
char kind[DG_KIND_MAX];
snprintf(kind, sizeof(kind), "%.*s", (int)(kl - SKIP_ONE), decls[k]);
dg_element(dg, id, label, dg_dup(dg, kind, kl - SKIP_ONE), line);
decl = true;
}
if (decl) {
continue;
}
static const char *const ops[] = {"<-->", "-->", "<--", "--", NULL};
for (int k = 0; ops[k]; k++) {
size_t ol = strlen(ops[k]);
const char *at = NULL;
for (size_t i = 0; i + ol <= ln.n && !at; i++) {
at = memcmp(ln.p + i, ops[k], ol) == 0 ? ln.p + i : NULL;
}
if (!at) {
continue;
}
dg_str_t l = dg_arch_end(dg_s(ln.p, (size_t)(at - ln.p)));
dg_str_t r = dg_arch_end(dg_s(at + ol, (size_t)(ln.p + ln.n - at) - ol));
if (k == PAIR_LEN) { /* <-- points left */
dg_arrow(dg, r, l, dg_s(NULL, 0), "arrow", line);
} else {
dg_arrow(dg, l, r, dg_s(NULL, 0),
k == 0 ? "bidirectional"
: k == 1 ? "arrow"
: "line",
line);
}
break;
}
}
}
/* ── Mermaid: the header ─────────────────────────────────────────── */
typedef void (*dg_parser_fn)(dg_t *dg, dg_lines_t *it);
typedef struct {
const char *keyword;
const char *kind;
const char *element_kind; /* of an element named only by an arrow */
dg_parser_fn parse;
} dg_kind_t;
static const dg_kind_t DG_MERMAID_KINDS[] = {
{"flowchart", "flowchart", "node", dg_flowchart},
{"graph", "flowchart", "node", dg_flowchart},
{"sequenceDiagram", "sequence", "participant", dg_sequence},
{"classDiagram", "class", "class", dg_classdiagram},
{"erDiagram", "er", "entity", dg_erdiagram},
{"stateDiagram", "state", "state", dg_statediagram},
{"C4Context", "c4_context", "c4", dg_c4},
{"C4Container", "c4_container", "c4", dg_c4},
{"C4Component", "c4_component", "c4", dg_c4},
{"C4Dynamic", "c4_dynamic", "c4", dg_c4},
{"C4Deployment", "c4_deployment", "c4", dg_c4},
{"architecture", "architecture", "service", dg_architecture},
{NULL, NULL, NULL, NULL},
};
/* A `title Text` line of the diagram (or `pie title Text` in its header),
* when the front matter named none. */
static dg_str_t dg_title_line(dg_lines_t it, dg_str_t head) {
for (size_t i = 0; i + CBM_SZ_7 <= head.n; i++) {
if (memcmp(head.p + i, " title ", CBM_SZ_7) == 0) {
return dg_unquote(dg_s(head.p + i + CBM_SZ_7, head.n - i - CBM_SZ_7));
}
}
dg_str_t ln;
uint32_t line = 0;
while (dg_next_line(&it, &ln, &line)) {
ln = dg_mermaid_strip(ln);
if (dg_starts(ln, "title ")) {
return dg_unquote(dg_s(ln.p + CBM_SZ_6, ln.n - CBM_SZ_6));
}
}
return dg_s(NULL, 0);
}
/* The first line that says what the diagram is: front matter (`---` ...
* `---`, its `title:` kept) and `%%` lines skipped. A title of the diagram's
* own replaces one given from outside (a caption); none keeps it. */
static dg_str_t dg_mermaid_header(dg_lines_t *it, dg_str_t *title) {
dg_str_t ln;
uint32_t line = 0;
bool front = false;
while (dg_next_line(it, &ln, &line)) {
dg_str_t t = dg_trim(ln);
if (dg_eq(t, "---")) {
front = !front;
continue;
}
if (front) {
if (dg_starts(t, "title:")) {
*title = dg_unquote(dg_s(t.p + CBM_SZ_6, t.n - CBM_SZ_6));
}
continue;
}
t = dg_mermaid_strip(t);
if (t.n) {
return t;
}
}
return dg_s(NULL, 0);
}
static void dg_mermaid(dg_t *dg, dg_lines_t *it, dg_str_t *title) {
dg_str_t head = dg_mermaid_header(it, title);
dg_str_t own = dg_title_line(*it, head);
*title = own.n ? own : *title;
size_t j = 0;
while (j < head.n && !isspace((unsigned char)head.p[j]) && head.p[j] != ';') {
j++;
}
dg_str_t word = dg_s(head.p, j);
for (int k = 0; DG_MERMAID_KINDS[k].keyword; k++) {
if (dg_starts(word, DG_MERMAID_KINDS[k].keyword)) {
snprintf(dg->kind, sizeof(dg->kind), "%s", DG_MERMAID_KINDS[k].kind);
dg->element_kind = DG_MERMAID_KINDS[k].element_kind;
DG_MERMAID_KINDS[k].parse(dg, it);
return;
}
}
snprintf(dg->kind, sizeof(dg->kind), "%.*s",
(int)(word.n < DG_KIND_MAX - SKIP_ONE ? word.n : DG_KIND_MAX - SKIP_ONE), word.p);
}
/* ── PlantUML ────────────────────────────────────────────────────── */
/* Declarations whose `{ ... }` body holds members, not elements. */
static const char *const DG_PUML_CLASSLIKE[] = {
"abstract class", "abstract", "class", "interface", "enum", "annotation", "protocol",
"struct", "exception", "object", "map", "metaclass", NULL};
/* Declarations whose `{ ... }` body holds further elements. */
static const char *const DG_PUML_ELEMENTS[] = {
"participant", "actor", "boundary", "control", "database", "collections", "queue",
"usecase", "component", "node", "cloud", "frame", "folder", "package",
"rectangle", "artifact", "file", "storage", "card", "agent", "stack",
"hexagon", "person", "process", "state", "entity", "namespace", NULL};
static bool dg_word_at(dg_str_t s, const char *word) {
size_t l = strlen(word);
return s.n > l && memcmp(s.p, word, l) == 0 && (s.p[l] == ' ' || s.p[l] == '\t');
}
/* An operand of a PlantUML line: `[Comp]`, `(use case)`, `:actor:`, `"text"`,
* an id, or `[*]`; its id in *id; the position after it (i when none). */
static size_t dg_puml_operand(dg_str_t s, size_t i, dg_str_t *id, dg_str_t *label) {
i = dg_skip_ws(s, i);
*label = dg_s(NULL, 0);
if (i + CBM_SZ_3 <= s.n && memcmp(s.p + i, "[*]", CBM_SZ_3) == 0) {
*id = dg_s(s.p + i, CBM_SZ_3);
return i + CBM_SZ_3;
}
static const char opens[] = "[(:\"";
static const char closes[] = "]):\"";
const char *o = i < s.n ? strchr(opens, s.p[i]) : NULL;
if (o && s.p[i]) {
char close = closes[o - opens];
const char *c = memchr(s.p + i + SKIP_ONE, close, s.n - i - SKIP_ONE);
if (!c) {
return i;
}
*id = dg_s(s.p + i + SKIP_ONE, (size_t)(c - s.p) - i - SKIP_ONE);
*label = *id;
return (size_t)(c - s.p) + SKIP_ONE;
}
size_t j = i;
while (j < s.n && (dg_id_char(s.p[j]) || s.p[j] == '.' || s.p[j] == ':' || s.p[j] == '$')) {
if (s.p[j] == '-' || s.p[j] == ':') { /* an arrow or a label starts */
break;
}
j++;
}
*id = dg_s(s.p + i, j - i);
return j;
}
/* What a PlantUML arrow means, and whether it points at its left side. */
static const char *dg_puml_arrow_style(dg_str_t op, bool sequence, bool *to_left) {
*to_left = false;
bool lt = memchr(op.p, '<', op.n) != NULL;
bool gt = memchr(op.p, '>', op.n) != NULL;
bool dotted = memchr(op.p, '.', op.n) != NULL;
bool inherit = false;
for (size_t i = 0; i + SKIP_ONE < op.n; i++) {
if (op.p[i] == '<' && op.p[i + SKIP_ONE] == '|') {
inherit = true;
*to_left = true;
}
if (op.p[i] == '|' && op.p[i + SKIP_ONE] == '>') {
inherit = true;
}
}
if (inherit) {
return dotted ? "realization" : "inheritance";
}
if (op.p[0] == '+' || op.p[op.n - SKIP_ONE] == '+') { /* an inner class */
*to_left = op.p[0] == '+'; /* nested -> its container, as `A ..+ B` reads */
return "nesting";
}
if (op.p[0] == '*' || op.p[op.n - SKIP_ONE] == '*') {
*to_left = op.p[op.n - SKIP_ONE] == '*';
return "composition";
}
if (op.p[0] == 'o' || op.p[op.n - SKIP_ONE] == 'o') {
*to_left = op.p[op.n - SKIP_ONE] == 'o';
return "aggregation";
}
if (lt && gt) {
return "bidirectional";
}
*to_left = lt;
if (sequence) {
return memchr(op.p, '-', op.n) && op.n >= PAIR_LEN && op.p[SKIP_ONE] == '-' ? "reply"
: "message";
}
return dotted ? (gt || lt ? "dependency" : "dotted") : (gt || lt ? "arrow" : "line");
}
static bool dg_puml_arrow_char(char c) {
return c == '-' || c == '.' || c == '<' || c == '>' || c == '|' || c == '*' || c == 'o' ||
c == '#' || c == '+' || c == '^' || c == '/' || c == '\\' || c == 'x' || c == '[' ||
c == ']' || c == ',' || c == '=' || isalpha((unsigned char)c);
}
/* `A "1" -up-> "n" B : label`: false when the line holds no relation. */
static bool dg_puml_relation(dg_t *dg, dg_str_t ln, uint32_t line, bool sequence) {
dg_str_t lid;
dg_str_t llab;
size_t i = dg_puml_operand(ln, 0, &lid, &llab);
if (!lid.n) {
return false;
}
i = dg_skip_ws(ln, i);
if (i < ln.n && ln.p[i] == '"') { /* a cardinality */
const char *q = memchr(ln.p + i + SKIP_ONE, '"', ln.n - i - SKIP_ONE);
i = q ? dg_skip_ws(ln, (size_t)(q - ln.p) + SKIP_ONE) : i;
}
size_t a = i;
while (i < ln.n && !isspace((unsigned char)ln.p[i]) && dg_puml_arrow_char(ln.p[i])) {
i++;
}
dg_str_t op = dg_s(ln.p + a, i - a);
bool arrow = false;
for (size_t k = 0; k + SKIP_ONE < op.n && !arrow; k++) {
arrow = (op.p[k] == '-' || op.p[k] == '.') &&
(op.p[k + SKIP_ONE] == '-' || op.p[k + SKIP_ONE] == '.' ||
op.p[k + SKIP_ONE] == '>' || op.p[k + SKIP_ONE] == '|' ||
op.p[k + SKIP_ONE] == '[' || isalpha((unsigned char)op.p[k + SKIP_ONE]));
arrow =
arrow || (op.p[k] == '<' && (op.p[k + SKIP_ONE] == '-' || op.p[k + SKIP_ONE] == '.'));
}
if (!arrow) {
return false;
}
i = dg_skip_ws(ln, i);
if (i < ln.n && ln.p[i] == '"') {
const char *q = memchr(ln.p + i + SKIP_ONE, '"', ln.n - i - SKIP_ONE);
i = q ? (size_t)(q - ln.p) + SKIP_ONE : i;
}
dg_str_t rid;
dg_str_t rlab;
i = dg_puml_operand(ln, i, &rid, &rlab);
if (!rid.n) {
return false;
}
i = dg_skip_ws(ln, i);
dg_str_t label =
i < ln.n && ln.p[i] == ':' ? dg_s(ln.p + i + SKIP_ONE, ln.n - i - SKIP_ONE) : dg_s(NULL, 0);
dg_element(dg, lid, llab, dg->element_kind, line);
dg_element(dg, rid, rlab, dg->element_kind, line);
bool to_left = false;
const char *style = dg_puml_arrow_style(op, sequence, &to_left);
if (to_left) {
dg_arrow(dg, rid, lid, label, style, line);
} else {
dg_arrow(dg, lid, rid, label, style, line);
}
return true;
}
/* `[[url]]`, `[[url label]]`, `[[url{tooltip}]]` in a PlantUML line: the URL. */
static dg_str_t dg_puml_url(dg_str_t ln) {
for (size_t i = 0; i + SKIP_ONE < ln.n; i++) {
if (ln.p[i] == '[' && ln.p[i + SKIP_ONE] == '[') {
size_t e = i + PAIR_LEN;
while (e < ln.n && ln.p[e] != ']' && ln.p[e] != '{' &&
!isspace((unsigned char)ln.p[e])) {
e++;
}
return dg_s(ln.p + i + PAIR_LEN, e - i - PAIR_LEN);
}
}
return dg_s(NULL, 0);
}
/* `!include path` (also _many, _once, url): the diagram's link to the file it
* takes in; `<stdlib>` names none of the repo's. `url of X is [[url]]`: X's. */
static bool dg_puml_link_line(dg_t *dg, dg_str_t ln, uint32_t line) {
if (dg_starts(ln, "!include")) {
size_t i = CBM_SZ_8;
while (i < ln.n && !isspace((unsigned char)ln.p[i])) {
i++;
}
dg_str_t arg = dg_trim(dg_s(ln.p + i, ln.n - i));
const char *bang = memchr(arg.p, '!', arg.n); /* `file!2`: a block of it */
arg = bang ? dg_trim(dg_s(arg.p, (size_t)(bang - arg.p))) : arg;
if (arg.n && arg.p[0] != '<') {
dg_link(dg, dg->qn, arg, CBM_DOCLINK_DG_PLANTUML_INCLUDE, line);
}
return true;
}
if (dg_starts(ln, "url of ") || dg_starts(ln, "url for ")) {
size_t kw = dg_starts(ln, "url of ") ? CBM_SZ_7 : CBM_SZ_8;
dg_str_t rest = dg_s(ln.p + kw, ln.n - kw);
size_t j = 0;
while (j + CBM_SZ_4 <= rest.n && memcmp(rest.p + j, " is ", CBM_SZ_4) != 0) {
j++;
}
if (j + CBM_SZ_4 <= rest.n) {
const char *qn =
dg_element(dg, dg_unquote(dg_s(rest.p, j)), dg_s(NULL, 0), dg->element_kind, line);
dg_link(dg, qn, dg_puml_url(rest), CBM_DOCLINK_DG_PLANTUML_URL, line);
}
return true;
}
return false;
}
/* `kw "Label" as Id`, `kw Id as "Label"`, `kw Id`, `kw [Label] as Id`: an
* element. *body is set when its `{` opens a member body. */
static bool dg_puml_decl(dg_t *dg, dg_str_t ln, uint32_t line, bool *members) {
const char *kw = NULL;
bool classlike = false;
for (int k = 0; DG_PUML_CLASSLIKE[k] && !kw; k++) {
if (dg_word_at(ln, DG_PUML_CLASSLIKE[k])) {
kw = DG_PUML_CLASSLIKE[k];
classlike = true;
}
}
for (int k = 0; DG_PUML_ELEMENTS[k] && !kw; k++) {
kw = dg_word_at(ln, DG_PUML_ELEMENTS[k]) ? DG_PUML_ELEMENTS[k] : NULL;
}
if (!kw) {
return false;
}
dg_str_t rest = dg_trim(dg_s(ln.p + strlen(kw), ln.n - strlen(kw)));
dg_str_t id;
dg_str_t label;
size_t j = dg_puml_operand(rest, 0, &id, &label);
dg_str_t after = dg_trim(dg_s(rest.p + j, rest.n - j));
if (dg_starts(after, "as ")) {
dg_str_t aid;
dg_str_t alab;
dg_puml_operand(dg_trim(dg_s(after.p + CBM_SZ_3, after.n - CBM_SZ_3)), 0, &aid, &alab);
if (label.n || !alab.n) {
label = label.n ? label : id;
id = aid;
} else {
label = alab; /* `kw Id as "Label"` */
}
}
*members = classlike && rest.n && rest.p[rest.n - SKIP_ONE] == '{';
const char *kind = strcmp(kw, "abstract class") == 0 ? "abstract" : kw;
const char *qn = dg_element(dg, id, label, kind, line);
dg_link(dg, qn, dg_puml_url(after), CBM_DOCLINK_DG_PLANTUML_URL, line);
return true;
}
/* What a PlantUML diagram is: the strongest kind any of its lines tells (the
* packages around classes make no component diagram). */
enum {
PK_C4,
PK_CLASS,
PK_SEQUENCE,
PK_USECASE,
PK_ACTOR, /* an actor alone: a sequence, unless use cases say otherwise */
PK_STATE,
PK_ACTIVITY,
PK_COMPONENT,
PK_COUNT
};
static const char *dg_puml_kind(dg_lines_t it) {
static const char *const names[PK_COUNT] = {"c4", "class", "sequence", "usecase",
"sequence", "state", "activity", "component"};
bool seen[PK_COUNT] = {false};
dg_str_t ln;
uint32_t line = 0;
while (dg_next_line(&it, &ln, &line)) {
ln = dg_trim(ln);
seen[PK_C4] |= dg_starts(ln, "Person(") || dg_starts(ln, "System(") ||
dg_starts(ln, "Container(") || dg_starts(ln, "Component(") ||
dg_starts(ln, "Rel(");
for (int k = 0; DG_PUML_CLASSLIKE[k]; k++) {
seen[PK_CLASS] |= dg_word_at(ln, DG_PUML_CLASSLIKE[k]);
}
seen[PK_SEQUENCE] |= dg_word_at(ln, "participant") || dg_word_at(ln, "boundary") ||
dg_word_at(ln, "control") || dg_word_at(ln, "collections");
seen[PK_USECASE] |= dg_word_at(ln, "usecase") || (ln.n && ln.p[0] == '(');
seen[PK_ACTOR] |= dg_word_at(ln, "actor");
seen[PK_STATE] |= dg_word_at(ln, "state") || dg_starts(ln, "[*]");
seen[PK_ACTIVITY] |=
dg_eq(ln, "start") || (ln.n && ln.p[0] == ':' && ln.p[ln.n - SKIP_ONE] == ';');
seen[PK_COMPONENT] |= dg_word_at(ln, "component") || dg_word_at(ln, "node") ||
dg_word_at(ln, "package") || dg_word_at(ln, "database") ||
(ln.n && ln.p[0] == '[');
}
for (int k = 0; k < PK_COUNT; k++) {
if (seen[k]) {
return names[k];
}
}
return "uml";
}
static bool dg_c4_line(dg_t *dg, dg_str_t ln, uint32_t line);
static void dg_plantuml(dg_t *dg, dg_lines_t *it, dg_str_t *title) {
const char *kind = dg_puml_kind(*it);
snprintf(dg->kind, sizeof(dg->kind), "%s", kind);
bool sequence = strcmp(kind, "sequence") == 0;
dg->element_kind = sequence ? "participant"
: strcmp(kind, "class") == 0 ? "class"
: strcmp(kind, "usecase") == 0 ? "usecase"
: strcmp(kind, "state") == 0 ? "state"
: strcmp(kind, "component") == 0 ? "component"
: "node";
if (strcmp(kind, "activity") == 0) {
return; /* an activity's steps are prose: the Diagram alone */
}
dg_str_t ln;
uint32_t line = 0;
int members = 0;
bool comment = false;
while (dg_next_line(it, &ln, &line)) {
ln = dg_trim(ln);
if (comment) {
comment = !(ln.n >= PAIR_LEN && ln.p[ln.n - PAIR_LEN] == '\'' &&
ln.p[ln.n - SKIP_ONE] == '/');
continue;
}
if (dg_starts(ln, "/'")) {
comment = !(ln.n >= CBM_SZ_4 && ln.p[ln.n - PAIR_LEN] == '\'' &&
ln.p[ln.n - SKIP_ONE] == '/');
continue;
}
if (members > 0) {
members += ln.n && ln.p[ln.n - SKIP_ONE] == '{';
members -= ln.n && ln.p[0] == '}';
continue;
}
if (dg_puml_link_line(dg, ln, line)) {
continue;
}
if (!ln.n || ln.p[0] == '\'' || ln.p[0] == '@' || ln.p[0] == '!' || dg_eq(ln, "}") ||
dg_starts(ln, "skinparam") || dg_starts(ln, "hide ") || dg_starts(ln, "show ") ||
dg_starts(ln, "note ") || dg_starts(ln, "end note") || dg_starts(ln, "legend") ||
dg_starts(ln, "header") || dg_starts(ln, "footer") || dg_starts(ln, "left to right") ||
dg_starts(ln, "top to bottom") || dg_starts(ln, "autonumber") ||
dg_starts(ln, "activate") || dg_starts(ln, "deactivate")) {
continue;
}
if (dg_starts(ln, "title ")) {
if (!title->n) {
*title = dg_unquote(dg_s(ln.p + CBM_SZ_6, ln.n - CBM_SZ_6));
}
continue;
}
if (strcmp(kind, "c4") == 0 && dg_c4_line(dg, ln, line)) {
continue;
}
bool body = false;
if (dg_puml_decl(dg, ln, line, &body)) {
members += body;
continue;
}
(void)dg_puml_relation(dg, ln, line, sequence);
}
}
/* ── Graphviz DOT ────────────────────────────────────────────────── */
typedef enum {
DT_END = 0,
DT_ID,
DT_LBRACE,
DT_RBRACE,
DT_LBRACK,
DT_RBRACK,
DT_SEMI,
DT_COMMA,
DT_EQ,
DT_COLON,
DT_EDGE,
} dt_kind_t;
typedef struct {
dt_kind_t k;
dg_str_t s;
uint32_t line;
} dt_tok_t;
typedef struct {
const char *p;
const char *end;
uint32_t line;
dt_tok_t tok; /* the current token */
} dt_lex_t;
static void dt_skip(dt_lex_t *lx) {
for (;;) {
while (lx->p < lx->end && isspace((unsigned char)*lx->p)) {
lx->line += *lx->p == '\n';
lx->p++;
}
if (lx->p + SKIP_ONE < lx->end && lx->p[0] == '/' && lx->p[SKIP_ONE] == '/') {
while (lx->p < lx->end && *lx->p != '\n') {
lx->p++;
}
continue;
}
if (lx->p < lx->end && lx->p[0] == '#') {
while (lx->p < lx->end && *lx->p != '\n') {
lx->p++;
}
continue;
}
if (lx->p + SKIP_ONE < lx->end && lx->p[0] == '/' && lx->p[SKIP_ONE] == '*') {
lx->p += PAIR_LEN;
while (lx->p + SKIP_ONE < lx->end && !(lx->p[0] == '*' && lx->p[SKIP_ONE] == '/')) {
lx->line += *lx->p == '\n';
lx->p++;
}
lx->p = lx->p + PAIR_LEN <= lx->end ? lx->p + PAIR_LEN : lx->end;
continue;
}
return;
}
}
static void dt_next(dt_lex_t *lx) {
dt_skip(lx);
lx->tok = (dt_tok_t){DT_END, dg_s(NULL, 0), lx->line};
if (lx->p >= lx->end) {
return;
}
char c = *lx->p;
static const char single[] = "{}[];,=:";
static const dt_kind_t kinds[] = {DT_LBRACE, DT_RBRACE, DT_LBRACK, DT_RBRACK,
DT_SEMI, DT_COMMA, DT_EQ, DT_COLON};
const char *hit = strchr(single, c);
if (c && hit) {
lx->tok.k = kinds[hit - single];
lx->p++;
return;
}
if (c == '-' && lx->p + SKIP_ONE < lx->end &&
(lx->p[SKIP_ONE] == '>' || lx->p[SKIP_ONE] == '-')) {
lx->tok.k = DT_EDGE;
lx->tok.s = dg_s(lx->p, PAIR_LEN);
lx->p += PAIR_LEN;
return;
}
if (c == '"') {
const char *q = lx->p + SKIP_ONE;
while (q < lx->end && *q != '"') {
lx->line += *q == '\n';
q += *q == '\\' && q + SKIP_ONE < lx->end ? PAIR_LEN : SKIP_ONE;
}
lx->tok.k = DT_ID;
lx->tok.s = dg_s(lx->p + SKIP_ONE, (size_t)(q - lx->p) - SKIP_ONE);
lx->p = q < lx->end ? q + SKIP_ONE : lx->end;
return;
}
if (c == '<') { /* an HTML string */
int depth = 0;
const char *q = lx->p;
for (; q < lx->end; q++) {
depth += *q == '<';
depth -= *q == '>';
lx->line += *q == '\n';
if (depth == 0) {
break;
}
}
lx->tok.k = DT_ID;
lx->tok.s = dg_s(lx->p + SKIP_ONE, (size_t)(q - lx->p) - (q > lx->p ? SKIP_ONE : 0));
lx->p = q < lx->end ? q + SKIP_ONE : lx->end;
return;
}
const char *q = lx->p;
while (q < lx->end && (isalnum((unsigned char)*q) || *q == '_' || *q == '.' ||
(unsigned char)*q >= 0x80 || (*q == '-' && q == lx->p))) {
q++;
}
if (q == lx->p) {
lx->p++; /* a stray byte */
dt_next(lx);
return;
}
lx->tok.k = DT_ID;
lx->tok.s = dg_s(lx->p, (size_t)(q - lx->p));
lx->p = q;
}
/* `[a=b, c=d]` lists: the value of `label`, when one is set; url (when given)
* gets the value of `URL` or `href`. */
static dg_str_t dt_attrs(dt_lex_t *lx, dg_str_t *url) {
dg_str_t label = dg_s(NULL, 0);
while (lx->tok.k == DT_LBRACK) {
dt_next(lx);
while (lx->tok.k != DT_RBRACK && lx->tok.k != DT_END) {
if (lx->tok.k == DT_ID) {
bool is_label = dg_eq(lx->tok.s, "label");
bool is_url = dg_eq(lx->tok.s, "URL") || dg_eq(lx->tok.s, "href");
dt_next(lx);
if (lx->tok.k == DT_EQ) {
dt_next(lx);
if (lx->tok.k == DT_ID && is_label) {
label = lx->tok.s;
} else if (lx->tok.k == DT_ID && is_url && url) {
*url = lx->tok.s;
}
}
}
dt_next(lx);
}
dt_next(lx);
}
return label;
}
enum { DT_SET_MAX = 64, DT_DEPTH_MAX = 32 };
typedef struct {
dg_str_t ids[DT_SET_MAX];
int n;
} dt_set_t;
static void dt_stmts(dg_t *dg, dt_lex_t *lx, bool directed, dt_set_t *all, int depth,
dg_str_t *title);
/* An edge operand: a node id (its port dropped) or a subgraph's nodes. */
static bool dt_operand(dg_t *dg, dt_lex_t *lx, bool directed, dt_set_t *out, int depth,
dg_str_t *title) {
out->n = 0;
if (lx->tok.k == DT_ID && dg_eq(lx->tok.s, "subgraph")) {
dt_next(lx);
if (lx->tok.k == DT_ID) {
dt_next(lx);
}
}
if (lx->tok.k == DT_LBRACE) {
dt_next(lx);
dt_stmts(dg, lx, directed, out, depth + SKIP_ONE, title);
if (lx->tok.k == DT_RBRACE) {
dt_next(lx);
}
return true;
}
if (lx->tok.k != DT_ID) {
return false;
}
out->ids[out->n++] = lx->tok.s;
dt_next(lx);
while (lx->tok.k == DT_COLON) { /* a port, a compass point */
dt_next(lx);
if (lx->tok.k == DT_ID) {
dt_next(lx);
}
}
return true;
}
static void dt_stmt(dg_t *dg, dt_lex_t *lx, bool directed, dt_set_t *all, int depth,
dg_str_t *title) {
uint32_t line = lx->tok.line;
if (lx->tok.k == DT_ID &&
(dg_eq(lx->tok.s, "graph") || dg_eq(lx->tok.s, "node") || dg_eq(lx->tok.s, "edge"))) {
dt_next(lx);
(void)dt_attrs(lx, NULL);
return;
}
dt_set_t left;
if (!dt_operand(dg, lx, directed, &left, depth, title)) {
dt_next(lx);
return;
}
if (lx->tok.k == DT_EQ) { /* a graph attribute */
dt_next(lx);
if (lx->tok.k == DT_ID && left.n && dg_eq(left.ids[0], "label") && depth == 0 &&
!title->n) {
*title = lx->tok.s;
}
dt_next(lx);
return;
}
if (lx->tok.k != DT_EDGE) {
dg_str_t url = dg_s(NULL, 0);
dg_str_t label = dt_attrs(lx, &url);
for (int i = 0; i < left.n; i++) {
dg_link(dg, dg_element(dg, left.ids[i], label, "node", line), url,
CBM_DOCLINK_DG_DOT_URL, line);
if (all->n < DT_SET_MAX) {
all->ids[all->n++] = left.ids[i];
}
}
return;
}
/* an edge chain: a -> b -> {c d} [label=x] */
dt_set_t chain[DT_DEPTH_MAX];
int nc = 0;
chain[nc++] = left;
while (lx->tok.k == DT_EDGE && nc < DT_DEPTH_MAX) {
dt_next(lx);
if (!dt_operand(dg, lx, directed, &chain[nc], depth, title)) {
break;
}
nc++;
}
dg_str_t label = dt_attrs(lx, NULL);
for (int k = 0; k < nc; k++) {
for (int i = 0; i < chain[k].n; i++) {
dg_element(dg, chain[k].ids[i], dg_s(NULL, 0), "node", line);
if (all->n < DT_SET_MAX) {
all->ids[all->n++] = chain[k].ids[i];
}
}
}
for (int k = 0; k + SKIP_ONE < nc; k++) {
for (int i = 0; i < chain[k].n; i++) {
for (int j = 0; j < chain[k + SKIP_ONE].n; j++) {
dg_arrow(dg, chain[k].ids[i], chain[k + SKIP_ONE].ids[j], label,
directed ? "arrow" : "line", line);
}
}
}
}
static void dt_stmts(dg_t *dg, dt_lex_t *lx, bool directed, dt_set_t *all, int depth,
dg_str_t *title) {
if (depth > DT_DEPTH_MAX) {
return;
}
while (lx->tok.k != DT_RBRACE && lx->tok.k != DT_END && !dg->oom) {
if (lx->tok.k == DT_SEMI || lx->tok.k == DT_COMMA) {
dt_next(lx);
continue;
}
dt_stmt(dg, lx, directed, all, depth, title);
}
}
static void dg_dot(dg_t *dg, dg_lines_t *it, dg_str_t *title) {
dt_lex_t lx = {it->p, it->end, it->line, {DT_END, {NULL, 0}, 0}};
dt_next(&lx);
if (lx.tok.k == DT_ID && dg_eq(lx.tok.s, "strict")) {
dt_next(&lx);
}
bool directed = lx.tok.k == DT_ID && dg_eq(lx.tok.s, "digraph");
snprintf(dg->kind, sizeof(dg->kind), "%s", directed ? "digraph" : "graph");
dg->element_kind = "node";
while (lx.tok.k != DT_LBRACE && lx.tok.k != DT_END) {
dt_next(&lx);
}
if (lx.tok.k == DT_END) {
return;
}
dt_next(&lx);
dt_set_t all = {.n = 0};
dt_stmts(dg, &lx, directed, &all, 0, title);
}
/* ── D2 ──────────────────────────────────────────────────────────── */
enum { D2_DEPTH_MAX = 16, D2_PATH_MAX = 512, D2_DELIM_MAX = 8 };
/* Keys that set an attribute of the shape they are written in. */
static bool d2_reserved(dg_str_t key) {
static const char *const words[] = {
"shape", "style", "icon", "near", "direction", "tooltip",
"link", "width", "height", "class", "classes", "vars",
"label", "constraint", "top", "left", "grid-rows", "grid-columns",
"grid-gap", "layers", "scenarios", "steps", "source-arrowhead", "target-arrowhead",
"filled", "font-size", NULL};
for (int k = 0; words[k]; k++) {
if (dg_eq(key, words[k])) {
return true;
}
}
return false;
}
/* The containers a line is written in: the path of ids, and how far it
* reached at each enclosing level (restored by the closing brace). */
typedef struct {
char path[D2_PATH_MAX];
size_t lens[D2_DEPTH_MAX];
int depth;
int skip_depth; /* inside a style, vars or connection block; -1 for none */
} d2_scope_t;
/* One level deeper: into the container `id`, or (no id) into a block that
* keeps the path. */
static void d2_push(d2_scope_t *sc, dg_str_t id) {
if (sc->depth < D2_DEPTH_MAX) {
sc->lens[sc->depth] = strlen(sc->path);
if (id.n) {
char next[D2_PATH_MAX];
snprintf(next, sizeof(next), "%.*s", (int)id.n, id.p);
memcpy(sc->path, next, sizeof(next));
}
}
sc->depth++;
}
static void d2_pop(d2_scope_t *sc) {
if (sc->depth == 0) {
return;
}
if (sc->skip_depth == sc->depth) {
sc->skip_depth = -1;
}
sc->depth--;
if (sc->depth < D2_DEPTH_MAX) {
sc->path[sc->lens[sc->depth]] = '\0';
}
}
/* The full id of a reference written inside the containers `path`. */
static dg_str_t d2_id(dg_str_t ref, const char *path, char *buf, size_t cap) {
ref = dg_unquote(ref);
if (!path[0]) {
return ref;
}
int n = snprintf(buf, cap, "%s.%.*s", path, (int)ref.n, ref.p);
return n > 0 && (size_t)n < cap ? dg_s(buf, (size_t)n) : ref;
}
/* A line without its `#` comment; a block string (`|`) ends the scan, its
* text is no comment. */
static dg_str_t d2_strip_comment(dg_str_t ln) {
bool quoted = false;
for (size_t i = 0; i < ln.n; i++) {
quoted = ln.p[i] == '"' ? !quoted : quoted;
if (quoted) {
continue;
}
if (ln.p[i] == '|') {
break;
}
if (ln.p[i] == '#') {
return dg_trim(dg_s(ln.p, i));
}
}
return ln;
}
/* The first `:` outside quotes, NULL for none. */
static const char *d2_colon(dg_str_t ln) {
bool quoted = false;
for (size_t i = 0; i < ln.n; i++) {
quoted = ln.p[i] == '"' ? !quoted : quoted;
if (!quoted && ln.p[i] == ':') {
return ln.p + i;
}
}
return NULL;
}
/* A block string value (`|md ...|`, `||...||`, `|||...|||`, |`...`|): its
* text, read on through the lines it spans, and what follows its closing
* delimiter on that line. False when value opens none. */
static bool d2_block(dg_lines_t *it, dg_str_t value, dg_str_t *text, dg_str_t *after) {
size_t pipes = 0;
while (pipes < value.n && value.p[pipes] == '|') {
pipes++;
}
if (pipes == 0 || pipes + PAIR_LEN > D2_DELIM_MAX) {
return false;
}
char close[D2_DELIM_MAX];
size_t cl = 0;
size_t open = pipes;
if (open < value.n && value.p[open] == '`') {
close[cl++] = '`';
open++;
}
for (size_t i = 0; i < pipes; i++) {
close[cl++] = '|';
}
while (open < value.n && !isspace((unsigned char)value.p[open])) { /* the language tag */
open++;
}
const char *from = value.p + open;
const char *at = NULL;
for (const char *q = from; q + cl <= it->end && !at; q++) {
at = memcmp(q, close, cl) == 0 ? q : NULL;
}
at = at ? at : it->end;
*text = dg_s(from, (size_t)(at - from));
const char *rest = at + (at < it->end ? cl : 0);
const char *eol = memchr(rest, '\n', (size_t)(it->end - rest));
eol = eol ? eol : it->end;
*after = dg_trim(dg_s(rest, (size_t)(eol - rest)));
dg_str_t skipped;
uint32_t l = 0;
while (it->p <= at && dg_next_line(it, &skipped, &l)) { /* the lines the block spans */
}
return true;
}
/* A block string's label: its first line of text, tags and heading marks
* dropped. */
static size_t d2_block_label(dg_str_t text, char *out, size_t cap) {
dg_lines_t li = {text.p, text.p + text.n, 0};
dg_str_t ln;
uint32_t l = 0;
while (dg_next_line(&li, &ln, &l)) {
size_t n = dg_label_text(ln, out, cap);
size_t k = 0;
while (k < n && (out[k] == '#' || out[k] == ' ')) {
k++;
}
if (k < n) {
memmove(out, out + k, n - k + SKIP_ONE);
return n - k;
}
}
out[0] = '\0';
return 0;
}
/* A value that is a block string read whole: the label it gives in buf, its
* text added to the diagram's docstring, and whether a `{` follows it. */
static dg_str_t d2_value(dg_t *dg, dg_lines_t *it, dg_str_t value, char *buf, size_t cap,
bool *opens) {
dg_str_t text;
dg_str_t after;
if (!value.n || value.p[0] != '|' || !d2_block(it, value, &text, &after)) {
return value;
}
*opens = after.n && after.p[after.n - SKIP_ONE] == '{';
size_t n = d2_block_label(text, buf, cap);
char all[DG_LABEL_MAX];
size_t an = dg_label_text(text, all, sizeof(all));
if (an > n) {
dg_doc_add(dg, all, an);
}
return dg_s(buf, n);
}
/* A connection chain `a -> b <- c: label`: true when the line is one. */
static bool d2_connection(dg_t *dg, dg_lines_t *it, dg_str_t ln, const char *path, uint32_t line,
bool *opens) {
static const char *const ops[] = {"<->", "->", "<-", "--", NULL};
dg_str_t parts[DT_SET_MAX];
const char *styles[DT_SET_MAX];
int np = 0;
const char *colon = NULL;
bool quoted = false;
size_t start = 0;
for (size_t i = 0; i < ln.n && np + SKIP_ONE < DT_SET_MAX; i++) {
quoted = ln.p[i] == '"' ? !quoted : quoted;
if (quoted) {
continue;
}
if (ln.p[i] == ':') {
colon = ln.p + i;
break;
}
for (int k = 0; ops[k]; k++) {
size_t l = strlen(ops[k]);
if (i + l <= ln.n && memcmp(ln.p + i, ops[k], l) == 0) {
parts[np] = dg_trim(dg_s(ln.p + start, i - start));
styles[np++] = k == 0 ? "bidirectional"
: k == 1 ? "arrow"
: k == PAIR_LEN ? "<"
: "line";
start = i + l;
i += l - SKIP_ONE;
break;
}
}
}
if (np == 0) {
return false;
}
size_t end = colon ? (size_t)(colon - ln.p) : ln.n;
parts[np] = dg_trim(dg_s(ln.p + start, end - start));
dg_str_t label = colon
? dg_trim(dg_s(colon + SKIP_ONE, (size_t)(ln.p + ln.n - colon) - SKIP_ONE))
: dg_s(NULL, 0);
char buf[DG_LABEL_MAX];
label = d2_value(dg, it, label, buf, sizeof(buf), opens);
if (label.n && label.p[label.n - SKIP_ONE] == '{') {
label = dg_trim(dg_s(label.p, label.n - SKIP_ONE));
}
for (int k = 0; k < np; k++) {
char ba[D2_PATH_MAX];
char bb[D2_PATH_MAX];
dg_str_t a = d2_id(parts[k], path, ba, sizeof(ba));
dg_str_t b = d2_id(parts[k + SKIP_ONE], path, bb, sizeof(bb));
if (!a.n || !b.n) {
continue;
}
if (strcmp(styles[k], "<") == 0) {
dg_arrow(dg, b, a, label, "arrow", line);
} else {
dg_arrow(dg, a, b, label, styles[k], line);
}
}
return true;
}
/* The first reserved segment of a dotted key: its index, -1 for none. */
static int d2_reserved_segment(dg_str_t key, size_t *seg_start, size_t *seg_len) {
int index = 0;
size_t s = 0;
bool quoted = false;
for (size_t i = 0; i <= key.n; i++) {
quoted = i < key.n && key.p[i] == '"' ? !quoted : quoted;
if (i < key.n && (quoted || key.p[i] != '.')) {
continue;
}
if (d2_reserved(dg_trim(dg_s(key.p + s, i - s)))) {
*seg_start = s;
*seg_len = i - s;
return index;
}
index++;
s = i + SKIP_ONE;
}
return -1;
}
/* `key: value` (or `key: value {`) that is no connection. */
static void d2_key(dg_t *dg, d2_scope_t *sc, dg_lines_t *it, dg_str_t ln, uint32_t line, bool opens,
dg_str_t *title) {
const char *colon = d2_colon(ln);
dg_str_t key = dg_trim(colon ? dg_s(ln.p, (size_t)(colon - ln.p))
: dg_s(ln.p, ln.n - (opens ? SKIP_ONE : 0)));
dg_str_t value = colon
? dg_trim(dg_s(colon + SKIP_ONE, (size_t)(ln.p + ln.n - colon) - SKIP_ONE))
: dg_s(NULL, 0);
char vbuf[DG_LABEL_MAX];
value = d2_value(dg, it, value, vbuf, sizeof(vbuf), &opens);
if (value.n && value.p[value.n - SKIP_ONE] == '{') {
value = dg_trim(dg_s(value.p, value.n - SKIP_ONE));
}
size_t rs = 0;
size_t rl = 0;
int r = d2_reserved_segment(key, &rs, &rl);
if (r >= 0) {
/* an attribute: `label` names the shape it is set on */
bool label = dg_eq(dg_trim(dg_s(key.p + rs, rl)), "label") && rs + rl == key.n;
char buf[D2_PATH_MAX];
dg_str_t owner = r == 0 ? dg_s(sc->path, strlen(sc->path))
: d2_id(dg_s(key.p, rs - SKIP_ONE), sc->path, buf, sizeof(buf));
bool link = dg_eq(dg_trim(dg_s(key.p + rs, rl)), "link") && rs + rl == key.n;
if (label && owner.n && value.n) {
dg_element(dg, owner, value, "shape", line);
} else if (link && owner.n && value.n) {
dg_link(dg, dg_element(dg, owner, dg_s(NULL, 0), "shape", line), value,
CBM_DOCLINK_DG_D2_LINK, line);
} else if (label && value.n && r == 0) { /* the board's own label */
const char *t = dg_dup(dg, value.p, value.n);
*title = t ? dg_s(t, value.n) : *title;
}
if (opens) {
d2_push(sc, dg_s(NULL, 0));
sc->skip_depth = sc->depth;
}
return;
}
char buf[D2_PATH_MAX];
dg_str_t id = d2_id(key, sc->path, buf, sizeof(buf));
dg_element(dg, id, value, opens ? "container" : "shape", line);
if (opens) {
d2_push(sc, id);
}
}
static void dg_d2(dg_t *dg, dg_lines_t *it, dg_str_t *title) {
snprintf(dg->kind, sizeof(dg->kind), "%s", "d2");
dg->element_kind = "shape";
d2_scope_t sc;
memset(&sc, 0, sizeof(sc));
sc.skip_depth = -1;
dg_str_t ln;
uint32_t line = 0;
while (dg_next_line(it, &ln, &line)) {
ln = d2_strip_comment(dg_trim(ln));
if (!ln.n) {
continue;
}
if (dg_eq(ln, "}")) {
d2_pop(&sc);
continue;
}
bool opens = ln.p[ln.n - SKIP_ONE] == '{';
if (sc.skip_depth >= 0 || ln.p[0] == '(' || ln.p[0] == '|' || ln.p[0] == '*' ||
ln.p[0] == '.' || ln.p[0] == '@') {
if (opens) {
d2_push(&sc, dg_s(NULL, 0));
}
continue;
}
if (d2_connection(dg, it, ln, sc.path, line, &opens)) {
if (opens) {
d2_push(&sc, dg_s(NULL, 0));
sc.skip_depth = sc.depth; /* a connection's attribute block */
}
continue;
}
d2_key(dg, &sc, it, ln, line, opens, title);
}
}
/* ── XML attributes (draw.io) ────────────────────────────────────── */
/* The value of attribute `name` in the tag text [p, end), entities
* decoded, into out; false when the tag has none. */
static bool dg_xml_attr(const char *p, const char *end, const char *name, char *out, size_t cap) {
size_t nl = strlen(name);
for (const char *q = p; q + nl + PAIR_LEN < end; q++) {
if (memcmp(q, name, nl) != 0 || q[nl] != '=' ||
(q[nl + SKIP_ONE] != '"' && q[nl + SKIP_ONE] != '\'') ||
(q > p && !isspace((unsigned char)q[-SKIP_ONE]))) {
continue;
}
char quote = q[nl + SKIP_ONE];
const char *v = q + nl + PAIR_LEN;
size_t w = 0;
for (; v < end && *v != quote && w + SKIP_ONE < cap; v++) {
if (*v != '&') {
out[w++] = *v;
continue;
}
static const struct {
const char *ent;
char c;
} ents[] = {{"&lt;", '<'}, {"&gt;", '>'}, {"&amp;", '&'}, {"&quot;", '"'},
{"&apos;", '\''}, {"&#10;", ' '}, {"&#xa;", ' '}, {"&#xA;", ' '},
{"&nbsp;", ' '}, {NULL, 0}};
bool hit = false;
for (int k = 0; ents[k].ent && !hit; k++) {
size_t el = strlen(ents[k].ent);
if (v + el <= end && memcmp(v, ents[k].ent, el) == 0) {
out[w++] = ents[k].c;
v += el - SKIP_ONE;
hit = true;
}
}
if (!hit) {
out[w++] = '&';
}
}
out[w] = '\0';
return true;
}
out[0] = '\0';
return false;
}
enum { DG_DRAWIO_CELLS = 4096, DG_ATTR_MAX = 4096 };
/* The text of a draw.io label: its HTML entities once more (an HTML label is
* escaped twice), its tags dropped. */
static size_t dg_drawio_text(const char *html, char *out, size_t cap) {
char tmp[DG_ATTR_MAX];
size_t w = 0;
for (const char *v = html; *v && w + SKIP_ONE < sizeof(tmp); v++) {
static const struct {
const char *ent;
char c;
} ents[] = {{"&lt;", '<'}, {"&gt;", '>'}, {"&amp;", '&'},
{"&quot;", '"'}, {"&nbsp;", ' '}, {NULL, 0}};
bool hit = false;
for (int k = 0; ents[k].ent && !hit && *v == '&'; k++) {
size_t el = strlen(ents[k].ent);
if (strncmp(v, ents[k].ent, el) == 0) {
tmp[w++] = ents[k].c;
v += el - SKIP_ONE;
hit = true;
}
}
if (!hit) {
tmp[w++] = *v;
}
}
tmp[w] = '\0';
/* a label cut short can end inside a tag: drop it, not show it */
char *lt = strrchr(tmp, '<');
if (lt && !strchr(lt, '>') &&
(isalpha((unsigned char)lt[SKIP_ONE]) || lt[SKIP_ONE] == '/' || lt[SKIP_ONE] == '!')) {
*lt = '\0';
w = (size_t)(lt - tmp);
}
return dg_label_text(dg_s(tmp, w), out, cap);
}
typedef struct {
char id[DG_ID_MAX];
char label[DG_LABEL_MAX];
char source[DG_ID_MAX];
char target[DG_ID_MAX];
char parent[DG_ID_MAX];
bool vertex;
bool edge;
bool text; /* a free text box */
bool edge_label; /* the label of the edge `parent` */
bool dashed;
bool no_arrow;
char kind[DG_KIND_MAX];
const char *link; /* the cell's `link` attribute, NULL for none */
} dg_cell_t;
/* The cell a `<mxCell ...>` tag (inside an `<object>` wrapper: `wrap`) says. */
static void dg_drawio_cell(CBMArena *a, const char *tag, const char *tag_end, const char *wrap,
const char *wrap_end, dg_cell_t *c) {
char buf[DG_ATTR_MAX];
memset(c, 0, sizeof(*c));
if (dg_xml_attr(wrap ? wrap : tag, wrap ? wrap_end : tag_end, "link", buf, sizeof(buf)) &&
buf[0]) {
c->link = cbm_arena_strdup(a, buf);
}
const char *idp = wrap ? wrap : tag;
const char *ide = wrap ? wrap_end : tag_end;
dg_xml_attr(idp, ide, "id", c->id, sizeof(c->id));
if (dg_xml_attr(wrap ? wrap : tag, wrap ? wrap_end : tag_end, wrap ? "label" : "value", buf,
sizeof(buf))) {
dg_drawio_text(buf, c->label, sizeof(c->label));
}
c->vertex = dg_xml_attr(tag, tag_end, "vertex", buf, sizeof(buf)) && buf[0] == '1';
c->edge = dg_xml_attr(tag, tag_end, "edge", buf, sizeof(buf)) && buf[0] == '1';
dg_xml_attr(tag, tag_end, "source", c->source, sizeof(c->source));
dg_xml_attr(tag, tag_end, "target", c->target, sizeof(c->target));
dg_xml_attr(tag, tag_end, "parent", c->parent, sizeof(c->parent));
if (dg_xml_attr(tag, tag_end, "style", buf, sizeof(buf))) {
c->text = strncmp(buf, "text;", CBM_SZ_5) == 0 || strncmp(buf, "text", CBM_SZ_4) == 0;
c->edge_label = strstr(buf, "edgeLabel") != NULL;
c->dashed = strstr(buf, "dashed=1") != NULL;
c->no_arrow = strstr(buf, "endArrow=none") != NULL;
const char *shape = strstr(buf, "shape=");
const char *k = shape ? shape + CBM_SZ_6 : buf;
size_t kl = 0;
while (k[kl] && k[kl] != ';' && k[kl] != '=' && kl + SKIP_ONE < sizeof(c->kind)) {
kl++;
}
if (k[kl] == '=' && !shape) {
kl = 0; /* the first entry is a setting, not a shape */
}
memcpy(c->kind, k, kl);
c->kind[kl] = '\0';
}
if (!c->kind[0]) {
snprintf(c->kind, sizeof(c->kind), "%s", "shape");
}
}
/* The elements and arrows of one draw.io page's mxGraphModel. */
static void dg_drawio(dg_t *dg, dg_lines_t *it, dg_str_t *title) {
(void)title;
snprintf(dg->kind, sizeof(dg->kind), "%s", "drawio");
dg->element_kind = "shape";
const char *p = it->p;
const char *end = it->end;
dg_cell_t *cells =
(dg_cell_t *)cbm_arena_alloc(dg->ctx->arena, DG_DRAWIO_CELLS * sizeof(dg_cell_t));
if (!cells) {
dg->oom = true;
return;
}
int n = 0;
for (const char *q = p; q < end && n < DG_DRAWIO_CELLS;) {
const char *lt = memchr(q, '<', (size_t)(end - q));
if (!lt) {
break;
}
const char *gt = memchr(lt, '>', (size_t)(end - lt));
if (!gt) {
break;
}
bool object = (size_t)(end - lt) > CBM_SZ_8 && (memcmp(lt, "<object ", CBM_SZ_8) == 0 ||
memcmp(lt, "<UserObject ", 12) == 0);
if (object) {
const char *cell = strstr(gt, "<mxCell");
if (cell && cell < end) {
const char *cend = memchr(cell, '>', (size_t)(end - cell));
if (cend) {
dg_drawio_cell(dg->ctx->arena, cell, cend, lt, gt, &cells[n++]);
q = cend + SKIP_ONE;
continue;
}
}
} else if ((size_t)(end - lt) > CBM_SZ_7 && memcmp(lt, "<mxCell", CBM_SZ_7) == 0) {
dg_drawio_cell(dg->ctx->arena, lt, gt, NULL, NULL, &cells[n++]);
}
q = gt + SKIP_ONE;
}
uint32_t line = dg->line0;
for (int i = 0; i < n; i++) { /* the shapes with text */
dg_cell_t *c = &cells[i];
if (c->vertex && !c->edge_label && c->label[0]) {
if (c->text) {
dg_doc_add(dg, c->label, strlen(c->label));
} else {
const char *qn = dg_element(dg, dg_s(c->id, strlen(c->id)),
dg_s(c->label, strlen(c->label)), c->kind, line);
dg_link(dg, qn, dg_s(c->link, c->link ? strlen(c->link) : 0),
CBM_DOCLINK_DG_DRAWIO_LINK, line);
}
}
}
for (int i = 0; i < n; i++) { /* the connections */
dg_cell_t *c = &cells[i];
if (!c->edge || !c->source[0] || !c->target[0]) {
continue;
}
const char *label = c->label;
for (int k = 0; k < n && !label[0]; k++) {
if (cells[k].edge_label && strcmp(cells[k].parent, c->id) == 0) {
label = cells[k].label;
}
}
if (!cbm_ht_get(dg->ids, c->source) || !cbm_ht_get(dg->ids, c->target)) {
continue; /* an end without text names nothing */
}
dg_arrow(dg, dg_s(c->source, strlen(c->source)), dg_s(c->target, strlen(c->target)),
dg_s(label, strlen(label)),
c->dashed ? "dotted"
: c->no_arrow ? "line"
: "arrow",
line);
}
}
/* ── Excalidraw ──────────────────────────────────────────────────── */
enum { DG_EX_MARGIN = 8 }; /* px: how near a loose arrow end comes to the shape it meets */
/* A shape of the scene: its box, and the label it gets. */
typedef struct {
const char *id;
const char *type;
double x0;
double y0;
double x1;
double y1;
double area;
const char *label;
double label_x; /* a loose label's place: the topmost text inside wins */
double label_y;
const char *link; /* the shape's `link`, NULL for none */
bool fixed; /* bound text or a frame's name: no loose text replaces it */
bool region; /* a box that can hold loose text and meet loose arrow ends */
bool frame; /* a region around every other shape, the drawing's frame: loose
* arrow ends meet it at its border only */
} ex_shape_t;
static const char *dg_json_str(yyjson_val *obj, const char *key) {
yyjson_val *v = yyjson_obj_get(obj, key);
return yyjson_is_str(v) ? yyjson_get_str(v) : NULL;
}
static double ex_num(yyjson_val *obj, const char *key) {
yyjson_val *v = yyjson_obj_get(obj, key);
return yyjson_is_num(v) ? yyjson_get_num(v) : 0.0;
}
static bool ex_live(yyjson_val *e) {
return !yyjson_get_bool(yyjson_obj_get(e, "isDeleted"));
}
static bool ex_is(const char *type, const char *const *kinds) {
for (int k = 0; kinds[k]; k++) {
if (strcmp(type, kinds[k]) == 0) {
return true;
}
}
return false;
}
static const char *const EX_REGIONS[] = {"rectangle", "ellipse", "diamond", "frame",
"magicframe", "embeddable", "iframe", NULL};
static const char *const EX_NOT_SHAPES[] = {"text", "arrow", "line", "freedraw", "selection", NULL};
static int ex_by_area(const void *a, const void *b) {
const ex_shape_t *x = (const ex_shape_t *)a;
const ex_shape_t *y = (const ex_shape_t *)b;
if (x->area != y->area) {
return x->area < y->area ? -1 : SKIP_ONE;
}
return strcmp(x->id, y->id);
}
/* The smallest region whose box, grown by margin, holds (x, y): -1 for none.
* The shapes are sorted by area, so the first hit is the smallest. An arrow
* end (labelled) deep inside the drawing's frame points at something in the
* drawing, not at the frame: none. */
static int ex_region_at(const ex_shape_t *s, int n, double x, double y, double margin,
bool labelled) {
for (int i = 0; i < n; i++) {
if (!s[i].region || (labelled && !s[i].label)) {
continue;
}
if (x >= s[i].x0 - margin && x <= s[i].x1 + margin && y >= s[i].y0 - margin &&
y <= s[i].y1 + margin) {
bool deep = x > s[i].x0 + margin && x < s[i].x1 - margin && y > s[i].y0 + margin &&
y < s[i].y1 - margin;
return labelled && s[i].frame && deep ? -1 : i;
}
}
return -1;
}
/* A loose text: the label of the smallest region it sits in (the topmost
* text of several), else the diagram's docstring. */
static void ex_loose_text(dg_t *dg, ex_shape_t *s, int n, yyjson_val *e, const char *text) {
double x = ex_num(e, "x");
double y = ex_num(e, "y");
int i = ex_region_at(s, n, x + ex_num(e, "width") / PAIR_LEN,
y + ex_num(e, "height") / PAIR_LEN, 0.0, false);
if (i < 0 || s[i].fixed) {
dg_doc_add(dg, text, strlen(text));
return;
}
if (s[i].label && (s[i].label_y < y || (s[i].label_y == y && s[i].label_x <= x))) {
dg_doc_add(dg, text, strlen(text));
return;
}
if (s[i].label) {
dg_doc_add(dg, s[i].label, strlen(s[i].label));
}
s[i].label = text;
s[i].label_x = x;
s[i].label_y = y;
}
/* The end bound to free text: the shape that text names, else the text as an
* element of its own. */
static const char *ex_text_end(dg_t *dg, const ex_shape_t *s, int n, CBMHashTable *texts,
const char *id) {
const char *text = (const char *)cbm_ht_get(texts, id);
if (!text) {
return NULL;
}
for (int i = 0; i < n; i++) {
if (s[i].label == text) {
return s[i].id;
}
}
return dg_element(dg, dg_s(id, strlen(id)), dg_s(text, strlen(text)), "text", dg->line0) ? id
: NULL;
}
/* An arrow end: the shape (or free text) it is bound to, else (arrows only)
* the smallest labelled region the point meets. */
static const char *ex_end(dg_t *dg, const ex_shape_t *s, int n, CBMHashTable *texts, yyjson_val *e,
const char *binding, bool first, bool loose_ok) {
const char *bound = dg_json_str(yyjson_obj_get(e, binding), "elementId");
if (bound) {
return cbm_ht_get(dg->ids, bound) ? bound : ex_text_end(dg, s, n, texts, bound);
}
yyjson_val *pts = yyjson_obj_get(e, "points");
yyjson_val *pt = first ? yyjson_arr_get_first(pts) : yyjson_arr_get_last(pts);
if (!loose_ok || !yyjson_is_arr(pt)) {
return NULL;
}
double x = ex_num(e, "x") + yyjson_get_num(yyjson_arr_get(pt, 0));
double y = ex_num(e, "y") + yyjson_get_num(yyjson_arr_get(pt, SKIP_ONE));
int i = ex_region_at(s, n, x, y, DG_EX_MARGIN, true);
return i >= 0 ? s[i].id : NULL;
}
static void ex_arrows(dg_t *dg, const ex_shape_t *s, int n, yyjson_val *els, CBMHashTable *labels,
CBMHashTable *texts) {
size_t i;
size_t max;
yyjson_val *e;
yyjson_arr_foreach(els, i, max, e) {
const char *type = dg_json_str(e, "type");
const char *id = dg_json_str(e, "id");
if (!type || !id || !ex_live(e) ||
(strcmp(type, "arrow") != 0 && strcmp(type, "line") != 0)) {
continue;
}
bool arrow = strcmp(type, "arrow") == 0;
const char *from = ex_end(dg, s, n, texts, e, "startBinding", true, arrow);
const char *to = ex_end(dg, s, n, texts, e, "endBinding", false, arrow);
bool loose = !dg_json_str(yyjson_obj_get(e, "startBinding"), "elementId") ||
!dg_json_str(yyjson_obj_get(e, "endBinding"), "elementId");
if (!from || !to || (loose && strcmp(from, to) == 0)) {
continue; /* unbound, bound to a shape without text, or a loose loop in one box */
}
const char *label = (const char *)cbm_ht_get(labels, id);
dg_arrow(dg, dg_s(from, strlen(from)), dg_s(to, strlen(to)),
dg_s(label ? label : "", label ? strlen(label) : 0), arrow ? "arrow" : "line",
dg->line0);
}
}
static void dg_excalidraw(dg_t *dg, dg_lines_t *it, dg_str_t *title) {
(void)title;
snprintf(dg->kind, sizeof(dg->kind), "%s", "excalidraw");
dg->element_kind = "shape";
yyjson_doc *doc = yyjson_read(it->p, (size_t)(it->end - it->p), 0);
yyjson_val *els = doc ? yyjson_obj_get(yyjson_doc_get_root(doc), "elements") : NULL;
size_t total = yyjson_arr_size(els);
CBMHashTable *labels =
yyjson_is_arr(els) ? cbm_ht_create_in(CBM_MEM_CLASS_OTHER, CBM_SZ_64) : NULL;
ex_shape_t *s = labels && total
? (ex_shape_t *)cbm_arena_alloc(dg->ctx->arena, total * sizeof(ex_shape_t))
: NULL;
CBMHashTable *texts = s ? cbm_ht_create_in(CBM_MEM_CLASS_OTHER, CBM_SZ_64) : NULL;
if (!texts) {
cbm_ht_free(labels);
yyjson_doc_free(doc);
return;
}
size_t i;
size_t max;
yyjson_val *e;
yyjson_arr_foreach(els, i, max, e) { /* bound text: the label of its container */
const char *text = dg_json_str(e, "originalText");
text = text ? text : dg_json_str(e, "text");
const char *type = dg_json_str(e, "type");
const char *container = dg_json_str(e, "containerId");
const char *id = dg_json_str(e, "id");
if (ex_live(e) && type && strcmp(type, "text") == 0 && text && container) {
cbm_ht_set(labels, container, (void *)text);
} else if (ex_live(e) && type && strcmp(type, "text") == 0 && text && id) {
cbm_ht_set(texts, id, (void *)text); /* free text an arrow may be bound to */
}
}
int n = 0;
yyjson_arr_foreach(els, i, max, e) { /* the shapes */
const char *type = dg_json_str(e, "type");
const char *id = dg_json_str(e, "id");
if (!type || !id || !ex_live(e) || ex_is(type, EX_NOT_SHAPES)) {
continue;
}
ex_shape_t *sh = &s[n++];
memset(sh, 0, sizeof(*sh));
sh->id = id;
sh->type = type;
sh->x0 = ex_num(e, "x");
sh->y0 = ex_num(e, "y");
sh->x1 = sh->x0 + ex_num(e, "width");
sh->y1 = sh->y0 + ex_num(e, "height");
sh->area = (sh->x1 - sh->x0) * (sh->y1 - sh->y0);
sh->region = ex_is(type, EX_REGIONS);
sh->label = (const char *)cbm_ht_get(labels, id);
sh->label = sh->label ? sh->label : dg_json_str(e, "name"); /* a frame's name */
sh->fixed = sh->label != NULL;
sh->link = dg_json_str(e, "link");
}
qsort(s, (size_t)n, sizeof(ex_shape_t), ex_by_area);
yyjson_arr_foreach(els, i, max, e) { /* loose text: in a shape, or the docstring */
const char *type = dg_json_str(e, "type");
const char *text = dg_json_str(e, "originalText");
text = text ? text : dg_json_str(e, "text");
if (ex_live(e) && type && strcmp(type, "text") == 0 && text &&
!dg_json_str(e, "containerId")) {
ex_loose_text(dg, s, n, e, text);
}
}
if (n > SKIP_ONE && s[n - SKIP_ONE].region) { /* only the largest box can frame the rest */
ex_shape_t *f = &s[n - SKIP_ONE];
bool all = true;
for (int j = 0; j < n - SKIP_ONE && all; j++) {
all = s[j].x0 >= f->x0 && s[j].x1 <= f->x1 && s[j].y0 >= f->y0 && s[j].y1 <= f->y1;
}
f->frame = all;
}
for (int k = 0; k < n; k++) {
if (s[k].label) {
const char *qn = dg_element(dg, dg_s(s[k].id, strlen(s[k].id)),
dg_s(s[k].label, strlen(s[k].label)), s[k].type, dg->line0);
dg_link(dg, qn, dg_s(s[k].link, s[k].link ? strlen(s[k].link) : 0),
CBM_DOCLINK_DG_EXCALIDRAW_LINK, dg->line0);
}
}
ex_arrows(dg, s, n, els, labels, texts);
cbm_ht_free(texts);
cbm_ht_free(labels);
yyjson_doc_free(doc);
}
/* ── One diagram ─────────────────────────────────────────────────── */
typedef struct {
const char *format;
const char *title; /* a draw.io page's name, NULL for none */
const char *fallback; /* the name when the text gives none: a diagram file's */
const char *parent_qn; /* the Section's, NULL: the file */
int ordinal;
const char *text;
size_t len;
uint32_t first_line; /* the text's first line in the file */
uint32_t start_line; /* the block's lines */
uint32_t end_line;
} dg_source_t;
static void dg_build(CBMExtractCtx *ctx, const dg_source_t *src) {
dg_t dg;
memset(&dg, 0, sizeof(dg));
dg.ctx = ctx;
dg.format = src->format;
const char *parent = src->parent_qn ? src->parent_qn : ctx->module_qn;
if (!parent) {
return;
}
dg.qn = cbm_arena_sprintf(ctx->arena, "%s#diagram-%d", parent, src->ordinal);
dg.ids = cbm_ht_create_in(CBM_MEM_CLASS_OTHER, CBM_SZ_32);
if (!dg.qn || !dg.ids) {
cbm_ht_free(dg.ids);
return;
}
CBMDefinition def;
memset(&def, 0, sizeof(def));
def.label = "Diagram";
def.qualified_name = dg.qn;
def.file_path = ctx->rel_path;
def.start_line = src->start_line;
def.end_line = src->end_line;
def.parent_class = src->parent_qn;
def.name = src->format;
cbm_defs_push(&ctx->result->defs, ctx->arena, def);
dg.def_index = ctx->result->defs.count - SKIP_ONE;
dg.line0 = src->first_line;
dg_lines_t it = {src->text, src->text + src->len, src->first_line};
dg_str_t title = src->title ? dg_s(src->title, strlen(src->title)) : dg_s(NULL, 0);
if (strcmp(src->format, "mermaid") == 0) {
dg_mermaid(&dg, &it, &title);
} else if (strcmp(src->format, "plantuml") == 0) {
dg_plantuml(&dg, &it, &title);
} else if (strcmp(src->format, "dot") == 0) {
dg_dot(&dg, &it, &title);
} else if (strcmp(src->format, "d2") == 0) {
dg_d2(&dg, &it, &title);
} else if (strcmp(src->format, "drawio") == 0) {
dg_drawio(&dg, &it, &title);
} else if (strcmp(src->format, "excalidraw") == 0) {
dg_excalidraw(&dg, &it, &title);
}
cbm_ht_free(dg.ids);
CBMDefinition *d = &ctx->result->defs.items[dg.def_index];
char tbuf[DG_LABEL_MAX];
size_t tl = dg_label_text(title, tbuf, sizeof(tbuf));
d->name = tl ? cbm_arena_strndup(ctx->arena, tbuf, tl)
: src->fallback ? src->fallback
: cbm_arena_sprintf(ctx->arena, "%s %s", src->format,
dg.kind[0] ? dg.kind : "diagram");
d->docstring = dg.doc_len ? cbm_arena_strndup(ctx->arena, dg.doc, dg.doc_len) : NULL;
const char *kind = dg.kind[0] ? cbm_arena_strdup(ctx->arena, dg.kind) : "unknown";
const char **kv = (const char **)cbm_arena_alloc(ctx->arena, CBM_SZ_5 * sizeof(char *));
if (kv) {
kv[0] = "format";
kv[SKIP_ONE] = src->format;
kv[PAIR_LEN] = "diagram_kind";
kv[CBM_SZ_3] = kind;
kv[CBM_SZ_4] = NULL;
d->extra_props = kv;
}
}
/* ── Sources ─────────────────────────────────────────────────────── */
/* The diagram format an info string names, NULL for none. */
static const char *dg_format_of(const char *s, size_t n) {
static const struct {
const char *word;
const char *format;
} names[] = {
{"mermaid", "mermaid"}, {"plantuml", "plantuml"}, {"puml", "plantuml"}, {"uml", "plantuml"},
{"dot", "dot"}, {"graphviz", "dot"}, {"d2", "d2"}, {NULL, NULL}};
for (int k = 0; names[k].word; k++) {
size_t l = strlen(names[k].word);
bool same = n == l;
for (size_t i = 0; same && i < l; i++) {
same = tolower((unsigned char)s[i]) == names[k].word[i];
}
if (same) {
return names[k].format;
}
}
return NULL;
}
/* The Section a line of the file is written under: the last heading at or
* before it, NULL above the first. */
static const char *dg_section_at(const CBMExtractCtx *ctx, uint32_t line) {
const char *qn = NULL;
uint32_t best = 0;
const CBMDefArray *defs = &ctx->result->defs;
for (int i = 0; i < defs->count; i++) {
const CBMDefinition *d = &defs->items[i];
if (d->label && strcmp(d->label, "Section") == 0 && d->start_line <= line &&
d->start_line >= best) {
best = d->start_line;
qn = d->qualified_name;
}
}
return qn;
}
typedef struct {
const char *parent;
int count;
} dg_ordinal_t;
enum { DG_PARENTS_MAX = 512 };
/* The next ordinal of a parent's diagrams. */
static int dg_next_ordinal(dg_ordinal_t *ords, int *n, const char *parent) {
for (int i = 0; i < *n; i++) {
if (ords[i].parent == parent) {
return ++ords[i].count;
}
}
if (*n < DG_PARENTS_MAX) {
ords[(*n)++] = (dg_ordinal_t){parent, SKIP_ONE};
}
return SKIP_ONE;
}
static bool dg_kind(TSNode n, const char *k) {
return !ts_node_is_null(n) && strcmp(ts_node_type(n), k) == 0;
}
/* The diagram format a Markdown fenced block's info string names, NULL for
* none; *content is the block's text. */
static const char *dg_fence_format(const char *source, TSNode block, TSNode *content) {
TSNode info = {0};
*content = (TSNode){0};
uint32_t nc = ts_node_named_child_count(block);
for (uint32_t i = 0; i < nc; i++) {
TSNode c = ts_node_named_child(block, i);
info = dg_kind(c, "info_string") ? c : info;
*content = dg_kind(c, "code_fence_content") ? c : *content;
}
if (ts_node_is_null(info) || ts_node_is_null(*content)) {
return NULL;
}
TSNode lang = ts_node_named_child(info, 0);
TSNode word = dg_kind(lang, "language") ? lang : info;
uint32_t wb = ts_node_start_byte(word);
uint32_t we = ts_node_end_byte(word);
return dg_format_of(source + wb, we - wb);
}
/* A fenced block of a Markdown file: a diagram when its info string says so. */
static void dg_markdown_fence(CBMExtractCtx *ctx, TSNode block, dg_ordinal_t *ords, int *nords) {
TSNode content;
const char *format = dg_fence_format(ctx->source, block, &content);
if (!format) {
return;
}
uint32_t start = ts_node_start_point(block).row + SKIP_ONE;
const char *parent = dg_section_at(ctx, start);
uint32_t cb = ts_node_start_byte(content);
dg_source_t src = {
.format = format,
.parent_qn = parent,
.ordinal = dg_next_ordinal(ords, nords, parent),
.text = ctx->source + cb,
.len = ts_node_end_byte(content) - cb,
.first_line = ts_node_start_point(content).row + SKIP_ONE,
.start_line = start,
.end_line = ts_node_end_point(block).row + SKIP_ONE,
};
dg_build(ctx, &src);
}
static void dg_markdown(CBMExtractCtx *ctx) {
dg_ordinal_t *ords =
(dg_ordinal_t *)cbm_arena_alloc(ctx->arena, DG_PARENTS_MAX * sizeof(dg_ordinal_t));
if (!ords) {
return;
}
int nords = 0;
TSTreeCursor cur = ts_tree_cursor_new(ctx->root);
for (bool more = true; more;) {
TSNode n = ts_tree_cursor_current_node(&cur);
if (dg_kind(n, "fenced_code_block")) {
dg_markdown_fence(ctx, n, ords, &nords);
} else if (ts_tree_cursor_goto_first_child(&cur)) {
continue;
}
while (!ts_tree_cursor_goto_next_sibling(&cur)) {
if (!ts_tree_cursor_goto_parent(&cur)) {
more = false;
break;
}
}
}
ts_tree_cursor_delete(&cur);
}
/* ── RST directives and AsciiDoc blocks ──────────────────────────── */
/* The diagram format a directive (RST) or block style (AsciiDoc) names; for
* Sphinx's `digraph` / `graph` also the graph keyword its body goes inside. */
static const char *dg_block_format(dg_str_t word, const char **wrap) {
static const struct {
const char *word;
const char *format;
const char *wrap;
} names[] = {
{"mermaid", "mermaid", NULL}, {"uml", "plantuml", NULL}, {"plantuml", "plantuml", NULL},
{"graphviz", "dot", NULL}, {"dot", "dot", NULL}, {"digraph", "dot", "digraph"},
{"graph", "dot", "graph"}, {"d2", "d2", NULL}, {NULL, NULL, NULL}};
*wrap = NULL;
for (int k = 0; names[k].word; k++) {
if (dg_eq(word, names[k].word)) {
*wrap = names[k].wrap;
return names[k].format;
}
}
return NULL;
}
static dg_str_t dg_node_text(const CBMExtractCtx *ctx, TSNode n) {
uint32_t b = ts_node_start_byte(n);
return dg_s(ctx->source + b, ts_node_end_byte(n) - b);
}
/* The first descendant of kind k, a null node for none. */
static TSNode dg_find(TSNode n, const char *k) {
uint32_t nc = ts_node_named_child_count(n);
for (uint32_t i = 0; i < nc; i++) {
TSNode c = ts_node_named_child(n, i);
if (dg_kind(c, k)) {
return c;
}
TSNode d = dg_find(c, k);
if (!ts_node_is_null(d)) {
return d;
}
}
return (TSNode){0};
}
/* A directive's `:caption:` option, NULL for none. */
static const char *dg_rst_caption(const CBMExtractCtx *ctx, TSNode options) {
uint32_t nc = ts_node_is_null(options) ? 0 : ts_node_named_child_count(options);
for (uint32_t i = 0; i < nc; i++) {
TSNode f = ts_node_named_child(options, i);
TSNode name = dg_find(f, "field_name");
TSNode body = dg_find(f, "field_body");
dg_str_t key = ts_node_is_null(name) ? dg_s("", 0) : dg_trim(dg_node_text(ctx, name));
while (key.n && key.p[0] == ':') { /* `:caption:` or `caption` */
key = dg_s(key.p + SKIP_ONE, key.n - SKIP_ONE);
}
while (key.n && key.p[key.n - SKIP_ONE] == ':') {
key.n--;
}
if (!ts_node_is_null(body) && dg_eq(key, "caption")) {
dg_str_t v = dg_trim(dg_node_text(ctx, body));
return cbm_arena_strndup(ctx->arena, v.p, v.n);
}
}
return NULL;
}
/* `.. mermaid::`, `.. uml::`, `.. graphviz::`, `.. digraph:: name`: a
* diagram when the directive holds its text (a file argument is the file,
* indexed as itself). */
static void dg_rst_directive(CBMExtractCtx *ctx, TSNode dir, dg_ordinal_t *ords, int *nords) {
TSNode type = dg_find(dir, "type");
const char *wrap = NULL;
const char *format =
ts_node_is_null(type) ? NULL : dg_block_format(dg_trim(dg_node_text(ctx, type)), &wrap);
TSNode content = format ? dg_find(dir, "content") : (TSNode){0};
if (ts_node_is_null(content)) {
return;
}
dg_str_t text = dg_node_text(ctx, content);
uint32_t first = ts_node_start_point(content).row + SKIP_ONE;
if (wrap) { /* Sphinx writes the statements only */
TSNode args = dg_find(dir, "arguments");
dg_str_t name = ts_node_is_null(args) ? dg_s("", 0) : dg_trim(dg_node_text(ctx, args));
char *w = cbm_arena_sprintf(ctx->arena, "%s \"%.*s\" {\n%.*s\n}\n", wrap, (int)name.n,
name.p, (int)text.n, text.p);
if (!w) {
return;
}
text = dg_s(w, strlen(w));
first--;
}
uint32_t start = ts_node_start_point(dir).row + SKIP_ONE;
const char *parent = dg_section_at(ctx, start);
dg_source_t src = {
.format = format,
.title = dg_rst_caption(ctx, dg_find(dir, "options")),
.parent_qn = parent,
.ordinal = dg_next_ordinal(ords, nords, parent),
.text = text.p,
.len = text.n,
.first_line = first,
.start_line = start,
.end_line = ts_node_end_point(dir).row + SKIP_ONE,
};
dg_build(ctx, &src);
}
static void dg_rst(CBMExtractCtx *ctx) {
dg_ordinal_t *ords =
(dg_ordinal_t *)cbm_arena_alloc(ctx->arena, DG_PARENTS_MAX * sizeof(dg_ordinal_t));
if (!ords) {
return;
}
int nords = 0;
TSTreeCursor cur = ts_tree_cursor_new(ctx->root);
for (bool more = true; more;) {
TSNode n = ts_tree_cursor_current_node(&cur);
if (dg_kind(n, "directive")) {
dg_rst_directive(ctx, n, ords, &nords);
} else if (ts_tree_cursor_goto_first_child(&cur)) {
continue;
}
while (!ts_tree_cursor_goto_next_sibling(&cur)) {
if (!ts_tree_cursor_goto_parent(&cur)) {
more = false;
break;
}
}
}
ts_tree_cursor_delete(&cur);
}
/* An AsciiDoc block attribute line `[plantuml, target=x]` or
* `[source,mermaid]`: the diagram format it names, NULL for none. */
static const char *dg_adoc_style(dg_str_t ln) {
if (ln.n < CBM_SZ_3 || ln.p[0] != '[' || ln.p[ln.n - SKIP_ONE] != ']' ||
ln.p[SKIP_ONE] == '[') {
return NULL;
}
dg_str_t attrs = dg_s(ln.p + SKIP_ONE, ln.n - PAIR_LEN);
const char *comma = memchr(attrs.p, ',', attrs.n);
dg_str_t style = dg_trim(dg_s(attrs.p, comma ? (size_t)(comma - attrs.p) : attrs.n));
if (dg_eq(style, "source") && comma) { /* a listing in a diagram language */
dg_str_t rest = dg_s(comma + SKIP_ONE, (size_t)(attrs.p + attrs.n - comma) - SKIP_ONE);
const char *c2 = memchr(rest.p, ',', rest.n);
style = dg_trim(dg_s(rest.p, c2 ? (size_t)(c2 - rest.p) : rest.n));
}
const char *wrap = NULL;
const char *format = dg_block_format(style, &wrap);
return wrap ? NULL : format;
}
/* `----` or `....` (four or more): a listing or literal block delimiter. */
static bool dg_adoc_delimiter(dg_str_t ln) {
if (ln.n < CBM_SZ_4 || (ln.p[0] != '-' && ln.p[0] != '.')) {
return false;
}
for (size_t i = SKIP_ONE; i < ln.n; i++) {
if (ln.p[i] != ln.p[0]) {
return false;
}
}
return true;
}
/* AsciiDoc diagram blocks (Asciidoctor Diagram): the delimited block after a
* `[mermaid]`, `[plantuml]`, `[graphviz]` or `[d2]` line, titled by the
* `.Title` line before it. */
static void dg_asciidoc(CBMExtractCtx *ctx) {
dg_ordinal_t *ords =
(dg_ordinal_t *)cbm_arena_alloc(ctx->arena, DG_PARENTS_MAX * sizeof(dg_ordinal_t));
if (!ords) {
return;
}
int nords = 0;
dg_lines_t it = {ctx->source, ctx->source + ctx->source_len, SKIP_ONE};
dg_str_t title = dg_s(NULL, 0);
dg_str_t ln;
uint32_t line = 0;
while (dg_next_line(&it, &ln, &line)) {
dg_str_t t = dg_trim(ln);
if (t.n > SKIP_ONE && t.p[0] == '.' && t.p[SKIP_ONE] != '.' &&
!isspace((unsigned char)t.p[SKIP_ONE])) {
title = dg_s(t.p + SKIP_ONE, t.n - SKIP_ONE);
continue;
}
const char *format = dg_adoc_style(t);
dg_str_t delim;
uint32_t attr_line = line;
if (!format || !dg_next_line(&it, &delim, &line) || !dg_adoc_delimiter(dg_trim(delim))) {
title = t.n && t.p[0] == '[' ? title : dg_s(NULL, 0); /* attribute lines stack */
continue;
}
delim = dg_trim(delim);
const char *body = it.p;
uint32_t first = it.line;
const char *stop = it.end;
uint32_t last = line;
while (dg_next_line(&it, &ln, &line)) {
dg_str_t c = dg_trim(ln);
if (c.n == delim.n && memcmp(c.p, delim.p, c.n) == 0) {
stop = ln.p;
last = line;
break;
}
}
const char *parent = dg_section_at(ctx, attr_line);
dg_source_t src = {
.format = format,
.title = title.n ? cbm_arena_strndup(ctx->arena, title.p, title.n) : NULL,
.parent_qn = parent,
.ordinal = dg_next_ordinal(ords, &nords, parent),
.text = body,
.len = (size_t)(stop - body),
.first_line = first,
.start_line = attr_line,
.end_line = last,
};
dg_build(ctx, &src);
title = dg_s(NULL, 0);
}
}
/* ── Diagram files ───────────────────────────────────────────────── */
enum {
DG_INFLATE_MAX = 64 * 1024 * 1024, /* a draw.io page; beyond it the page is skipped */
DG_STARTUML_LEN = 9, /* @startuml */
DG_DIAGRAM_CLOSE_LEN = 10, /* </diagram> */
DG_CONTENT_ATTR_LEN = 9, /* content=\" */
DG_PAYLOAD_START_LEN = 22, /* <!-- payload-start --> */
DG_PAYLOAD_VERSION_LEN = 21, /* <!-- payload-version: */
DG_ASCII_END = 0x80,
DG_UTF8_LEAD_MASK = 0xE0, /* a two-byte sequence's lead: 110xxxxx */
DG_UTF8_LEAD2 = 0xC0,
DG_UTF8_LEAD_LATIN1 = 0xC3, /* the last lead below U+0100 */
DG_UTF8_LOW5 = 0x1F,
DG_UTF8_LOW6 = 0x3F,
};
static bool dg_ends(const char *path, const char *suffix) {
size_t pl = strlen(path);
size_t sl = strlen(suffix);
return pl >= sl && strcmp(path + pl - sl, suffix) == 0;
}
/* The file line of p; line 1 for text that is no slice of the file (a page
* inflated or unescaped into a buffer of its own). */
static uint32_t dg_line_of(const CBMExtractCtx *ctx, const char *p) {
uint32_t line = SKIP_ONE;
if (p < ctx->source || p > ctx->source + ctx->source_len) {
return line;
}
for (const char *q = ctx->source; q < p; q++) {
line += *q == '\n';
}
return line;
}
/* A diagram file's name: the file name without its extension (nor a .drawio
* or .excalidraw before it). */
static const char *dg_file_stem(CBMExtractCtx *ctx) {
const char *base = strrchr(ctx->rel_path, '/');
base = base ? base + SKIP_ONE : ctx->rel_path;
const char *dot = strrchr(base, '.');
size_t n = dot && dot > base ? (size_t)(dot - base) : strlen(base);
static const char *const inner[] = {".drawio", ".excalidraw", NULL};
for (int k = 0; inner[k]; k++) {
size_t l = strlen(inner[k]);
if (n > l && strncmp(base + n - l, inner[k], l) == 0) {
n -= l;
}
}
return cbm_arena_strndup(ctx->arena, base, n);
}
static void dg_file_one(CBMExtractCtx *ctx, const char *format, const char *title, int ordinal,
const char *text, size_t len, uint32_t first, uint32_t last) {
dg_source_t src = {
.format = format,
.title = title,
.fallback = dg_file_stem(ctx),
.parent_qn = NULL,
.ordinal = ordinal,
.text = text,
.len = len,
.first_line = first,
.start_line = first,
.end_line = last,
};
dg_build(ctx, &src);
}
/* PlantUML: each @startuml ... @enduml block a diagram, its name the title. */
static void dg_file_plantuml(CBMExtractCtx *ctx) {
const char *p = ctx->source;
const char *end = ctx->source + ctx->source_len;
int ordinal = 0;
for (;;) {
const char *start = NULL;
for (const char *q = p; q + DG_STARTUML_LEN <= end && !start; q++) {
start = memcmp(q, "@startuml", DG_STARTUML_LEN) == 0 ? q : NULL;
}
if (!start) {
break;
}
const char *body = memchr(start, '\n', (size_t)(end - start));
body = body ? body + SKIP_ONE : end;
const char *stop = NULL;
for (const char *q = body; q + CBM_SZ_7 <= end && !stop; q++) {
stop = memcmp(q, "@enduml", CBM_SZ_7) == 0 ? q : NULL;
}
stop = stop ? stop : end;
dg_str_t name =
dg_trim(dg_s(start + DG_STARTUML_LEN, (size_t)(body - start) - DG_STARTUML_LEN));
const char *title = name.n ? cbm_arena_strndup(ctx->arena, name.p, name.n) : NULL;
uint32_t first = dg_line_of(ctx, body);
dg_file_one(ctx, "plantuml", title, ++ordinal, body, (size_t)(stop - body), first,
dg_line_of(ctx, stop));
p = stop;
}
if (ordinal == 0) {
dg_file_one(ctx, "plantuml", NULL, SKIP_ONE, ctx->source, (size_t)ctx->source_len, SKIP_ONE,
dg_line_of(ctx, ctx->source + ctx->source_len));
}
}
/* Base64 into out (cap bytes): the decoded length, 0 on a bad digit. */
static size_t dg_base64(const char *s, size_t n, unsigned char *out, size_t cap) {
uint32_t acc = 0;
int bits = 0;
size_t w = 0;
for (size_t i = 0; i < n; i++) {
char c = s[i];
int v = c >= 'A' && c <= 'Z' ? c - 'A'
: c >= 'a' && c <= 'z' ? c - 'a' + 26
: c >= '0' && c <= '9' ? c - '0' + 52
: c == '+' ? 62
: c == '/' ? 63
: -1;
if (v < 0) {
if (c == '=' || isspace((unsigned char)c)) {
continue;
}
return 0;
}
acc = (acc << CBM_SZ_6) | (uint32_t)v;
bits += CBM_SZ_6;
if (bits >= CBM_SZ_8) {
bits -= CBM_SZ_8;
if (w >= cap) {
return 0;
}
out[w++] = (unsigned char)(acc >> bits);
}
}
return w;
}
/* zlib data (wbits: raw deflate when negative) inflated into the arena,
* NUL-terminated. NULL when it is none or would pass DG_INFLATE_MAX. */
static char *dg_inflate(CBMExtractCtx *ctx, unsigned char *raw, size_t rn, int wbits, size_t *len) {
size_t cap = rn * CBM_SZ_8 + CBM_SZ_1K;
for (;;) {
if (cap > DG_INFLATE_MAX) {
return NULL; /* a bomb, or a page too large: the page is skipped whole */
}
char *out = (char *)cbm_arena_alloc(ctx->arena, cap + SKIP_ONE);
if (!out) {
return NULL;
}
z_stream z;
memset(&z, 0, sizeof(z));
if (inflateInit2(&z, wbits) != Z_OK) {
return NULL;
}
z.next_in = raw;
z.avail_in = (uInt)rn;
z.next_out = (Bytef *)out;
z.avail_out = (uInt)cap;
int r = inflate(&z, Z_FINISH);
inflateEnd(&z);
if (r == Z_STREAM_END) {
*len = z.total_out;
out[*len] = '\0';
return out;
}
if (r != Z_BUF_ERROR && r != Z_OK) {
return NULL;
}
cap *= PAIR_LEN;
}
}
/* A compressed draw.io page: base64, raw deflate, then URI-encoded XML. NULL
* when it is none or would pass DG_INFLATE_MAX. */
static char *dg_drawio_inflate(CBMExtractCtx *ctx, dg_str_t b64, size_t *len) {
unsigned char *raw = (unsigned char *)cbm_arena_alloc(ctx->arena, b64.n + SKIP_ONE);
size_t rn = raw ? dg_base64(b64.p, b64.n, raw, b64.n) : 0;
size_t out = 0;
char *xml = rn ? dg_inflate(ctx, raw, rn, -MAX_WBITS, &out) : NULL;
if (!xml) {
return NULL;
}
size_t w = 0; /* URI-decode in place */
for (size_t i = 0; i < out; i++) {
if (xml[i] == '%' && i + PAIR_LEN < out && isxdigit((unsigned char)xml[i + SKIP_ONE]) &&
isxdigit((unsigned char)xml[i + PAIR_LEN])) {
char hex[CBM_SZ_3] = {xml[i + SKIP_ONE], xml[i + PAIR_LEN], '\0'};
xml[w++] = (char)strtol(hex, NULL, CBM_SZ_16);
i += PAIR_LEN;
} else {
xml[w++] = xml[i];
}
}
xml[w] = '\0';
*len = w;
return xml;
}
/* Code points up to U+00FF of UTF-8 text back to the bytes they stand for;
* false on any other. */
static bool dg_utf8_bytes(const unsigned char *s, size_t n, unsigned char *out, size_t *len) {
size_t w = 0;
for (size_t i = 0; i < n; i++) {
if (s[i] < DG_ASCII_END) {
out[w++] = s[i];
} else if ((s[i] & DG_UTF8_LEAD_MASK) == DG_UTF8_LEAD2 && i + SKIP_ONE < n &&
s[i] <= DG_UTF8_LEAD_LATIN1) {
out[w++] = (unsigned char)(((s[i] & DG_UTF8_LOW5) << CBM_SZ_6) |
(s[i + SKIP_ONE] & DG_UTF8_LOW6));
i++;
} else {
return false;
}
}
*len = w;
return true;
}
/* An Excalidraw SVG export keeps its scene in a payload comment: base64 of a
* JSON wrapper whose `encoded` byte string is the scene, zlib-compressed when
* `compressed` says so. Version 1 wrote the wrapper as UTF-8, later ones
* byte for byte; a wrapper without `encoded` is the scene itself. NULL for
* none. */
static char *dg_excalidraw_svg_scene(CBMExtractCtx *ctx, size_t *len) {
const char *start = strstr(ctx->source, "<!-- payload-start -->");
const char *stop = start ? strstr(start, "<!-- payload-end -->") : NULL;
if (!stop) {
return NULL;
}
const char *b = start + DG_PAYLOAD_START_LEN;
size_t bn = (size_t)(stop - b);
unsigned char *raw = (unsigned char *)cbm_arena_alloc(ctx->arena, bn + SKIP_ONE);
size_t rn = raw ? dg_base64(b, bn, raw, bn) : 0;
if (!rn) {
return NULL;
}
raw[rn] = '\0';
const char *ver = strstr(ctx->source, "<!-- payload-version:");
bool v1 = !ver || ver[DG_PAYLOAD_VERSION_LEN] == '1';
yyjson_doc *doc = yyjson_read((const char *)raw, rn, YYJSON_READ_ALLOW_INVALID_UNICODE);
yyjson_val *root = yyjson_doc_get_root(doc);
yyjson_val *enc = yyjson_obj_get(root, "encoded");
char *scene = NULL;
if (yyjson_is_str(enc)) {
size_t sn = yyjson_get_len(enc);
unsigned char *bytes = (unsigned char *)cbm_arena_alloc(ctx->arena, sn + SKIP_ONE);
size_t nb = 0;
const unsigned char *s = (const unsigned char *)yyjson_get_str(enc);
bool ok = bytes != NULL;
if (ok && v1) {
ok = dg_utf8_bytes(s, sn, bytes, &nb);
} else if (ok) {
memcpy(bytes, s, sn);
nb = sn;
}
if (ok && yyjson_get_bool(yyjson_obj_get(root, "compressed"))) {
scene = dg_inflate(ctx, bytes, nb, MAX_WBITS, len);
} else if (ok) {
bytes[nb] = '\0';
scene = (char *)bytes;
*len = nb;
}
} else if (yyjson_is_obj(root)) {
scene = (char *)raw;
*len = rn;
}
yyjson_doc_free(doc);
return scene;
}
/* Entities of an attribute value decoded (a .drawio.svg holds the file in its
* `content` attribute). */
static char *dg_xml_unescape(CBMExtractCtx *ctx, const char *s, size_t n, size_t *len) {
char *out = (char *)cbm_arena_alloc(ctx->arena, n + SKIP_ONE);
if (!out) {
return NULL;
}
size_t w = 0;
for (size_t i = 0; i < n; i++) {
static const struct {
const char *ent;
char c;
} ents[] = {{"&lt;", '<'}, {"&gt;", '>'}, {"&quot;", '"'},
{"&apos;", '\''}, {"&amp;", '&'}, {NULL, 0}};
bool hit = false;
for (int k = 0; ents[k].ent && !hit && s[i] == '&'; k++) {
size_t el = strlen(ents[k].ent);
if (i + el <= n && memcmp(s + i, ents[k].ent, el) == 0) {
out[w++] = ents[k].c;
i += el - SKIP_ONE;
hit = true;
}
}
if (!hit) {
out[w++] = s[i];
}
}
out[w] = '\0';
*len = w;
return out;
}
/* A page's name. draw.io's default `Page-<n>` names nothing: the first page
* goes by the file, the others by the file and the page. */
static const char *dg_drawio_page_title(CBMExtractCtx *ctx, const char *name) {
if (!name[0]) {
return NULL;
}
bool dflt = strncmp(name, "Page-", CBM_SZ_5) == 0 && name[CBM_SZ_5];
for (const char *q = name + (dflt ? CBM_SZ_5 : 0); dflt && *q; q++) {
dflt = isdigit((unsigned char)*q) != 0;
}
if (!dflt) {
return cbm_arena_strdup(ctx->arena, name);
}
if (strcmp(name, "Page-1") == 0) {
return NULL;
}
return cbm_arena_sprintf(ctx->arena, "%s %s", dg_file_stem(ctx), name);
}
/* draw.io: each <diagram> page a diagram, named by the page. */
static void dg_file_drawio(CBMExtractCtx *ctx, const char *text, size_t len) {
const char *end = text + len;
int ordinal = 0;
for (const char *p = text; p < end;) {
const char *tag = NULL;
for (const char *q = p; q + CBM_SZ_8 <= end && !tag; q++) {
tag = memcmp(q, "<diagram", CBM_SZ_8) == 0 && (q[CBM_SZ_8] == ' ' || q[CBM_SZ_8] == '>')
? q
: NULL;
}
if (!tag) {
break;
}
const char *gt = memchr(tag, '>', (size_t)(end - tag));
if (!gt) {
break;
}
const char *close = NULL;
for (const char *q = gt; q + DG_DIAGRAM_CLOSE_LEN <= end && !close; q++) {
close = memcmp(q, "</diagram>", DG_DIAGRAM_CLOSE_LEN) == 0 ? q : NULL;
}
close = close ? close : end;
char name[DG_LABEL_MAX];
dg_xml_attr(tag, gt, "name", name, sizeof(name));
const char *title = dg_drawio_page_title(ctx, name);
dg_str_t inner = dg_trim(dg_s(gt + SKIP_ONE, (size_t)(close - gt) - SKIP_ONE));
uint32_t line = dg_line_of(ctx, tag);
if (inner.n && inner.p[0] == '<') {
dg_file_one(ctx, "drawio", title, ++ordinal, inner.p, inner.n, line,
dg_line_of(ctx, close));
} else if (inner.n) {
size_t xl = 0;
char *xml = dg_drawio_inflate(ctx, inner, &xl);
if (xml) {
dg_file_one(ctx, "drawio", title, ++ordinal, xml, xl, line, line);
}
}
p = close;
}
if (ordinal == 0 && strstr(text, "<mxGraphModel")) {
dg_file_one(ctx, "drawio", NULL, SKIP_ONE, text, len, SKIP_ONE, SKIP_ONE);
}
}
void cbm_extract_diagram_file(CBMExtractCtx *ctx) {
if (!ctx || !ctx->result || !ctx->source || !ctx->rel_path) {
return;
}
const char *path = ctx->rel_path;
size_t len = (size_t)ctx->source_len;
uint32_t last = dg_line_of(ctx, ctx->source + len);
if (dg_ends(path, ".puml") || dg_ends(path, ".plantuml") || dg_ends(path, ".iuml") ||
dg_ends(path, ".wsd")) {
dg_file_plantuml(ctx);
} else if (dg_ends(path, ".dot") || dg_ends(path, ".gv")) {
dg_file_one(ctx, "dot", NULL, SKIP_ONE, ctx->source, len, SKIP_ONE, last);
} else if (dg_ends(path, ".d2")) {
dg_file_one(ctx, "d2", NULL, SKIP_ONE, ctx->source, len, SKIP_ONE, last);
} else if (dg_ends(path, ".excalidraw") || dg_ends(path, ".excalidraw.json")) {
dg_file_one(ctx, "excalidraw", NULL, SKIP_ONE, ctx->source, len, SKIP_ONE, last);
} else if (dg_ends(path, ".excalidraw.svg")) {
size_t sl = 0;
char *scene = dg_excalidraw_svg_scene(ctx, &sl);
if (scene) {
dg_file_one(ctx, "excalidraw", NULL, SKIP_ONE, scene, sl, SKIP_ONE, last);
}
} else if (dg_ends(path, ".drawio.svg")) {
/* the editable SVG keeps the draw.io file in its `content` attribute */
const char *svg = strstr(ctx->source, "<svg");
const char *gt = svg ? strchr(svg, '>') : NULL;
char probe[CBM_SZ_8];
if (svg && gt && dg_xml_attr(svg, gt, "content", probe, sizeof(probe))) {
const char *v = strstr(svg, "content=\"");
const char *ve =
v ? memchr(v + DG_CONTENT_ATTR_LEN, '"', (size_t)(gt - v - DG_CONTENT_ATTR_LEN))
: NULL;
size_t xl = 0;
char *xml = ve ? dg_xml_unescape(ctx, v + DG_CONTENT_ATTR_LEN,
(size_t)(ve - v - DG_CONTENT_ATTR_LEN), &xl)
: NULL;
if (xml) {
dg_file_drawio(ctx, xml, xl);
}
}
} else if (dg_ends(path, ".drawio") || dg_ends(path, ".dio")) {
dg_file_drawio(ctx, ctx->source, len);
}
}
/* ── What a Markdown section's text keeps of its diagram fences ───── */
bool cbm_diagram_fence_is_diagram(const char *source, TSNode block) {
TSNode content;
return dg_kind(block, "fenced_code_block") && dg_fence_format(source, block, &content);
}
/* The labels of one Markdown diagram fence, space-joined in source order: the
* DiagramElement names and arrow texts its parser produces, without the
* format's syntax. The parse runs into a scratch result, so the file's own
* definitions are untouched. NULL when the fence says nothing. */
const char *cbm_diagram_fence_labels(CBMExtractCtx *ctx, TSNode block) {
TSNode content;
const char *format = dg_fence_format(ctx->source, block, &content);
if (!format) {
return NULL;
}
CBMFileResult scratch;
memset(&scratch, 0, sizeof(scratch));
CBMExtractCtx sc = *ctx;
sc.result = &scratch;
uint32_t cb = ts_node_start_byte(content);
dg_source_t src = {
.format = format,
.parent_qn = "e1",
.ordinal = SKIP_ONE,
.text = ctx->source + cb,
.len = ts_node_end_byte(content) - cb,
.first_line = ts_node_start_point(content).row + SKIP_ONE,
.start_line = ts_node_start_point(block).row + SKIP_ONE,
.end_line = ts_node_end_point(block).row + SKIP_ONE,
};
dg_build(&sc, &src);
size_t total = 0;
for (int i = 0; i < scratch.defs.count; i++) {
const CBMDefinition *d = &scratch.defs.items[i];
if (d->name && d->label && strcmp(d->label, "DiagramElement") == 0) {
total += strlen(d->name) + SKIP_ONE;
}
}
for (int i = 0; i < scratch.diagram_arrows.count; i++) {
const char *l = scratch.diagram_arrows.items[i].label;
total += l ? strlen(l) + SKIP_ONE : 0;
}
char *out = total ? (char *)cbm_arena_alloc(ctx->arena, total + SKIP_ONE) : NULL;
if (!out) {
return NULL;
}
size_t w = 0;
for (int i = 0; i < scratch.defs.count; i++) {
const CBMDefinition *d = &scratch.defs.items[i];
if (d->name && d->label && strcmp(d->label, "DiagramElement") == 0) {
size_t l = strlen(d->name);
memcpy(out + w, d->name, l);
w += l;
out[w++] = ' ';
}
}
for (int i = 0; i < scratch.diagram_arrows.count; i++) {
const char *l = scratch.diagram_arrows.items[i].label;
if (l) {
size_t n = strlen(l);
memcpy(out + w, l, n);
w += n;
out[w++] = ' ';
}
}
out[w] = '\0';
return out;
}
void cbm_extract_diagrams(CBMExtractCtx *ctx) {
if (!ctx || !ctx->result || !ctx->source) {
return;
}
if (ctx->language == CBM_LANG_MARKDOWN && !ts_node_is_null(ctx->root)) {
dg_markdown(ctx);
return;
}
if (ctx->language == CBM_LANG_RST && !ts_node_is_null(ctx->root)) {
dg_rst(ctx);
return;
}
if (ctx->language == CBM_LANG_ASCIIDOC) {
dg_asciidoc(ctx);
return;
}
if (ctx->language == CBM_LANG_MERMAID) {
dg_source_t src = {
.format = "mermaid",
.fallback = dg_file_stem(ctx),
.parent_qn = NULL,
.ordinal = SKIP_ONE,
.text = ctx->source,
.len = (size_t)ctx->source_len,
.first_line = SKIP_ONE,
.start_line = SKIP_ONE,
.end_line = SKIP_ONE,
};
for (int i = 0; i < ctx->source_len; i++) {
src.end_line += ctx->source[i] == '\n';
}
dg_build(ctx, &src);
}
}