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