22import argparse
33import re
44from 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
640class 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+
237270if __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