Skip to content

Commit 2c19c87

Browse files
committed
further refinement
1 parent 638a9a0 commit 2c19c87

3 files changed

Lines changed: 160 additions & 95 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ dependencies = [
1111
"mkdocs-include-markdown-plugin>=7.2.2",
1212
"mkdocs-material>=9.7.6",
1313
"mkdoxy>=1.2.8",
14+
"ruff>=0.15.11",
1415
]

scripts/gen_concept_docs.py

Lines changed: 132 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,48 @@
22
import argparse
33
import re
44
from pathlib import Path
5+
from dataclasses import dataclass
6+
7+
GROUPS = {
8+
"GL": {
9+
"prefix": "gl::",
10+
"filename": "gl_traits.md",
11+
"anchor": "gl-traits-concepts-documentation",
12+
"title": "GL Traits & Concepts",
13+
"description": "This page documents the C++20 concepts and type traits used to constrain templates across the GL library.",
14+
},
15+
"HGL": {
16+
"prefix": "hgl::",
17+
"filename": "hgl_traits.md",
18+
"anchor": "hgl-traits-concepts-documentation",
19+
"title": "HGL Traits & Concepts",
20+
"description": "This page documents the C++20 concepts and type traits used to constrain templates across the HGL library.",
21+
},
22+
}
23+
24+
25+
@dataclass
26+
class TParamDescriptor:
27+
name: str
28+
desc: str
29+
30+
@dataclass
31+
class ConceptDescriptor:
32+
name: str
33+
anchor: str
34+
brief: str
35+
details: str
36+
params: list[TParamDescriptor]
37+
definition: str
38+
539

640
class ConceptParser:
7-
# Configuration for module groups
8-
GROUPS = {
9-
"GL": {
10-
"prefix": "gl::",
11-
"filename": "gl_traits.md",
12-
"anchor": "gl-traits-concepts-documentation",
13-
"title": "GL Traits & Concepts",
14-
"description": "This page documents the C++20 concepts and type traits used to constrain templates across the GL library."
15-
},
16-
"HGL": {
17-
"prefix": "hgl::",
18-
"filename": "hgl_traits.md",
19-
"anchor": "hgl-traits-concepts-documentation",
20-
"title": "HGL Traits & Concepts",
21-
"description": "This page documents the C++20 concepts and type traits used to constrain templates across the HGL library."
22-
}
23-
}
24-
25-
# Hidden from the rendered website, but visible to developers opening the raw .md file
26-
DEV_WARNING = "\n"
27-
28-
def __init__(self, xml_dir: Path, out_dir: Path):
41+
def __init__(self, xml_dir: Path, out_dir: Path, groups: dict = GROUPS):
2942
self.xml_dir = xml_dir
3043
self.out_dir = out_dir
31-
self.concept_links = {} # Registry mapping refid -> file#anchor
32-
self.categorized_concepts = {key: [] for key in self.GROUPS.keys()}
44+
self.groups = groups
45+
self.concept_links = {} # Registry mapping refid -> file#anchor
46+
self.categorized_concepts = {key: [] for key in self.groups.keys()}
3347

3448
@staticmethod
3549
def _get_text(element: ET.Element) -> str:
@@ -44,49 +58,61 @@ def _xml_to_md(self, elem: ET.Element) -> str:
4458
return ""
4559

4660
# Ignore template parameter lists (they are handled separately for the table)
47-
if elem.tag == 'parameterlist':
61+
if elem.tag == "parameterlist":
4862
return ""
4963

5064
res = elem.text or ""
5165
for child in elem:
52-
if child.tag == 'ref':
66+
if child.tag == "ref":
5367
ref_text = "".join(child.itertext())
54-
refid = child.get('refid', '')
68+
refid = child.get("refid", "")
5569

