1# A basic mkdocstrings handler for {fmt}. 2# Copyright (c) 2012 - present, Victor Zverovich 3# https://github.com/fmtlib/fmt/blob/master/LICENSE 4 5import os 6import xml.etree.ElementTree as ElementTree 7from pathlib import Path 8from subprocess import PIPE, STDOUT, CalledProcessError, Popen 9from typing import Any, List, Mapping, Optional 10 11from mkdocstrings.handlers.base import BaseHandler 12 13 14class Definition: 15 """A definition extracted by Doxygen.""" 16 17 def __init__(self, name: str, kind: Optional[str] = None, 18 node: Optional[ElementTree.Element] = None, 19 is_member: bool = False): 20 self.name = name 21 self.kind = kind if kind is not None else node.get('kind') 22 self.desc = None 23 self.id = name if not is_member else None 24 self.members = None 25 self.params = None 26 self.template_params = None 27 self.trailing_return_type = None 28 self.type = None 29 30 31# A map from Doxygen to HTML tags. 32tag_map = { 33 'bold': 'b', 34 'emphasis': 'em', 35 'computeroutput': 'code', 36 'para': 'p', 37 'programlisting': 'pre', 38 'verbatim': 'pre' 39} 40 41# A map from Doxygen tags to text. 42tag_text_map = { 43 'codeline': '', 44 'highlight': '', 45 'sp': ' ' 46} 47 48 49def escape_html(s: str) -> str: 50 return s.replace("<", "<") 51 52 53def doxyxml2html(nodes: List[ElementTree.Element]): 54 out = '' 55 for n in nodes: 56 tag = tag_map.get(n.tag) 57 if not tag: 58 out += tag_text_map[n.tag] 59 out += '<' + tag + '>' if tag else '' 60 out += '<code class="language-cpp">' if tag == 'pre' else '' 61 if n.text: 62 out += escape_html(n.text) 63 out += doxyxml2html(list(n)) 64 out += '</code>' if tag == 'pre' else '' 65 out += '</' + tag + '>' if tag else '' 66 if n.tail: 67 out += n.tail 68 return out 69 70 71def convert_template_params(node: ElementTree.Element) -> Optional[List[Definition]]: 72 template_param_list = node.find('templateparamlist') 73 if template_param_list is None: 74 return None 75 params = [] 76 for param_node in template_param_list.findall('param'): 77 name = param_node.find('declname') 78 param = Definition(name.text if name is not None else '', 'param') 79 param.type = param_node.find('type').text 80 params.append(param) 81 return params 82 83 84def get_description(node: ElementTree.Element) -> List[ElementTree.Element]: 85 return node.findall('briefdescription/para') + \ 86 node.findall('detaileddescription/para') 87 88 89def normalize_type(type_: str) -> str: 90 type_ = type_.replace('< ', '<').replace(' >', '>') 91 return type_.replace(' &', '&').replace(' *', '*') 92 93 94def convert_type(type_: ElementTree.Element) -> Optional[str]: 95 if type_ is None: 96 return None 97 result = type_.text if type_.text else '' 98 for ref in type_: 99 result += ref.text 100 if ref.tail: 101 result += ref.tail 102 result += type_.tail.strip() 103 return normalize_type(result) 104 105 106def convert_params(func: ElementTree.Element) -> List[Definition]: 107 params = [] 108 for p in func.findall('param'): 109 d = Definition(p.find('declname').text, 'param') 110 d.type = convert_type(p.find('type')) 111 params.append(d) 112 return params 113 114 115def convert_return_type(d: Definition, node: ElementTree.Element) -> None: 116 d.trailing_return_type = None 117 if d.type == 'auto' or d.type == 'constexpr auto': 118 parts = node.find('argsstring').text.split(' -> ') 119 if len(parts) > 1: 120 d.trailing_return_type = normalize_type(parts[1]) 121 122 123def render_param(param: Definition) -> str: 124 return param.type + (f' {param.name}' if len(param.name) > 0 else '') 125 126 127def render_decl(d: Definition) -> str: 128 text = '' 129 if d.id is not None: 130 text += f'<a id="{d.id}">\n' 131 text += '<pre><code class="language-cpp decl">' 132 133 text += '<div>' 134 if d.template_params is not None: 135 text += 'template <' 136 text += ', '.join([render_param(p) for p in d.template_params]) 137 text += '>\n' 138 text += '</div>' 139 140 text += '<div>' 141 end = ';' 142 if d.kind == 'function' or d.kind == 'variable': 143 text += d.type + ' ' if len(d.type) > 0 else '' 144 elif d.kind == 'typedef': 145 text += 'using ' 146 elif d.kind == 'define': 147 end = '' 148 else: 149 text += d.kind + ' ' 150 text += d.name 151 152 if d.params is not None: 153 params = ', '.join([ 154 (p.type + ' ' if p.type else '') + p.name for p in d.params]) 155 text += '(' + escape_html(params) + ')' 156 if d.trailing_return_type: 157 text += ' -⁠> ' + escape_html(d.trailing_return_type) 158 elif d.kind == 'typedef': 159 text += ' = ' + escape_html(d.type) 160 161 text += end 162 text += '</div>' 163 text += '</code></pre>\n' 164 if d.id is not None: 165 text += f'</a>\n' 166 return text 167 168 169class CxxHandler(BaseHandler): 170 def __init__(self, **kwargs: Any) -> None: 171 super().__init__(handler='cxx', **kwargs) 172 173 headers = [ 174 'args.h', 'base.h', 'chrono.h', 'color.h', 'compile.h', 'format.h', 175 'os.h', 'ostream.h', 'printf.h', 'ranges.h', 'std.h', 'xchar.h' 176 ] 177 178 # Run doxygen. 179 cmd = ['doxygen', '-'] 180 support_dir = Path(__file__).parents[3] 181 top_dir = os.path.dirname(support_dir) 182 include_dir = os.path.join(top_dir, 'include', 'fmt') 183 self._ns2doxyxml = {} 184 build_dir = os.path.join(top_dir, 'build') 185 os.makedirs(build_dir, exist_ok=True) 186 self._doxyxml_dir = os.path.join(build_dir, 'doxyxml') 187 p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT) 188 _, _ = p.communicate(input=r''' 189 PROJECT_NAME = fmt 190 GENERATE_XML = YES 191 GENERATE_LATEX = NO 192 GENERATE_HTML = NO 193 INPUT = {0} 194 XML_OUTPUT = {1} 195 QUIET = YES 196 AUTOLINK_SUPPORT = NO 197 MACRO_EXPANSION = YES 198 PREDEFINED = _WIN32=1 \ 199 __linux__=1 \ 200 FMT_ENABLE_IF(...)= \ 201 FMT_USE_USER_LITERALS=1 \ 202 FMT_USE_ALIAS_TEMPLATES=1 \ 203 FMT_USE_NONTYPE_TEMPLATE_ARGS=1 \ 204 FMT_API= \ 205 "FMT_BEGIN_NAMESPACE=namespace fmt {{" \ 206 "FMT_END_NAMESPACE=}}" \ 207 "FMT_DOC=1" 208 '''.format( 209 ' '.join([os.path.join(include_dir, h) for h in headers]), 210 self._doxyxml_dir).encode('utf-8')) 211 if p.returncode != 0: 212 raise CalledProcessError(p.returncode, cmd) 213 214 # Merge all file-level XMLs into one to simplify search. 215 self._file_doxyxml = None 216 for h in headers: 217 filename = h.replace(".h", "_8h.xml") 218 with open(os.path.join(self._doxyxml_dir, filename)) as f: 219 doxyxml = ElementTree.parse(f) 220 if self._file_doxyxml is None: 221 self._file_doxyxml = doxyxml 222 continue 223 root = self._file_doxyxml.getroot() 224 for node in doxyxml.getroot(): 225 root.append(node) 226 227 def collect_compound(self, identifier: str, 228 cls: List[ElementTree.Element]) -> Definition: 229 """Collect a compound definition such as a struct.""" 230 path = os.path.join(self._doxyxml_dir, cls[0].get('refid') + '.xml') 231 with open(path) as f: 232 xml = ElementTree.parse(f) 233 node = xml.find('compounddef') 234 d = Definition(identifier, node=node) 235 d.template_params = convert_template_params(node) 236 d.desc = get_description(node) 237 d.members = [] 238 for m in \ 239 node.findall('sectiondef[@kind="public-attrib"]/memberdef') + \ 240 node.findall('sectiondef[@kind="public-func"]/memberdef'): 241 name = m.find('name').text 242 # Doxygen incorrectly classifies members of private unnamed unions as 243 # public members of the containing class. 244 if name.endswith('_'): 245 continue 246 desc = get_description(m) 247 if len(desc) == 0: 248 continue 249 kind = m.get('kind') 250 member = Definition(name if name else '', kind=kind, is_member=True) 251 type_text = m.find('type').text 252 member.type = type_text if type_text else '' 253 if kind == 'function': 254 member.params = convert_params(m) 255 convert_return_type(member, m) 256 member.template_params = None 257 member.desc = desc 258 d.members.append(member) 259 return d 260 261 def collect(self, identifier: str, _config: Mapping[str, Any]) -> Definition: 262 qual_name = 'fmt::' + identifier 263 264 param_str = None 265 paren = qual_name.find('(') 266 if paren > 0: 267 qual_name, param_str = qual_name[:paren], qual_name[paren + 1:-1] 268 269 colons = qual_name.rfind('::') 270 namespace, name = qual_name[:colons], qual_name[colons + 2:] 271 272 # Load XML. 273 doxyxml = self._ns2doxyxml.get(namespace) 274 if doxyxml is None: 275 path = f'namespace{namespace.replace("::", "_1_1")}.xml' 276 with open(os.path.join(self._doxyxml_dir, path)) as f: 277 doxyxml = ElementTree.parse(f) 278 self._ns2doxyxml[namespace] = doxyxml 279 280 nodes = doxyxml.findall( 281 f"compounddef/sectiondef/memberdef/name[.='{name}']/..") 282 if len(nodes) == 0: 283 nodes = self._file_doxyxml.findall( 284 f"compounddef/sectiondef/memberdef/name[.='{name}']/..") 285 candidates = [] 286 for node in nodes: 287 # Process a function or a typedef. 288 params = None 289 d = Definition(name, node=node) 290 if d.kind == 'function': 291 params = convert_params(node) 292 node_param_str = ', '.join([p.type for p in params]) 293 if param_str and param_str != node_param_str: 294 candidates.append(f'{name}({node_param_str})') 295 continue 296 elif d.kind == 'define': 297 params = [] 298 for p in node.findall('param'): 299 param = Definition(p.find('defname').text, kind='param') 300 param.type = None 301 params.append(param) 302 d.type = convert_type(node.find('type')) 303 d.template_params = convert_template_params(node) 304 d.params = params 305 convert_return_type(d, node) 306 d.desc = get_description(node) 307 return d 308 309 cls = doxyxml.findall(f"compounddef/innerclass[.='{qual_name}']") 310 if not cls: 311 raise Exception(f'Cannot find {identifier}. Candidates: {candidates}') 312 return self.collect_compound(identifier, cls) 313 314 def render(self, d: Definition, config: dict) -> str: 315 if d.id is not None: 316 self.do_heading('', 0, id=d.id) 317 text = '<div class="docblock">\n' 318 text += render_decl(d) 319 text += '<div class="docblock-desc">\n' 320 text += doxyxml2html(d.desc) 321 if d.members is not None: 322 for m in d.members: 323 text += self.render(m, config) 324 text += '</div>\n' 325 text += '</div>\n' 326 return text 327 328 329def get_handler(theme: str, custom_templates: Optional[str] = None, 330 **_config: Any) -> CxxHandler: 331 """Return an instance of `CxxHandler`. 332 333 Arguments: 334 theme: The theme to use when rendering contents. 335 custom_templates: Directory containing custom templates. 336 **_config: Configuration passed to the handler. 337 """ 338 return CxxHandler(theme=theme, custom_templates=custom_templates) 339