diff --git a/.gitea/workflows/doxygen-pages.yml b/.gitea/workflows/doxygen-pages.yml index f4bb546..fe65d82 100644 --- a/.gitea/workflows/doxygen-pages.yml +++ b/.gitea/workflows/doxygen-pages.yml @@ -47,6 +47,24 @@ jobs: head -30 doc/doxygen/doxygen-warnings.log fi + - name: Report Doxygen coverage + run: bash scripts/doxygen-coverage.sh + + - name: Regenerate doc/api/headers.md from XML + run: | + python3 scripts/gen-headers-md.py + # If the auto-generated headers.md drifted from main, note it. + # This job runs on every main push so a drift only persists + # for the duration of one push — the next push that lands + # will fold the new headers.md back into main (via the + # codeberg pages branch). For deterministic regeneration + # within main itself, run `bash scripts/regen-docs.sh` + # locally before pushing. + if ! git diff --quiet -- doc/api/headers.md; then + echo "::warning::doc/api/headers.md drifted — run scripts/regen-docs.sh locally and commit before next push" + git --no-pager diff -- doc/api/headers.md | head -30 + fi + - name: Publish HTML to codeberg pages branch env: CODEBERG_TOKEN: ${{ secrets.CODEBERG_TOKEN }} diff --git a/Doxyfile b/Doxyfile index 9ed899c..e4a16c0 100644 --- a/Doxyfile +++ b/Doxyfile @@ -104,7 +104,8 @@ HTML_COLORSTYLE = LIGHT HTML_COLORSTYLE_HUE = 220 HTML_COLORSTYLE_SAT = 100 HTML_COLORSTYLE_GAMMA = 80 -HTML_TIMESTAMP = NO +# HTML_TIMESTAMP was removed in Doxygen 1.10; use TIMESTAMP=NO instead. +TIMESTAMP = NO HTML_DYNAMIC_SECTIONS = YES GENERATE_TREEVIEW = YES DISABLE_INDEX = NO diff --git a/doc/api/headers.md b/doc/api/headers.md index 41b06c7..a59dc2f 100644 --- a/doc/api/headers.md +++ b/doc/api/headers.md @@ -1,111 +1,69 @@ + + + # Public Headers (`code/include/`) -All algorithms are header-only. Include the headers you need directly — -there is no compiled library to link against (only GTest for tests and -CGAL/Eigen for the CGAL-dependent headers). +All algorithms are header-only. Include the headers you need +directly — there is no compiled library to link against (only +GTest for tests and CGAL/Eigen for the CGAL-dependent headers). -## Core mesh type +This page is **regenerated** from Doxygen XML on every push to +`main` that touches a public header (see +`.gitea/workflows/doxygen-pages.yml` and `scripts/gen-headers-md.py`). +To improve a description, edit the `\file` brief at the top of +the corresponding header, then re-run `bash scripts/regen-docs.sh`. -| Header | Description | -|---|---| -| `conformal_mesh.hpp` | `ConformalMesh` = `CGAL::Surface_mesh`. Property-map naming convention. Index type aliases. `GeometryType` enum. | -| `constants.hpp` | `conformallab::PI`, `TWO_PI` | -| `mesh_builder.hpp` | `make_triangle()` / `make_tetrahedron()` / `make_quad_strip()` / `make_fan()` / `make_open_cylinder()` — test mesh factories | -| `mesh_io.hpp` | `load_mesh()` / `save_mesh()` via `CGAL::IO` (OFF / OBJ / PLY) | -| `mesh_utils.hpp` | `cgal_to_eigen()` — convert `ConformalMesh` vertex positions to `Eigen::MatrixXd` | +## CGAL public API (``) -## Special functions +| Header | Brief | Public symbols | +|---|---|---| +| `CGAL/Conformal_layout.h` | Thin CGAL-style wrapper around the legacy euclidean_layout(), spherical_layout() and hyper_ideal_layout() functions defined in code/include/layout.hpp. | `Layout2D`, `Layout3D`, `HolonomyData`, `CutGraph` | +| `CGAL/Conformal_map_traits.h` | Defines the ConformalMapTraits concept and the default model Default_conformal_map_traits for the package. | _(no public symbols)_ | +| `CGAL/Discrete_circle_packing.h` | User-facing entry for the face-based circle-packing functional of Bobenko-Pinkall-Springborn 2010. | `Circle_packing_result` | +| `CGAL/Discrete_conformal_map.h` | User-facing entry point for the Discrete_conformal_map package. | `Conformal_map_result`, `Hyper_ideal_map_result` | +| `CGAL/Discrete_inversive_distance.h` | User-facing entry for the vertex-based inversive-distance circle- packing functional of Luo (2004), with the Bowers-Stephenson (2004) initialisation. | _(no public symbols)_ | -| Header | Description | -|---|---| -| `clausen.hpp` | `clausen_cl2(θ)` (Clausen Cl₂), `lobachevsky(θ)` (Л), `im_li2(θ)` (ImLi₂ = imaginary part of dilogarithm) | +## CGAL internals (``) -## HyperIdeal geometry (H²) +| Header | Brief | Public symbols | +|---|---|---| +| `CGAL/Conformal_map/doxygen_groups.h` | Copyright (c) 2024-2026 Tarik Moussa. | _(no public symbols)_ | +| `CGAL/Conformal_map/doxygen_namespaces.h` | Copyright (c) 2024-2026 Tarik Moussa. | _(no public symbols)_ | +| `CGAL/Conformal_map/internal/parameters.h` | Copyright (c) 2024-2026 Tarik Moussa. | _(no public symbols)_ | -| Header | Description | -|---|---| -| `hyper_ideal_geometry.hpp` | `ζ₁₃`, `ζ₁₄`, `ζ₁₅` (Springborn 2020), `l_from_zeta()`, `alpha_ij()`, `beta_i()`, `sigma_i()`, `sigma_ij()` | -| `hyper_ideal_utility.hpp` | Tetrahedron volumes (Meyerhoff formula, Kolpakov–Mednykh) | -| `hyper_ideal_visualization_utility.hpp` | Poincaré disk projection, circumcircle helpers, Lorentz boost | -| `hyper_ideal_functional.hpp` | `HyperIdealMaps`, `setup_hyper_ideal_maps()`, `compute_hyper_ideal_lambda0_from_mesh()`, `assign_all_dof_indices()`, `hyper_ideal_gradient()`, `hyper_ideal_energy()` | -| `hyper_ideal_hessian.hpp` | `hyper_ideal_hessian()` — symmetric FD Hessian (Phase 9b: analytic) | +## Core (`conformallab` namespace, `<...>`) -## Spherical geometry (S²) +| Header | Brief | Public symbols | +|---|---|---| +| `clausen.hpp` | Clausen integral, Lobachevsky function, and Im(Li2). | _(no public symbols)_ | +| `conformal_mesh.hpp` | conformal_mesh.hpp Central mesh type for the discrete conformal mapping algorithms. | _(no public symbols)_ | +| `constants.hpp` | constants.hpp Single source of truth for mathematical constants used throughout conformallab++. | _(no public symbols)_ | +| `cp_euclidean_functional.hpp` | cp_euclidean_functional.hpp Phase 9a.1 — Circle-Packing Euclidean functional (CP-Euclidean). | `CPEuclideanMaps` | +| `cut_graph.hpp` | cut_graph.hpp Phase 6 — Tree-cotree algorithm for computing a cut graph of a triangulated surface. | `CutGraph` | +| `discrete_elliptic_utility.hpp` | Ported from de.varylab.discreteconformal.util.DiscreteEllipticUtility (Java). | _(no public symbols)_ | +| `euclidean_functional.hpp` | euclidean_functional.hpp Energy and gradient of the Euclidean discrete conformal functional (EuclideanCyclicFunctional) evaluated on a ConformalMesh. | `EuclideanMaps`, `EuclideanResult` | +| `euclidean_geometry.hpp` | euclidean_geometry.hpp Corner-angle formula for Euclidean triangles in the discrete conformal (log-length) parametrisation. | `EuclideanFaceAngles` | +| `euclidean_hessian.hpp` | euclidean_hessian.hpp Analytical Hessian of the Euclidean discrete conformal energy — the cotangent-Laplace operator. | `EuclCotWeights` | +| `fundamental_domain.hpp` | fundamental_domain.hpp Phase 7 — Fundamental domain polygon for closed surfaces. | `FundamentalDomain` | +| `gauss_bonnet.hpp` | gauss_bonnet.hpp Phase 6 — Gauss–Bonnet consistency check for prescribed target angles. | _(no public symbols)_ | +| `hyper_ideal_functional.hpp` | hyper_ideal_functional.hpp Energy and gradient of the hyper-ideal discrete conformal map functional evaluated on a ConformalMesh (CGAL::Surface_mesh). | `HyperIdealMaps`, `HyperIdealResult`, `FaceAngleOutputs`, `FaceAngles` | +| `hyper_ideal_geometry.hpp` | hyper_ideal_geometry.hpp Pure-math building blocks for the hyper-ideal discrete conformal map. | _(no public symbols)_ | +| `hyper_ideal_hessian.hpp` | hyper_ideal_hessian.hpp Phase 4a — Hessian of the hyper-ideal discrete conformal functional. | _(no public symbols)_ | +| `hyper_ideal_utility.hpp` | Hyperbolic tetrahedron volume formulas. | _(no public symbols)_ | +| `hyper_ideal_visualization_utility.hpp` | Port of the static helper HyperIdealVisualizationPlugin.getEuclideanCircleFromHyperbolic() from de.varylab.discreteconformal.plugin. | _(no public symbols)_ | +| `inversive_distance_functional.hpp` | inversive_distance_functional.hpp Phase 9a.2 — Inversive-distance circle-packing functional (Luo 2004). | `InversiveDistanceMaps` | +| `layout.hpp` | layout.hpp Phase 5/6/7 — Layout / embedding: DOF vector → vertex coordinates in the target geometry via BFS-trilateration. | `MobiusMap`, `Layout2D`, `Layout3D`, `HolonomyData` | +| `matrix_utility.hpp` | 4x4 mapping matrix from corresponding point pairs. | _(no public symbols)_ | +| `mesh_builder.hpp` | mesh_builder.hpp Factory functions that build simple reference meshes for testing and examples. | _(no public symbols)_ | +| `mesh_io.hpp` | mesh_io.hpp Phase 4b — CGAL::IO wrappers for ConformalMesh. | _(no public symbols)_ | +| `mesh_utils.hpp` | mesh_utils.hpp Conversions between CGAL::Surface_mesh and Eigen matrices. | _(no public symbols)_ | +| `newton_solver.hpp` | newton_solver.hpp Phase 4a — Newton solver for all three discrete conformal functionals. | `NewtonResult` | +| `p2_utility.hpp` | 2-D projective geometry utilities for the Euclidean signature. | _(no public symbols)_ | +| `period_matrix.hpp` | period_matrix.hpp Phase 7 — Period matrix for closed surfaces with Euclidean (flat) metric. | `PeriodData` | +| `projective_math.hpp` | Projective and hyperbolic geometry utilities. | _(no public symbols)_ | +| `serialization.hpp` | serialization.hpp Phase 5 — Save and load conformal map results in JSON and XML formats. | _(no public symbols)_ | +| `spherical_functional.hpp` | spherical_functional.hpp Energy and gradient of the spherical discrete conformal functional evaluated on a ConformalMesh (CGAL::Surface_mesh). | `SphericalMaps`, `SphericalResult` | +| `spherical_geometry.hpp` | spherical_geometry.hpp Pure-math building blocks for the spherical discrete conformal map. | `SphericalFaceAngles` | +| `spherical_hessian.hpp` | spherical_hessian.hpp Analytical Hessian of the spherical discrete conformal energy — the spherical cotangent-Laplace operator. | `SpherCotWeights` | +| `viewer_utils.h` | _(undocumented — add a `\file` brief at the top of the header)_ | _(no public symbols)_ | -| Header | Description | -|---|---| -| `spherical_geometry.hpp` | Spherical arc-length, half-angle formula, spherical law of cosines | -| `spherical_functional.hpp` | `SphericalMaps`, `setup_spherical_maps()`, `compute_spherical_lambda0_from_mesh()`, `spherical_gradient()`, `spherical_energy()` | -| `spherical_hessian.hpp` | `spherical_hessian()` — analytic Hessian via ∂α/∂u from law of cosines | - -## Euclidean geometry (ℝ²) - -| Header | Description | -|---|---| -| `euclidean_geometry.hpp` | Euclidean corner angle (t-value / atan2), edge lengths from DOF vector | -| `euclidean_functional.hpp` | `EuclideanMaps`, `setup_euclidean_maps()`, `compute_euclidean_lambda0_from_mesh()`, `euclidean_gradient()`, `euclidean_energy()` | -| `euclidean_hessian.hpp` | `euclidean_hessian()` — cotangent Laplacian (Pinkall–Polthier 1993) | - -## Circle-packing functionals (Phase 9a) - -| Header | Description | -|---|---| -| `cp_euclidean_functional.hpp` | **Face-based** CP-Euclidean (Bobenko–Pinkall–Springborn 2010), port of Java `CPEuclideanFunctional`. `CPEuclideanMaps`, `setup_cp_euclidean_maps()`, `assign_cp_euclidean_face_dof_indices()`, `cp_euclidean_gradient()`, `cp_euclidean_energy()`, **analytic** `cp_euclidean_hessian()` (2×2-per-edge `h_jk = sin θ / (cosh Δρ − cos θ)`) | -| `inversive_distance_functional.hpp` | **Vertex-based** inversive-distance (Luo 2004, no Java original). `InversiveDistanceMaps`, `setup_inversive_distance_maps()`, `compute_inversive_distance_init_from_mesh()` (Bowers–Stephenson 2004 init), `inversive_distance_gradient()`, `inversive_distance_energy()` | - -## Solver - -| Header | Description | -|---|---| -| `newton_solver.hpp` | Five Newton solvers — `newton_euclidean()`, `newton_spherical()`, `newton_hyper_ideal()` (uses block-FD Hessian since Phase 9b), `newton_cp_euclidean()` (analytic Hessian, Phase 9a-Newton), `newton_inversive_distance()` (FD Hessian, Phase 9a-Newton). Plus `solve_linear_system()` (SimplicialLDLT + SparseQR fallback) and the `NewtonResult` struct. | - -## Preprocessing - -| Header | Description | -|---|---| -| `gauss_bonnet.hpp` | `euler_characteristic()`, `genus()`, `gauss_bonnet_sum()`, `gauss_bonnet_rhs()`, `gauss_bonnet_deficit()`, `check_gauss_bonnet()`, `enforce_gauss_bonnet()` | - -## Layout and holonomy - -| Header | Description | -|---|---| -| `cut_graph.hpp` | `CutGraph` struct, `compute_cut_graph()` — tree-cotree algorithm (Erickson–Whittlesey 2005), produces 2g seam edges | -| `layout.hpp` | `euclidean_layout()`, `spherical_layout()`, `hyper_ideal_layout()`, `normalise_{euclidean,hyperbolic,spherical}()`. Structs: `Layout2D`, `Layout3D`, `HolonomyData`. `MobiusMap` (T(z)=(az+b)/(cz+d), `from_three`, `compose`, `inverse`, `apply`). Priority-BFS, `halfedge_uv`. | - -## Post-processing - -| Header | Description | -|---|---| -| `period_matrix.hpp` | `PeriodData`, `compute_period_matrix()`, `reduce_to_fundamental_domain()`, `is_in_fundamental_domain()` — period ratio τ = ω₂/ω₁ ∈ ℍ, SL(2,ℤ) reduction | -| `fundamental_domain.hpp` | `FundamentalDomain`, `compute_fundamental_domain()` (genus 1: CCW parallelogram; genus > 1: empty, TODO Phase 9c), `tiling_copy()`, `tiling_neighbourhood()` | -| `serialization.hpp` | `save_result_json()`, `load_result_json()`, `save_result_xml()`, `load_result_xml()`, `save_layout_off()` | - -## Math utilities - -These headers contain pure-math helpers used internally across the -package. All are also re-exported on the public API so downstream -consumers (e.g. user code that wants to assemble a custom functional) -can use them directly. - -| Header | Description | -|---|---| -| `matrix_utility.hpp` | Small linear-algebra helpers used by `period_matrix.hpp` (2×2 determinant, …) | -| `projective_math.hpp` | Real projective 2-space helpers (point on line, lines through points, dual) | -| `p2_utility.hpp` | Higher-level P² utilities used by hyper-ideal layout (perpendicular bisectors in Poincaré disk, etc.) | -| `discrete_elliptic_utility.hpp` | Genus-1 / elliptic-curve helpers — half-period ratio τ from texture coords; bridge to genus-1 fundamental domain | - -## CGAL public API (Phase 8b-Lite) - -These headers live under `include/CGAL/` and are the user-facing entry -points. They are thin wrappers over the implementation in -`code/include/*.hpp`, exposing the five DCE models through a -CGAL-conventional interface. - -| Header | Description | -|---|---| -| `CGAL/Conformal_map_traits.h` | `ConformalMapTraits` concept + `Default_conformal_map_traits` (Euclidean-flavoured base trait) | -| `CGAL/Discrete_conformal_map.h` | `discrete_conformal_map_euclidean()`, `discrete_conformal_map_spherical()`, `discrete_conformal_map_hyper_ideal()`. Result types `Conformal_map_result` and `Hyper_ideal_map_result` | -| `CGAL/Discrete_circle_packing.h` | `Default_cp_euclidean_traits` + `discrete_circle_packing_euclidean()`. Result: `Circle_packing_result` with `rho_per_face` | -| `CGAL/Discrete_inversive_distance.h` | `Default_inversive_distance_traits` + `discrete_inversive_distance_map()`. Result: `Conformal_map_result` reused | -| `CGAL/Conformal_layout.h` | Thin re-export of `euclidean_layout`, `spherical_layout`, `hyper_ideal_layout` into the `CGAL::` namespace | -| `CGAL/Conformal_map/internal/parameters.h` | (internal) Named-parameter tags for `CGAL::parameters::*` | diff --git a/scripts/gen-headers-md.py b/scripts/gen-headers-md.py new file mode 100755 index 0000000..ef2dda7 --- /dev/null +++ b/scripts/gen-headers-md.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +"""scripts/gen-headers-md.py + +Auto-generate `doc/api/headers.md` from the Doxygen XML index. For +each public header under `code/include/`, the script extracts: + + * the header's `@file` brief description (if present), else falls + back to the first leading `//` comment block in the source file; + * the names of all public classes, structs, free functions, enums + and typedefs declared at file scope (one-line list). + +Headers are grouped by their directory: + + * `code/include/CGAL/` → "CGAL public API" + * `code/include/CGAL/.../` → nested CGAL subgroups (internal_np, …) + * `code/include/*.hpp` → "Core (conformallab namespace)" + +The file is auto-generated; do NOT hand-edit. Regenerate via: + + bash scripts/regen-docs.sh # convenience wrapper + # or: + doxygen Doxyfile && python3 scripts/gen-headers-md.py + +Prerequisite: `GENERATE_XML = YES` in the Doxyfile (already set). +""" + +from __future__ import annotations +import os, sys, glob, xml.etree.ElementTree as ET +from collections import defaultdict + +REPO_ROOT = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) +XML_DIR = os.path.join(REPO_ROOT, "doc", "doxygen", "xml") +OUT_PATH = os.path.join(REPO_ROOT, "doc", "api", "headers.md") +PUBLIC_ABS = os.path.join(REPO_ROOT, "code", "include") + os.sep +PUBLIC_REL = "code/include/" + + +def is_public(path: str) -> bool: + return path.startswith(PUBLIC_ABS) or path.startswith(PUBLIC_REL) + + +def relpath(path: str) -> str: + if path.startswith(PUBLIC_ABS): + return path[len(PUBLIC_ABS):] + if path.startswith(PUBLIC_REL): + return path[len(PUBLIC_REL):] + return path + + +def text_of(elem: ET.Element | None) -> str: + """Concatenate all text under elem, trimming whitespace.""" + if elem is None: + return "" + return " ".join("".join(elem.itertext()).split()).strip() + + +def first_sentence(text: str, max_chars: int = 240) -> str: + """Return the first sentence (or first `max_chars` chars) of text. + Heuristic: split at the first ". " (period + space) that follows + at least 30 characters, else hard-truncate.""" + text = text.strip() + if not text: + return "" + # Strip Doxygen tag-class artefacts that bleed into briefs + text = text.replace("CGAL::Discrete_conformal_map", "") # spurious self-reference + for cut in [". ", ".\n", "; "]: + i = text.find(cut, 30) + if 0 < i < max_chars: + return text[: i + 1].strip() + if len(text) > max_chars: + return text[: max_chars - 1].rstrip() + "…" + return text + + +def first_leading_comment(path: str) -> str: + """Fallback: extract the first contiguous //-comment block at top of file.""" + try: + with open(path, encoding="utf-8") as f: + lines = [] + started = False + for ln in f: + s = ln.strip() + if s.startswith("//"): + lines.append(s.lstrip("/").strip()) + started = True + elif started: + break + elif not s or s.startswith("#pragma"): + continue + else: + break + return " ".join(l for l in lines if l) + except OSError: + return "" + + +def scan_xml() -> dict[str, dict]: + """Walk all compounddef kind='file' XMLs and return a dict + keyed by absolute path → {'brief': str, 'symbols': [list]}. + """ + out: dict[str, dict] = {} + for xml in sorted(glob.glob(os.path.join(XML_DIR, "*.xml"))): + bn = os.path.basename(xml) + if bn in {"index.xml", "Doxyfile.xml", "indexpage.xml"}: + continue + try: + tree = ET.parse(xml) + except ET.ParseError: + continue + for cd in tree.iter("compounddef"): + if cd.attrib.get("kind") != "file": + continue + loc = cd.find("location") + if loc is None: + continue + path = loc.attrib.get("file", "") + if not is_public(path): + continue + # Normalise to absolute for downstream file IO + if path.startswith(PUBLIC_REL): + path = os.path.join(REPO_ROOT, path) + ext = os.path.splitext(path)[1] + if ext not in {".h", ".hpp"}: + continue + brief = text_of(cd.find("briefdescription")) + if not brief: + brief = text_of(cd.find("detaileddescription")) + if not brief: + brief = first_leading_comment(path) + brief = first_sentence(brief) + + symbols: list[str] = [] + # Free symbols defined directly in the file + for sect in cd.iter("sectiondef"): + for m in sect.iter("memberdef"): + mk = m.attrib.get("kind", "") + if mk not in {"function", "typedef", "enum", "variable"}: + continue + if m.attrib.get("prot", "public") != "public": + continue + name = m.findtext("name", "?") + qual = m.findtext("qualifiedname", name) + if "::detail" in qual or "::cp_detail" in qual or \ + "::id_detail" in qual or "::detail_xml" in qual: + continue + if mk == "function": + symbols.append(f"`{name}()`") + elif mk == "typedef": + symbols.append(f"`{name}`") + elif mk == "enum": + symbols.append(f"`enum {name}`") + else: + symbols.append(f"`{name}`") + # Classes/structs declared in the file (skip template + # specialisations whose name contains angle brackets — those + # appear as duplicates of the primary template). + for cref in cd.iter("innerclass"): + cname = cref.text or "" + if "detail" in cname or "<" in cname: + continue + short = cname.rsplit("::", 1)[-1] + symbols.append(f"`{short}`") + # De-duplicate, preserve order + seen = set() + uniq = [] + for s in symbols: + if s in seen: + continue + seen.add(s); uniq.append(s) + out[path] = {"brief": brief, "symbols": uniq} + return out + + +def group_key(path: str) -> tuple[str, int]: + rel = relpath(path) + parts = rel.split(os.sep) + if parts[0] == "CGAL": + if len(parts) == 2: + return ("CGAL public API (``)", 1) + return (f"CGAL internals (``)", 2) + return ("Core (`conformallab` namespace, `<...>`)", 3) + + +def render(headers: dict[str, dict]) -> str: + groups: dict[tuple[str, int], list[tuple[str, dict]]] = defaultdict(list) + for path, info in headers.items(): + groups[group_key(path)].append((path, info)) + + lines: list[str] = [] + lines.append("") + lines.append("") + lines.append("") + lines.append("# Public Headers (`code/include/`)") + lines.append("") + lines.append("All algorithms are header-only. Include the headers you need") + lines.append("directly — there is no compiled library to link against (only") + lines.append("GTest for tests and CGAL/Eigen for the CGAL-dependent headers).") + lines.append("") + lines.append("This page is **regenerated** from Doxygen XML on every push to") + lines.append("`main` that touches a public header (see") + lines.append("`.gitea/workflows/doxygen-pages.yml` and `scripts/gen-headers-md.py`).") + lines.append("To improve a description, edit the `\\file` brief at the top of") + lines.append("the corresponding header, then re-run `bash scripts/regen-docs.sh`.") + lines.append("") + + for key in sorted(groups.keys(), key=lambda k: (k[1], k[0])): + title, _ = key + lines.append(f"## {title}") + lines.append("") + lines.append("| Header | Brief | Public symbols |") + lines.append("|---|---|---|") + for path, info in sorted(groups[key], key=lambda x: x[0]): + rel = relpath(path).replace(os.sep, "/") + brief = info["brief"] or "_(undocumented — add a `\\file` brief at the top of the header)_" + # Defensive: collapse pipe/newline chars that would break the table + brief = brief.replace("|", "\\|").replace("\n", " ").strip() + syms = ", ".join(info["symbols"][:10]) or "_(no public symbols)_" + if len(info["symbols"]) > 10: + syms += f", … (+{len(info['symbols']) - 10} more)" + lines.append(f"| `{rel}` | {brief} | {syms} |") + lines.append("") + + return "\n".join(lines) + "\n" + + +def main() -> int: + if not os.path.isdir(XML_DIR): + print(f"ERROR: {XML_DIR} missing — run `doxygen Doxyfile` first", file=sys.stderr) + return 2 + headers = scan_xml() + if not headers: + print("ERROR: no public headers found in XML output", file=sys.stderr) + return 2 + text = render(headers) + with open(OUT_PATH, "w", encoding="utf-8") as f: + f.write(text) + print(f"Wrote {OUT_PATH} ({len(headers)} headers, {sum(len(h['symbols']) for h in headers.values())} symbols)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/regen-docs.sh b/scripts/regen-docs.sh new file mode 100755 index 0000000..fbd6af5 --- /dev/null +++ b/scripts/regen-docs.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# scripts/regen-docs.sh — convenience wrapper: +# 1. run doxygen to refresh XML + HTML output +# 2. regenerate doc/api/headers.md from the XML +# 3. report coverage so the operator sees the impact +# +# Intended for local use ("I just added a \file brief — regenerate +# the markdown landing page"); the same steps run automatically in +# .gitea/workflows/doxygen-pages.yml on every push to main that touches +# the public headers. +set -eu +cd "$(dirname "$0")/.." +doxygen Doxyfile >/dev/null +python3 scripts/gen-headers-md.py +bash scripts/doxygen-coverage.sh