• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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("<", "&lt;")
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'&nbsp;{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 &lt;'
136        text += ', '.join([render_param(p) for p in d.template_params])
137        text += '&gt;\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 += ' -&NoBreak;>&nbsp;' + 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