11import xml .etree .ElementTree as ET
22import argparse
33import re
4+ import json
45from pathlib import Path
56from dataclasses import dataclass
67
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-
258@dataclass
269class TParamDescriptor :
2710 name : str
@@ -36,9 +19,8 @@ class ConceptDescriptor:
3619 params : list [TParamDescriptor ]
3720 definition : str
3821
39-
4022class ConceptParser :
41- def __init__ (self , xml_dir : Path , out_dir : Path , groups : dict = GROUPS ):
23+ def __init__ (self , xml_dir : Path , out_dir : Path , groups : dict ):
4224 self .xml_dir = xml_dir
4325 self .out_dir = out_dir
4426 self .groups = groups
@@ -85,23 +67,46 @@ def _xml_to_md(self, elem: ET.Element) -> str:
8567 res += f"`{ ref_text } `"
8668
8769 elif child .tag in ["computeroutput" , "preformatted" ]:
88- res += f"`{ self ._xml_to_md (child )} `"
70+ inner = self ._xml_to_md (child )
71+
72+ # SMART LINK MERGE:
73+ # Prevents wrapping a Markdown link in backticks, which breaks the link.
74+ # Instead, it places backticks INSIDE the brackets.
75+ match = re .fullmatch (r'\[(`?)(.*?)\1\]\((.*?)\)' , inner .strip ())
76+ if match :
77+ res += f"[`{ match .group (2 )} `]({ match .group (3 )} )"
78+ elif "](" in inner :
79+ # Failsafe for complex strings containing links
80+ res += inner
81+ else :
82+ res += f"`{ inner } `"
83+
8984 elif child .tag == "bold" :
9085 res += f"**{ self ._xml_to_md (child )} **"
86+
9187 elif child .tag == "emphasis" :
9288 res += f"*{ self ._xml_to_md (child )} *"
89+
9390 elif child .tag == "itemizedlist" :
9491 res += "\n \n "
9592 for item in child .findall ("listitem" ):
9693 res += f"- { self ._xml_to_md (item ).strip ()} \n "
9794 res += "\n \n "
95+
96+ elif child .tag == "orderedlist" :
97+ res += "\n \n "
98+ for i , item in enumerate (child .findall ("listitem" ), start = 1 ):
99+ res += f"{ i } . { self ._xml_to_md (item ).strip ()} \n "
100+ res += "\n \n "
101+
98102 elif child .tag == "blockquote" :
99103 bq_text = self ._xml_to_md (child ).strip ()
100104 res += (
101105 "\n \n "
102106 + "\n " .join (f"> { line } " for line in bq_text .splitlines ())
103107 + "\n \n "
104108 )
109+
105110 elif child .tag == "simplesect" :
106111 kind = child .get ("kind" , "note" ).upper ()
107112 sect_text = self ._xml_to_md (child ).strip ()
@@ -110,10 +115,13 @@ def _xml_to_md(self, elem: ET.Element) -> str:
110115 + "\n " .join (f"> { line } " for line in sect_text .splitlines ())
111116 + "\n \n "
112117 )
118+
113119 elif child .tag == "para" :
114120 res += self ._xml_to_md (child ).strip () + "\n \n "
121+
115122 elif child .tag == "title" :
116123 res += f"### { self ._xml_to_md (child ).strip ()} \n \n "
124+
117125 else :
118126 res += self ._xml_to_md (child )
119127
@@ -136,14 +144,10 @@ def _parse_concept_xml(self, xml_path: Path) -> ConceptDescriptor | None:
136144
137145 detailed_desc = root .find ("detaileddescription" )
138146 if detailed_desc is not None :
139- for param_list in detailed_desc .findall (
140- './/parameterlist[@kind="templateparam"]'
141- ):
147+ for param_list in detailed_desc .findall ('.//parameterlist[@kind="templateparam"]' ):
142148 for item in param_list .findall ("parameteritem" ):
143149 p_name = self ._xml_to_md (item .find (".//parametername" )).strip ()
144- p_desc = self ._xml_to_md (
145- item .find (".//parameterdescription" )
146- ).strip ()
150+ p_desc = self ._xml_to_md (item .find (".//parameterdescription" )).strip ()
147151 params .append (TParamDescriptor (name = p_name , desc = p_desc ))
148152 param_list .clear ()
149153
@@ -161,9 +165,7 @@ def _parse_concept_xml(self, xml_path: Path) -> ConceptDescriptor | None:
161165 tpl_nodes = root .findall (".//templateparamlist/param" )
162166 tpl_strings = [self ._get_text (p ).strip () for p in tpl_nodes ]
163167 template_decl += ", " .join (tpl_strings ) + ">\n "
164- definition = (
165- f"{ template_decl } concept { name .split ('::' )[- 1 ]} = { constraint } ;"
166- )
168+ definition = f"{ template_decl } concept { name .split ('::' )[- 1 ]} = { constraint } ;"
167169
168170 return ConceptDescriptor (
169171 name = name ,
@@ -178,9 +180,7 @@ def process(self):
178180 """Main execution flow: builds registry, parses data, and generates markdown."""
179181 index_xml = self .xml_dir / "index.xml"
180182 if not index_xml .exists ():
181- print (
182- f"Error: Could not find Doxygen index at { index_xml } . Run Doxygen first."
183- )
183+ print (f"Error: Could not find Doxygen index at { index_xml } . Run Doxygen first." )
184184 return
185185
186186 tree = ET .parse (index_xml )
@@ -266,16 +266,20 @@ def _generate_index_file(self):
266266 index_path .write_text (md , encoding = "utf-8" )
267267 print (f"Generated { index_path } (API Index)" )
268268
269-
270269if __name__ == "__main__" :
271270 parser = argparse .ArgumentParser ()
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- )
271+ parser .add_argument ("--xml" , default = "xml" , type = Path , help = "Path to Doxygen XML output" )
272+ parser .add_argument ("--out" , default = "docs/cpp-gl" , type = Path , help = "Path to MkDocs output folder" )
273+ parser .add_argument ("--config" , default = "groups.json" , type = Path , help = "Path to the groups JSON configuration file" )
278274 args = parser .parse_args ()
279275
280- app = ConceptParser (args .xml , args .out )
276+ # Load configuration from JSON
277+ if not args .config .exists ():
278+ print (f"Error: Configuration file '{ args .config } ' not found." )
279+ exit (1 )
280+
281+ with open (args .config , "r" , encoding = "utf-8" ) as f :
282+ groups_config = json .load (f )
283+
284+ app = ConceptParser (args .xml , args .out , groups = groups_config )
281285 app .process ()
0 commit comments