doc-links(asciidoc): AsciiDoc sections linked to the code they include and name

AsciiDoc (.adoc, .asciidoc) is now indexed, without a grammar like PDF: a
Section per heading (`=` .. `======`, or `#`), the text up to the next
heading as its docstring, delimited blocks respected (listing, literal,
comment, passthrough, example, sidebar, quote, table, fences). Its
references become MENTIONS edges, via "asciidoc", from the section they are
written in:

  include     include::target[...] -- Asciidoctor's preprocessor directive,
              read in every block but a comment block; the page's own
              attributes, then the Antora component's and the playbook's,
              substituted
  attribute   an {attribute} whose value is an Antora javadoc link
              ({javadoc-root}/<module>/<package path>/<Type>.html[#member])
              binds the Java or Kotlin type, or its one member, that the
              repository's main sources declare under that path
  code_path,  monospace spans naming a path or a qualified name (the
  code_name   Markdown code-span rules)

The rules are the field test's (H8 Asciidoctor/Antora harvester, audited
~100 % precise): an Antora resource ID ([component:][module:]family$path)
names <component>/modules/<module>/<family dir>/<path>, or the file a
collector scan contributes there (a scan of build output is generated: no
row); any other target is relative to the including file. `lines=` and
`tag=`/`tags=` regions bind by H8's rule: edge blank and comment lines
trimmed, the innermost enclosing definition, unless that is a type (or there
is none) and the definitions inside cover every other line (an annotation
right above one counts) -- then that definition; tag regions come from the
included file's own `tag::x[]` / `end::x[]` comments, read from that indexed
file; a tag the file lacks binds the file. One reference makes one edge: a
region holding several top-level definitions binds the file.

Antora settings travel as YAML scope blobs (antora.yml: component name,
asciidoc attributes, collector scans; antora-playbook*.yml: attributes),
read as text, so an incremental run resolves as a full one; a change to them
is GLOBAL. AsciiDoc joins the documents bound by lines: a body edit of an
included file re-resolves its pages.

Also: the Markdown resolver's module index no longer takes AsciiDoc or PDF
files for code modules.

Proof on junit-team/junit-framework at 6c8ca116 (H8's corpus and commit),
C against H8's recorded links: javadoc attributes 362/362 identical;
includes 292 identical, 60 bind the type enclosing the several definitions
H8 names, 7 regions with several top-level definitions bind the file. 317
sections, 668 edges, no include rows (9 generated partials, 1 file type the
indexer does not read); monospace spans 30/30 correct by hand. 8.3 s for the
whole index.

Tests: suite doc_links_adoc (structure and tokens, the Antora blob, the
pipeline: resource IDs through a collector scan, tag regions, a region with
its imports binding its class, lines=, javadoc attributes, spans; incremental
== full: a moved tag region, a changed component attribute). Revert check:
without H8's region rule and without AsciiDoc among the line-bound documents
exactly their assertions fail (observed).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-10-09 02:55:23 +02:00
1 parent c2c87f6202
commit 92f5edfe32
17 files changed
+2459 -13

No files matched your search

+3 -1
View File
@@ -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
+19
View File
@@ -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);
+1
View File
@@ -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;
+944
View File
@@ -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 <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}
+5
View File
@@ -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) {
+21
View File
@@ -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. */
+3
View File
@@ -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
+4
View File
@@ -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",
+1
View File
@@ -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},
+3 -5
View File
@@ -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) {
+14 -1
View File
@@ -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);
+919
View File
@@ -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
* <component>/modules/<module>/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
* <component>/modules/<module>/<family dir>/<path>, 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}/<module>/<package path>/<Type>.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 <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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/<module>/... */
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/<module>/<pkg path>/<Type>.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 <pkg dir>/<Outer>.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",
};
+226 -2
View File
@@ -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("<!--") && memcmp(t, "<!--", strlen("<!--")) == 0)) {
return true;
}
return t[0] == '#' && !(r >= PAIR_LEN && (t[1] == '[' || t[1] == '!'));
}
static bool mdr_annotation(const char *s, size_t n) {
size_t i = 0;
while (i < n && (s[i] == ' ' || s[i] == '\t')) {
i++;
}
return i < n && (s[i] == '@' || (i + SKIP_ONE < n && s[i] == '#' && s[i + SKIP_ONE] == '['));
}
static bool mdr_container(const char *label) {
static const char *const labels[] = {"Function", "Method", "Class", "Struct", "Interface",
"Enum", "Type", "Trait", "Record", NULL};
for (int i = 0; label && labels[i]; i++) {
if (strcmp(label, labels[i]) == 0) {
return true;
}
}
return false;
}
static int mdr_label_prio(const char *label) {
if (!label) {
return MDR_PRIO_OTHER;
}
if (mdr_classlike(label) && strcmp(label, "Type") != 0) {
return 0;
}
if (strcmp(label, "Type") == 0) {
return SKIP_ONE;
}
if (strcmp(label, "Function") == 0 || strcmp(label, "Method") == 0) {
return PAIR_LEN;
}
return strcmp(label, "Field") == 0 ? MDR_PRIO_FIELD : MDR_PRIO_OTHER;
}
/* (s, -e, label priority, id): the order H8 picks maximal nodes in. */
static int mdr_region_cmp(const void *a, const void *b) {
const cbm_gbuf_node_t *x = *(const cbm_gbuf_node_t *const *)a;
const cbm_gbuf_node_t *y = *(const cbm_gbuf_node_t *const *)b;
if (x->start_line != y->start_line) {
return x->start_line < y->start_line ? -1 : 1;
}
if (x->end_line != y->end_line) {
return x->end_line > y->end_line ? -1 : 1;
}
int px = mdr_label_prio(x->label);
int py = mdr_label_prio(y->label);
if (px != py) {
return px < py ? -1 : 1;
}
return (x->id > y->id) - (x->id < y->id);
}
/* Do the maximal nodes cover every line of [a, b] that is not blank or a
* comment (an annotation line counts when it sits right above a covered
* node, H8's exact-cover)? */
static bool mdr_exact_cover(const cbm_gbuf_node_t *const *max, int nmax, const size_t *starts,
const char *text, size_t text_len, uint32_t a, uint32_t b) {
for (uint32_t ln = a; ln <= b; ln++) {
bool covered = false;
for (int k = 0; k < nmax && !covered; k++) {
covered = (uint32_t)max[k]->start_line <= ln && (uint32_t)max[k]->end_line >= ln;
}
size_t s0 = starts[ln - SKIP_ONE];
size_t s1 = starts[ln] > s0 ? starts[ln] - SKIP_ONE : s0;
if (covered || s0 >= text_len || mdr_skippable(text + s0, s1 - s0)) {
continue;
}
if (!mdr_annotation(text + s0, s1 - s0)) {
return false;
}
/* annotations, blanks and comments down to a covered node's first line */
uint32_t nxt = ln + SKIP_ONE;
while (nxt <= b) {
size_t t0 = starts[nxt - SKIP_ONE];
size_t t1 = starts[nxt] > t0 ? starts[nxt] - SKIP_ONE : t0;
if (!mdr_skippable(text + t0, t1 - t0) && !mdr_annotation(text + t0, t1 - t0)) {
break;
}
nxt++;
}
bool starts_node = false;
for (int k = 0; k < nmax && !starts_node; k++) {
starts_node = (uint32_t)max[k]->start_line <= nxt && (uint32_t)max[k]->end_line >= nxt;
}
if (!starts_node) {
return false;
}
}
return true;
}
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) {
const mdr_index_t *x = (const mdr_index_t *)md_index;
if (!x || !text || first == 0 || last < first) {
return NULL;
}
/* line starts: starts[k] is the offset of line k + 1; one past the end */
uint32_t nlines = SKIP_ONE;
for (size_t i = 0; i < text_len; i++) {
nlines += text[i] == '\n';
}
if (last > nlines) {
last = nlines;
}
if (first > last) {
return NULL;
}
size_t *starts =
(size_t *)cbm_alloc(CBM_MEM_CLASS_OTHER, ((size_t)nlines + SKIP_ONE) * sizeof(size_t));
if (!starts) {
return NULL;
}
uint32_t k = 0;
starts[k++] = 0;
for (size_t i = 0; i < text_len; i++) {
if (text[i] == '\n') {
starts[k++] = i + SKIP_ONE;
}
}
starts[k] = text_len + SKIP_ONE;
/* edge blank and comment lines trimmed */
uint32_t a = first;
uint32_t b = last;
while (a <= b && mdr_skippable(text + starts[a - SKIP_ONE],
starts[a] - SKIP_ONE - starts[a - SKIP_ONE])) {
a++;
}
while (b >= a && mdr_skippable(text + starts[b - SKIP_ONE],
starts[b] - SKIP_ONE - starts[b - SKIP_ONE])) {
b--;
}
if (a > b) {
a = first;
b = last;
}
int lo = 0;
int hi = 0;
mdr_file_defs(x, path, &lo, &hi);
const cbm_gbuf_node_t *enc = NULL;
for (int i = lo; i < hi; i++) {
const cbm_gbuf_node_t *n = x->defs[i].node;
if (!mdr_container(n->label) || n->start_line <= 0 || (uint32_t)n->start_line > a ||
(uint32_t)n->end_line < b) {
continue;
}
int span = n->end_line - n->start_line;
int best = enc ? enc->end_line - enc->start_line : 0;
if (!enc || span < best ||
(span == best && (n->start_line > enc->start_line ||
(n->start_line == enc->start_line &&
mdr_label_prio(n->label) < mdr_label_prio(enc->label))))) {
enc = n;
}
}
const cbm_gbuf_node_t **inside = (const cbm_gbuf_node_t **)cbm_alloc(
CBM_MEM_CLASS_OTHER, (size_t)(hi - lo + SKIP_ONE) * sizeof(*inside));
const cbm_gbuf_node_t *pick = NULL;
if (inside) {
int ni = 0;
for (int i = lo; i < hi; i++) {
const cbm_gbuf_node_t *n = x->defs[i].node;
if (n == enc || n->start_line <= 0 || (uint32_t)n->start_line < a ||
(uint32_t)n->end_line > b ||
(enc && (n->start_line < enc->start_line || n->end_line > enc->end_line))) {
continue;
}
inside[ni++] = n;
}
qsort(inside, (size_t)ni, sizeof(*inside), mdr_region_cmp);
int nmax = 0; /* maximal ones, compacted in place */
for (int i = 0; i < ni; i++) {
bool nested = false;
for (int m = 0; m < nmax && !nested; m++) {
nested = inside[m]->start_line <= inside[i]->start_line &&
inside[i]->end_line <= inside[m]->end_line;
}
if (!nested) {
inside[nmax++] = inside[i];
}
}
bool type_enc = !enc || (mdr_classlike(enc->label));
bool exact =
nmax > 0 && type_enc && mdr_exact_cover(inside, nmax, starts, text, text_len, a, b);
if (exact && enc) {
pick = nmax == SKIP_ONE ? inside[0] : enc; /* several: the type holding them */
} else if (enc) {
pick = enc;
} else if (nmax == SKIP_ONE) {
pick = inside[0];
}
cbm_free(CBM_MEM_CLASS_OTHER, (void *)inside);
}
cbm_free(CBM_MEM_CLASS_OTHER, starts);
return pick;
}
const cbm_gbuf_node_t *cbm_doclink_md_folder(const void *md_index, const char *path) {
return md_index && path ? mdr_folder((const mdr_index_t *)md_index, NULL, path) : NULL;
}
+3 -2
View File
@@ -549,9 +549,10 @@ TEST(repro_call_node_manifest_live_specs_contain_only_expected_primary_entries)
/* The registry reproduction owns Nim's stale enum and the
* intentionally grammar-free classifications: Studio Export's
* transform-only one and PDF's DOCUMENT one (doc_pdf.c). */
* transform-only one and the DOCUMENT ones of PDF and AsciiDoc
* (doc_pdf.c, doc_adoc.c). */
if (language == CBM_LANG_NIM || language == CBM_LANG_OBJECTSCRIPT_EXPORT ||
language == CBM_LANG_PDF) {
language == CBM_LANG_PDF || language == CBM_LANG_ASCIIDOC) {
continue;
}
if (!spec || spec->language != language) {
+4 -2
View File
@@ -218,6 +218,7 @@ static const LanguageCapabilityEntry LANGUAGE_CAPABILITIES[CBM_LANG_COUNT] = {
CALL_WITHOUT_REFERENCE_VOCAB(PLSQL),
CALL_WITHOUT_REFERENCE_VOCAB(CHIALISP),
DOCUMENT(PDF),
DOCUMENT(ASCIIDOC),
};
#undef CALL_WITH_REFERENCE_VOCAB
@@ -311,7 +312,7 @@ TEST(repro_language_capability_ledger_covers_every_enum) {
if (counts[CAP_CALL_WITH_REFERENCE_VOCAB] != 88 ||
counts[CAP_CALL_WITHOUT_REFERENCE_VOCAB] != 27 || counts[CAP_NO_CALL] != 49 ||
counts[CAP_TRANSFORM_ONLY] != 1 || counts[CAP_UNSUPPORTED] != 1 ||
counts[CAP_DOCUMENT] != 1) {
counts[CAP_DOCUMENT] != 2) {
fprintf(stderr,
" [language-registry] invariant=capability_partition call_ref_vocab=%d ref_gap=%d "
"no_call=%d transform_only=%d unsupported=%d document=%d\n",
@@ -330,7 +331,8 @@ TEST(repro_call_argument_matrices_equal_call_capability_ledger) {
EXPECTED_MATRIX_A_ROWS = 68,
EXPECTED_MATRIX_B_ROWS = 49,
EXPECTED_CALL_CAPABLE_LANGUAGES = 115,
EXPECTED_NON_CALL_LANGUAGES = 52, /* 49 no-call + transform-only + unsupported + PDF */
EXPECTED_NON_CALL_LANGUAGES =
53, /* 49 no-call + transform-only + unsupported + PDF, AsciiDoc */
EXPECTED_NON_CALL_DOMAIN_CONTROLS = 2,
};
CBMLanguage matrix_a_ids[CBM_LANG_COUNT];
+287
View File
@@ -0,0 +1,287 @@
/*
* test_doc_links_adoc.c — AsciiDoc documents -> Sections and MENTIONS edges
* (doc_adoc.c, doc_links_adoc.c).
*
* Headings and delimited blocks, the tokens (include with the page's
* attributes, attribute references, monospace spans), the Antora scope blob,
* and the resolver: resource IDs and collector scans, tag regions read from
* the included file, lines, javadoc attributes to types and members, the
* monospace spans through the Markdown rules, incremental == full.
*/
#include "../src/foundation/compat.h"
#include "test_framework.h"
#include "test_helpers.h"
#include "test_doc_mentions_helpers.h"
#include "cbm.h"
#include "doclink.h"
#include "pipeline/pipeline.h"
#include "pipeline/pipeline_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static const char *const ADOC_PAGE =
"= Writing Tests\n" /* 1 */
":testDir: example$java\n" /* 2 */
"\n" /* 3 */
"Intro with {Assertions} and {nbsp}.\n" /* 4 */
"\n" /* 5 */
"== First Test\n" /* 6 */
"\n" /* 7 */
"[source,java]\n" /* 8 */
"----\n" /* 9 */
"include::{testDir}/example/FirstTests.java[tags=user_guide]\n" /* 10 */
"== Not A Heading\n" /* 11 */
"----\n" /* 12 */
"\n" /* 13 */
"////\n" /* 14 */
"include::example$java/Hidden.java[]\n" /* 15 */
"////\n" /* 16 */
"// include::example$java/Commented.java[]\n" /* 17 */
"\n" /* 18 */
"=== Details\n" /* 19 */
"\n" /* 20 */
"See `src/main/java/org/x/Assertions.java` and `org.x.Assertions`.\n" /* 21 */
"include::../../../../src/main/java/org/x/Calc.java[lines=4..6]\n" /* 22 */
"include::{testDir}/example/WithImports.java[tag=all]\n"; /* 23 */
static const CBMDefinition *adoc_def(const CBMFileResult *r, const char *name) {
for (int i = 0; i < r->defs.count; i++) {
const CBMDefinition *d = &r->defs.items[i];
if (d->label && strcmp(d->label, "Section") == 0 && d->name && strcmp(d->name, name) == 0) {
return d;
}
}
return NULL;
}
static bool adoc_raw_has(const CBMFileResult *r, const char *needle) {
for (int i = 0; i < r->doc_links.count; i++) {
if (strstr(r->doc_links.items[i].raw, needle)) {
return true;
}
}
return false;
}
TEST(adoc_scan_structure) {
CBMFileResult *r =
dm_extract(ADOC_PAGE, CBM_LANG_ASCIIDOC, "docs/modules/ROOT/pages/writing.adoc");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->doc_links.failed);
const CBMDefinition *top = adoc_def(r, "Writing Tests");
const CBMDefinition *first = adoc_def(r, "First Test");
const CBMDefinition *details = adoc_def(r, "Details");
ASSERT_NOT_NULL(top);
ASSERT_NOT_NULL(first);
ASSERT_NOT_NULL(details);
ASSERT_NULL(adoc_def(r, "Not A Heading")); /* inside a listing block */
ASSERT_EQ(first->start_line, 6);
ASSERT_EQ(first->end_line, 17);
ASSERT_STR_EQ(details->qualified_name, "p.docs.modules.ROOT.pages.writing.Details");
/* an include with the page's attribute substituted, its tags */
const CBMDocLink *inc =
dm_find_token(r, "include::{testDir}/example/FirstTests.java[tags=user_guide]\t"
"example$java/example/FirstTests.java\tuser_guide\t");
ASSERT_NOT_NULL(inc);
ASSERT_EQ(inc->syntax, CBM_DOCLINK_ADOC_INCLUDE);
ASSERT_EQ(inc->line, 10);
ASSERT_STR_EQ(inc->source_qn, first->qualified_name);
ASSERT_NOT_NULL(dm_find_token(r,
"include::../../../../src/main/java/org/x/Calc.java[lines=4..6]"
"\t../../../../src/main/java/org/x/Calc.java\t\t4..6"));
/* nothing from a comment block or a line comment */
ASSERT_FALSE(adoc_raw_has(r, "Hidden.java"));
ASSERT_FALSE(adoc_raw_has(r, "Commented.java"));
/* attribute references: built-ins are none; the page's own carry its value */
ASSERT_NOT_NULL(dm_find_token(r, "{Assertions}\tAssertions\t\t"));
ASSERT_FALSE(adoc_raw_has(r, "{nbsp}"));
/* monospace spans */
ASSERT_EQ(dm_find_token(r, "src/main/java/org/x/Assertions.java")->syntax,
CBM_DOCLINK_ADOC_CODE_PATH);
ASSERT_EQ(dm_find_token(r, "org.x.Assertions")->syntax, CBM_DOCLINK_ADOC_CODE_NAME);
cbm_free_result(r);
PASS();
}
static const char *const ANTORA_YML =
"name: guide\n"
"version: true\n"
"ext:\n"
" collector:\n"
" scan:\n"
" - dir: ./build/generated\n"
" clean: true\n"
" - dir: ./src/test\n"
" into: modules/ROOT/examples\n"
"asciidoc:\n"
" attributes:\n"
" javadoc-root: \"xref:attachment$api\"\n"
" # API\n"
" Assertions: '{javadoc-root}/org.x/org/x/Assertions.html[Assertions]'\n"
" assertEquals: '{javadoc-root}/org.x/org/x/Assertions.html#assertEquals(int,int)[x]'\n"
" Missing: '{javadoc-root}/org.x/org/x/Gone.html[Gone]'\n";
TEST(adoc_antora_scope_blob) {
CBMFileResult *r = dm_extract(ANTORA_YML, CBM_LANG_YAML, "docs/antora.yml");
ASSERT_NOT_NULL(r);
ASSERT_NOT_NULL(r->doc_scope);
ASSERT_STR_EQ(r->doc_scope,
"ad1\n"
"C\n"
"N\tguide\n"
"D\t./build/generated\n"
"D\t./src/test\n"
"I\tmodules/ROOT/examples\n"
"A\tjavadoc-root\txref:attachment$api\n"
"A\tAssertions\t{javadoc-root}/org.x/org/x/Assertions.html[Assertions]\n"
"A\tassertEquals\t{javadoc-root}/org.x/org/x/Assertions.html#assertEquals(int,"
"int)[x]\n"
"A\tMissing\t{javadoc-root}/org.x/org/x/Gone.html[Gone]\n");
cbm_free_result(r);
r = dm_extract("site:\n title: x\n", CBM_LANG_YAML, "config/other.yml");
ASSERT_NOT_NULL(r);
ASSERT_NULL(r->doc_scope);
cbm_free_result(r);
PASS();
}
static void adoc_write_repo(const char *repo, const char *first_tests) {
th_write_file(TH_PATH(repo, "docs/antora.yml"), ANTORA_YML);
th_write_file(TH_PATH(repo, "docs/modules/ROOT/pages/writing.adoc"), ADOC_PAGE);
th_write_file(TH_PATH(repo, "docs/src/test/java/example/FirstTests.java"), first_tests);
th_write_file(TH_PATH(repo, "src/main/java/org/x/Assertions.java"),
"package org.x;\n"
"\n"
"public class Assertions {\n"
" public static void assertEquals(int a, int b) {}\n"
" public static void fail() {}\n"
"}\n");
/* a tag region that takes the imports with the class: it binds the class */
th_write_file(TH_PATH(repo, "docs/src/test/java/example/WithImports.java"),
"package example;\n"
"\n"
"// tag::all[]\n"
"import java.util.List;\n"
"\n"
"class WithImports {\n"
" List<String> names() { return null; }\n"
"}\n"
"// end::all[]\n");
th_write_file(TH_PATH(repo, "src/main/java/org/x/Calc.java"),
"package org.x;\n"
"\n"
"public class Calc {\n"
" public int add(int a, int b) {\n"
" return a + b;\n"
" }\n"
"}\n");
}
static const char *const FIRST_TESTS = "package example;\n" /* 1 */
"\n" /* 2 */
"class FirstTests {\n" /* 3 */
" void helper() {}\n" /* 4 */
"\n" /* 5 */
" // tag::user_guide[]\n" /* 6 */
" void addition() {\n" /* 7 */
" helper();\n" /* 8 */
" }\n" /* 9 */
" // end::user_guide[]\n" /* 10 */
"}\n"; /* 11 */
static bool adoc_edge(const char *db, const char *src, const char *tgt, const char *needle) {
char props[512];
int n = 0;
dm_edge(db, src, tgt, props, sizeof(props), &n);
return n == 1 && (!needle || strstr(props, needle));
}
TEST(adoc_links_pipeline) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_dladoc_XXXXXX");
ASSERT_NOT_NULL(cbm_mkdtemp(tmp));
char repo[512];
char db[512];
snprintf(repo, sizeof(repo), "%s/repo", tmp);
snprintf(db, sizeof(db), "%s/g.db", tmp);
adoc_write_repo(repo, FIRST_TESTS);
ASSERT_EQ(dm_index(repo, db, NULL), 0);
const char *page = "docs.modules.ROOT.pages.writing";
char src[256];
/* a resource ID through a collector scan; the tag region's innermost method */
snprintf(src, sizeof(src), "%s.First-Test", page);
ASSERT_TRUE(adoc_edge(db, src, "example.FirstTests.addition",
"{\"via\":\"asciidoc\",\"syntax\":\"include\",\"tier\":\"exact\""));
ASSERT_TRUE(adoc_edge(db, src, "example.FirstTests.addition", "\"target_lines\":[7,9]"));
/* a javadoc attribute (under the document title): the type it documents */
snprintf(src, sizeof(src), "%s.Writing-Tests", page);
ASSERT_TRUE(adoc_edge(db, src, "org.x.Assertions", "\"syntax\":\"attribute\""));
/* lines= and a relative include */
snprintf(src, sizeof(src), "%s.Details", page);
ASSERT_TRUE(adoc_edge(db, src, "org.x.Calc.add", "\"target_lines\":[4,6]"));
/* the region's imports are no definition: the one it holds whole binds */
ASSERT_TRUE(adoc_edge(db, src, "example.WithImports", "\"target_lines\":[4,8]"));
/* monospace spans through the Markdown rules */
ASSERT_TRUE(adoc_edge(db, src, "src.main.java.org.x.Assertions.java.__file__",
"\"syntax\":\"code_path\""));
/* back navigation: the method knows the section that includes it */
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id JOIN "
"nodes t ON t.id=e.target_id WHERE e.type='MENTIONS' AND "
"t.name='addition' AND s.label='Section' AND s.name='First Test'"),
1);
th_cleanup(tmp);
PASS();
}
TEST(adoc_links_incremental) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_dladoc_inc_XXXXXX");
ASSERT_NOT_NULL(cbm_mkdtemp(tmp));
char repo[512];
char db[512];
char full_db[512];
snprintf(repo, sizeof(repo), "%s/repo", tmp);
snprintf(db, sizeof(db), "%s/inc.db", tmp);
snprintf(full_db, sizeof(full_db), "%s/full.db", tmp);
adoc_write_repo(repo, FIRST_TESTS);
ASSERT_EQ(dm_index(repo, db, NULL), 0);
/* the tagged region moves to another method: the page follows it */
th_write_file(TH_PATH(repo, "docs/src/test/java/example/FirstTests.java"),
"package example;\n"
"\n"
"class FirstTests {\n"
" // tag::user_guide[]\n"
" void helper() {}\n"
" // end::user_guide[]\n"
"\n"
" void addition() {\n"
" helper();\n"
" }\n"
"}\n");
ASSERT_EQ(dm_step(repo, db, full_db, "tag moved", CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR), 0);
/* a component attribute changes: every page re-resolves */
th_write_file(TH_PATH(repo, "docs/antora.yml"),
"name: guide\n"
"ext:\n"
" collector:\n"
" scan:\n"
" - dir: ./src/test\n"
" into: modules/ROOT/examples\n"
"asciidoc:\n"
" attributes:\n"
" javadoc-root: \"xref:attachment$api\"\n"
" Assertions: '{javadoc-root}/org.x/org/x/Calc.html[Calc]'\n");
ASSERT_EQ(dm_step(repo, db, full_db, "attribute change", CBM_INCREMENTAL_ROUTE_FORCED_FULL), 0);
th_cleanup(tmp);
PASS();
}
SUITE(doc_links_adoc) {
RUN_TEST(adoc_scan_structure);
RUN_TEST(adoc_antora_scope_blob);
RUN_TEST(adoc_links_pipeline);
RUN_TEST(adoc_links_incremental);
}
+2
View File
@@ -1824,6 +1824,7 @@ extern void suite_doc_mentions_msbuild(void);
extern void suite_doc_links_md(void);
extern void suite_doc_links_pdf(void);
extern void suite_doc_links_rst(void);
extern void suite_doc_links_adoc(void);
extern void suite_infrascan(void);
extern void suite_cli(void);
extern void suite_agent_clients(void);
@@ -2294,6 +2295,7 @@ int main(int argc, char **argv) {
RUN_SELECTED_SUITE(doc_links_md);
RUN_SELECTED_SUITE(doc_links_pdf);
RUN_SELECTED_SUITE(doc_links_rst);
RUN_SELECTED_SUITE(doc_links_adoc);
/* Infrastructure scanning */
RUN_SELECTED_SUITE(infrascan);