ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
doxygen_md_filter.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# SPDX-License-Identifier: MIT
3# Copyright (c) 2026 Brighton Sikarskie
4"""Doxygen input filter for Markdown pages on the published docs site.
5
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
9untouched):
10
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
15 visitor.
16
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.
27
28Both transformations skip fenced code blocks, where such text is a
29literal example rather than a live link.
30
31Wired up via ``FILTER_PATTERNS`` in the top-level ``Doxyfile``::
32
33 FILTER_PATTERNS = *.md="python3 scripts/gen/doxygen_md_filter.py"
34
35Doxygen invokes the filter once per matching input file with the file
36path as the single argument and reads the filtered content from stdout.
37"""
38
39from __future__ import annotations
40
41import re
42import sys
43from pathlib import Path, PurePosixPath
44
45REPO_ROOT = Path(__file__).resolve().parents[2]
46
47# A fenced-code-block delimiter line (``` or ~~~, optionally indented).
48FENCE = re.compile(r"^\s*(?:```|~~~)")
49
50# A Markdown image (plain or link-wrapped) whose image URL is a GitHub
51# Actions workflow badge. Matched occurrences are removed from the line;
52# lines left empty by the removal are dropped entirely.
53ACTIONS_BADGE = re.compile(
54 r"\‍[?!\‍[[^\‍]]*\‍]\‍([^)]*/actions/workflows/[^)]*badge\.svg[^)]*\‍)(?:\‍]\‍([^)]*\‍))?"
55)
56
57# The target of a Markdown link written as ``[text](<target>)``.
58MD_LINK = re.compile(r"(?<!\!)(\‍[[^\‍]]*\‍])\‍(([^)\s]+)\‍)")
59
60
61def rewrite_readme_link(source_dir: PurePosixPath, target: str) -> str | None:
62 """Return the ``@ref`` form of a relative ``<dir>/README.md`` target.
63
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.
68 """
69 if not target.endswith("README.md"):
70 return None
71 if target.startswith(("http://", "https://", "mailto:", "/", "#")):
72 return None
73 resolved = PurePosixPath(*(source_dir / target).parts)
74 parts: list[str] = []
75 for part in resolved.parts:
76 if part == "..":
77 if not parts:
78 return None # escapes the repository root
79 parts.pop()
80 elif part != ".":
81 parts.append(part)
82 normalized = PurePosixPath(*parts)
83 if not (REPO_ROOT / normalized).is_file():
84 return None
85 ref_dir = normalized.parent
86 if not ref_dir.parts:
87 return None # the top-level README.md has no directory page
88 return f"@ref {ref_dir}"
89
90
91def filter_markdown(text: str, source_dir: PurePosixPath) -> str:
92 """Return ``text`` with badges removed and README links rewritten."""
93
94 def replace_link(match: re.Match[str]) -> str:
95 ref = rewrite_readme_link(source_dir, match.group(2))
96 if ref is None:
97 return match.group(0)
98 return f"{match.group(1)}({ref})"
99
100 kept: list[str] = []
101 in_fence = False
102 for line in text.splitlines(keepends=True):
103 if FENCE.match(line):
104 in_fence = not in_fence
105 kept.append(line)
106 continue
107 if in_fence:
108 kept.append(line)
109 continue
110 stripped_line = ACTIONS_BADGE.sub("", line)
111 if not stripped_line.strip() and ACTIONS_BADGE.search(line):
112 continue # the line held nothing but a badge -- drop it
113 kept.append(MD_LINK.sub(replace_link, stripped_line))
114 return "".join(kept)
115
116
117def main(argv: list[str]) -> int:
118 """Filter the file named in ``argv`` to stdout; 0 on success."""
119 args = argv[1:]
120 try:
121 (source_name,) = args
122 except ValueError:
123 print("usage: doxygen_md_filter.py <file.md>", file=sys.stderr)
124 return 2
125 source = Path(source_name).resolve()
126 try:
127 source_dir = PurePosixPath(source.parent.relative_to(REPO_ROOT).as_posix())
128 except ValueError:
129 source_dir = PurePosixPath() # outside the repo: badge-strip only
130 sys.stdout.write(filter_markdown(source.read_text(encoding="utf-8"), source_dir))
131 return 0
132
133
134if __name__ == "__main__":
135 sys.exit(main(sys.argv))
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298