4"""Doxygen input filter for Markdown pages on the published docs site.
6Markdown in this repository is written for github.com first; two GitHub
7idioms degrade on the public Doxygen site, and this filter maps them to
8their docs-site equivalent at build time (the files on GitHub are
111. **GitHub Actions status badges** are dropped. The repository is
12 private but the gh-pages site is public, so a badge image URL
13 (``.../actions/workflows/<wf>.yml/badge.svg``) answers 404 without
14 repo credentials and renders as a broken-image icon for every site
172. **Links to ``<dir>/README.md`` become ``@ref <dir>``.** Doxygen merges
18 each ``README.md`` into its directory's page, so such links already
19 land on the directory page -- but when resolved from the raw
20 ``.md`` target, doxygen emits the Markdown file's *absolute build
21 path* as the link tooltip, leaking the workspace path of whichever
22 machine built the docs into the published HTML. The equivalent
23 ``@ref`` form resolves to the same directory page with a clean,
24 repo-relative tooltip. Only links whose target provably exists in
25 the repo are rewritten; anchored links (``README.md#...``) and
26 external URLs pass through untouched.
28Both transformations skip fenced code blocks, where such text is a
29literal example rather than a live link.
31Wired up via ``FILTER_PATTERNS`` in the top-level ``Doxyfile``::
33 FILTER_PATTERNS = *.md="python3 scripts/gen/doxygen_md_filter.py"
35Doxygen invokes the filter once per matching input file with the file
36path as the single argument and reads the filtered content from stdout.
39from __future__
import annotations
43from pathlib
import Path, PurePosixPath
45REPO_ROOT = Path(__file__).resolve().parents[2]
48FENCE = re.compile(
r"^\s*(?:```|~~~)")
53ACTIONS_BADGE = re.compile(
54 r"\[?!\[[^\]]*\]\([^)]*/actions/workflows/[^)]*badge\.svg[^)]*\)(?:\]\([^)]*\))?"
58MD_LINK = re.compile(
r"(?<!\!)(\[[^\]]*\])\(([^)\s]+)\)")
61def rewrite_readme_link(source_dir: PurePosixPath, target: str) -> str |
None:
62 """Return the ``@ref`` form of a relative ``<dir>/README.md`` target.
64 ``source_dir`` is the repo-relative directory of the Markdown file
65 being filtered. Returns ``None`` when the target is external, is
66 anchored, escapes the repo, or does not resolve to an existing
67 ``README.md`` -- callers must then leave the link untouched.
69 if not target.endswith(
"README.md"):
71 if target.startswith((
"http://",
"https://",
"mailto:",
"/",
"#")):
73 resolved = PurePosixPath(*(source_dir / target).parts)
75 for part
in resolved.parts:
82 normalized = PurePosixPath(*parts)
83 if not (REPO_ROOT / normalized).is_file():
85 ref_dir = normalized.parent
88 return f
"@ref {ref_dir}"
91def filter_markdown(text: str, source_dir: PurePosixPath) -> str:
92 """Return ``text`` with badges removed and README links rewritten."""
94 def replace_link(match: re.Match[str]) -> str:
95 ref = rewrite_readme_link(source_dir, match.group(2))
98 return f
"{match.group(1)}({ref})"
102 for line
in text.splitlines(keepends=
True):
103 if FENCE.match(line):
104 in_fence =
not in_fence
110 stripped_line = ACTIONS_BADGE.sub(
"", line)
111 if not stripped_line.strip()
and ACTIONS_BADGE.search(line):
113 kept.append(MD_LINK.sub(replace_link, stripped_line))
117def main(argv: list[str]) -> int:
118 """Filter the file named in ``argv`` to stdout; 0 on success."""
121 (source_name,) = args
123 print(
"usage: doxygen_md_filter.py <file.md>", file=sys.stderr)
125 source = Path(source_name).resolve()
127 source_dir = PurePosixPath(source.parent.relative_to(REPO_ROOT).as_posix())
129 source_dir = PurePosixPath()
130 sys.stdout.write(filter_markdown(source.read_text(encoding=
"utf-8"), source_dir))
134if __name__ ==
"__main__":
135 sys.exit(
main(sys.argv))
void main(void)
The application entry point Reset_Handler hands control to.