56-
# 1. Known Concepts: Link using the class registry
70+
# Known concept with a registered link
5771
if refid in self.concept_links:
5872
res += f"[`{ref_text}`]({self.concept_links[refid]})"
59-
# 2. External/Unknown Concepts
60-
elif refid.startswith('concept'):
73+
# External/Unknown Concepts
74+
elif refid.startswith("concept"):
6175
ref_anchor = ref_text.replace("::", "-").replace("_", "-")
6276
res += f"[`{ref_text}`](#{ref_anchor})"
63-
# 3. Classes, Structs, Namespaces
64-
elif refid.startswith('class') or refid.startswith('struct') or refid.startswith('namespace'):
77+
# Classes, Structs, Namespaces
78+
elif (
79+
refid.startswith("class")
80+
or refid.startswith("struct")
81+
or refid.startswith("namespace")
82+
):
6583
res += f"[`{ref_text}`]({refid}.md)"
6684
else:
6785
res += f"`{ref_text}`"
6886

69-
elif child.tag in ['computeroutput', 'preformatted']:
87+
elif child.tag in ["computeroutput", "preformatted"]:
7088
res += f"`{self._xml_to_md(child)}`"
71-
elif child.tag == 'bold':
89+
elif child.tag == "bold":
7290
res += f"**{self._xml_to_md(child)}**"
73-
elif child.tag == 'emphasis':
91+
elif child.tag == "emphasis":
7492
res += f"*{self._xml_to_md(child)}*"
75-
elif child.tag == 'itemizedlist':
93+
elif child.tag == "itemizedlist":
7694
res += "\n\n"
77-
for item in child.findall('listitem'):
95+
for item in child.findall("listitem"):
7896
res += f"- {self._xml_to_md(item).strip()}\n"
7997
res += "\n\n"
80-
elif child.tag == 'blockquote':
98+
elif child.tag == "blockquote":
8199
bq_text = self._xml_to_md(child).strip()
82-
res += "\n\n" + "\n".join(f"> {line}" for line in bq_text.splitlines()) + "\n\n"
83-
elif child.tag == 'simplesect':
84-
kind = child.get('kind', 'note').upper()
100+
res += (
101+
"\n\n"
102+
+ "\n".join(f"> {line}" for line in bq_text.splitlines())
103+
+ "\n\n"
104+
)
105+
elif child.tag == "simplesect":
106+
kind = child.get("kind", "note").upper()
85107
sect_text = self._xml_to_md(child).strip()
86-
res += f"\n\n> [!{kind}]\n" + "\n".join(f"> {line}" for line in sect_text.splitlines()) + "\n\n"
87-
elif child.tag == 'para':
108+
res += (
109+
f"\n\n> [!{kind}]\n"
110+
+ "\n".join(f"> {line}" for line in sect_text.splitlines())
111+
+ "\n\n"
112+
)
113+
elif child.tag == "para":
88114
res += self._xml_to_md(child).strip() + "\n\n"
89-
elif child.tag == 'title':
115+
elif child.tag == "title":
90116
res += f"### {self._xml_to_md(child).strip()}\n\n"
91117
else:
92118
res += self._xml_to_md(child)
@@ -95,72 +121,80 @@ def _xml_to_md(self, elem: ET.Element) -> str:
95121

96122
return res
97123

98-
def _parse_concept_xml(self, xml_path: Path) -> dict | None:
124+
def _parse_concept_xml(self, xml_path: Path) -> ConceptDescriptor | None:
99125
"""Parses a single Doxygen concept XML file."""
100126
tree = ET.parse(xml_path)
101-
root = tree.find('compounddef')
127+
root = tree.find("compounddef")
102128

103-
if root is None or root.get('kind') != 'concept':
129+
if root is None or root.get("kind") != "concept":
104130
return None
105131

106-
name = root.findtext('compoundname')
132+
name = root.findtext("compoundname")
107133
anchor = name.replace("::", "-").replace("_", "-")
108-
brief = self._xml_to_md(root.find('briefdescription')).strip()
134+
brief = self._xml_to_md(root.find("briefdescription")).strip()
109135
params = []
110136

