diff --git a/Core/Interface/SolidSyslogBlockStore.h b/Core/Interface/SolidSyslogBlockStore.h index e79555ec..e1d5128c 100644 --- a/Core/Interface/SolidSyslogBlockStore.h +++ b/Core/Interface/SolidSyslogBlockStore.h @@ -66,7 +66,7 @@ EXTERN_C_BEGIN struct SolidSyslogBlockDevice* BlockDevice; /**< Required; block store is the sole writer. */ size_t MaxBlocks; /**< Retention ceiling in blocks; DiscardPolicy governs the overflow. */ enum SolidSyslogDiscardPolicy DiscardPolicy; - struct SolidSyslogSecurityPolicy* SecurityPolicy; /**< NULL means no per-record integrity trailer. */ + struct SolidSyslogSecurityPolicy* SecurityPolicy; /**< @optional no per-record integrity trailer when NULL. */ SolidSyslogStoreFullCallback OnStoreFull; void* StoreFullContext; SolidSyslogStoreThresholdFunction GetCapacityThreshold; diff --git a/Core/Interface/SolidSyslogConfig.h b/Core/Interface/SolidSyslogConfig.h index d129bf1f..6381c69f 100644 --- a/Core/Interface/SolidSyslogConfig.h +++ b/Core/Interface/SolidSyslogConfig.h @@ -37,7 +37,7 @@ EXTERN_C_BEGIN SolidSyslogHeaderFieldFunction GetProcessId; /**< RFC 5424 PROCID; NULL emits "-". */ void* GetProcessIdContext; /**< Passed back to the callback unchanged. */ struct SolidSyslogStore* Store; /**< NULL (or SolidSyslogNullStore_Get()) means no store-and-forward. */ - struct SolidSyslogStructuredData** Sd; /**< Per-instance SD-ELEMENTs on every message, before per-message SD. */ + struct SolidSyslogStructuredData** Sd; /**< @optional per-instance SD-ELEMENTs, before any per-message SD. */ size_t SdCount; }; diff --git a/hooks/relationship_data.py b/hooks/relationship_data.py index 5813596d..c27ae26f 100644 --- a/hooks/relationship_data.py +++ b/hooks/relationship_data.py @@ -12,6 +12,7 @@ import os import re import glob +import functools # struct SolidSyslog * SolidSyslog_(Create|Get) ( _FACTORY = re.compile( @@ -33,12 +34,41 @@ def _package(path, root): return "Core" +# A struct DEFINED with a body: mkdoxy renders a structSolidSyslog compound +# page for it and links every use of the type name there. An opaque forward-decl +# (the SolidSyslog handle, SolidSyslogAddress) has no such page, so its name +# resolves to a file page instead. This is what decides a node's link target. +_STRUCT_DEF = re.compile(r"struct\s+SolidSyslog(\w+)\s*\{") + + +@functools.lru_cache(maxsize=None) +def _defined_structs(root): + names = set() + for directory in _scan_dirs(root): + for path in glob.glob(os.path.join(directory, "*.h")): + with open(path, encoding="utf-8") as handle: + names.update(_STRUCT_DEF.findall(handle.read())) + return names + + +def _canonical(name, defined, file_page): + """The page mkdoxy links a type name to: its compound page when the struct is + defined, else the given file page. Diagram nodes point here, and each diagram + is injected here, so a name-click (in prose or a diagram) always lands with it.""" + return "structSolidSyslog" + name if name in defined else file_page + + +@functools.lru_cache(maxsize=None) def build_graph(root): - """Return {role: {"implementers": [ {name, page, package, is_null}, ... ]}}.""" + """Return {role: {"implementers": [ {name, page, package, is_null}, ... ]}}. + + Memoized (pure in root): build_reverse/build_facade/build_consumers and the + diagram hook each call it, so caching collapses the header rescan to one pass + per build. Callers must treat the returned graph as read-only.""" roles = {} for directory in _scan_dirs(root): for path in sorted(glob.glob(os.path.join(directory, "*.h"))): - with open(path, "r", encoding="utf-8") as handle: + with open(path, encoding="utf-8") as handle: text = handle.read() page = os.path.basename(path)[:-2] + "_8h" for role, impl, _factory in _FACTORY.findall(text): @@ -49,22 +79,170 @@ def build_graph(root): "page": page, "package": _package(path, root), "is_null": impl.startswith("Null"), + "header": path, } ) core = os.path.join(root, "Core", "Interface") + defined = _defined_structs(root) for role, info in roles.items(): - # Inject on the consumer dispatch header if there is one (what users - # browse); else on the Definition header (e.g. SecurityPolicy has no - # plain SolidSyslogSecurityPolicy.h, only the vtable Definition). + # The file page: the dispatch header if there is one (what users browse), + # else the Definition header (e.g. SecurityPolicy has no plain + # SolidSyslogSecurityPolicy.h, only the vtable Definition). if os.path.exists(os.path.join(core, "SolidSyslog" + role + ".h")): info["base_page"] = "SolidSyslog" + role + "_8h" else: info["base_page"] = "SolidSyslog" + role + "Definition_8h" - info["struct_page"] = "structSolidSyslog" + role + # The link target / injection page: the vtable's compound page where one + # exists (Stream, Sender, …), else the file page for an opaque role (Address). + info["link_page"] = _canonical(role, defined, info["base_page"]) info["implementers"].sort(key=lambda i: (i["is_null"], i["name"])) return roles +# struct SolidSyslog * (or ** for a list) — a role-typed collaborator slot. +_ROLE_PTR = re.compile(r"struct\s+SolidSyslog(\w+)\s*(\*+)") +_COMMENT = re.compile(r"/\*.*?\*/", re.S) + + +def _balanced(text, open_char, close_char, at): + """Return the substring between the delimiters starting at index ``at``.""" + depth, i = 0, at + while i < len(text): + if text[i] == open_char: + depth += 1 + elif text[i] == close_char: + depth -= 1 + if depth == 0: + return text[at + 1 : i] + i += 1 + return "" + + +# A config struct field: role pointer, member name, ';', then the rest of the +# line — where a trailing /**< @optional ... */ marker (if any) lives. +_CONFIG_FIELD = re.compile(r"struct\s+SolidSyslog(\w+)\s*(\*+)\s*\w+\s*;[ \t]*([^\n]*)") + + +def _region(text, pattern): + """The balanced { } or ( ) region a pattern opens, comments intact.""" + match = re.search(pattern, text) + if not match: + return "" + opener = text[match.end() - 1] + return _balanced(text, opener, "}" if opener == "{" else ")", match.end() - 1) + + +def _collaborators(header, impl, roles): + """Role-typed slots this implementer is wired with — its ``Config`` struct + fields and its ``_Create`` parameters, in that order, filtered to known roles. + A ``**`` slot is a list (upper bound ``*``); a config field whose trailing + doc-comment carries the ``@optional`` marker has lower bound 0 (else 1, the + header's "required" contract). ``_Create`` params are injected directly and are + never optional today.""" + with open(header, encoding="utf-8") as handle: + text = handle.read() + out, seen = [], set() + config = _region(text, r"struct\s+SolidSyslog" + impl + r"Config\s*\{") + for role, stars, rest in _CONFIG_FIELD.findall(config): + if role in roles and role not in seen: + seen.add(role) + out.append({"role": role, "is_list": len(stars) >= 2, "is_optional": "@optional" in rest}) + params = _COMMENT.sub("", _region(text, r"SolidSyslog" + impl + r"_Create\s*\(")) + for role, stars in _ROLE_PTR.findall(params): + if role in roles and role not in seen: + seen.add(role) + out.append({"role": role, "is_list": len(stars) >= 2, "is_optional": False}) + return out + + +@functools.lru_cache(maxsize=None) +def build_reverse(root): + """Return {impl_page: {name, package, is_null, base_role, base_page, config_page, + collaborators:[{role, base_page, is_list, is_optional}]}} — the per-implementer + view: the role it realises (upward) and the roles it is wired to use (outward). + base_page here is a *link target* (canonical page), not necessarily a file page. + config_page is the implementer's config compound page, if it has one.""" + graph = build_graph(root) + defined = _defined_structs(root) + role_link = {role: info["link_page"] for role, info in graph.items()} + reverse = {} + for role, info in graph.items(): + for impl in info["implementers"]: + collaborators = _collaborators(impl["header"], impl["name"], role_link) + config = impl["name"] + "Config" + reverse[impl["page"]] = { + "name": impl["name"], + "package": impl["package"], + "is_null": impl["is_null"], + "base_role": role, + "base_page": role_link[role], + "config_page": "structSolidSyslog" + config if config in defined else None, + "collaborators": [ + {"role": c["role"], "base_page": role_link[c["role"]], + "is_list": c["is_list"], "is_optional": c["is_optional"]} + for c in collaborators + ], + } + return reverse + + +# The composition root: the one _Create that returns the bare `struct SolidSyslog*` +# handle (no role suffix), taking a `SolidSyslog<...>Config`. This is the structural +# signal that distinguishes the facade from a role implementer — no hand-kept list. +_FACADE_FACTORY = re.compile( + r"struct\s+SolidSyslog\s*\*\s*SolidSyslog_Create\s*\(\s*const\s+struct\s+(SolidSyslog\w*Config)\b" +) + + +@functools.lru_cache(maxsize=None) +def build_facade(root): + """The composition-root view: the SolidSyslog facade and the roles its config + wires it to (uses-only — the facade realises no role). Collaborators are + scraped from the config struct exactly as for an implementer. Returns None if + the facade factory is not found.""" + role_link = {role: info["link_page"] for role, info in build_graph(root).items()} + header = os.path.join(root, "Core", "Interface", "SolidSyslogConfig.h") + if not os.path.exists(header): + return None + with open(header, encoding="utf-8") as handle: + text = handle.read() + match = _FACADE_FACTORY.search(text) + if not match: + return None + config = _region(text, r"struct\s+" + match.group(1) + r"\s*\{") + collaborators, seen = [], set() + for role, stars, rest in _CONFIG_FIELD.findall(config): + if role in role_link and role not in seen: + seen.add(role) + collaborators.append({"role": role, "base_page": role_link[role], + "is_list": len(stars) >= 2, "is_optional": "@optional" in rest}) + # pages[0] = the class view's page: the opaque facade handle has no compound + # page, so its name resolves to the SolidSyslog file page. pages[1] = the config + # view's page: SolidSyslogConfig has a compound page, where its name links. + return {"name": "SolidSyslog", "pages": ["SolidSyslog_8h", "structSolidSyslogConfig"], + "collaborators": collaborators} + + +def build_consumers(root): + """Return {role: [ {name, page, package}, ... ]} — who *uses* each role: the + transpose of the collaborator map (every implementer's ``uses`` edges), plus + the SolidSyslog facade. Completes the navigation graph — from a role page you + can reach both its implementers and its clients.""" + consumers = {} + for page, entry in build_reverse(root).items(): + for collab in entry["collaborators"]: + consumers.setdefault(collab["role"], []).append( + {"name": entry["name"], "page": page, "package": entry["package"]}) + facade = build_facade(root) + if facade is not None: + for collab in facade["collaborators"]: + consumers.setdefault(collab["role"], []).append( + {"name": facade["name"], "page": facade["pages"][0], "package": "Core"}) + for role in consumers: + consumers[role].sort(key=lambda c: c["name"]) + return consumers + + if __name__ == "__main__": here = os.path.dirname(os.path.abspath(__file__)) repo = os.path.dirname(here) @@ -77,4 +255,4 @@ def build_graph(root): longest = max(longest, len(i["name"])) flag = " [null]" if i["is_null"] else "" print(" {:22} {:10}{}".format(i["name"], i["package"], flag)) - print("\nroles: {} longest backend name: {} chars".format(len(graph), longest)) + print(f"\nroles: {len(graph)} longest backend name: {longest} chars") diff --git a/hooks/relationship_diagrams.py b/hooks/relationship_diagrams.py index dbcd7157..ead98ab3 100644 --- a/hooks/relationship_diagrams.py +++ b/hooks/relationship_diagrams.py @@ -1,13 +1,17 @@ """MkDocs build hook: inject generated relationship diagrams onto API pages. -For every vtable role, the graph is extracted from the headers -(``relationship_data``) and a post-it "realised by" diagram is rendered -(``relationship_render``) and prepended, inline, onto the role's base API page — -its consumer dispatch header where there is one, else the vtable Definition. - -The SVG is inline (not a Markdown image or ) so its per-backend links -stay clickable; its styles are id-scoped so nothing leaks into the page. Nothing -is committed or written to disk — the diagrams are built from code each run. +Three diagram types, all extracted from the headers (``relationship_data``) and +rendered as inline post-it SVG (``relationship_render``), prepended to the page: + +* a role's base page gets the "realised by" view — its concrete backends; +* each concrete implementer's page gets the reverse view — the role it realises + and the roles it is wired to use; +* the SolidSyslogConfig page gets the facade view — the composition root and the + roles it wires together. + +The SVG is inline (not a Markdown image or ) so its per-node links stay +clickable; its styles are id-scoped so nothing leaks into the page. Nothing is +committed or written to disk — the diagrams are built from code each run. """ import os @@ -20,22 +24,33 @@ _CACHE = {} -def _by_base_page(config): - """Return {base_page_stem: (role, info)} for this repo, built once.""" +def _diagrams(config): + """Return {api_page_stem: rendered_svg} for this repo, built once.""" root = os.path.dirname(config["config_file_path"]) if root not in _CACHE: - graph = relationship_data.build_graph(root) - _CACHE[root] = {info["base_page"]: (role, info) for role, info in graph.items()} + out = {} + consumers = relationship_data.build_consumers(root) + for role, info in relationship_data.build_graph(root).items(): + clients = consumers.get(role, []) + out[info["link_page"]] = relationship_render.render_role_combined(role, info, clients) + for page, entry in relationship_data.build_reverse(root).items(): + svg = relationship_render.render_reverse(entry) + out[page] = svg + if entry["config_page"]: + out[entry["config_page"]] = svg + facade = relationship_data.build_facade(root) + if facade is not None: + out[facade["pages"][0]] = relationship_render.render_facade_class(facade) + out[facade["pages"][1]] = relationship_render.render_facade_stacked(facade) + _CACHE[root] = out return _CACHE[root] def on_page_markdown(markdown, page, config, files, **kwargs): src = getattr(page.file, "src_uri", None) or page.file.src_path.replace(os.sep, "/") - if not (src.startswith("api/") and src.endswith("_8h.md")): + if not (src.startswith("api/") and src.endswith(".md")): return markdown - stem = src[len("api/"):-len(".md")] - match = _by_base_page(config).get(stem) - if match is None: - return markdown - role, info = match - return relationship_render.render_svg(role, info) + "\n\n" + markdown + # Keyed by canonical page stem — a role's compound page, an implementer's file + # page, a config compound page. Unmatched stems (source listings, etc.) skip. + svg = _diagrams(config).get(src[len("api/"):-len(".md")]) + return svg + "\n\n" + markdown if svg else markdown diff --git a/hooks/relationship_render.py b/hooks/relationship_render.py index 0990d9e4..42fae5d4 100644 --- a/hooks/relationship_render.py +++ b/hooks/relationship_render.py @@ -21,6 +21,7 @@ _PERI = ("#cdd4f5", "#8f9bd6", "#15324f") _GREY = ("#e4e4e2", "#b0b0a8", "#45453d") _PINK = ("#f6c6c6", "#cf8a8a", "#4a2c2c") +_YELLOW = ("#ffe7a3", "#b89a3c", "#3a3320") _PKG = "#3a4a80" _PKG_NULL = "#6a6a60" @@ -50,24 +51,288 @@ def _sticky(cx, y, impl, index, name_font, pkg_font, role, wraps): cy = y + _STICKY_H / 2 rot = _ROT[index % len(_ROT)] out = [''.format(BASE_URL, impl["page"]), - ''.format(rot, cx, cy)] + f''] if impl["name"] in wraps: ty = y + _STICKY_H - 6 - out.append(''.format(cx - 34, ty, _PINK[0], _PINK[1])) - out.append('wraps a {}'.format(cx, ty + 15, _UI, _PINK[2], role)) - out.append(''.format(left, y, _STICKY_W, _STICKY_H, fill, stroke)) + out.append(f'') + out.append(f'wraps a {role}') + out.append(f'') out.append('{}'.format(cx, y + 26, name_font, _HAND, text, impl["name"])) - out.append('{}'.format(cx, y + 45, pkg_font, _UI, pkg_color, pkg)) + out.append(f'{pkg}') out.append("") return "".join(out) -def render_svg(role, info): +# --------------------------------------------------------------------------- +# Reverse view: one implementer, the role it realises (up) + the roles it uses +# (down). Injected on each concrete implementer's own API page. +# --------------------------------------------------------------------------- + +_IF_W, _IF_H = 196, 60 +_SUB_W, _SUB_H = 158, 64 +_COL_GAP = 28 +_V_REALISES, _V_USES = 46, 52 + +_REVERSE_DEFS = ( + '' + # userSpaceOnUse (not the default objectBoundingBox): an all-vertical connector + # group has a zero-width bbox, which collapses an objectBoundingBox filter region + # to zero and makes browsers drop the element. Tying the region to the viewport + # keeps it non-degenerate for leaf/self-wrap diagrams whose only line is vertical. + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' +) + + +def _interface_box(cx, top, role, page, rot, font, is_list): + """Green «interface» sticky (the base role, or a collaborator role), linked to + its API page. A list collaborator gets a second card offset behind it.""" + left = cx - _IF_W / 2 + out = [f'', + f''] + if is_list: + out.append(f'') + out.append(f'') + out.append(f'«interface»') + out.append(f'SolidSyslog{role}') + out.append("") + return "".join(out) + + +def _subject_box(cx, top, name, package, is_null, font, page=None): + """Blue sticky for the class the page is about — the one concrete node, so it + reads as the focus. Grey for a Null object. Links to @p page when given (as a + sidecar node on another page's diagram); unlinked when it is the page itself.""" + fill, stroke, text = _GREY if is_null else _PERI + pkg_color = _PKG_NULL if is_null else _PKG + pkg = package + (" · no-op" if is_null else "") + left = cx - _SUB_W / 2 + out = [f'', + f'', + f'{name}', + f'{pkg}', + ""] + body = "".join(out) + if page: + return f'{body}' + return body + + +def _multiplicity(is_optional, is_list): + """UML multiplicity from the two structural facts. Lower bound 0 iff the slot + is @optional, else 1 (the header's "required" contract); upper bound * iff a + list. Exactly-one (1..1) is conventionally left unlabelled.""" + lower = "0" if is_optional else "1" + upper = "*" if is_list else "1" + return None if (lower, upper) == ("1", "1") else lower + ".." + upper + + +def render_reverse(entry): + name = entry["name"] + base_role = entry["base_role"] + collabs = entry["collaborators"] + n = len(collabs) + + row_w = n * _IF_W + (n - 1) * _COL_GAP if n else 0 + width = round(_MARGIN * 2 + max(row_w, _SUB_W, _IF_W)) + cx = width / 2 + + base_y = _MARGIN + sub_y = base_y + _IF_H + _V_REALISES + col_y = sub_y + _SUB_H + _V_USES + height = round((col_y + _IF_H if n else sub_y + _SUB_H) + _MARGIN) + + col_cx = [cx - row_w / 2 + _IF_W / 2 + i * (_IF_W + _COL_GAP) for i in range(n)] + + base_font = _fit(len("SolidSyslog" + base_role), _IF_W - 20, 13.5) + sub_font = _fit(len(name), _SUB_W - 14, 13.0) + col_font = base_font + if n: + longest = max(len("SolidSyslog" + c["role"]) for c in collabs) + col_font = _fit(longest, _IF_W - 20, 13.5) + + svg_id = "ssrev-" + name.lower() + parts = ['
', + f'', + _REVERSE_DEFS, + f''] + + # connectors, drawn behind the stickies + edges = ['', + f''] + for ccx in col_cx: + edges.append(f'') + edges.append("") + parts += edges + + parts.append(_interface_box(cx, base_y, base_role, entry["base_page"], -1, base_font, False)) + parts.append(_subject_box(cx, sub_y, name, entry["package"], entry["is_null"], sub_font)) + _place_collaborators(parts, col_cx, collabs, col_y, col_font) + + parts.append("
") + return "".join(parts) + + +def _place_collaborators(parts, col_cx, collabs, col_y, col_font): + """Draw the collaborator interface boxes and their UML multiplicity labels + (1..1 left unlabelled). Shared by the reverse and facade views.""" + for idx, (ccx, collab) in enumerate(zip(col_cx, collabs, strict=True)): + parts.append(_interface_box(ccx, col_y, collab["role"], collab["base_page"], + _ROT[idx % len(_ROT)], col_font, collab["is_list"])) + mult = _multiplicity(collab["is_optional"], collab["is_list"]) + if mult: + parts.append(f'{mult}') + + +# --------------------------------------------------------------------------- +# Facade view: the composition root (SolidSyslog) and the roles its config wires +# it to. Uses-only — no realises edge, because the facade implements no role. +# --------------------------------------------------------------------------- + +_FAC_W, _FAC_H = 176, 64 + + +def render_facade_class(facade): + """The SolidSyslog page: the class with direct arrows to the interfaces it + uses, and its Config as a linked sidecar (dashed — built from, not retained).""" + _fac_page, cfg_page, cfg_name, collabs, fac_font, cfg_font, col_font = _facade_common(facade) + n = len(collabs) + pair_gap = 244 + row_w = n * _IF_W + (n - 1) * _COL_GAP if n else 0 + cx_main = _MARGIN + row_w / 2 + cx_cfg = cx_main + pair_gap + width = round(max(_MARGIN * 2 + row_w, cx_cfg + _FAC_W / 2 + _MARGIN)) + top_y = _MARGIN + col_y = top_y + _FAC_H + 46 + height = round(col_y + _IF_H + _MARGIN) + col_cx = [cx_main - row_w / 2 + _IF_W / 2 + i * (_IF_W + _COL_GAP) for i in range(n)] + + parts = _facade_open("ssfacc-" + facade["name"].lower(), width, height) + parts.append('') + parts.append(f'') + for ccx in col_cx: + parts.append(f'') + parts.append("") + parts.append(f'built from') + parts.append(_subject_box(cx_main, top_y, facade["name"], "Core", False, fac_font)) + parts.append(_pod_box(cx_cfg, top_y, cfg_name, cfg_font, 1, page=cfg_page)) + _place_collaborators(parts, col_cx, collabs, col_y, col_font) + parts.append("") + return "".join(parts) + + +# The config POD, shown as a distinct linked node beside the SolidSyslog class. +_POD = ("#dfe3ea", "#9aa6b8", "#33404f") + + +def _pod_box(cx, top, name, font, rot, page=None): + """Pale-blue «struct» sticky for a POD (the config). Links to @p page when + given (a sidecar on the class page); unlinked when it is the page itself.""" + left = cx - _FAC_W / 2 + body = "".join([ + f'', + f'', + f'«struct»', + f'{name}', + ""]) + if page: + return f'{body}' + return body + + +def _facade_open(svg_id, width, height): + return ['
', + f'', + _REVERSE_DEFS, + f''] + + +def _facade_common(facade): + fac_page, cfg_page = facade["pages"][0], facade["pages"][1] + cfg_name = cfg_page[len("struct"):] if cfg_page.startswith("struct") else cfg_page[:-3] + collabs = facade["collaborators"] + fac_font = _fit(len(facade["name"]), _FAC_W - 16, 14.0) + cfg_font = _fit(len(cfg_name), _FAC_W - 16, 14.0) + col_font = _fit(max(len("SolidSyslog" + c["role"]) for c in collabs), _IF_W - 20, 13.5) if collabs else 13.0 + return fac_page, cfg_page, cfg_name, collabs, fac_font, cfg_font, col_font + + +def render_facade_stacked(facade): + """The SolidSyslogConfig page: the config POD with its interface members below, + and the SolidSyslog class it builds as a linked node above (dashed — built from).""" + fac_page, _cfg_page, cfg_name, collabs, fac_font, cfg_font, col_font = _facade_common(facade) + n = len(collabs) + row_w = n * _IF_W + (n - 1) * _COL_GAP if n else 0 + width = round(_MARGIN * 2 + max(row_w, _FAC_W)) + cx = width / 2 + fac_y = _MARGIN + cfg_y = fac_y + _SUB_H + 40 + col_y = cfg_y + _FAC_H + 46 + height = round(col_y + _IF_H + _MARGIN) + col_cx = [cx - row_w / 2 + _IF_W / 2 + i * (_IF_W + _COL_GAP) for i in range(n)] + + parts = _facade_open("ssfacs-" + facade["name"].lower(), width, height) + parts.append('') + parts.append(f'') + for ccx in col_cx: + parts.append(f'') + parts.append("") + parts.append(f'built from') + parts.append(_subject_box(cx, fac_y, facade["name"], "Core", False, fac_font, page=fac_page)) + parts.append(_pod_box(cx, cfg_y, cfg_name, cfg_font, 1)) + _place_collaborators(parts, col_cx, collabs, col_y, col_font) + parts.append("
") + return "".join(parts) + + +# --------------------------------------------------------------------------- +# Combined role view: an interface in its ecosystem — its clients above (uses, +# solid) and its implementers below (realises, hollow triangle). Injected on the +# role page, completing the navigation graph in both directions. +# --------------------------------------------------------------------------- + +_CONS_GAP = 46 + + +def render_role_combined(role, info, consumers): impls = info["implementers"] n = len(impls) two_rows = n >= 5 @@ -81,65 +346,80 @@ def render_svg(role, info): span_min, span_max = min(all_cx), max(all_cx) base_cx = (span_min + span_max) / 2 - base_bottom = _BASE_Y + _BASE_H + m = len(consumers) + cons_pitch = _SUB_W + _GAP + cons_row_w = m * _SUB_W + (m - 1) * _GAP if m else 0 + cons_cx = [base_cx - cons_row_w / 2 + _SUB_W / 2 + i * cons_pitch for i in range(m)] + + # A consumer row wider than the implementer span, centred on base_cx, can spill + # left of the margin (as can the wider interface box for a lone implementer). + # Shift every x right so the leftmost box sits at the margin, and take width from + # the true left/right extents rather than the rightmost box alone. + boxes = [(cx, _STICKY_W) for cx in all_cx] + [(base_cx, _BASE_W)] + [(cx, _SUB_W) for cx in cons_cx] + shift = _MARGIN - min(cx - w / 2 for cx, w in boxes) + top_cx = [cx + shift for cx in top_cx] + bot_cx = [cx + shift for cx in bot_cx] + cons_cx = [cx + shift for cx in cons_cx] + base_cx += shift + span_min += shift + span_max += shift + width = round(max(cx + w / 2 for cx, w in boxes) + shift + _MARGIN) + + cons_y = _MARGIN + base_y = (cons_y + _SUB_H + _CONS_GAP) if m else _MARGIN + base_bottom = base_y + _BASE_H bus_y = base_bottom + _BUS_GAP row1_y = bus_y + _ROW_GAP row2_y = row1_y + _STICKY_H + _ROW_GAP wraps = _WRAPS.get(role, set()) - max_name = max(len(i["name"]) for i in impls) - name_font = _fit(max_name, _STICKY_W - 16, 12.0) + name_font = _fit(max(len(i["name"]) for i in impls), _STICKY_W - 16, 12.0) pkg_font = max(8.0, name_font - 2.0) base_font = _fit(len("SolidSyslog" + role), _BASE_W - 24, 15.5) + cons_font = _fit(max(len(c["name"]) for c in consumers), _SUB_W - 14, 12.5) if m else 12.0 last_bottom = (row2_y if bottom else row1_y) + _STICKY_H height = round(last_bottom + (20 if wraps else 8) + _MARGIN) - width = round(max([cx + _STICKY_W / 2 for cx in all_cx] + [base_cx + _BASE_W / 2]) + _MARGIN) - svg_id = "ssrel-" + role.lower() - parts = ['
', - ''.format( - svg_id, width, height, _HAND, min(width, 880)), - '' - '' - '' - '' - '' - '' - '' - '', - ''.format(svg_id)] - - # realises generalisation tree (behind the stickies) + parts = _facade_open("ssuses-" + role.lower(), width, height) + + # realises tree (implementers below the interface) tree = ['', - ''.format(base_cx, bus_y, base_bottom + 1), - ''.format(span_min, bus_y, span_max)] + f'', + f''] for cx in top_cx: - tree.append(''.format(cx, bus_y, row1_y)) + tree.append(f'') for cx in bot_cx: - tree.append(''.format(cx, bus_y, row2_y)) + tree.append(f'') tree.append("") parts += tree + # uses edges (clients above -> into the interface top) + parts.append('') + for ccx in cons_cx: + parts.append(f'') + parts.append("") + # interface (base) parts.append(''.format( - BASE_URL, info["struct_page"], base_cx, _BASE_Y + _BASE_H / 2)) - parts.append(''.format( - base_cx - _BASE_W / 2, _BASE_Y, _BASE_W, _BASE_H, _GREEN[0], _GREEN[1])) - parts.append('«interface»'.format(base_cx, _BASE_Y + 26, _UI, _GREEN[2])) - parts.append('SolidSyslog{}'.format(base_cx, _BASE_Y + 50, base_font, _GREEN[2], role)) + BASE_URL, info["link_page"], base_cx, base_y + _BASE_H / 2)) + parts.append(f'') + parts.append(f'«interface»') + parts.append(f'SolidSyslog{role}') parts.append("") - for idx, (cx, impl) in enumerate(zip(top_cx, top)): + for idx, (cx, impl) in enumerate(zip(top_cx, top, strict=True)): parts.append(_sticky(cx, row1_y, impl, idx, name_font, pkg_font, role, wraps)) - for j, (cx, impl) in enumerate(zip(bot_cx, bottom)): + for j, (cx, impl) in enumerate(zip(bot_cx, bottom, strict=True)): parts.append(_sticky(cx, row2_y, impl, top_n + j, name_font, pkg_font, role, wraps)) + for ccx, consumer in zip(cons_cx, consumers, strict=True): + parts.append(_subject_box(ccx, cons_y, consumer["name"], consumer["package"], + False, cons_font, page=consumer["page"])) + parts.append("
") return "".join(parts) diff --git a/mkdocs.yml b/mkdocs.yml index e86e9391..02db942d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -108,6 +108,10 @@ plugins: OPTIMIZE_OUTPUT_FOR_C: true # First sentence of each /** */ block becomes the API-index brief. JAVADOC_AUTOBRIEF: true + # Custom command: @optional tags a config member the caller may leave + # NULL. Doubles as the machine-readable marker the relationship-diagram + # hook reads to draw a 0.. lower bound (see hooks/relationship_data.py). + ALIASES: '"optional=**Optional:** "' # Expand the C-linkage guard macros to nothing so Doxygen sees plain # file-scope C declarations. Without this the unexpanded EXTERN_C_BEGIN # token gets glued onto the first declaration in each header (e.g.