Improve mkdocstrings handler

This commit is contained in:
Victor Zverovich 2024-05-29 17:29:25 -07:00
parent 33eba1049d
commit 5735048b2d

View File

@ -1,4 +1,4 @@
# A basic mkdocstrings handler for C++. # A basic mkdocstrings handler for {fmt}.
# Copyright (c) 2012 - present, Victor Zverovich # Copyright (c) 2012 - present, Victor Zverovich
import os import os
@ -7,8 +7,56 @@ from mkdocstrings.handlers.base import BaseHandler
from typing import Any, Mapping, Optional from typing import Any, Mapping, Optional
from subprocess import CalledProcessError, PIPE, Popen, STDOUT from subprocess import CalledProcessError, PIPE, Popen, STDOUT
class Definition: class Decl:
pass def __init__(self, name: str):
self.name = name
# A map from Doxygen to HTML tags.
tag_map = {
'bold': 'b',
'computeroutput': 'code',
'para': 'p',
'programlisting': 'pre',
'verbatim': 'pre'
}
# A map from Doxygen tags to text.
tag_text_map = {
'codeline': '',
'highlight': '',
'sp': ' '
}
def doxyxml2html(nodes: list[et.Element]):
out = ''
for n in nodes:
tag = tag_map.get(n.tag)
if not tag:
out += tag_text_map[n.tag]
out += '<' + tag + '>' if tag else ''
out += '<code>' if tag == 'pre' else ''
if n.text:
out += n.text
out += doxyxml2html(n)
out += '</code>' if tag == 'pre' else ''
out += '</' + tag + '>' if tag else ''
if n.tail:
out += n.tail
return out
def convert_param(param: et.Element) -> Decl:
d = Decl(param.find('declname').text)
type = param.find('type')
type_str = type.text if type.text else ''
for ref in type:
type_str += ref.text
if ref.tail:
type_str += ref.tail
type_str += type.tail.strip()
type_str = type_str.replace('< ', '<').replace(' >', '>')
type_str = type_str.replace(' &', '&').replace(' *', '*')
d.type = type_str
return d
class CxxHandler(BaseHandler): class CxxHandler(BaseHandler):
def __init__(self, **kwargs: Any) -> None: def __init__(self, **kwargs: Any) -> None:
@ -18,7 +66,7 @@ class CxxHandler(BaseHandler):
cmd = ['doxygen', '-'] cmd = ['doxygen', '-']
doc_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) doc_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
include_dir = os.path.join(os.path.dirname(doc_dir), 'include', 'fmt') include_dir = os.path.join(os.path.dirname(doc_dir), 'include', 'fmt')
doxyxml_dir = 'doxyxml' self._doxyxml_dir = 'doxyxml'
p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT) p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT)
out, _ = p.communicate(input=r''' out, _ = p.communicate(input=r'''
PROJECT_NAME = fmt PROJECT_NAME = fmt
@ -30,7 +78,7 @@ class CxxHandler(BaseHandler):
{0}/core.h {0}/compile.h {0}/format.h {0}/os.h \ {0}/core.h {0}/compile.h {0}/format.h {0}/os.h \
{0}/ostream.h {0}/printf.h {0}/ranges.h {0}/xchar.h {0}/ostream.h {0}/printf.h {0}/ranges.h {0}/xchar.h
QUIET = YES QUIET = YES
JAVADOC_AUTOBRIEF = YES JAVADOC_AUTOBRIEF = NO
AUTOLINK_SUPPORT = NO AUTOLINK_SUPPORT = NO
GENERATE_HTML = NO GENERATE_HTML = NO
GENERATE_XML = YES GENERATE_XML = YES
@ -55,44 +103,57 @@ class CxxHandler(BaseHandler):
"FMT_DOC=1" "FMT_DOC=1"
EXCLUDE_SYMBOLS = fmt::formatter fmt::printf_formatter fmt::arg_join \ EXCLUDE_SYMBOLS = fmt::formatter fmt::printf_formatter fmt::arg_join \
fmt::basic_format_arg::handle fmt::basic_format_arg::handle
'''.format(include_dir, doxyxml_dir).encode('utf-8')) '''.format(include_dir, self._doxyxml_dir).encode('utf-8'))
if p.returncode != 0: if p.returncode != 0:
raise CalledProcessError(p.returncode, cmd) raise CalledProcessError(p.returncode, cmd)
# Load XML. # Load XML.
with open(os.path.join(doxyxml_dir, 'namespacefmt.xml')) as f: with open(os.path.join(self._doxyxml_dir, 'namespacefmt.xml')) as f:
self._doxyxml = et.parse(f) self._doxyxml = et.parse(f)
def collect(self, identifier: str, def collect(self, identifier: str, config: Mapping[str, Any]) -> Decl:
config: Mapping[str, Any]) -> list[Definition]: name = identifier
paren = identifier.find('(') paren = name.find('(')
args = None param_str = None
if paren > 0: if paren > 0:
identifier, args = identifier[:paren], identifier[paren:] name, param_str = name[:paren], name[paren:]
nodes = self._doxyxml.findall( nodes = self._doxyxml.findall(
f"./compounddef/sectiondef/memberdef/name[.='{identifier}']/..") f"compounddef/sectiondef/memberdef/name[.='{name}']/..")
defs = [] candidates = []
for n in nodes: for node in nodes:
node_args = n.find('argsstring').text params = [convert_param(p) for p in node.findall('param')]
if args != None and args != node_args: node_param_str = '(' + ', '.join([p.type for p in params]) + ')'
if param_str and param_str != node_param_str:
candidates.append(name + node_param_str)
continue continue
d = Definition() d = Decl(name)
d.type = n.find('type').text d.type = node.find('type').text
d.name = identifier d.params = params
d.args = node_args d.desc = node.findall('detaileddescription/para')
desc = n.find('detaileddescription/para/verbatim') return d
d.desc = desc.text if desc != None else '' cls = self._doxyxml.findall(f"compounddef/innerclass[.='fmt::{name}']")
defs.append(d) if not cls:
return defs raise Exception(f'Cannot find {identifier}. Candidates: {candidates}')
with open(os.path.join(self._doxyxml_dir, cls[0].get('refid') + '.xml')) as f:
xml = et.parse(f)
node = xml.find('compounddef')
d = Decl(name)
d.type = node.get('kind')
d.params = None
d.desc = node.findall('detaileddescription/para')
return d
def render(self, defs: list[Definition], config: dict) -> str: def render(self, d: Decl, config: dict) -> str:
text = '' text = '<pre><code>'
for d in defs: text += d.type + ' ' + d.name
text += '<pre><code>' if d.params:
text += d.type + ' ' + d.name + d.args params = ', '.join([p.type for p in d.params])
text += '</code></pre>\n' text += '(' + params + ')'
text += d.desc text += ';'
text += '</code></pre>\n'
desc = doxyxml2html(d.desc)
text += desc
return text return text
def get_handler(theme: str, custom_templates: Optional[str] = None, def get_handler(theme: str, custom_templates: Optional[str] = None,