111-
detailed_desc = root.find('detaileddescription')
137+
detailed_desc = root.find("detaileddescription")
112138
if detailed_desc is not None:
113-
for param_list in detailed_desc.findall('.//parameterlist[@kind="templateparam"]'):
114-
for item in param_list.findall('parameteritem'):
115-
p_name = self._xml_to_md(item.find('.//parametername')).strip()
116-
p_desc = self._xml_to_md(item.find('.//parameterdescription')).strip()
117-
params.append({"name": p_name, "desc": p_desc})
139+
for param_list in detailed_desc.findall(
140+
'.//parameterlist[@kind="templateparam"]'
141+
):
142+
for item in param_list.findall("parameteritem"):
143+
p_name = self._xml_to_md(item.find(".//parametername")).strip()
144+
p_desc = self._xml_to_md(
145+
item.find(".//parameterdescription")
146+
).strip()
147+
params.append(TParamDescriptor(name=p_name, desc=p_desc))
118148
param_list.clear()
119149

120150
details = self._xml_to_md(detailed_desc).strip()
121151

122-
constraint = self._get_text(root.find('initializer')).strip()
123-
constraint = re.sub(r'=\s+', '= ', constraint)
152+
constraint = self._get_text(root.find("initializer")).strip()
153+
constraint = re.sub(r"=\s+", "= ", constraint)
124154

125155
if constraint.startswith("template"):
126156
definition = constraint
127157
if not definition.endswith(";"):
128158
definition += ";"
129159
else:
130160
template_decl = "template <"
131-
tpl_nodes = root.findall('.//templateparamlist/param')
161+
tpl_nodes = root.findall(".//templateparamlist/param")
132162
tpl_strings = [self._get_text(p).strip() for p in tpl_nodes]
133163
template_decl += ", ".join(tpl_strings) + ">\n"
134-
definition = f"{template_decl}concept {name.split('::')[-1]} = {constraint};"
135-
136-
return {
137-
"name": name,
138-
"anchor": anchor,
139-
"brief": brief,
140-
"details": details,
141-
"params": params,
142-
"definition": definition
143-
}
164+
definition = (
165+
f"{template_decl}concept {name.split('::')[-1]} = {constraint};"
166+
)
167+
168+
return ConceptDescriptor(
169+
name=name,
170+
anchor=anchor,
171+
brief=brief,
172+
details=details,
173+
params=params,
174+
definition=definition,
175+
)
144176

145177
def process(self):
146178
"""Main execution flow: builds registry, parses data, and generates markdown."""
147179
index_xml = self.xml_dir / "index.xml"
148180
if not index_xml.exists():
149-
print(f"Error: Could not find Doxygen index at {index_xml}. Run Doxygen first.")
181+
print(
182+
f"Error: Could not find Doxygen index at {index_xml}. Run Doxygen first."
183+
)
150184
return
151185

152186
tree = ET.parse(index_xml)
153187

154-
# PASS 1: Build the global concept dictionary for cross-linking
188+
# PASS 1: Build the concept registry for cross-linking
155189
for compound in tree.findall("compound[@kind='concept']"):
156-
name = compound.findtext('name')
190+
name = compound.findtext("name")
157191
if not name:
158192
continue
159193

160194
refid = compound.get("refid")
161195
anchor = name.replace("::", "-").replace("_", "-")
162196

163-
for group_key, group_info in self.GROUPS.items():
197+
for group_key, group_info in self.groups.items():
164198
if name.startswith(group_info["prefix"]):
165199
self.concept_links[refid] = f"{group_info['filename']}#{anchor}"
166200
break
@@ -171,8 +205,8 @@ def process(self):
171205
if xml_path.exists():
172206
data = self._parse_concept_xml(xml_path)
173207
if data:
174-
for group_key, group_info in self.GROUPS.items():
175-
if data["name"].startswith(group_info["prefix"]):
208+
for group_key, group_info in self.groups.items():
209+
if data.name.startswith(group_info["prefix"]):
176210
self.categorized_concepts[group_key].append(data)
177211
break
178212

@@ -183,41 +217,39 @@ def process(self):
183217

184218
def _generate_group_files(self):
185219
"""Generates the specific group documentation files (e.g., gl_traits.md)."""
186-
for group_key, group_info in self.GROUPS.items():
187-
self.categorized_concepts[group_key].sort(key=lambda x: x['name'])
220+
for group_key, group_info in self.groups.items():
221+
self.categorized_concepts[group_key].sort(key=lambda x: x.name)
188222
concepts = self.categorized_concepts[group_key]
189223

