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 """!
24 @brief Perform doxygen file page.
25 @param[in] name Argument passed to `doxygen_file_page()`.
26 @return Value returned by `doxygen_file_page()`.
27 """
28 return name.replace("_", "__").replace(".", "_8") + ".html"
29
30
31def doxygen_file_page_with_path(rel_path: str) -> str:
32 """!
33 @brief Perform doxygen file page with path.
34 @param[in] rel_path Argument passed to `doxygen_file_page_with_path()`.
35 @return Value returned by `doxygen_file_page_with_path()`.
36 """
37 return rel_path.replace("_", "__").replace("/", "_2").replace(".", "_8") + ".html"
38
39
40def needs_files_fallback(path: Path) -> bool:
41 """!
42 @brief Perform needs files fallback.
43 @param[in] path Filesystem path argument passed to `needs_files_fallback()`.
44 @return Value returned by `needs_files_fallback()`.
45 """
46 if not path.exists():
47 return True
48 text = path.read_text(encoding="utf-8", errors="ignore")
49 return "Detailed file index was not generated in this build." in text
50
51
52def needs_structs_fallback(path: Path) -> bool:
53 """!
54 @brief Perform needs structs fallback.
55 @param[in] path Filesystem path argument passed to `needs_structs_fallback()`.
56 @return Value returned by `needs_structs_fallback()`.
57 """
58 if not path.exists():
59 return True
60 text = path.read_text(encoding="utf-8", errors="ignore")
61 return "Detailed structure index was not generated in this build." in text
62
63
64def resolve_doxygen_file_href(html_dir: Path, rel_path: str) -> str:
65 """!
66 @brief Resolve doxygen file href.
67 @param[in] html_dir Argument passed to `resolve_doxygen_file_href()`.
68 @param[in] rel_path Argument passed to `resolve_doxygen_file_href()`.
69 @return Value returned by `resolve_doxygen_file_href()`.
70 """
71 name = Path(rel_path).name
72 candidate = doxygen_file_page(name)
73 if (html_dir / candidate).exists():
74 return candidate
75 candidate_path = doxygen_file_page_with_path(rel_path)
76 if (html_dir / candidate_path).exists():
77 return candidate_path
78 return ""
79
80
81def make_repo_href(rel_path: str) -> str:
82 """!
83 @brief Perform make repo href.
84 @param[in] rel_path Argument passed to `make_repo_href()`.
85 @return Value returned by `make_repo_href()`.
86 """
87 return REPO_BLOB_URL + rel_path
88
89
90def collect_file_rows(repo_root: Path, html_dir: Path, base_dir: str, suffixes: set[str]) -> list[tuple[str, str, str]]:
91 """!
92 @brief Collect file rows.
93 @param[in] repo_root Argument passed to `collect_file_rows()`.
94 @param[in] html_dir Argument passed to `collect_file_rows()`.
95 @param[in] base_dir Argument passed to `collect_file_rows()`.
96 @param[in] suffixes Argument passed to `collect_file_rows()`.
97 @return Value returned by `collect_file_rows()`.
98 """
99 rows: list[tuple[str, str, str]] = []
100 root = repo_root / base_dir
101 if not root.exists():
102 return rows
103 for path in sorted(root.rglob("*")):
104 if not path.is_file():
105 continue
106 if suffixes and path.suffix.lower() not in suffixes:
107 continue
108 rel = path.relative_to(repo_root).as_posix()
109 href = resolve_doxygen_file_href(html_dir, rel) or make_repo_href(rel)
110 rows.append((path.name, rel, href))
111 return rows
112
113
114def collect_all_source_like_files(repo_root: Path, html_dir: Path) -> list[tuple[str, str, str]]:
115 """!
116 @brief Collect all source like files.
117 @param[in] repo_root Argument passed to `collect_all_source_like_files()`.
118 @param[in] html_dir Argument passed to `collect_all_source_like_files()`.
119 @return Value returned by `collect_all_source_like_files()`.
120 """
121 rows: list[tuple[str, str, str]] = []
122 rows.extend(collect_file_rows(repo_root, html_dir, "include", HEADER_SUFFIXES))
123 rows.extend(collect_file_rows(repo_root, html_dir, "src", SOURCE_SUFFIXES))
124 rows.extend(collect_file_rows(repo_root, html_dir, "scripts", SCRIPT_SUFFIXES))
125 return rows
126
127
128def collect_struct_rows(repo_root: Path, html_dir: Path) -> list[tuple[str, str, str]]:
129 """!
130 @brief Collect struct rows.
131 @param[in] repo_root Argument passed to `collect_struct_rows()`.
132 @param[in] html_dir Argument passed to `collect_struct_rows()`.
133 @return Value returned by `collect_struct_rows()`.
134 """
135 struct_to_header: dict[str, str] = {}
136 include_dir = repo_root / "include"
137 if not include_dir.exists():
138 return []
139
140 for header in sorted(include_dir.rglob("*.h")):
141 text = header.read_text(encoding="utf-8", errors="ignore")
142 header_rel = header.relative_to(repo_root).as_posix()
143 names = extract_struct_names(text)
144 for name in names:
145 struct_to_header.setdefault(name, header_rel)
146
147 rows: list[tuple[str, str, str]] = []
148 for name, header_rel in sorted(struct_to_header.items(), key=lambda item: item[0].lower()):
149 if name in IGNORED_STRUCT_NAMES:
150 continue
151 struct_page = f"struct{name}.html"
152 if (html_dir / struct_page).exists():
153 href = struct_page
154 else:
155 href = resolve_doxygen_file_href(html_dir, header_rel) or make_repo_href(header_rel)
156 rows.append((name, header_rel, href))
157 return rows
158
159
160def extract_struct_names(text: str) -> set[str]:
161 """!
162 @brief Extract struct names.
163 @param[in] text Argument passed to `extract_struct_names()`.
164 @return Value returned by `extract_struct_names()`.
165 """
166 names: set[str] = set()
167
168 for match in NAMED_STRUCT_RE.finditer(text):
169 names.add(match.group(1))
170
171 in_typedef_struct = False
172 brace_depth = 0
173 typedef_tag_name: str | None = None
174 for line in text.splitlines():
175 if not in_typedef_struct:
176 start = TYPEDEF_START_RE.search(line)
177 if not start:
178 continue
179 in_typedef_struct = True
180 typedef_tag_name = start.group(1)
181 if typedef_tag_name:
182 names.add(typedef_tag_name)
183 brace_depth = line.count("{") - line.count("}")
184 if brace_depth <= 0:
185 in_typedef_struct = False
186 typedef_tag_name = None
187 continue
188
189 brace_depth += line.count("{") - line.count("}")
190 end = TYPEDEF_END_RE.search(line)
191 if end:
192 names.add(end.group(1))
193 if brace_depth <= 0:
194 in_typedef_struct = False
195 typedef_tag_name = None
196
197 return names
198
199
200def categorize_struct(name: str) -> str:
201 """!
202 @brief Categorize struct.
203 @param[in] name Argument passed to `categorize_struct()`.
204 @return Value returned by `categorize_struct()`.
205 """
206 if name.startswith("BC") or "Boundary" in name or name == "FlowWave":
207 return "Boundary Condition System"
208 if name.startswith("IBM") or name in {"FSInfo", "SurfElmtInfo", "Cstart"}:
209 return "Immersed Boundary and FSI"
210 if name.startswith("Particle") or name in {"MigrationInfo"}:
211 return "Particle Transport and Statistics"
212 if name in {"SimCtx", "UserCtx", "UserMG", "MGCtx", "ScalingCtx", "DualMonitorCtx", "ProfiledFunction"}:
213 return "Runtime Control and Solver Orchestration"
214 if name.startswith("VTK") or name == "PostProcessParams":
215 return "I/O and Postprocessing"
216 if name in {"BoundingBox", "Cell", "Cmpnts", "Cmpnts2", "Cpt2D", "RankCellInfo", "RankNeighbors"}:
217 return "Grid and Geometry"
218 return "Generic Containers and Utilities"
219
220
221def render_link(label: str, href: str) -> str:
222 """!
223 @brief Render link.
224 @param[in] label Argument passed to `render_link()`.
225 @param[in] href Argument passed to `render_link()`.
226 @return Value returned by `render_link()`.
227 """
228 label_esc = html.escape(label)
229 href_esc = html.escape(href)
230 if href.startswith("http"):
231 return f"<a class='el' href='{href_esc}' target='_blank' rel='noopener'>{label_esc}</a>"
232 return f"<a class='el' href='{href_esc}'>{label_esc}</a>"
233
234
235def render_rows(rows: list[tuple[str, str, str]], empty_msg: str) -> str:
236 """!
237 @brief Render rows.
238 @param[in] rows Argument passed to `render_rows()`.
239 @param[in] empty_msg Argument passed to `render_rows()`.
240 @return Value returned by `render_rows()`.
241 """
242 if not rows:
243 return f"<tr><td colspan='2'>{html.escape(empty_msg)}</td></tr>"
244 out: list[str] = []
245 for name, rel, href in rows:
246 out.append(
247 "<tr>"
248 f"<td class='indexkey'>{render_link(name, href)}</td>"
249 f"<td class='indexvalue'><code>{html.escape(rel)}</code></td>"
250 "</tr>"
251 )
252 return "\n".join(out)
253
254
255def section_table(title: str, rows_html: str) -> str:
256 """!
257 @brief Perform section table.
258 @param[in] title Argument passed to `section_table()`.
259 @param[in] rows_html Argument passed to `section_table()`.
260 @return Value returned by `section_table()`.
261 """
262 return (
263 f"<h2>{html.escape(title)}</h2>\n"
264 "<table class='doxtable'>\n"
265 "<thead><tr><th>Name</th><th>Location</th></tr></thead>\n"
266 f"<tbody>\n{rows_html}\n</tbody>\n"
267 "</table>\n"
268 )
269
270
271def render_page(title: str, intro: str, body_html: str) -> str:
272 """!
273 @brief Render page.
274 @param[in] title Argument passed to `render_page()`.
275 @param[in] intro Argument passed to `render_page()`.
276 @param[in] body_html Argument passed to `render_page()`.
277 @return Value returned by `render_page()`.
278 """
279 return f"""<!DOCTYPE html>
280<html lang="en">
281<head>
282 <meta charset="utf-8" />
283 <meta name="viewport" content="width=device-width, initial-scale=1" />
284 <title>PICurv: {html.escape(title)}</title>
285 <link href="doxygen.css" rel="stylesheet" />
286 <link href="custom.css" rel="stylesheet" />
287 <script type="text/javascript" src="theme-sync.js"></script>
288</head>
289<body>
290 <div class="header">
291 <div class="headertitle"><div class="title">{html.escape(title)}</div></div>
292 </div>
293 <div class="contents">
294 <p>{html.escape(intro)}</p>
295{body_html}
296 <p>See <a href="Documentation_Map.html">Documentation Map</a> for structural navigation.</p>
297 </div>
298</body>
299</html>
300"""
301
302
303def write_structured_file_index(repo_root: Path, html_dir: Path) -> None:
304 """!
305 @brief Write structured file index.
306 @param[in] repo_root Argument passed to `write_structured_file_index()`.
307 @param[in] html_dir Argument passed to `write_structured_file_index()`.
308 """
309 headers = collect_file_rows(repo_root, html_dir, "include", HEADER_SUFFIXES)
310 sources = collect_file_rows(repo_root, html_dir, "src", SOURCE_SUFFIXES)
311 scripts = collect_file_rows(repo_root, html_dir, "scripts", SCRIPT_SUFFIXES)
312 body = (
313 section_table("Header Files", render_rows(headers, "No header files found."))
314 + section_table("Source Files", render_rows(sources, "No source files found."))
315 + section_table("Scripts", render_rows(scripts, "No script files found."))
316 )
317 out = html_dir / "files_structured.html"
318 out.write_text(
320 "File List (Structured)",
321 "Organized by file role: headers, source files, and scripts.",
322 body,
323 ),
324 encoding="utf-8",
325 )
326 print(f"[index] wrote {out}")
327
328
329def write_structured_struct_index(repo_root: Path, html_dir: Path) -> None:
330 """!
331 @brief Write structured struct index.
332 @param[in] repo_root Argument passed to `write_structured_struct_index()`.
333 @param[in] html_dir Argument passed to `write_structured_struct_index()`.
334 """
335 rows = collect_struct_rows(repo_root, html_dir)
336 grouped: dict[str, list[tuple[str, str, str]]] = {}
337 for row in rows:
338 grouped.setdefault(categorize_struct(row[0]), []).append(row)
339
340 ordered_sections = [
341 "Runtime Control and Solver Orchestration",
342 "Grid and Geometry",
343 "Boundary Condition System",
344 "Particle Transport and Statistics",
345 "Immersed Boundary and FSI",
346 "I/O and Postprocessing",
347 "Generic Containers and Utilities",
348 ]
349 body_parts: list[str] = []
350 for section in ordered_sections:
351 body_parts.append(
353 section,
354 render_rows(grouped.get(section, []), f"No structures found for section: {section}."),
355 )
356 )
357 out = html_dir / "annotated_structured.html"
358 out.write_text(
360 "Data Structures (By Module)",
361 "Grouped by major solver modules and responsibilities.",
362 "\n".join(body_parts),
363 ),
364 encoding="utf-8",
365 )
366 print(f"[index] wrote {out}")
367
368
369def write_fallback_files_page(repo_root: Path, html_dir: Path) -> None:
370 """!
371 @brief Write fallback files page.
372 @param[in] repo_root Argument passed to `write_fallback_files_page()`.
373 @param[in] html_dir Argument passed to `write_fallback_files_page()`.
374 """
375 rows = collect_all_source_like_files(repo_root, html_dir)
376 body = section_table("Files", render_rows(rows, "No source-like files found."))
377 out = html_dir / "files.html"
378 out.write_text(
380 "File List",
381 "Fallback file list generated from include/src/scripts.",
382 body,
383 ),
384 encoding="utf-8",
385 )
386 print(f"[fallback] wrote {out}")
387
388
389def write_fallback_struct_page(repo_root: Path, html_dir: Path) -> None:
390 """!
391 @brief Write fallback struct page.
392 @param[in] repo_root Argument passed to `write_fallback_struct_page()`.
393 @param[in] html_dir Argument passed to `write_fallback_struct_page()`.
394 """
395 rows = collect_struct_rows(repo_root, html_dir)
396 body = section_table("Data Structures", render_rows(rows, "No C struct declarations found."))
397 out = html_dir / "annotated.html"
398 out.write_text(
400 "Data Structures",
401 "Fallback structure list generated from C headers.",
402 body,
403 ),
404 encoding="utf-8",
405 )
406 print(f"[fallback] wrote {out}")
407
408
409def main() -> int:
410 """!
411 @brief Entry point for this script.
412 @return Value returned by `main()`.
413 """
414 parser = argparse.ArgumentParser(
415 description=(
416 "Generate structured Doxygen index pages and fallback replacements when\n"
417 "files.html or annotated.html are missing/empty after doc generation."
418 ),
419 formatter_class=argparse.RawTextHelpFormatter,
420 epilog=(
421 "Examples:\n"
422 " python3 scripts/generate_doxygen_fallback_indexes.py \\\n"
423 " --repo-root . --html-dir docs_build/html\n"
424 " python3 scripts/generate_doxygen_fallback_indexes.py \\\n"
425 " --repo-root /path/to/repo --html-dir /path/to/repo/docs_build/html"
426 ),
427 )
428 parser.add_argument(
429 "--repo-root",
430 required=True,
431 type=Path,
432 help="Repository root used to scan include/src/scripts and headers.",
433 )
434 parser.add_argument(
435 "--html-dir",
436 required=True,
437 type=Path,
438 help="Doxygen HTML output directory (where files.html/annotated.html live).",
439 )
440 args = parser.parse_args()
441
442 repo_root = args.repo_root.resolve()
443 html_dir = args.html_dir.resolve()
444
445 write_structured_file_index(repo_root, html_dir)
446 write_structured_struct_index(repo_root, html_dir)
447
448 files_page = html_dir / "files.html"
449 structs_page = html_dir / "annotated.html"
450 if needs_files_fallback(files_page):
451 write_fallback_files_page(repo_root, html_dir)
452 if needs_structs_fallback(structs_page):
453 write_fallback_struct_page(repo_root, html_dir)
454
455 return 0
456
457
458if __name__ == "__main__":
459 raise SystemExit(main())
list[tuple[str, str, str]] collect_all_source_like_files(Path repo_root, Path html_dir)
Collect all source like files.
str doxygen_file_page(str name)
Perform doxygen file page.
str render_link(str label, str href)
Render link.
bool needs_files_fallback(Path path)
Perform needs files fallback.
bool needs_structs_fallback(Path path)
Perform needs structs fallback.
None write_structured_file_index(Path repo_root, Path html_dir)
Write structured file index.
str doxygen_file_page_with_path(str rel_path)
Perform doxygen file page with path.
None write_structured_struct_index(Path repo_root, Path html_dir)
Write structured struct index.
str render_rows(list[tuple[str, str, str]] rows, str empty_msg)
Render rows.
None write_fallback_struct_page(Path repo_root, Path html_dir)
Write fallback struct page.
str render_page(str title, str intro, str body_html)
Render page.
str resolve_doxygen_file_href(Path html_dir, str rel_path)
Resolve doxygen file href.
str make_repo_href(str rel_path)
Perform make repo href.
list[tuple[str, str, str]] collect_struct_rows(Path repo_root, Path html_dir)
Collect struct rows.
set[str] extract_struct_names(str text)
Extract struct names.
list[tuple[str, str, str]] collect_file_rows(Path repo_root, Path html_dir, str base_dir, set[str] suffixes)
Collect file rows.
str section_table(str title, str rows_html)
Perform section table.
None write_fallback_files_page(Path repo_root, Path html_dir)
Write fallback files page.