PICurv 0.1.0
A Parallel Particle-In-Cell Solver for Curvilinear LES
Loading...
Searching...
No Matches
generate_doxygen_fallback_indexes.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Generate robust Doxygen index pages and structured reference views."""
3
4from __future__ import annotations
5
6import argparse
7import html
8import re
9from pathlib import Path
10
11HEADER_SUFFIXES = {".h", ".hpp"}
12SOURCE_SUFFIXES = {".c", ".cc", ".cpp"}
13SCRIPT_SUFFIXES = {".py", ".sh", ".flow"}
14REPO_BLOB_URL = "https://github.com/VishalKandala/PICurv/blob/main/"
15IGNORED_STRUCT_NAMES = {"Name"}
16
17NAMED_STRUCT_RE = re.compile(r"\bstruct\s+([A-Za-z_]\w*)\s*\{")
18TYPEDEF_START_RE = re.compile(r"^\s*typedef\s+struct(?:\s+([A-Za-z_]\w*))?")
19TYPEDEF_END_RE = re.compile(r"^\s*}\s*([A-Za-z_]\w*)\s*;")
20
21
22def doxygen_file_page(name: str) -> str:
23 return name.replace("_", "__").replace(".", "_8") + ".html"
24
25
26def doxygen_file_page_with_path(rel_path: str) -> str:
27 return rel_path.replace("_", "__").replace("/", "_2").replace(".", "_8") + ".html"
28
29
30def needs_files_fallback(path: Path) -> bool:
31 if not path.exists():
32 return True
33 text = path.read_text(encoding="utf-8", errors="ignore")
34 return "Detailed file index was not generated in this build." in text
35
36
37def needs_structs_fallback(path: Path) -> bool:
38 if not path.exists():
39 return True
40 text = path.read_text(encoding="utf-8", errors="ignore")
41 return "Detailed structure index was not generated in this build." in text
42
43
44def resolve_doxygen_file_href(html_dir: Path, rel_path: str) -> str:
45 name = Path(rel_path).name
46 candidate = doxygen_file_page(name)
47 if (html_dir / candidate).exists():
48 return candidate
49 candidate_path = doxygen_file_page_with_path(rel_path)
50 if (html_dir / candidate_path).exists():
51 return candidate_path
52 return ""
53
54
55def make_repo_href(rel_path: str) -> str:
56 return REPO_BLOB_URL + rel_path
57
58
59def collect_file_rows(repo_root: Path, html_dir: Path, base_dir: str, suffixes: set[str]) -> list[tuple[str, str, str]]:
60 rows: list[tuple[str, str, str]] = []
61 root = repo_root / base_dir
62 if not root.exists():
63 return rows
64 for path in sorted(root.rglob("*")):
65 if not path.is_file():
66 continue
67 if suffixes and path.suffix.lower() not in suffixes:
68 continue
69 rel = path.relative_to(repo_root).as_posix()
70 href = resolve_doxygen_file_href(html_dir, rel) or make_repo_href(rel)
71 rows.append((path.name, rel, href))
72 return rows
73
74
75def collect_all_source_like_files(repo_root: Path, html_dir: Path) -> list[tuple[str, str, str]]:
76 rows: list[tuple[str, str, str]] = []
77 rows.extend(collect_file_rows(repo_root, html_dir, "include", HEADER_SUFFIXES))
78 rows.extend(collect_file_rows(repo_root, html_dir, "src", SOURCE_SUFFIXES))
79 rows.extend(collect_file_rows(repo_root, html_dir, "scripts", SCRIPT_SUFFIXES))
80 return rows
81
82
83def collect_struct_rows(repo_root: Path, html_dir: Path) -> list[tuple[str, str, str]]:
84 struct_to_header: dict[str, str] = {}
85 include_dir = repo_root / "include"
86 if not include_dir.exists():
87 return []
88
89 for header in sorted(include_dir.rglob("*.h")):
90 text = header.read_text(encoding="utf-8", errors="ignore")
91 header_rel = header.relative_to(repo_root).as_posix()
92 names = extract_struct_names(text)
93 for name in names:
94 struct_to_header.setdefault(name, header_rel)
95
96 rows: list[tuple[str, str, str]] = []
97 for name, header_rel in sorted(struct_to_header.items(), key=lambda item: item[0].lower()):
98 if name in IGNORED_STRUCT_NAMES:
99 continue
100 struct_page = f"struct{name}.html"
101 if (html_dir / struct_page).exists():
102 href = struct_page
103 else:
104 href = resolve_doxygen_file_href(html_dir, header_rel) or make_repo_href(header_rel)
105 rows.append((name, header_rel, href))
106 return rows
107
108
109def extract_struct_names(text: str) -> set[str]:
110 names: set[str] = set()
111
112 for match in NAMED_STRUCT_RE.finditer(text):
113 names.add(match.group(1))
114
115 in_typedef_struct = False
116 brace_depth = 0
117 typedef_tag_name: str | None = None
118 for line in text.splitlines():
119 if not in_typedef_struct:
120 start = TYPEDEF_START_RE.search(line)
121 if not start:
122 continue
123 in_typedef_struct = True
124 typedef_tag_name = start.group(1)
125 if typedef_tag_name:
126 names.add(typedef_tag_name)
127 brace_depth = line.count("{") - line.count("}")
128 if brace_depth <= 0:
129 in_typedef_struct = False
130 typedef_tag_name = None
131 continue
132
133 brace_depth += line.count("{") - line.count("}")
134 end = TYPEDEF_END_RE.search(line)
135 if end:
136 names.add(end.group(1))
137 if brace_depth <= 0:
138 in_typedef_struct = False
139 typedef_tag_name = None
140
141 return names
142
143
144def categorize_struct(name: str) -> str:
145 if name.startswith("BC") or "Boundary" in name or name == "FlowWave":
146 return "Boundary Condition System"
147 if name.startswith("IBM") or name in {"FSInfo", "SurfElmtInfo", "Cstart"}:
148 return "Immersed Boundary and FSI"
149 if name.startswith("Particle") or name in {"MigrationInfo"}:
150 return "Particle Transport and Statistics"
151 if name in {"SimCtx", "UserCtx", "UserMG", "MGCtx", "ScalingCtx", "DualMonitorCtx", "ProfiledFunction"}:
152 return "Runtime Control and Solver Orchestration"
153 if name.startswith("VTK") or name == "PostProcessParams":
154 return "I/O and Postprocessing"
155 if name in {"BoundingBox", "Cell", "Cmpnts", "Cmpnts2", "Cpt2D", "RankCellInfo", "RankNeighbors"}:
156 return "Grid and Geometry"
157 return "Generic Containers and Utilities"
158
159
160def render_link(label: str, href: str) -> str:
161 label_esc = html.escape(label)
162 href_esc = html.escape(href)
163 if href.startswith("http"):
164 return f"<a class='el' href='{href_esc}' target='_blank' rel='noopener'>{label_esc}</a>"
165 return f"<a class='el' href='{href_esc}'>{label_esc}</a>"
166
167
168def render_rows(rows: list[tuple[str, str, str]], empty_msg: str) -> str:
169 if not rows:
170 return f"<tr><td colspan='2'>{html.escape(empty_msg)}</td></tr>"
171 out: list[str] = []
172 for name, rel, href in rows:
173 out.append(
174 "<tr>"
175 f"<td class='indexkey'>{render_link(name, href)}</td>"
176 f"<td class='indexvalue'><code>{html.escape(rel)}</code></td>"
177 "</tr>"
178 )
179 return "\n".join(out)
180
181
182def section_table(title: str, rows_html: str) -> str:
183 return (
184 f"<h2>{html.escape(title)}</h2>\n"
185 "<table class='doxtable'>\n"
186 "<thead><tr><th>Name</th><th>Location</th></tr></thead>\n"
187 f"<tbody>\n{rows_html}\n</tbody>\n"
188 "</table>\n"
189 )
190
191
192def render_page(title: str, intro: str, body_html: str) -> str:
193 return f"""<!DOCTYPE html>
194<html lang="en">
195<head>
196 <meta charset="utf-8" />
197 <meta name="viewport" content="width=device-width, initial-scale=1" />
198 <title>PICurv: {html.escape(title)}</title>
199 <link href="doxygen.css" rel="stylesheet" />
200 <link href="custom.css" rel="stylesheet" />
201 <script type="text/javascript" src="theme-sync.js"></script>
202</head>
203<body>
204 <div class="header">
205 <div class="headertitle"><div class="title">{html.escape(title)}</div></div>
206 </div>
207 <div class="contents">
208 <p>{html.escape(intro)}</p>
209{body_html}
210 <p>See <a href="Documentation_Map.html">Documentation Map</a> for structural navigation.</p>
211 </div>
212</body>
213</html>
214"""
215
216
217def write_structured_file_index(repo_root: Path, html_dir: Path) -> None:
218 headers = collect_file_rows(repo_root, html_dir, "include", HEADER_SUFFIXES)
219 sources = collect_file_rows(repo_root, html_dir, "src", SOURCE_SUFFIXES)
220 scripts = collect_file_rows(repo_root, html_dir, "scripts", SCRIPT_SUFFIXES)
221 body = (
222 section_table("Header Files", render_rows(headers, "No header files found."))
223 + section_table("Source Files", render_rows(sources, "No source files found."))
224 + section_table("Scripts", render_rows(scripts, "No script files found."))
225 )
226 out = html_dir / "files_structured.html"
227 out.write_text(
229 "File List (Structured)",
230 "Organized by file role: headers, source files, and scripts.",
231 body,
232 ),
233 encoding="utf-8",
234 )
235 print(f"[index] wrote {out}")
236
237
238def write_structured_struct_index(repo_root: Path, html_dir: Path) -> None:
239 rows = collect_struct_rows(repo_root, html_dir)
240 grouped: dict[str, list[tuple[str, str, str]]] = {}
241 for row in rows:
242 grouped.setdefault(categorize_struct(row[0]), []).append(row)
243
244 ordered_sections = [
245 "Runtime Control and Solver Orchestration",
246 "Grid and Geometry",
247 "Boundary Condition System",
248 "Particle Transport and Statistics",
249 "Immersed Boundary and FSI",
250 "I/O and Postprocessing",
251 "Generic Containers and Utilities",
252 ]
253 body_parts: list[str] = []
254 for section in ordered_sections:
255 body_parts.append(
257 section,
258 render_rows(grouped.get(section, []), f"No structures found for section: {section}."),
259 )
260 )
261 out = html_dir / "annotated_structured.html"
262 out.write_text(
264 "Data Structures (By Module)",
265 "Grouped by major solver modules and responsibilities.",
266 "\n".join(body_parts),
267 ),
268 encoding="utf-8",
269 )
270 print(f"[index] wrote {out}")
271
272
273def write_fallback_files_page(repo_root: Path, html_dir: Path) -> None:
274 rows = collect_all_source_like_files(repo_root, html_dir)
275 body = section_table("Files", render_rows(rows, "No source-like files found."))
276 out = html_dir / "files.html"
277 out.write_text(
279 "File List",
280 "Fallback file list generated from include/src/scripts.",
281 body,
282 ),
283 encoding="utf-8",
284 )
285 print(f"[fallback] wrote {out}")
286
287
288def write_fallback_struct_page(repo_root: Path, html_dir: Path) -> None:
289 rows = collect_struct_rows(repo_root, html_dir)
290 body = section_table("Data Structures", render_rows(rows, "No C struct declarations found."))
291 out = html_dir / "annotated.html"
292 out.write_text(
294 "Data Structures",
295 "Fallback structure list generated from C headers.",
296 body,
297 ),
298 encoding="utf-8",
299 )
300 print(f"[fallback] wrote {out}")
301
302
303def main() -> int:
304 parser = argparse.ArgumentParser()
305 parser.add_argument("--repo-root", required=True, type=Path)
306 parser.add_argument("--html-dir", required=True, type=Path)
307 args = parser.parse_args()
308
309 repo_root = args.repo_root.resolve()
310 html_dir = args.html_dir.resolve()
311
312 write_structured_file_index(repo_root, html_dir)
313 write_structured_struct_index(repo_root, html_dir)
314
315 files_page = html_dir / "files.html"
316 structs_page = html_dir / "annotated.html"
317 if needs_files_fallback(files_page):
318 write_fallback_files_page(repo_root, html_dir)
319 if needs_structs_fallback(structs_page):
320 write_fallback_struct_page(repo_root, html_dir)
321
322 return 0
323
324
325if __name__ == "__main__":
326 raise SystemExit(main())
list[tuple[str, str, str]] collect_all_source_like_files(Path repo_root, Path html_dir)
None write_structured_file_index(Path repo_root, Path html_dir)
None write_structured_struct_index(Path repo_root, Path html_dir)
str render_rows(list[tuple[str, str, str]] rows, str empty_msg)
None write_fallback_struct_page(Path repo_root, Path html_dir)
str render_page(str title, str intro, str body_html)
str resolve_doxygen_file_href(Path html_dir, str rel_path)
list[tuple[str, str, str]] collect_struct_rows(Path repo_root, Path html_dir)
list[tuple[str, str, str]] collect_file_rows(Path repo_root, Path html_dir, str base_dir, set[str] suffixes)
None write_fallback_files_page(Path repo_root, Path html_dir)