190-
md = f"{self.DEV_WARNING}\n"
191-
md += f"# {group_info['title']} {{: #{group_info['anchor']} }}\n\n"
224+
md = f"# {group_info['title']} {{: #{group_info['anchor']} }}\n\n"
192225
md += f"{group_info['description']}\n\n---\n\n"
193226

194227
if not concepts:
195228
md += "*No concepts are currently documented for this module.*\n"
196229
else:
197230
for c in concepts:
198-
md += f"## `{c['name']}` {{: #{c['anchor']} }}\n\n"
199-
if c['brief']:
200-
md += f"{c['brief']}\n\n"
201-
if c['details']:
202-
md += f"### Detailed Description\n\n{c['details']}\n\n"
203-
if c['params']:
231+
md += f"## `{c.name}` {{: #{c.anchor} }}\n\n"
232+
if c.brief:
233+
md += f"{c.brief}\n\n"
234+
if c.details:
235+
md += f"### Detailed Description\n\n{c.details}\n\n"
236+
if c.params:
204237
md += "### Template Parameters\n\n| Parameter | Description |\n| :--- | :--- |\n"
205-
for p in c['params']:
206-
md += f"| `{p['name']}` | {p['desc']} |\n"
238+
for p in c.params:
239+
md += f"| `{p.name}` | {p.desc} |\n"
207240
md += "\n"
208-
md += f"### Definition\n\n```cpp\n{c['definition']}\n```\n\n---\n\n"
241+
md += f"### Definition\n\n```cpp\n{c.definition}\n```\n\n---\n\n"
209242

210-
out_path = self.out_dir / group_info['filename']
243+
out_path = self.out_dir / group_info["filename"]
211244
out_path.write_text(md, encoding="utf-8")
212245
print(f"Generated {out_path} ({len(concepts)} concepts)")
213246

214247
def _generate_index_file(self):
215248
"""Generates the central API index mapping to all grouped concepts."""
216-
md = f"{self.DEV_WARNING}\n"
217-
md += "# Concepts API Reference {: #concepts-api-reference }\n\n"
249+
md = "# Concepts API Reference {: #concepts-api-reference }\n\n"
218250
md += "This page serves as the central index for all C++20 concepts used across the library to enforce type safety and template constraints.\n\n---\n\n"
219251

220-
for group_key, group_info in self.GROUPS.items():
252+
for group_key, group_info in self.groups.items():
221253
md += f"## {group_key} Concepts\n\n"
222254
md += f"- **[{group_info['title']}]({group_info['filename']})**: Full API reference.\n"
223255

@@ -226,18 +258,23 @@ def _generate_index_file(self):
226258
md += f" - *(Documentation coming soon)*\n"
227259
else:
228260
for c in concepts:
229-
desc_text = f": {c['brief']}" if c['brief'] else ""
230-
md += f" - [`{c['name']}`]({group_info['filename']}#{c['anchor']}){desc_text}\n"
261+
desc_text = f": {c.brief}" if c.brief else ""
262+
md += f" - [`{c.name}`]({group_info['filename']}#{c.anchor}){desc_text}\n"
231263
md += "\n---\n\n"
232264

233265
index_path = self.out_dir / "concepts.md"
234266
index_path.write_text(md, encoding="utf-8")
235267
print(f"Generated {index_path} (API Index)")
236268

269+
237270
if __name__ == "__main__":
238271
parser = argparse.ArgumentParser()
239-
parser.add_argument("--xml", default="xml", type=Path, help="Path to Doxygen XML output")
240-
parser.add_argument("--out", default="docs/cpp-gl", type=Path, help="Path to MkDocs output folder")
272+
parser.add_argument(
273+
"--xml", default="xml", type=Path, help="Path to Doxygen XML output"
274+
)
275+
parser.add_argument(
276+
"--out", default="docs/cpp-gl", type=Path, help="Path to MkDocs output folder"
277+
)
241278
args = parser.parse_args()
242279

243280
app = ConceptParser(args.xml, args.out)

0 commit comments

Comments
 (0)