diff --git a/Makefile.cbm b/Makefile.cbm index 9e1a7ac94..cd5eb4124 100644 --- a/Makefile.cbm +++ b/Makefile.cbm @@ -380,6 +380,7 @@ EXTRACTION_SRCS = \ $(CBM_DIR)/doclink_py.c \ $(CBM_DIR)/doc_adr.c \ $(CBM_DIR)/doc_pdf.c \ + $(CBM_DIR)/doc_adoc.c \ $(CBM_DIR)/pdf/pdf_obj.c \ $(CBM_DIR)/pdf/pdf_font.c \ $(CBM_DIR)/pdf/pdf_text.c \ @@ -510,6 +511,7 @@ PIPELINE_SRCS = \ src/pipeline/doc_links_md.c \ src/pipeline/doc_links_pdf.c \ src/pipeline/doc_links_rst.c \ + src/pipeline/doc_links_adoc.c \ src/pipeline/doc_links_msbuild.c \ src/pipeline/pass_route_nodes.c \ src/pipeline/pass_enrichment.c \ @@ -772,7 +774,7 @@ TEST_DISCOVER_SRCS = \ TEST_GRAPH_BUFFER_SRCS = tests/test_graph_buffer.c -TEST_PIPELINE_SRCS = tests/test_registry.c tests/test_pipeline.c tests/test_pipeline_conventions.c tests/test_pipeline_frozen.c tests/test_importance.c tests/test_cross_repo.c tests/test_fqn.c tests/test_route_canon.c tests/test_path_alias.c tests/test_configlink.c tests/test_doclinks.c tests/test_doc_mentions.c tests/test_doc_links_md.c tests/test_doc_links_pdf.c tests/test_doc_links_rst.c tests/test_infrascan.c tests/test_worker_pool.c tests/test_parallel.c tests/test_index_resilience.c tests/test_index_format.c tests/test_call_reference_contract.c tests/repro/repro_call_scope_usages.c tests/repro/repro_call_argument_usages.c tests/repro/repro_reference_precision.c tests/repro/repro_lexical_binding_precision.c tests/repro/repro_call_argument_matrix_a.c tests/repro/repro_call_argument_matrix_b.c tests/repro/repro_call_node_behaviors.c tests/repro/repro_language_registry.c tests/repro/repro_call_node_manifest.c tests/repro/repro_lsp_ordered_signatures.c tests/repro/repro_lsp_ordered_local.c tests/repro/repro_ts_overload_return_chains.c tests/repro/repro_harness_cleanup.c tests/repro/repro_runner_filter.c +TEST_PIPELINE_SRCS = tests/test_registry.c tests/test_pipeline.c tests/test_pipeline_conventions.c tests/test_pipeline_frozen.c tests/test_importance.c tests/test_cross_repo.c tests/test_fqn.c tests/test_route_canon.c tests/test_path_alias.c tests/test_configlink.c tests/test_doclinks.c tests/test_doc_mentions.c tests/test_doc_links_md.c tests/test_doc_links_pdf.c tests/test_doc_links_rst.c tests/test_doc_links_adoc.c tests/test_infrascan.c tests/test_worker_pool.c tests/test_parallel.c tests/test_index_resilience.c tests/test_index_format.c tests/test_call_reference_contract.c tests/repro/repro_call_scope_usages.c tests/repro/repro_call_argument_usages.c tests/repro/repro_reference_precision.c tests/repro/repro_lexical_binding_precision.c tests/repro/repro_call_argument_matrix_a.c tests/repro/repro_call_argument_matrix_b.c tests/repro/repro_call_node_behaviors.c tests/repro/repro_language_registry.c tests/repro/repro_call_node_manifest.c tests/repro/repro_lsp_ordered_signatures.c tests/repro/repro_lsp_ordered_local.c tests/repro/repro_ts_overload_return_chains.c tests/repro/repro_harness_cleanup.c tests/repro/repro_runner_filter.c TEST_WATCHER_SRCS = tests/test_watcher.c diff --git a/internal/cbm/cbm.c b/internal/cbm/cbm.c index c740953f6..b9e3237b2 100644 --- a/internal/cbm/cbm.c +++ b/internal/cbm/cbm.c @@ -2821,6 +2821,25 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C cbm_index_mark_done(rel_path); return result; } + /* AsciiDoc has no grammar either: its headings, includes, attribute + * references and monospace spans are read from the text (doc_adoc.c). */ + if (language == CBM_LANG_ASCIIDOC) { + result->module_qn = cbm_fqn_module_source_lang(a, project, rel_path, language); + CBMExtractCtx adoc_ctx = { + .arena = a, + .scratch = scratch, + .result = result, + .source = source, + .source_len = source_len, + .language = language, + .project = project, + .rel_path = rel_path, + .module_qn = result->module_qn, + }; + cbm_adoc_extract_document(&adoc_ctx); + cbm_index_mark_done(rel_path); + return result; + } // Get language spec const CBMLangSpec *spec = cbm_lang_spec(language); diff --git a/internal/cbm/cbm.h b/internal/cbm/cbm.h index 0cb9c323c..b1868baec 100644 --- a/internal/cbm/cbm.h +++ b/internal/cbm/cbm.h @@ -210,6 +210,7 @@ typedef enum { CBM_LANG_PLSQL, // Oracle PL/SQL CBM_LANG_CHIALISP, // Chialisp (.clsp/.clib/.clinc — Chia smart-coin s-expression language) CBM_LANG_PDF, // PDF documents: the text layer, read without a grammar (doc_pdf.c) + CBM_LANG_ASCIIDOC, // AsciiDoc documents, read without a grammar (doc_adoc.c) CBM_LANG_COUNT } CBMLanguage; diff --git a/internal/cbm/doc_adoc.c b/internal/cbm/doc_adoc.c new file mode 100644 index 000000000..bfe70b69a --- /dev/null +++ b/internal/cbm/doc_adoc.c @@ -0,0 +1,944 @@ +/* + * doc_adoc.c — AsciiDoc documents in the graph (the extraction half; + * src/pipeline/doc_links_adoc.c resolves), and the Antora scope blob. + * + * AsciiDoc has no grammar here: like a PDF, the file is read directly. Each + * heading (`= Title` .. `====== Title`, or `#` .. `######`) outside a + * delimited block becomes a Section (name the title, the text up to the next + * heading as its docstring, its lines up to the next heading); text before + * the first heading belongs to the file. Delimited blocks (`----` listing, + * `....` literal, `////` comment, `++++` passthrough, `====` example, `****` + * sidebar, `____` quote, `|===` table, ``` fences) open and close on the same + * delimiter line (the field test's H8 rules). + * + * References become tokens whose `raw` is the reference as written, then + * TAB-separated fields: + * include include::target[attrs] (Asciidoctor's preprocessor directive: + * read in every block but a comment block; `\include::` is + * escaped): written, target with the page's attributes + * substituted (references the page does not define stay as + * `{name}` for the component's), tags (`tag=` / `tags=`), + * lines (`lines=`) + * attribute an attribute reference `{name}` in the text: written, name, + * the page's value as written and as the page expands it (both + * "" when the page does not define it). + * Antora components define their API links as attributes + * (`{Assertions}` -> a javadoc page): the resolver reads them. + * code_path, a monospace span (`x`, ``x``, `+x+`) naming a path or a + * code_name qualified name (the Markdown code-span classifier); raw is + * the span's text + * Page attributes (`:name: value`, `:name!:` / `:!name:` to unset) apply from + * their line on, in document order. + * + * The Antora scope (cbm_doclink_antora_scan_scope): an antora.yml's component + * name, its asciidoc attributes and its collector scans; an + * antora-playbook*.yml's asciidoc attributes. Read as text, never run. + */ +#include "doclink.h" + +#include "arena.h" +#include "cbm.h" +#include "helpers.h" + +#include +#include +#include +#include +#include + +enum { + AD_HEAD_MAX = 6, /* `======` */ + AD_DELIM_MIN = 4, /* `----` and its kin */ + AD_TABLE_MIN = 3, /* `|===` */ + AD_FENCE_MIN = 3, /* ``` */ + AD_BODY_MAX = 500, /* a section's docstring (Markdown's MAX_COMMENT_LEN) */ + AD_SPAN_MAX = 150, /* a span the classifier reads (MD_SPAN_MAX) */ + AD_ATTRS_INIT = 16, /* first capacity of the page attribute list */ + AD_EXPAND_DEPTH = 10, /* nested attribute references expanded (H8) */ + AD_FIELD_MAX = 512, /* an attribute value or target longer than this is skipped */ +}; + +typedef struct { + const char *s; + int len; +} ad_line_t; + +typedef struct { + char *name; + char *value; +} ad_attr_t; + +typedef struct { + int line; /* 0-based */ + const char *title; + const char *qn; + bool shared_qn; +} ad_head_t; + +typedef struct { + CBMExtractCtx *ctx; + CBMArena *scratch; + ad_line_t *L; + int n; + unsigned char *in_block; /* 1: inside a delimited block; 2: inside a comment block */ + ad_head_t *heads; + int nheads; + ad_attr_t *attrs; + int nattrs; + int cap_attrs; + char *span_buf; + bool failed; +} ad_doc_t; + +static bool ad_blank(char c) { + return c == ' ' || c == '\t' || c == '\r'; +} + +static bool ad_word(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; +} + +/* The line without trailing whitespace: its length. */ +static int ad_rlen(const ad_line_t *l) { + int n = l->len; + while (n > 0 && ad_blank(l->s[n - 1])) { + n--; + } + return n; +} + +/* ADOC_DELIM_RE: the delimiter's length, or 0. */ +static int ad_delim(const ad_line_t *l) { + int n = ad_rlen(l); + if (n == 0) { + return 0; + } + const char *s = l->s; + if (s[0] == '|') { + int k = 1; + while (k < n && s[k] == '=') { + k++; + } + return (k == n && n - 1 >= AD_TABLE_MIN) ? n : 0; + } + if (!strchr("-.=*_+/`", s[0])) { + return 0; + } + for (int k = 1; k < n; k++) { + if (s[k] != s[0]) { + return 0; + } + } + int min = s[0] == '`' ? AD_FENCE_MIN : AD_DELIM_MIN; + return n >= min ? n : 0; +} + +/* ADOC_HEAD_RE ^(={1,6}|#{1,6})\s+(\S.*?)\s*$: the level and the title span. */ +static int ad_heading(const ad_line_t *l, int *ta, int *tb) { + const char *s = l->s; + int n = ad_rlen(l); + if (n == 0 || (s[0] != '=' && s[0] != '#')) { + return 0; + } + int k = 0; + while (k < n && s[k] == s[0]) { + k++; + } + if (k > AD_HEAD_MAX || k >= n || !ad_blank(s[k])) { + return 0; + } + int a = k; + while (a < n && ad_blank(s[a])) { + a++; + } + if (a >= n) { + return 0; + } + *ta = a; + *tb = n; + return k; +} + +static bool ad_lines(ad_doc_t *d, const char *src, int len) { + int count = 1; + for (int i = 0; i < len; i++) { + count += src[i] == '\n'; + } + d->L = (ad_line_t *)cbm_arena_alloc(d->scratch, (size_t)count * sizeof(*d->L)); + d->in_block = (unsigned char *)cbm_arena_alloc(d->scratch, (size_t)count); + if (!d->L || !d->in_block) { + return false; + } + memset(d->in_block, 0, (size_t)count); + int pos = 0; + for (int i = 0; i < count; i++) { + const char *nl = memchr(src + pos, '\n', (size_t)(len - pos)); + int end = nl ? (int)(nl - src) : len; + d->L[i] = (ad_line_t){src + pos, end - pos}; + pos = end + 1; + } + d->n = count; + return true; +} + +/* Delimited blocks, then the headings outside them. */ +static void ad_structure(ad_doc_t *d) { + int open = -1; + for (int i = 0; i < d->n; i++) { + int dl = ad_delim(&d->L[i]); + if (open < 0) { + if (dl) { + open = i; + d->in_block[i] = d->L[i].s[0] == '/' ? 2 : 1; + } + continue; + } + const ad_line_t *o = &d->L[open]; + d->in_block[i] = d->L[open].s[0] == '/' ? 2 : 1; + if (dl && dl == ad_rlen(o) && memcmp(d->L[i].s, o->s, (size_t)dl) == 0) { + open = -1; + } + } + d->heads = (ad_head_t *)cbm_arena_alloc(d->scratch, (size_t)d->n * sizeof(*d->heads)); + if (!d->heads) { + d->failed = true; + return; + } + for (int i = 0; i < d->n; i++) { + int ta; + int tb; + if (d->in_block[i] || ad_heading(&d->L[i], &ta, &tb) == 0) { + continue; + } + char *title = cbm_arena_strndup(d->ctx->arena, d->L[i].s + ta, (size_t)(tb - ta)); + if (!title) { + d->failed = true; + return; + } + d->heads[d->nheads++] = (ad_head_t){.line = i, .title = title}; + } +} + +/* qn_safe_segment (extract_defs.c): whitespace runs become '-'. */ +static const char *ad_qn_segment(CBMArena *a, const char *name) { + char *out = cbm_arena_strdup(a, name); + if (!out) { + return NULL; + } + size_t w = 0; + bool in_ws = false; + for (const char *p = name; *p; p++) { + if (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') { + in_ws = true; + continue; + } + if (in_ws && w > 0) { + out[w++] = '-'; + } + in_ws = false; + out[w++] = *p; + } + out[w] = '\0'; + return out; +} + +/* The section's own text, whitespace collapsed, at most AD_BODY_MAX bytes and + * never a cut UTF-8 character. */ +static const char *ad_body(ad_doc_t *d, int from, int to) { + char *out = (char *)cbm_arena_alloc(d->ctx->arena, AD_BODY_MAX + 1); + if (!out) { + return NULL; + } + int w = 0; + for (int k = from; k < to && w < AD_BODY_MAX; k++) { + const char *s = d->L[k].s; + int len = d->L[k].len; + for (int i = 0; i < len && w < AD_BODY_MAX; i++) { + char c = s[i]; + if (ad_blank(c)) { + if (w > 0 && out[w - 1] != ' ') { + out[w++] = ' '; + } + continue; + } + out[w++] = c; + } + if (w > 0 && w < AD_BODY_MAX && out[w - 1] != ' ') { + out[w++] = ' '; + } + } + while (w > 0 && ((unsigned char)out[w - 1] & 0xC0) == 0x80) { + w--; /* never end inside a character */ + } + if (w > 0 && ((unsigned char)out[w - 1] & 0xC0) == 0xC0) { + w--; + } + while (w > 0 && out[w - 1] == ' ') { + w--; + } + out[w] = '\0'; + return w > 0 ? out : NULL; +} + +static int ad_head_qn_cmp(const void *a, const void *b) { + const ad_head_t *x = *(const ad_head_t *const *)a; + const ad_head_t *y = *(const ad_head_t *const *)b; + return strcmp(x->qn, y->qn); +} + +static void ad_sections(ad_doc_t *d) { + CBMExtractCtx *ctx = d->ctx; + CBMArena *a = ctx->arena; + for (int h = 0; h < d->nheads; h++) { + const char *seg = ad_qn_segment(a, d->heads[h].title); + d->heads[h].qn = seg ? cbm_fqn_compute(a, ctx->project, ctx->rel_path, seg) : NULL; + if (!d->heads[h].qn) { + d->failed = true; + return; + } + } + if (d->nheads > 1) { + ad_head_t **by = (ad_head_t **)cbm_arena_alloc(d->scratch, (size_t)d->nheads * sizeof(*by)); + if (!by) { + d->failed = true; + return; + } + for (int h = 0; h < d->nheads; h++) { + by[h] = &d->heads[h]; + } + qsort(by, (size_t)d->nheads, sizeof(*by), ad_head_qn_cmp); + for (int h = 1; h < d->nheads; h++) { + if (strcmp(by[h]->qn, by[h - 1]->qn) == 0) { + by[h]->shared_qn = true; + by[h - 1]->shared_qn = true; + } + } + } + for (int h = 0; h < d->nheads; h++) { + const ad_head_t *hd = &d->heads[h]; + int end = h + 1 < d->nheads ? d->heads[h + 1].line : d->n; + int last = end - 1; + while (last > hd->line && ad_rlen(&d->L[last]) == 0) { + last--; + } + CBMDefinition def; + memset(&def, 0, sizeof(def)); + def.name = hd->title; + def.qualified_name = hd->qn; + def.label = "Section"; + def.file_path = ctx->rel_path; + def.start_line = (uint32_t)hd->line + 1; + def.end_line = (uint32_t)last + 1; + def.is_exported = true; + def.docstring = ad_body(d, hd->line + 1, end); + cbm_defs_push(&ctx->result->defs, a, def); + } +} + +/* ── Page attributes ─────────────────────────────────────────────── */ + +static ad_attr_t *ad_attr_find(ad_doc_t *d, const char *name, size_t n) { + for (int i = 0; i < d->nattrs; i++) { + if (strlen(d->attrs[i].name) == n && memcmp(d->attrs[i].name, name, n) == 0) { + return &d->attrs[i]; + } + } + return NULL; +} + +/* ATTR_ENTRY_RE ^:(!?)([\w][\w\-]*)(!?):(?:\s+(.*?))?\s*$ -- true when the + * line is an attribute entry (set or unset). */ +static bool ad_attr_entry(ad_doc_t *d, const ad_line_t *l) { + const char *s = l->s; + int n = ad_rlen(l); + if (n < 3 || s[0] != ':') { + return false; + } + int k = 1; + bool unset = false; + if (s[k] == '!') { + unset = true; + k++; + } + int na = k; + if (k >= n || !ad_word(s[k])) { + return false; + } + while (k < n && (ad_word(s[k]) || s[k] == '-')) { + k++; + } + int nb = k; + if (k < n && s[k] == '!') { + unset = true; + k++; + } + if (k >= n || s[k] != ':') { + return false; + } + k++; + if (k < n && !ad_blank(s[k])) { + return false; + } + while (k < n && ad_blank(s[k])) { + k++; + } + ad_attr_t *a = ad_attr_find(d, s + na, (size_t)(nb - na)); + if (unset) { + if (a) { + *a = d->attrs[--d->nattrs]; + } + return true; + } + char *value = cbm_arena_strndup(d->scratch, s + k, (size_t)(n - k)); + if (!value) { + d->failed = true; + return true; + } + if (a) { + a->value = value; + return true; + } + if (d->nattrs >= d->cap_attrs) { + int ncap = d->cap_attrs ? d->cap_attrs * 2 : AD_ATTRS_INIT; + ad_attr_t *grown = (ad_attr_t *)cbm_arena_alloc(d->scratch, (size_t)ncap * sizeof(*grown)); + if (!grown) { + d->failed = true; + return true; + } + if (d->nattrs) { + memcpy(grown, d->attrs, (size_t)d->nattrs * sizeof(*grown)); + } + d->attrs = grown; + d->cap_attrs = ncap; + } + char *name = cbm_arena_strndup(d->scratch, s + na, (size_t)(nb - na)); + if (!name) { + d->failed = true; + return true; + } + d->attrs[d->nattrs++] = (ad_attr_t){name, value}; + return true; +} + +/* A `{name}` reference at s[i] (not escaped `\{`): the name's span. */ +static bool ad_attr_ref(const char *s, int n, int i, int *a, int *b) { + if (s[i] != '{' || (i > 0 && s[i - 1] == '\\') || i + 2 >= n || !ad_word(s[i + 1])) { + return false; + } + int k = i + 1; + while (k < n && (ad_word(s[k]) || s[k] == '-')) { + k++; + } + if (k >= n || s[k] != '}') { + return false; + } + *a = i + 1; + *b = k; + return true; +} + +/* The text with the page's attribute references substituted, pass after pass + * (a value may reference others; at most AD_EXPAND_DEPTH passes, as H8's + * depth); references the page does not define stay as written. NULL when it + * grows past AD_FIELD_MAX or memory ran out. */ +static char *ad_expand(ad_doc_t *d, const char *s, int n) { + char cur[AD_FIELD_MAX + 1]; + char out[AD_FIELD_MAX + 1]; + if (n > AD_FIELD_MAX) { + return NULL; + } + memcpy(cur, s, (size_t)n); + int cl = n; + for (int pass = 0; pass < AD_EXPAND_DEPTH; pass++) { + int w = 0; + bool changed = false; + for (int i = 0; i < cl; i++) { + int a; + int b; + const ad_attr_t *at = NULL; + if (ad_attr_ref(cur, cl, i, &a, &b)) { + at = ad_attr_find(d, cur + a, (size_t)(b - a)); + } + if (at) { + int vl = (int)strlen(at->value); + if (w + vl > AD_FIELD_MAX) { + return NULL; + } + memcpy(out + w, at->value, (size_t)vl); + w += vl; + i = b; + changed = true; + continue; + } + if (w + 1 > AD_FIELD_MAX) { + return NULL; + } + out[w++] = cur[i]; + } + if (w > 0) { + memcpy(cur, out, (size_t)w); + } + cl = w; + if (!changed) { + break; + } + } + return cbm_arena_strndup(d->scratch, cur, (size_t)cl); +} + +/* ── Tokens ──────────────────────────────────────────────────────── */ + +static void ad_push(ad_doc_t *d, int line, int syntax, const char *raw) { + if (!raw) { + d->failed = true; + return; + } + CBMExtractCtx *ctx = d->ctx; + int lo = 0; + int hi = d->nheads; + while (lo < hi) { + int mid = lo + ((hi - lo) / 2); + if (d->heads[mid].line <= line) { + lo = mid + 1; + } else { + hi = mid; + } + } + const ad_head_t *sec = lo > 0 ? &d->heads[lo - 1] : NULL; + if (sec && sec->shared_qn) { + sec = NULL; + } + CBMDocLink link = { + .source_qn = sec ? sec->qn : (ctx->module_qn ? ctx->module_qn : ""), + .raw = raw, + .line = (uint32_t)line + 1, + .def_line = sec ? (uint32_t)sec->line + 1 : 1, + .syntax = (uint16_t)syntax, + .flags = sec ? 0 : CBM_DOCLINK_FLAG_FILE, + }; + cbm_doclinks_push(&ctx->result->doc_links, ctx->arena, link); +} + +/* A value of the include's attribute list (`tag=x`, `tags="a;b"`, `lines=1..5`). */ +static void ad_attrlist_value(const char *al, int n, const char *key, char *out, size_t cap) { + out[0] = '\0'; + size_t kl = strlen(key); + for (int i = 0; i + (int)kl < n; i++) { + if ((i > 0 && ad_word(al[i - 1])) || memcmp(al + i, key, kl) != 0) { + continue; + } + int k = i + (int)kl; + while (k < n && ad_blank(al[k])) { + k++; + } + if (k >= n || al[k] != '=') { + continue; + } + k++; + while (k < n && ad_blank(al[k])) { + k++; + } + int a = k; + int b; + if (k < n && al[k] == '"') { + a = k + 1; + const char *q = memchr(al + a, '"', (size_t)(n - a)); + b = q ? (int)(q - al) : n; + } else { + b = a; + while (b < n && al[b] != ',') { + b++; + } + } + while (b > a && ad_blank(al[b - 1])) { + b--; + } + snprintf(out, cap, "%.*s", b - a, al + a); + return; + } +} + +/* INCLUDE_RE ^(\\?)include::([^\[\s][^\[]*)\[(.*)\]\s*$ */ +static void ad_include(ad_doc_t *d, int i) { + const ad_line_t *l = &d->L[i]; + const char *s = l->s; + int n = ad_rlen(l); + static const char kw[] = "include::"; + int kl = (int)strlen(kw); + if (n < kl + 2 || memcmp(s, kw, (size_t)kl) != 0 || s[kl] == '[' || ad_blank(s[kl]) || + s[n - 1] != ']') { + return; + } + const char *open = memchr(s + kl, '[', (size_t)(n - kl)); + if (!open) { + return; + } + int ta = kl; + int tb = (int)(open - s); + int aa = tb + 1; + int ab = n - 1; + char *target = ad_expand(d, s + ta, tb - ta); + if (!target || strchr(target, '\t')) { + return; + } + char tags[AD_FIELD_MAX]; + char tag1[AD_FIELD_MAX]; + char lines[AD_FIELD_MAX]; + ad_attrlist_value(s + aa, ab - aa, "tags", tags, sizeof(tags)); + ad_attrlist_value(s + aa, ab - aa, "tag", tag1, sizeof(tag1)); + ad_attrlist_value(s + aa, ab - aa, "lines", lines, sizeof(lines)); + char *written = cbm_arena_strndup(d->ctx->arena, s, (size_t)n); + ad_push(d, i, CBM_DOCLINK_ADOC_INCLUDE, + written ? cbm_arena_sprintf(d->ctx->arena, "%s\t%s\t%s\t%s", written, target, + tags[0] ? tags : tag1, lines) + : NULL); +} + +/* Built-in attributes: characters and document settings, never a link. */ +static bool ad_builtin(const char *s, size_t n) { + static const char *const names[] = {"nbsp", "sp", + "empty", "blank", + "zwsp", "wj", + "apos", "quot", + "lsquo", "rsquo", + "ldquo", "rdquo", + "deg", "plus", + "brvbar", "vbar", + "amp", "lt", + "gt", "startsb", + "endsb", "caret", + "asterisk", "tilde", + "backslash", "backtick", + "two-colons", "two-semicolons", + "cpp", "cxx", + "pp", "docname", + "docdir", "docfile", + "doctitle", "toc", + "version", "revnumber", + "revdate", "author", + "email", "page-component-version", + NULL}; + for (int i = 0; names[i]; i++) { + if (strlen(names[i]) == n && memcmp(names[i], s, n) == 0) { + return true; + } + } + return false; +} + +static void ad_attr_refs(ad_doc_t *d, int i) { + const ad_line_t *l = &d->L[i]; + const char *s = l->s; + int n = l->len; + for (int k = 0; k < n; k++) { + int a; + int b; + if (!ad_attr_ref(s, n, k, &a, &b) || ad_builtin(s + a, (size_t)(b - a))) { + continue; + } + const ad_attr_t *at = ad_attr_find(d, s + a, (size_t)(b - a)); + char *value = at ? ad_expand(d, at->value, (int)strlen(at->value)) : NULL; + if (at && (!value || strchr(value, '\t') || strchr(at->value, '\t'))) { + k = b; + continue; + } + ad_push(d, i, CBM_DOCLINK_ADOC_ATTRIBUTE, + cbm_arena_sprintf(d->ctx->arena, "{%.*s}\t%.*s\t%s\t%s", b - a, s + a, b - a, s + a, + at ? at->value : "", value ? value : "")); + k = b; + } +} + +/* Monospace spans of a prose line: `x` and ``x``, a `+x+` passthrough's inner + * text; each a code_path / code_name when the classifier names one. */ +static void ad_spans(ad_doc_t *d, int i) { + const ad_line_t *l = &d->L[i]; + const char *s = l->s; + int n = l->len; + int k = 0; + while (k < n) { + if (s[k] != '`') { + k++; + continue; + } + int run = 1; + while (k + run < n && s[k + run] == '`') { + run++; + } + if (run > 2) { + k += run; + continue; + } + int a = k + run; + int e = a; + while (e + run <= n && !(memcmp(s + e, run == 2 ? "``" : "`", (size_t)run) == 0)) { + e++; + } + if (e + run > n || e == a) { + k = a; + continue; + } + int b = e; + if (b - a >= 2 && s[a] == '+' && s[b - 1] == '+') { + a++; + b--; + } + CBMDocLinkMdPath p; + if (b > a && b - a <= AD_SPAN_MAX && + cbm_doclink_md_classify_span(s + a, (size_t)(b - a), d->span_buf, AD_SPAN_MAX + 1, + &p)) { + ad_push(d, i, + p.shape == CBM_DOCLINK_MD_QUALIFIED ? CBM_DOCLINK_ADOC_CODE_NAME + : CBM_DOCLINK_ADOC_CODE_PATH, + cbm_arena_strndup(d->ctx->arena, s + a, (size_t)(b - a))); + } + k = e + run; + } +} + +static void ad_tokens(ad_doc_t *d) { + for (int i = 0; i < d->n && !d->failed; i++) { + if (d->in_block[i] == 2) { + continue; /* a comment block: nothing in it is read */ + } + const ad_line_t *l = &d->L[i]; + if (l->len >= 2 && l->s[0] == '/' && l->s[1] == '/' && !d->in_block[i]) { + continue; /* a line comment */ + } + if (!d->in_block[i] && ad_attr_entry(d, l)) { + continue; + } + if (l->len > 0 && l->s[0] == 'i') { + ad_include(d, i); + } + ad_attr_refs(d, i); + if (!d->in_block[i]) { + ad_spans(d, i); + } + } +} + +void cbm_adoc_extract_document(CBMExtractCtx *ctx) { + if (!ctx || !ctx->result || !ctx->source || ctx->source_len <= 0) { + return; + } + ad_doc_t d = {.ctx = ctx, .scratch = ctx->scratch ? ctx->scratch : ctx->arena}; + d.span_buf = (char *)cbm_arena_alloc(d.scratch, (size_t)(AD_SPAN_MAX + 1) * 2); + if (!d.span_buf || !ad_lines(&d, ctx->source, ctx->source_len)) { + ctx->result->doc_links.failed = true; + return; + } + ad_structure(&d); + if (!d.failed) { + ad_sections(&d); + } + if (!d.failed) { + ad_tokens(&d); + } + if (d.failed) { + ctx->result->doc_links.failed = true; + } +} + +/* ── The Antora scope (antora.yml, antora-playbook*.yml) ─────────── */ + +typedef struct { + CBMArena *a; + char *buf; + size_t len; + size_t cap; + bool failed; +} ad_sb_t; + +static void ad_put(ad_sb_t *sb, const char *s, size_t n) { + if (sb->failed) { + return; + } + if (sb->len + n + 1 > sb->cap) { + size_t ncap = sb->cap ? sb->cap : AD_FIELD_MAX; + while (ncap < sb->len + n + 1) { + ncap *= 2; + } + char *grown = (char *)cbm_arena_alloc(sb->a, ncap); + if (!grown) { + sb->failed = true; + return; + } + if (sb->len) { + memcpy(grown, sb->buf, sb->len); + } + sb->buf = grown; + sb->cap = ncap; + } + memcpy(sb->buf + sb->len, s, n); + sb->len += n; + sb->buf[sb->len] = '\0'; +} + +static void ad_rec(ad_sb_t *sb, const char *kind, const char *a, size_t an, const char *b, + size_t bn) { + for (size_t i = 0; i < an; i++) { + if (a[i] == '\t' || a[i] == '\n') { + return; + } + } + for (size_t i = 0; b && i < bn; i++) { + if (b[i] == '\t' || b[i] == '\n') { + return; + } + } + ad_put(sb, kind, strlen(kind)); + ad_put(sb, "\t", 1); + ad_put(sb, a, an); + if (b) { + ad_put(sb, "\t", 1); + ad_put(sb, b, bn); + } + ad_put(sb, "\n", 1); +} + +static int ad_indent(const char *s, int n) { + int k = 0; + while (k < n && s[k] == ' ') { + k++; + } + return k; +} + +/* `key: value` at s (after the indent): the key and value spans (value + * unquoted, comments not stripped: the field test's reading). */ +static bool ad_yaml_kv(const char *s, int n, int *ka, int *kb, int *va, int *vb) { + int k = 0; + if (k >= n || !(ad_word(s[k]))) { + return false; + } + while (k < n && (ad_word(s[k]) || s[k] == '-')) { + k++; + } + int e = k; + while (k < n && ad_blank(s[k])) { + k++; + } + if (k >= n || s[k] != ':') { + return false; + } + k++; + while (k < n && ad_blank(s[k])) { + k++; + } + *ka = 0; + *kb = e; + *va = k; + *vb = n; /* the caller's line has no trailing whitespace */ + return true; +} + +/* A value in matching quotes loses them. */ +static void ad_unquote(const char *s, int *va, int *vb) { + if (*vb - *va >= 2 && s[*va] == s[*vb - 1] && (s[*va] == '\'' || s[*va] == '"')) { + (*va)++; + (*vb)--; + } +} + +/* H8 parse_antora_yml: `name:`, `asciidoc: attributes:` and + * `ext: collector: scan: - dir: / into:`. */ +static void ad_antora(const char *src, int len, bool component, ad_sb_t *sb) { + bool in_asciidoc = false; + bool in_attrs = false; + int attrs_ind = -1; + bool in_scan = false; + int pos = 0; + while (pos <= len) { + const char *nl = memchr(src + pos, '\n', (size_t)(len - pos)); + int end = nl ? (int)(nl - src) : len; + const char *s = src + pos; + int n = end - pos; + while (n > 0 && ad_blank(s[n - 1])) { + n--; + } + int ind = ad_indent(s, n); + int ka; + int kb; + int va; + int vb; + bool kv = ad_yaml_kv(s + ind, n - ind, &ka, &kb, &va, &vb); + if (kv) { + ad_unquote(s + ind, &va, &vb); + } + if (ind == 0 && n > 0) { + in_asciidoc = kv && kb == (int)strlen("asciidoc") && memcmp(s, "asciidoc", 8) == 0; + in_attrs = false; + in_scan = false; + if (component && kv && kb == (int)strlen("name") && memcmp(s, "name", 4) == 0 && + vb > va) { + ad_rec(sb, "N", s + va, (size_t)(vb - va), NULL, 0); + } + } else if (in_asciidoc && kv && va == vb && kb == (int)strlen("attributes") && + memcmp(s + ind, "attributes", 10) == 0) { + in_attrs = true; + attrs_ind = -1; + } else if (in_attrs && n > ind && s[ind] != '#') { + if (attrs_ind < 0) { + attrs_ind = ind; + } + if (ind < attrs_ind) { + in_attrs = false; + } else if (ind == attrs_ind && kv) { + ad_rec(sb, "A", s + ind, (size_t)kb, s + ind + va, (size_t)(vb - va)); + } + } + if (component && kv && kb == (int)strlen("scan") && memcmp(s + ind, "scan", 4) == 0) { + in_scan = true; + } else if (component && in_scan) { + const char *t = s + ind; + int tn = n - ind; + if (tn > 1 && t[0] == '-' && ad_blank(t[1])) { + int sk = 1; + while (sk < tn && ad_blank(t[sk])) { + sk++; + } + t += sk; + tn -= sk; + } + int a; + int b; + int c; + int e; + bool tkv = ad_yaml_kv(t, tn, &a, &b, &c, &e); + if (tkv) { + ad_unquote(t, &c, &e); + } + if (tkv && e > c) { + if (b == 3 && memcmp(t, "dir", 3) == 0) { + ad_rec(sb, "D", t + c, (size_t)(e - c), NULL, 0); + } else if (b == 4 && memcmp(t, "into", 4) == 0) { + ad_rec(sb, "I", t + c, (size_t)(e - c), NULL, 0); + } + } + } + pos = end + 1; + } +} + +const char *cbm_doclink_antora_scan_scope(CBMExtractCtx *ctx) { + if (!ctx || !ctx->rel_path || !ctx->source || ctx->source_len <= 0) { + return NULL; + } + const char *base = strrchr(ctx->rel_path, '/'); + base = base ? base + 1 : ctx->rel_path; + bool component = strcmp(base, "antora.yml") == 0; + bool playbook = strncmp(base, "antora-playbook", strlen("antora-playbook")) == 0; + if (!component && !playbook) { + return NULL; + } + ad_sb_t sb = {.a = ctx->arena}; + ad_put(&sb, CBM_DOCLINK_ADOC_SCOPE_TAG "\n", strlen(CBM_DOCLINK_ADOC_SCOPE_TAG "\n")); + ad_put(&sb, component ? "C\n" : "P\n", 2); + ad_antora(ctx->source, ctx->source_len, component, &sb); + if (sb.failed) { + ctx->result->doc_links.failed = true; + return NULL; + } + return sb.buf; +} diff --git a/internal/cbm/doclink.c b/internal/cbm/doclink.c index 1a3d06b3c..09ee36c25 100644 --- a/internal/cbm/doclink.c +++ b/internal/cbm/doclink.c @@ -175,6 +175,11 @@ static const doclink_lang_t DOCLINK_LANGS[] = { {.lang = CBM_LANG_PYTHON, .scan_scope = cbm_doclink_py_scan_scope, .scope_tag = CBM_DOCLINK_PY_SCOPE_TAG}, + /* YAML: the Antora component and playbook settings the AsciiDoc resolver + * reads */ + {.lang = CBM_LANG_YAML, + .scan_scope = cbm_doclink_antora_scan_scope, + .scope_tag = CBM_DOCLINK_ADOC_SCOPE_TAG}, }; static const doclink_lang_t *doclink_lang(CBMLanguage lang) { diff --git a/internal/cbm/doclink.h b/internal/cbm/doclink.h index 741b95aaa..e97aaf488 100644 --- a/internal/cbm/doclink.h +++ b/internal/cbm/doclink.h @@ -92,6 +92,11 @@ X(CBM_DOCLINK_RST_KERNEL_DOC, CBM_LANG_RST, "kernel_doc", false, true, "MENTIONS") \ X(CBM_DOCLINK_RST_CODE_PATH, CBM_LANG_RST, "code_path", false, true, "MENTIONS") \ X(CBM_DOCLINK_RST_CODE_NAME, CBM_LANG_RST, "code_name", false, true, "MENTIONS") \ + /* asciidoc (doc_adoc.c): includes, Antora API attributes, monospace spans */ \ + X(CBM_DOCLINK_ADOC_INCLUDE, CBM_LANG_ASCIIDOC, "include", false, true, "MENTIONS") \ + X(CBM_DOCLINK_ADOC_ATTRIBUTE, CBM_LANG_ASCIIDOC, "attribute", false, true, "MENTIONS") \ + X(CBM_DOCLINK_ADOC_CODE_PATH, CBM_LANG_ASCIIDOC, "code_path", false, true, "MENTIONS") \ + X(CBM_DOCLINK_ADOC_CODE_NAME, CBM_LANG_ASCIIDOC, "code_name", false, true, "MENTIONS") \ /* any language (CBM_LANG_COUNT): a URL is never an edge */ \ X(CBM_DOCLINK_HREF, CBM_LANG_COUNT, "href", true, false, "MENTIONS") @@ -333,6 +338,22 @@ const char *cbm_doclink_py_scan_scope(CBMExtractCtx *ctx); /* The Python scope blob starts with this tag line. */ #define CBM_DOCLINK_PY_SCOPE_TAG "py1" +/* ── AsciiDoc (doc_adoc.c) ─────────────────────────────────────────── */ + +/* AsciiDoc documents: a Section per heading and the references to code + * (include::, Antora API attributes, monospace spans) as doc-link tokens. + * Runs instead of a grammar for CBM_LANG_ASCIIDOC. */ +void cbm_adoc_extract_document(CBMExtractCtx *ctx); + +/* The scope blob of a YAML file, which the AsciiDoc resolver reads: an + * antora.yml's component name, asciidoc attributes and collector scans, an + * antora-playbook*.yml's attributes (read as text). NULL for every other + * file. */ +const char *cbm_doclink_antora_scan_scope(CBMExtractCtx *ctx); + +/* The Antora scope blob starts with this tag line. */ +#define CBM_DOCLINK_ADOC_SCOPE_TAG "ad1" + /* PDF documents (doc_pdf.c): a Section per page (name "page N", the page's * text as docstring, property page) and the pages' structural code mentions * as doc-link tokens. Runs instead of a grammar for CBM_LANG_PDF. */ diff --git a/internal/cbm/lang_specs.c b/internal/cbm/lang_specs.c index a37c3e52c..6672011d0 100644 --- a/internal/cbm/lang_specs.c +++ b/internal/cbm/lang_specs.c @@ -2718,6 +2718,9 @@ static const CBMLangSpec lang_specs[CBM_LANG_COUNT] = { // looked up, so this language never reaches cbm_lang_spec()/cbm_ts_language(). // Left as a zero spec. + // CBM_LANG_ASCIIDOC — AsciiDoc documents. No grammar row, as PDF: extraction + // reads the file itself (cbm_adoc_extract_document, doc_adoc.c). Zero spec. + // CBM_LANG_OBJECTSCRIPT_EXPORT — Studio Export XML. No grammar row: the // pipeline transcodes Export XML to UDL (iris_export_xml.c) and re-extracts // each class as CBM_LANG_OBJECTSCRIPT_UDL, so this language never reaches diff --git a/src/discover/language.c b/src/discover/language.c index c15a1d43d..b1cd7d938 100644 --- a/src/discover/language.c +++ b/src/discover/language.c @@ -619,6 +619,9 @@ static const ext_entry_t EXT_TABLE[] = { {".clib", CBM_LANG_CHIALISP}, {".clinc", CBM_LANG_CHIALISP}, {".pdf", CBM_LANG_PDF}, + /* AsciiDoc; `.asc` is left out: it is as often an ASCII-armored signature */ + {".adoc", CBM_LANG_ASCIIDOC}, + {".asciidoc", CBM_LANG_ASCIIDOC}, /* Slang */ {".slang", CBM_LANG_SLANG}, @@ -831,6 +834,7 @@ static const char *LANG_NAMES[CBM_LANG_COUNT] = { [CBM_LANG_SCHEME] = "Scheme", [CBM_LANG_CHIALISP] = "Chialisp", [CBM_LANG_PDF] = "PDF", + [CBM_LANG_ASCIIDOC] = "AsciiDoc", [CBM_LANG_FENNEL] = "Fennel", [CBM_LANG_FISH] = "Fish", [CBM_LANG_AWK] = "AWK", diff --git a/src/discover/userconfig.c b/src/discover/userconfig.c index 7b1f3d2b5..a0d306008 100644 --- a/src/discover/userconfig.c +++ b/src/discover/userconfig.c @@ -161,6 +161,7 @@ static const lang_name_entry_t LANG_NAME_TABLE[] = { {"plsql", CBM_LANG_PLSQL}, {"chialisp", CBM_LANG_CHIALISP}, {"pdf", CBM_LANG_PDF}, + {"asciidoc", CBM_LANG_ASCIIDOC}, {"lean", CBM_LANG_LEAN}, {"form", CBM_LANG_FORM}, {"magma", CBM_LANG_MAGMA}, diff --git a/src/pipeline/doc_links.c b/src/pipeline/doc_links.c index 71159220d..c5023658d 100644 --- a/src/pipeline/doc_links.c +++ b/src/pipeline/doc_links.c @@ -45,10 +45,8 @@ const char *cbm_doclink_reason_name(int reason) { /* One pointer per language with a resolver: the one line a language leg adds * to this file. */ static const cbm_doclink_resolver_t *const DOCLINK_RESOLVERS[] = { - &cbm_doclink_cs_resolver, - &cbm_doclink_md_resolver, - &cbm_doclink_pdf_resolver, - &cbm_doclink_rst_resolver, + &cbm_doclink_cs_resolver, &cbm_doclink_md_resolver, &cbm_doclink_pdf_resolver, + &cbm_doclink_rst_resolver, &cbm_doclink_adoc_resolver, }; enum { DOCLINK_RESOLVER_COUNT = sizeof(DOCLINK_RESOLVERS) / sizeof(DOCLINK_RESOLVERS[0]) }; @@ -93,7 +91,7 @@ static const cbm_doclink_resolver_t *resolver_of_scope(const char *scope) { /* ── Incremental scope rules ─────────────────────────────────────── */ bool cbm_doclinks_binds_lines(CBMLanguage lang) { - return lang == CBM_LANG_MARKDOWN || lang == CBM_LANG_RST; + return lang == CBM_LANG_MARKDOWN || lang == CBM_LANG_RST || lang == CBM_LANG_ASCIIDOC; } bool cbm_doclinks_is_scope_input(const char *rel_path) { diff --git a/src/pipeline/doc_links.h b/src/pipeline/doc_links.h index ec1416f27..77a5dfb67 100644 --- a/src/pipeline/doc_links.h +++ b/src/pipeline/doc_links.h @@ -245,6 +245,9 @@ extern const cbm_doclink_resolver_t cbm_doclink_pdf_resolver; /* reStructuredText documents and the Python scope they read (doc_links_rst.c): * via "rst". */ extern const cbm_doclink_resolver_t cbm_doclink_rst_resolver; +/* AsciiDoc documents and the Antora YAML scope they read (doc_links_adoc.c): + * via "asciidoc". */ +extern const cbm_doclink_resolver_t cbm_doclink_adoc_resolver; /* Over a Markdown resolver index (cbm_doclink_md_resolver.build), for the * other document resolvers: the innermost code definition of `path` holding @@ -253,6 +256,15 @@ extern const cbm_doclink_resolver_t cbm_doclink_rst_resolver; const cbm_gbuf_node_t *cbm_doclink_md_segment(const void *md_index, const char *path, uint32_t first, uint32_t last, const char *member); const cbm_gbuf_node_t *cbm_doclink_md_folder(const void *md_index, const char *path); +/* The definition a region [first, last] of `path` (its text in `text`) binds, + * by the field test's rule (H8 bind_range): edge blank and comment lines + * trimmed; the innermost enclosing definition, unless it is a type (or there + * is none) and the definitions inside cover every other line of the region + * (an annotation right above one counts) -- then that one definition (the + * type for several). NULL: the file. */ +const cbm_gbuf_node_t *cbm_doclink_md_region(const void *md_index, const char *path, + const char *text, size_t text_len, uint32_t first, + uint32_t last); bool cbm_doclink_md_test_path(const char *path); #if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS @@ -278,7 +290,8 @@ void cbm_doclink_cs_test_unit_memo(bool on); #endif /* True for a document language whose references may bind the LINES of a file - * (Markdown `file#L3-L9`, reST :lines:): a body edit of that file keeps its + * (Markdown `file#L3-L9`, reST :lines:, AsciiDoc tags and lines=): a body + * edit of that file keeps its * names but can move what those lines hold, so an incremental run * re-resolves such documents with the edited file. */ bool cbm_doclinks_binds_lines(CBMLanguage lang); diff --git a/src/pipeline/doc_links_adoc.c b/src/pipeline/doc_links_adoc.c new file mode 100644 index 000000000..863462e41 --- /dev/null +++ b/src/pipeline/doc_links_adoc.c @@ -0,0 +1,919 @@ +/* + * doc_links_adoc.c — AsciiDoc references -> MENTIONS edges, via "asciidoc" + * (the resolving half of internal/cbm/doc_adoc.c), with the Antora rules the + * field test's harvester measured (H8: includes 97.6-100 % resolved, ~100 % + * precise; javadoc attributes 320/320). + * + * Antora: a component is the directory of an antora.yml; its pages live under + * /modules//pages. Its attributes and collector scans, and + * a playbook's attributes, come from the YAML scope blobs (doc_adoc.c), so an + * incremental run resolves as a full one. + * + * include::target[...]: references the page left are expanded from the + * component's attributes, then the playbook's. A resource ID + * ([[version@]component:]module:]family$path) names + * /modules///, or a file a collector scan + * contributes there; any other target is relative to the including file. Its + * `lines=` bind the innermost definition holding them; its `tag=` / `tags=` + * regions -- the included file's own `tag::x[]` .. `end::x[]` comments, read + * from that indexed file -- likewise; a tag the file does not have binds the + * file (the document stays a dependent of that file, so it re-resolves when + * the file changes, on an incremental run as on a full one). + * + * {attribute} references whose value is an Antora javadoc link + * ({javadoc-root}///.html[#member]) bind the + * Java or Kotlin type (or its member) that the repository's main sources + * declare under that package path. + * + * Monospace spans: the Markdown resolver's code-span rules (doc_links_md.c). + */ +#include "pipeline/doc_links.h" + +#include "discover/discover.h" /* cbm_language_for_filename */ +#include "doclink.h" +#include "foundation/arena.h" +#include "foundation/compat_fs.h" +#include "foundation/constants.h" +#include "foundation/mem_core.h" +#include "graph_buffer/graph_buffer.h" + +#include +#include +#include +#include + +enum { + RA_PATH_CAP = 1024, + RA_FIELDS = 6, + RA_EXPAND_PASSES = 10, /* nested attribute references (H8's depth) */ + RA_TAG_FILE_MAX = 8 * 1024 * 1024, /* an included file read for its tags */ + RA_TAGS = 16, /* tags of one include */ + RA_TAG_LEN = 128, /* a tag name */ + RA_INIT = 8, +}; + +typedef struct { + const char *key; + const char *value; +} ra_attr_t; + +typedef struct { + const char *dir; + const char *into; +} ra_scan_t; + +typedef struct { + const char *root; /* the antora.yml's directory ("" for the repository root) */ + const char *name; + ra_attr_t *attrs; + int nattrs; + ra_scan_t *scans; + int nscans; +} ra_comp_t; + +typedef struct { + const char *project; + const char *repo; /* the repository on disk: included files are read for their tags */ + void *md; + const char **run_paths; + int run_count; + CBMArena arena; + ra_comp_t *comps; + int ncomps; + ra_attr_t *pattrs; /* playbook attributes, the first definition of a key wins */ + int npattrs; +} ra_index_t; + +static void ra_destroy(void *index); + +static const char *ra_base(const char *path) { + const char *s = strrchr(path, '/'); + return s ? s + SKIP_ONE : path; +} + +static bool ra_ends(const char *s, const char *suffix) { + size_t n = strlen(s); + size_t k = strlen(suffix); + return n >= k && strcmp(s + n - k, suffix) == 0; +} + +static bool ra_grow(CBMArena *a, void **items, int count, int *cap, size_t size) { + if (count < *cap) { + return true; + } + int ncap = *cap ? *cap * PAIR_LEN : RA_INIT; + void *grown = cbm_arena_alloc(a, (size_t)ncap * size); + if (!grown) { + return false; + } + if (count > 0) { + memcpy(grown, *items, (size_t)count * size); + } + *items = grown; + *cap = ncap; + return true; +} + +/* One YAML scope blob: a component (C) or a playbook (P). */ +static bool ra_read_scope(ra_index_t *x, const char *path, const char *scope, int *comp_cap, + int *pattr_cap) { + char *copy = cbm_arena_strdup(&x->arena, scope); + if (!copy) { + return false; + } + char *save = NULL; + (void)strtok_r(copy, "\n", &save); /* the tag line */ + char *line = strtok_r(NULL, "\n", &save); + if (!line || (strcmp(line, "C") != 0 && strcmp(line, "P") != 0)) { + return true; + } + bool component = line[0] == 'C'; + ra_comp_t comp = {0}; + int acap = 0; + int scap = 0; + if (component) { + const char *b = ra_base(path); + comp.root = + cbm_arena_strndup(&x->arena, path, b > path ? (size_t)(b - path - SKIP_ONE) : 0); + comp.name = ""; + if (!comp.root) { + return false; + } + } + for (line = strtok_r(NULL, "\n", &save); line; line = strtok_r(NULL, "\n", &save)) { + char *tab = strchr(line, '\t'); + if (!tab) { + continue; + } + *tab = '\0'; + char *v = tab + SKIP_ONE; + if (strcmp(line, "N") == 0 && component) { + comp.name = v; + } else if (strcmp(line, "A") == 0) { + char *t2 = strchr(v, '\t'); + if (!t2) { + continue; + } + *t2 = '\0'; + ra_attr_t at = {v, t2 + SKIP_ONE}; + if (component) { + if (!ra_grow(&x->arena, (void **)&comp.attrs, comp.nattrs, &acap, sizeof(at))) { + return false; + } + comp.attrs[comp.nattrs++] = at; + } else { + bool seen = false; + for (int i = 0; i < x->npattrs; i++) { + seen = seen || strcmp(x->pattrs[i].key, at.key) == 0; + } + if (!seen) { + if (!ra_grow(&x->arena, (void **)&x->pattrs, x->npattrs, pattr_cap, + sizeof(at))) { + return false; + } + x->pattrs[x->npattrs++] = at; + } + } + } else if (strcmp(line, "D") == 0 && component) { + if (!ra_grow(&x->arena, (void **)&comp.scans, comp.nscans, &scap, sizeof(ra_scan_t))) { + return false; + } + comp.scans[comp.nscans++] = (ra_scan_t){v, ""}; + } else if (strcmp(line, "I") == 0 && component && comp.nscans > 0) { + comp.scans[comp.nscans - 1].into = v; + } + } + if (component) { + if (!ra_grow(&x->arena, (void **)&x->comps, x->ncomps, comp_cap, sizeof(comp))) { + return false; + } + x->comps[x->ncomps++] = comp; + } + return true; +} + +static void *ra_build(const cbm_doclink_build_in_t *in) { + ra_index_t *x = (ra_index_t *)cbm_calloc(CBM_MEM_CLASS_OTHER, sizeof(*x)); + if (!x) { + return NULL; + } + cbm_arena_init(&x->arena); + x->project = in->ctx ? in->ctx->project_name : NULL; + x->repo = in->ctx ? in->ctx->repo_path : NULL; + x->run_count = in->run_file_count; + x->run_paths = in->run_file_count > 0 + ? (const char **)cbm_calloc(CBM_MEM_CLASS_OTHER, + (size_t)in->run_file_count * sizeof(char *)) + : NULL; + x->md = cbm_doclink_md_resolver.build(in); + if ((in->run_file_count > 0 && !x->run_paths) || !x->md || !x->project) { + ra_destroy(x); + return NULL; + } + int comp_cap = 0; + int pattr_cap = 0; + for (int i = 0; i < in->file_count; i++) { + const cbm_doclink_file_t *f = &in->files[i]; + if (f->run_file >= 0 && f->run_file < x->run_count) { + x->run_paths[f->run_file] = f->rel_path; /* valid until destroy */ + } + if (f->scope && + strncmp(f->scope, CBM_DOCLINK_ADOC_SCOPE_TAG "\n", + strlen(CBM_DOCLINK_ADOC_SCOPE_TAG "\n")) == 0 && + !ra_read_scope(x, f->rel_path, f->scope, &comp_cap, &pattr_cap)) { + ra_destroy(x); + return NULL; + } + } + return x; +} + +static void ra_destroy(void *index) { + ra_index_t *x = (ra_index_t *)index; + if (!x) { + return; + } + if (x->md) { + cbm_doclink_md_resolver.destroy(x->md); + } + cbm_arena_destroy(&x->arena); + cbm_free(CBM_MEM_CLASS_OTHER, x->run_paths); + cbm_free(CBM_MEM_CLASS_OTHER, x); +} + +/* H8 component_of: the deepest component whose modules/ hold the document. */ +static const ra_comp_t *ra_component_of(const ra_index_t *x, const char *doc) { + const ra_comp_t *best = NULL; + for (int i = 0; i < x->ncomps; i++) { + const ra_comp_t *c = &x->comps[i]; + char pref[RA_PATH_CAP]; + snprintf(pref, sizeof(pref), "%s%smodules/", c->root, c->root[0] ? "/" : ""); + if (strncmp(doc, pref, strlen(pref)) == 0 && + (!best || strlen(c->root) > strlen(best->root))) { + best = c; + } + } + return best; +} + +static const char *ra_attr(const ra_index_t *x, const ra_comp_t *c, const char *key, size_t n) { + for (int i = 0; c && i < c->nattrs; i++) { + if (strlen(c->attrs[i].key) == n && memcmp(c->attrs[i].key, key, n) == 0) { + return c->attrs[i].value; + } + } + for (int i = 0; i < x->npattrs; i++) { + if (strlen(x->pattrs[i].key) == n && memcmp(x->pattrs[i].key, key, n) == 0) { + return x->pattrs[i].value; + } + } + return NULL; +} + +static bool ra_word(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; +} + +/* The text with the component's and the playbook's attribute references + * substituted, pass after pass. False when one stays undefined or it does + * not fit. */ +static bool ra_expand(const ra_index_t *x, const ra_comp_t *c, const char *s, char *out, + size_t cap) { + char cur[RA_PATH_CAP]; + char nxt[RA_PATH_CAP]; + if (snprintf(cur, sizeof(cur), "%s", s) >= (int)sizeof(cur)) { + return false; + } + bool undefined = false; + for (int pass = 0; pass < RA_EXPAND_PASSES; pass++) { + size_t w = 0; + bool changed = false; + undefined = false; + for (size_t i = 0; cur[i]; i++) { + const char *v = NULL; + size_t e = i + SKIP_ONE; + if (cur[i] == '{' && (i == 0 || cur[i - SKIP_ONE] != '\\') && ra_word(cur[e])) { + while (cur[e] && (ra_word(cur[e]) || cur[e] == '-')) { + e++; + } + if (cur[e] == '}') { + v = ra_attr(x, c, cur + i + SKIP_ONE, e - i - SKIP_ONE); + undefined = undefined || !v; + } + } + if (v) { + size_t vl = strlen(v); + if (w + vl >= sizeof(nxt)) { + return false; + } + memcpy(nxt + w, v, vl); + w += vl; + i = e; + changed = true; + continue; + } + if (w + SKIP_ONE >= sizeof(nxt)) { + return false; + } + nxt[w++] = cur[i]; + } + nxt[w] = '\0'; + memcpy(cur, nxt, w + SKIP_ONE); + if (!changed) { + break; + } + } + if (undefined || snprintf(out, cap, "%s", cur) >= (int)cap) { + return false; + } + return true; +} + +/* os.path.normpath(base/p); false when it leaves the repository. */ +static bool ra_norm(const char *base, const char *p, char *out, size_t cap) { + char tmp[RA_PATH_CAP * PAIR_LEN]; + snprintf(tmp, sizeof(tmp), "%s%s%s", base, base[0] && p[0] ? "/" : "", p); + size_t w = 0; + out[0] = '\0'; + for (char *s = tmp; *s;) { + char *e = strchr(s, '/'); + size_t l = e ? (size_t)(e - s) : strlen(s); + if (l == PAIR_LEN && s[0] == '.' && s[1] == '.') { + if (w == 0) { + return false; + } + while (w > 0 && out[w - SKIP_ONE] != '/') { + w--; + } + if (w > 0) { + w--; + } + out[w] = '\0'; + } else if (l > 0 && !(l == SKIP_ONE && s[0] == '.')) { + if (w + l + PAIR_LEN > cap) { + return false; + } + if (w > 0) { + out[w++] = '/'; + } + memcpy(out + w, s, l); + w += l; + out[w] = '\0'; + } + if (!e) { + break; + } + s = e + SKIP_ONE; + } + return true; +} + +static void ra_unresolved(cbm_doclink_outcome_t *out, int reason) { + out->kind = CBM_DOCLINK_UNRESOLVED; + out->reason = reason; +} + +static void ra_edge(cbm_doclink_outcome_t *out, const cbm_gbuf_node_t *n, uint32_t a, uint32_t b) { + out->kind = CBM_DOCLINK_EDGE; + out->target = n; + out->exact = true; + out->target_first = a; + out->target_last = b; +} + +/* A path of a type the indexer does not read (a service file, an image): no + * reference to code, decided by its name as the Markdown resolver does -- never + * by the disk, which an incremental run does not watch for such files. */ +static bool ra_unindexable(const char *path) { + return cbm_language_for_filename(ra_base(path)) == CBM_LANG_COUNT; +} + +/* A collector scan of build output (`./build/...`): what it contributes is + * generated, not a file of the repository (H8). */ +static bool ra_build_dir(const char *dir) { + while (dir[0] == '.' && dir[1] == '/') { + dir += PAIR_LEN; + } + return strcmp(dir, "build") == 0 || strncmp(dir, "build/", strlen("build/")) == 0 || + strstr(dir, "/build/") != NULL; +} + +static const char *const RA_FAMILIES[][2] = {{"example", "examples"}, + {"partial", "partials"}, + {"page", "pages"}, + {"image", "images"}, + {"attachment", "attachments"}}; + +/* H8 resolve_target: an Antora resource ID or a path relative to the including + * file. Returns the repository path in `out`, or false with `out->kind` set. */ +static bool ra_target(const ra_index_t *x, const cbm_gbuf_t *graph, const char *doc, + const ra_comp_t *comp, const char *target, char *path, size_t cap, + cbm_doclink_outcome_t *out) { + out->kind = CBM_DOCLINK_LOCAL; + if (strstr(target, "://")) { + return false; /* a URL include */ + } + const char *dollar = strchr(target, '$'); + if (dollar && comp) { + char pre[RA_PATH_CAP]; + snprintf(pre, sizeof(pre), "%.*s", (int)(dollar - target), target); + const char *at = strrchr(pre, '@'); + char *segs = at ? pre + (at - pre) + SKIP_ONE : pre; + /* family is the last segment; module and component before it */ + char *fam = strrchr(segs, ':'); + const char *module = NULL; + if (fam) { + *fam = '\0'; + fam++; + char *comp_sep = strchr(segs, ':'); + if (comp_sep) { + *comp_sep = '\0'; + if (strcmp(segs, comp->name) != 0) { + return false; /* another component's resource */ + } + module = comp_sep + SKIP_ONE; + } else { + module = segs; + } + } else { + fam = segs; + } + char doc_module[RA_PATH_CAP] = "ROOT"; + if (!module) { + /* the including page's own module: modules//... */ + const char *rel = doc + strlen(comp->root) + (comp->root[0] ? SKIP_ONE : 0); + if (strncmp(rel, "modules/", strlen("modules/")) == 0) { + const char *m = rel + strlen("modules/"); + const char *slash = strchr(m, '/'); + if (slash) { + snprintf(doc_module, sizeof(doc_module), "%.*s", (int)(slash - m), m); + } + } + module = doc_module; + } + const char *famdir = NULL; + for (size_t i = 0; i < sizeof(RA_FAMILIES) / sizeof(RA_FAMILIES[0]); i++) { + if (strcmp(fam, RA_FAMILIES[i][0]) == 0) { + famdir = RA_FAMILIES[i][1]; + } + } + if (!famdir) { + ra_unresolved(out, CBM_DOCLINK_REASON_UNPARSEABLE); + return false; + } + char virt[RA_PATH_CAP]; + snprintf(virt, sizeof(virt), "modules/%s/%s/%s", module, famdir, dollar + SKIP_ONE); + if (ra_norm(comp->root, virt, path, cap) && + cbm_pipeline_file_node(graph, x->project, path)) { + return true; + } + /* a file a collector scan contributes into the component */ + for (int i = 0; i < comp->nscans; i++) { + const char *into = comp->scans[i].into; + while (into[0] == '.' && into[1] == '/') { + into += PAIR_LEN; + } + size_t il = strlen(into); + while (il > 0 && into[il - SKIP_ONE] == '/') { + il--; + } + if (il == 0 || strncmp(virt, into, il) != 0 || virt[il] != '/') { + continue; + } + char sub[RA_PATH_CAP]; + snprintf(sub, sizeof(sub), "%s/%s", comp->scans[i].dir, virt + il + SKIP_ONE); + if (ra_norm(comp->root, sub, path, cap) && + cbm_pipeline_file_node(graph, x->project, path)) { + return true; + } + if (ra_build_dir(comp->scans[i].dir)) { + return false; /* build output (H8 generated): no file of the repository */ + } + } + if (ra_unindexable(virt)) { + return false; + } + ra_unresolved(out, CBM_DOCLINK_REASON_MISSING); + return false; + } + char dir[RA_PATH_CAP]; + const char *b = ra_base(doc); + snprintf(dir, sizeof(dir), "%.*s", b > doc ? (int)(b - doc - SKIP_ONE) : 0, doc); + if (!ra_norm(dir, target, path, cap)) { + return false; /* above the repository root */ + } + if (cbm_pipeline_file_node(graph, x->project, path)) { + return true; + } + char parent[RA_PATH_CAP]; + const char *pb = ra_base(path); + snprintf(parent, sizeof(parent), "%.*s", pb > path ? (int)(pb - path - SKIP_ONE) : 0, path); + if (!ra_unindexable(path) && parent[0] && cbm_doclink_md_folder(x->md, parent)) { + ra_unresolved(out, CBM_DOCLINK_REASON_MISSING); + } + return false; +} + +/* Asciidoctor's TagDirectiveRx \b(?:tag|(e)nd)::(\S+?)\[\](?=$|[ \r]) over the + * file: the 1-based lines from the first region's first content line to the + * last region's last one (directive lines excluded). False when a tag has no + * region. */ +static bool ra_tag_span(const char *src, size_t n, const char *const *tags, int ntags, + uint32_t *first, uint32_t *last) { + *first = 0; + *last = 0; + for (int t = 0; t < ntags; t++) { + size_t tl = strlen(tags[t]); + uint32_t line = 1; + uint32_t start = 0; + bool found = false; + size_t pos = 0; + while (pos < n) { + const char *nl = memchr(src + pos, '\n', n - pos); + size_t end = nl ? (size_t)(nl - src) : n; + for (size_t i = pos; i + PAIR_LEN < end; i++) { + bool is_end = false; + size_t k; + if (i + strlen("tag::") <= end && memcmp(src + i, "tag::", strlen("tag::")) == 0) { + k = i + strlen("tag::"); + } else if (i + strlen("end::") <= end && + memcmp(src + i, "end::", strlen("end::")) == 0) { + k = i + strlen("end::"); + is_end = true; + } else { + continue; + } + if (i > pos && ra_word(src[i - SKIP_ONE])) { + continue; /* \b */ + } + if (k + tl + PAIR_LEN > end || memcmp(src + k, tags[t], tl) != 0 || + src[k + tl] != '[' || src[k + tl + SKIP_ONE] != ']') { + continue; + } + size_t after = k + tl + PAIR_LEN; + if (after != end && src[after] != ' ' && src[after] != '\r') { + continue; + } + if (!is_end && start == 0) { + start = line + SKIP_ONE; + } else if (is_end && start != 0) { + if (start <= line - SKIP_ONE) { + *first = (*first == 0 || start < *first) ? start : *first; + *last = line - SKIP_ONE > *last ? line - SKIP_ONE : *last; + found = true; + } + start = 0; + } + break; + } + line++; + pos = end + SKIP_ONE; + } + if (start != 0 && start <= line - SKIP_ONE) { + *first = (*first == 0 || start < *first) ? start : *first; + *last = line - SKIP_ONE > *last ? line - SKIP_ONE : *last; + found = true; + } + if (!found) { + return false; + } + } + return *first > 0; +} + +/* The included file's text, read from the indexed file on disk (at most + * RA_TAG_FILE_MAX bytes): its tag regions and the lines of a region. NULL + * when it cannot be read; the caller frees it (cbm_free, OTHER). */ +static char *ra_read(const ra_index_t *x, const char *path, size_t *n) { + *n = 0; + if (!x->repo) { + return NULL; + } + char abs[RA_PATH_CAP * PAIR_LEN]; + snprintf(abs, sizeof(abs), "%s/%s", x->repo, path); + FILE *f = cbm_fopen(abs, "rb"); + if (!f) { + return NULL; + } + char *buf = (char *)cbm_alloc(CBM_MEM_CLASS_OTHER, RA_TAG_FILE_MAX); + *n = buf ? fread(buf, SKIP_ONE, RA_TAG_FILE_MAX, f) : 0; + (void)fclose(f); + return buf; +} + +/* `lines=a..b;c;d..-1`: the span of all ranges (-1: the file's end). */ +static bool ra_lines(const char *v, uint32_t *first, uint32_t *last) { + *first = 0; + *last = 0; + const char *p = v; + while (*p) { + while (*p == ';' || *p == ',' || *p == ' ' || *p == '"') { + p++; + } + if (!*p) { + break; + } + if (*p < '0' || *p > '9') { + return false; + } + uint32_t a = (uint32_t)strtoul(p, (char **)&p, 10); + uint32_t b = a; + if (p[0] == '.' && p[1] == '.') { + p += PAIR_LEN; + if (p[0] == '-' && p[1] == '1') { + b = UINT32_MAX; + p += PAIR_LEN; + } else if (*p >= '0' && *p <= '9') { + b = (uint32_t)strtoul(p, (char **)&p, 10); + } else { + return false; + } + } + if (a == 0 || b < a) { + return false; + } + *first = (*first == 0 || a < *first) ? a : *first; + *last = b > *last ? b : *last; + } + return *first > 0; +} + +typedef struct { + char buf[RA_PATH_CAP * PAIR_LEN]; + char *f[RA_FIELDS]; + int n; +} ra_rec_t; + +static bool ra_record(const char *raw, ra_rec_t *r) { + if (snprintf(r->buf, sizeof(r->buf), "%s", raw) >= (int)sizeof(r->buf)) { + return false; + } + r->n = 0; + char *p = r->buf; + while (r->n < RA_FIELDS) { + r->f[r->n++] = p; + char *tab = strchr(p, '\t'); + if (!tab) { + break; + } + *tab = '\0'; + p = tab + SKIP_ONE; + } + for (int i = r->n; i < RA_FIELDS; i++) { + r->f[i] = ""; + } + return true; +} + +static void ra_include(const ra_index_t *x, const cbm_gbuf_t *graph, const char *doc, + const ra_rec_t *r, cbm_doclink_outcome_t *out) { + const ra_comp_t *comp = ra_component_of(x, doc); + char target[RA_PATH_CAP]; + if (strchr(r->f[1], '{')) { + if (!comp || !ra_expand(x, comp, r->f[1], target, sizeof(target))) { + ra_unresolved(out, CBM_DOCLINK_REASON_UNPARSEABLE); /* an undefined attribute */ + return; + } + } else { + snprintf(target, sizeof(target), "%s", r->f[1]); + } + char path[RA_PATH_CAP]; + if (!ra_target(x, graph, doc, comp, target, path, sizeof(path), out)) { + return; + } + const cbm_gbuf_node_t *file = cbm_pipeline_file_node(graph, x->project, path); + if (!file) { + out->kind = CBM_DOCLINK_LOCAL; + return; + } + uint32_t first = 0; + uint32_t last = 0; + const char *tagv = r->f[2]; + if (!tagv[0] && r->f[3][0] && !ra_lines(r->f[3], &first, &last)) { + ra_unresolved(out, CBM_DOCLINK_REASON_UNPARSEABLE); + return; + } + size_t tn = 0; + char *text = (tagv[0] || first > 0) ? ra_read(x, path, &tn) : NULL; + if (tagv[0]) { + /* H8: negated and wildcard tags leave the positive ones; none left + * includes the whole file */ + char tags[RA_TAGS][RA_TAG_LEN]; + const char *tagp[RA_TAGS]; + int nt = 0; + for (const char *p = tagv; *p && nt < RA_TAGS;) { + size_t l = strcspn(p, ";,"); + if (l > 0 && l < RA_TAG_LEN && p[0] != '!' && !memchr(p, '*', l)) { + snprintf(tags[nt], RA_TAG_LEN, "%.*s", (int)l, p); + tagp[nt] = tags[nt]; + nt++; + } + p += l + (p[l] ? SKIP_ONE : 0); + } + if (nt == 0 || !text || !ra_tag_span(text, tn, tagp, nt, &first, &last)) { + first = 0; /* wildcards only, or a tag the file lacks: the file */ + last = 0; + } + } + if (first > 0) { + const cbm_gbuf_node_t *seg = text + ? cbm_doclink_md_region(x->md, path, text, tn, first, last) + : cbm_doclink_md_segment(x->md, path, first, last, NULL); + cbm_free(CBM_MEM_CLASS_OTHER, text); + ra_edge(out, seg ? seg : file, first, last == UINT32_MAX ? 0 : last); + return; + } + cbm_free(CBM_MEM_CLASS_OTHER, text); + ra_edge(out, file, 0, 0); +} + +static bool ra_class_label(const char *label) { + static const char *const labels[] = {"Class", "Interface", "Enum", "Type", "Trait", NULL}; + for (int i = 0; label && labels[i]; i++) { + if (strcmp(label, labels[i]) == 0) { + return true; + } + } + return false; +} + +static bool ra_member_label(const char *label) { + static const char *const labels[] = {"Method", "Field", "Variable", "Function", NULL}; + for (int i = 0; label && labels[i]; i++) { + if (strcmp(label, labels[i]) == 0) { + return true; + } + } + return false; +} + +/* H8 javadoc: xref:attachment$api///.html[#member]. */ +static void ra_javadoc(const ra_index_t *x, const cbm_gbuf_t *graph, const char *val, + cbm_doclink_outcome_t *out) { + static const char key[] = "xref:attachment$api/"; + const char *p = strstr(val, key); + if (!p) { + return; + } + p += strlen(key); + const char *slash = strchr(p, '/'); /* after the JPMS module */ + if (!slash) { + return; + } + const char *path = slash + SKIP_ONE; + const char *html = strstr(path, ".html"); + if (!html || memchr(path, '[', (size_t)(html - path))) { + return; + } + char rel[RA_PATH_CAP]; + snprintf(rel, sizeof(rel), "%.*s", (int)(html - path), path); + const char *tname = ra_base(rel); + if (strcmp(tname, "package-summary") == 0 || strcmp(tname, "module-summary") == 0 || + strcmp(rel, "index") == 0) { + return; /* a package or module page: no code node */ + } + char anchor[RA_PATH_CAP] = ""; + if (html[strlen(".html")] == '#') { + const char *a = html + strlen(".html") + SKIP_ONE; + size_t al = strcspn(a, "[(-"); + snprintf(anchor, sizeof(anchor), "%.*s", (int)al, a); + } + char pkgdir[RA_PATH_CAP]; + snprintf(pkgdir, sizeof(pkgdir), "%.*s", (int)(tname - rel), rel); /* "org/x/" */ + char outer[RA_PATH_CAP]; + snprintf(outer, sizeof(outer), "%.*s", (int)strcspn(tname, "."), tname); + const char *want = strrchr(tname, '.') ? strrchr(tname, '.') + SKIP_ONE : tname; + /* the source files declaring /.java|.kt, main sources first */ + const cbm_gbuf_node_t **nodes = NULL; + int count = 0; + if (cbm_gbuf_find_by_name(graph, outer, &nodes, &count) != 0) { + return; + } + const char *files[2] = {NULL, NULL}; + int nfiles = 0; + for (int pass = 0; pass < PAIR_LEN && nfiles == 0; pass++) { + bool main_only = pass == 0; + for (int i = 0; i < count; i++) { + const cbm_gbuf_node_t *n = nodes[i]; + const char *fp = n->file_path; + if (!ra_class_label(n->label) || !fp) { + continue; + } + char tail[RA_PATH_CAP * PAIR_LEN]; + bool java = ra_ends(fp, ".java"); + snprintf(tail, sizeof(tail), "%s%s%s", pkgdir, outer, java ? ".java" : ".kt"); + size_t fl = strlen(fp); + size_t tl = strlen(tail); + if (!(java || ra_ends(fp, ".kt")) || fl < tl || strcmp(fp + fl - tl, tail) != 0 || + (fl > tl && fp[fl - tl - SKIP_ONE] != '/') || + (main_only && !strstr(fp, "/src/main/"))) { + continue; + } + bool seen = false; + for (int k = 0; k < nfiles; k++) { + seen = seen || strcmp(files[k], fp) == 0; + } + if (!seen && nfiles < PAIR_LEN) { + files[nfiles++] = fp; + } else if (!seen) { + nfiles++; + } + } + } + if (nfiles == 0) { + ra_unresolved(out, CBM_DOCLINK_REASON_MISSING); + return; + } + if (nfiles > SKIP_ONE) { + ra_unresolved(out, CBM_DOCLINK_REASON_AMBIGUOUS); + return; + } + /* the type in that file (the first by line for a repeated name) */ + const cbm_gbuf_node_t *type = NULL; + if (cbm_gbuf_find_by_name(graph, want, &nodes, &count) == 0) { + for (int i = 0; i < count; i++) { + const cbm_gbuf_node_t *n = nodes[i]; + if (ra_class_label(n->label) && n->file_path && strcmp(n->file_path, files[0]) == 0 && + (!type || n->start_line < type->start_line || + (n->start_line == type->start_line && n->id < type->id))) { + type = n; + } + } + } + if (!type) { + ra_unresolved(out, CBM_DOCLINK_REASON_GRAPH_GAP); + return; + } + const cbm_gbuf_node_t *member = NULL; + int members = 0; + if (anchor[0] && cbm_gbuf_find_by_name(graph, anchor, &nodes, &count) == 0) { + for (int i = 0; i < count; i++) { + const cbm_gbuf_node_t *n = nodes[i]; + if (ra_member_label(n->label) && n->file_path && strcmp(n->file_path, files[0]) == 0 && + n->start_line >= type->start_line && n->start_line <= type->end_line) { + member = n; + members++; + } + } + } + /* an overloaded member: the type that declares it */ + ra_edge(out, members == SKIP_ONE ? member : type, 0, 0); +} + +static void ra_attribute(const ra_index_t *x, const cbm_gbuf_t *graph, const char *doc, + const ra_rec_t *r, cbm_doclink_outcome_t *out) { + const ra_comp_t *comp = ra_component_of(x, doc); + if (!comp) { + return; /* outside an Antora component: no API attributes (H8) */ + } + const char *raw = r->f[2][0] ? r->f[2] : ra_attr(x, comp, r->f[1], strlen(r->f[1])); + if (!raw || !strstr(raw, "{javadoc-root}")) { + return; + } + char val[RA_PATH_CAP]; + if (!ra_expand(x, comp, r->f[2][0] ? r->f[3] : raw, val, sizeof(val))) { + return; + } + ra_javadoc(x, graph, val, out); +} + +static void ra_resolve(const void *index, void *state, int run_file, const CBMDocLink *link, + const cbm_gbuf_t *graph, cbm_doclink_outcome_t *out) { + (void)state; + const ra_index_t *x = (const ra_index_t *)index; + out->kind = CBM_DOCLINK_LOCAL; + const char *doc = (run_file >= 0 && run_file < x->run_count) ? x->run_paths[run_file] : NULL; + if (!doc || !link->raw) { + ra_unresolved(out, CBM_DOCLINK_REASON_UNPARSEABLE); + return; + } + if (link->syntax == CBM_DOCLINK_ADOC_CODE_PATH || link->syntax == CBM_DOCLINK_ADOC_CODE_NAME) { + CBMDocLink md = *link; + md.syntax = link->syntax == CBM_DOCLINK_ADOC_CODE_PATH ? CBM_DOCLINK_MD_CODE_PATH + : CBM_DOCLINK_MD_CODE_NAME; + cbm_doclink_md_resolver.resolve(x->md, NULL, run_file, &md, graph, out); + return; + } + ra_rec_t *r = (ra_rec_t *)cbm_alloc(CBM_MEM_CLASS_OTHER, sizeof(*r)); + if (!r || !ra_record(link->raw, r)) { + ra_unresolved(out, CBM_DOCLINK_REASON_UNPARSEABLE); + cbm_free(CBM_MEM_CLASS_OTHER, r); + return; + } + if (link->syntax == CBM_DOCLINK_ADOC_INCLUDE) { + ra_include(x, graph, doc, r, out); + } else if (link->syntax == CBM_DOCLINK_ADOC_ATTRIBUTE) { + ra_attribute(x, graph, doc, r, out); + } + cbm_free(CBM_MEM_CLASS_OTHER, r); +} + +const cbm_doclink_resolver_t cbm_doclink_adoc_resolver = { + .langs = {CBM_LANG_ASCIIDOC, CBM_LANG_YAML}, + .lang_count = 2, + .scope_tag = CBM_DOCLINK_ADOC_SCOPE_TAG, + .build = ra_build, + .destroy = ra_destroy, + .resolve = ra_resolve, + .via = "asciidoc", +}; diff --git a/src/pipeline/doc_links_md.c b/src/pipeline/doc_links_md.c index b2a76594e..667ea724a 100644 --- a/src/pipeline/doc_links_md.c +++ b/src/pipeline/doc_links_md.c @@ -50,6 +50,8 @@ enum { MDR_HEX_BASE = 16, MDR_PCT_LEN = 3, /* `%XX` */ MDR_ADR_DIGITS = 5, /* an ADR number has at most this many digits */ + MDR_PRIO_FIELD = 3, /* H8's label priorities: types 0, Type 1, callables 2 */ + MDR_PRIO_OTHER = 4, }; /* Labels whose nodes are code a range or a member can name. */ @@ -108,8 +110,8 @@ static const char *mdr_base(const char *path) { static bool mdr_code_file(const char *path) { CBMLanguage lang = cbm_language_for_filename(mdr_base(path)); return lang != CBM_LANG_COUNT && lang != CBM_LANG_MARKDOWN && lang != CBM_LANG_RST && - lang != CBM_LANG_HTML && lang != CBM_LANG_CSS && lang != CBM_LANG_SCSS && - !cbm_has_config_extension(path); + lang != CBM_LANG_ASCIIDOC && lang != CBM_LANG_PDF && lang != CBM_LANG_HTML && + lang != CBM_LANG_CSS && lang != CBM_LANG_SCSS && !cbm_has_config_extension(path); } /* The stems of a package directory's index file. */ @@ -1205,6 +1207,228 @@ const cbm_gbuf_node_t *cbm_doclink_md_segment(const void *md_index, const char * return md_index ? mdr_segment((const mdr_index_t *)md_index, path, first, last, member) : NULL; } +/* ── A region of a file's text (the field test's H8 bind_range) ─────── */ + +/* A blank or comment-only line (H8 _skippable). */ +static bool mdr_skippable(const char *s, size_t n) { + size_t i = 0; + while (i < n && (s[i] == ' ' || s[i] == '\t' || s[i] == '\r')) { + i++; + } + if (i == n) { + return true; + } + const char *t = s + i; + size_t r = n - i; + if ((r >= PAIR_LEN && t[0] == '/' && (t[1] == '/' || t[1] == '*')) || t[0] == '*' || + t[0] == ';' || t[0] == '%' || (r >= PAIR_LEN && t[0] == '\'' && t[1] == ' ') || + (r >= PAIR_LEN && t[0] == '-' && t[1] == '-' && r > PAIR_LEN && + (t[PAIR_LEN] == ' ' || t[PAIR_LEN] == '\t')) || + (r >= strlen("