mirror of
https://github.com/boostorg/docca.git
synced 2026-01-19 04:12:08 +00:00
1444 lines
41 KiB
Python
Executable File
1444 lines
41 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
#
|
|
# Copyright (c) 2024 Dmitry Arkhipov (grisumbras@yandex.ru)
|
|
#
|
|
# Distributed under the Boost Software License, Version 1.0. (See accompanying
|
|
# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
|
#
|
|
# Official repository: https://github.com/boostorg/json
|
|
#
|
|
|
|
import argparse
|
|
import importlib
|
|
import io
|
|
import jinja2
|
|
import json
|
|
import os.path
|
|
import re
|
|
import sys
|
|
import xml.etree.ElementTree as ET
|
|
|
|
|
|
class Nullcontext():
|
|
def __init__(self):
|
|
pass
|
|
|
|
def __enter__(self):
|
|
pass
|
|
|
|
def __exit__(self, exc_type, exc_value, traceback):
|
|
pass
|
|
|
|
|
|
class Access:
|
|
public = 'public'
|
|
protected = 'protected'
|
|
private = 'private'
|
|
|
|
class FunctionKind:
|
|
static = 'static'
|
|
nonstatic = 'nonstatic'
|
|
friend = 'friend'
|
|
free = 'free'
|
|
|
|
class VirtualKind:
|
|
nonvirtual = 'non-virtual'
|
|
purevirtual = 'pure-virtual'
|
|
virtual = 'virtual'
|
|
|
|
|
|
class Linebreak():
|
|
pass
|
|
|
|
class PhraseContainer:
|
|
def __init__(self, parts):
|
|
self._parts = parts
|
|
|
|
def __getitem__(self, pos):
|
|
return self._parts[pos]
|
|
|
|
def __setitem__(self, pos, val):
|
|
self._parts[pos] = val
|
|
|
|
def __len__(self):
|
|
return len(self._parts)
|
|
|
|
class Phrase(PhraseContainer):
|
|
@property
|
|
def text(self):
|
|
return ''.join((
|
|
part if isinstance(part, str)
|
|
else '' if isinstance(part, Linebreak)
|
|
else part.text
|
|
for part in self
|
|
))
|
|
|
|
class Emphasised(Phrase):
|
|
pass
|
|
|
|
class Monospaced(Phrase):
|
|
pass
|
|
|
|
class Strong(Phrase):
|
|
pass
|
|
|
|
class EntityRef(Phrase):
|
|
def __init__(self, entity, parts):
|
|
super().__init__(parts)
|
|
self.entity = entity
|
|
|
|
@property
|
|
def text(self):
|
|
result = super().text
|
|
if not result:
|
|
result = self.entity.fully_qualified_name
|
|
return result
|
|
|
|
class UrlLink(Phrase):
|
|
def __init__(self, url, parts):
|
|
super().__init__(parts)
|
|
self.url = url
|
|
assert url
|
|
|
|
@property
|
|
def text(self):
|
|
result = super().text
|
|
if not result:
|
|
result = self.url
|
|
return result
|
|
|
|
class EmDash:
|
|
def __init__(self, *args, **kw):
|
|
pass
|
|
|
|
@property
|
|
def text(self):
|
|
return '\u2014'
|
|
|
|
def __len__(self):
|
|
return 0
|
|
|
|
class EnDash:
|
|
def __init__(self, *args, **kw):
|
|
pass
|
|
|
|
@property
|
|
def text(self):
|
|
return '\u2013'
|
|
|
|
def __len__(self):
|
|
return 0
|
|
|
|
class Block:
|
|
pass
|
|
|
|
class Paragraph(PhraseContainer, Block):
|
|
def __init__(self, parts):
|
|
super().__init__(parts)
|
|
|
|
def __getitem__(self, pos):
|
|
return self._parts[pos]
|
|
|
|
def __len__(self):
|
|
return len(self._parts)
|
|
|
|
@property
|
|
def text(self):
|
|
return ''.join([
|
|
(p if isinstance(p, str) else p.text) for p in self._parts
|
|
])
|
|
|
|
class List(Block):
|
|
Arabic = '1'
|
|
LowerLatin = 'a'
|
|
UpperLatin = 'A'
|
|
LowerRoman = 'i'
|
|
UpperRoman = 'I'
|
|
|
|
def __init__(self, kind, items):
|
|
self.kind = kind
|
|
self.is_ordered = kind is not None
|
|
self._items = items
|
|
|
|
def __getitem__(self, pos):
|
|
return self._items[pos]
|
|
|
|
def __len__(self):
|
|
return len(self._items)
|
|
|
|
class ListItem(Block):
|
|
def __init__(self, blocks):
|
|
assert blocks
|
|
self._blocks = blocks
|
|
|
|
def __getitem__(self, pos):
|
|
return self._blocks[pos]
|
|
|
|
def __len__(self):
|
|
return len(self.blocks)
|
|
|
|
class Section(Block):
|
|
See = 'see'
|
|
Returns = 'return'
|
|
Author = 'author'
|
|
Authors = 'authors'
|
|
Version = 'version'
|
|
Since = 'since'
|
|
Date = 'date'
|
|
Note = 'note'
|
|
Warning = 'warning'
|
|
Preconditions = 'pre'
|
|
Postconditions = 'post'
|
|
Copyright = 'copyright'
|
|
Invariants = 'invariant'
|
|
Remarks = 'remark'
|
|
Attention = 'attention'
|
|
Custom = 'par'
|
|
RCS = 'rcs'
|
|
|
|
def __init__(self, kind, title, blocks):
|
|
self.kind = kind
|
|
self.title = title
|
|
self._blocks = blocks
|
|
|
|
def __getitem__(self, pos):
|
|
return self._blocks[pos]
|
|
|
|
def __len__(self):
|
|
return len(self._blocks)
|
|
|
|
class ParameterList(Block):
|
|
Parameters = 'param'
|
|
ReturnValues = 'retval'
|
|
Exceptions = 'exception'
|
|
TemplateParameters = 'templateparam'
|
|
|
|
def __init__(self, kind, items):
|
|
self.kind = kind
|
|
self._items = items
|
|
|
|
def __getitem__(self, pos):
|
|
return self._items[pos]
|
|
|
|
def __len__(self):
|
|
return len(self._items)
|
|
|
|
class ParameterDescription(Block):
|
|
def __init__(self, description, params):
|
|
self.description = description
|
|
self._params = params
|
|
|
|
def __getitem__(self, pos):
|
|
return self._params[pos]
|
|
|
|
def __len__(self):
|
|
return len(self._params)
|
|
|
|
class ParameterItem(Block):
|
|
def __init__(self, type, name, direction):
|
|
self.type = type
|
|
self.name = name
|
|
self.direction = direction
|
|
|
|
@property
|
|
def is_in(self):
|
|
return self.direction in ('in', 'inout')
|
|
|
|
@property
|
|
def is_out(self):
|
|
return self.direction in ('out', 'inout')
|
|
|
|
class CodeBlock(Block):
|
|
def __init__(self, lines):
|
|
self._lines = lines
|
|
|
|
def __getitem__(self, pos):
|
|
return self._lines[pos]
|
|
|
|
def __len__(self):
|
|
return len(self._lines)
|
|
|
|
class Table(Block):
|
|
def __init__(self, cols, rows, caption=None, width=None):
|
|
self.cols = cols
|
|
self.width = width
|
|
self.caption = caption
|
|
self._rows = rows
|
|
|
|
def __getitem__(self, pos):
|
|
return self._rows[pos]
|
|
|
|
def __len__(self):
|
|
return len(self._rows)
|
|
|
|
class Cell(Block):
|
|
def __init__(
|
|
self, blocks,
|
|
col_span=1, row_span=1, is_header=False, horizontal_align=None,
|
|
vertical_align=None, width=None, role=None
|
|
):
|
|
self._blocks = blocks
|
|
|
|
self.col_span = int(col_span or 1)
|
|
self.row_span = int(row_span or 1)
|
|
self.is_header = is_header or False
|
|
self.horizontal_align = horizontal_align
|
|
self.vertical_align = vertical_align
|
|
self.width = width
|
|
self.role = role
|
|
|
|
def __getitem__(self, pos):
|
|
return self._blocks[pos]
|
|
|
|
def __len__(self):
|
|
return len(self._blocks)
|
|
|
|
def make_blocks(element, index):
|
|
if element is None:
|
|
return []
|
|
|
|
result = []
|
|
cur_para = []
|
|
|
|
def finish_paragraph(cur_para):
|
|
if cur_para:
|
|
if isinstance(cur_para[0], str):
|
|
cur_para[0] = cur_para[0].lstrip()
|
|
if not cur_para[0]:
|
|
cur_para = cur_para[1:]
|
|
if cur_para:
|
|
if isinstance(cur_para[-1], str):
|
|
cur_para[-1] = cur_para[-1].rstrip()
|
|
if not cur_para[-1]:
|
|
cur_para = cur_para[:-1]
|
|
|
|
# spaces after linebreaks usually cause issues
|
|
for n in range(1, len(cur_para)):
|
|
if (isinstance(cur_para[n - 1], Linebreak)
|
|
and isinstance(cur_para[n], str)):
|
|
cur_para[n] = cur_para[n].lstrip()
|
|
|
|
if cur_para:
|
|
result.append(Paragraph(cur_para))
|
|
|
|
return []
|
|
|
|
if element.text:
|
|
cur_para.append(remove_endlines(element.text))
|
|
|
|
for child in element:
|
|
func = {
|
|
'itemizedlist': make_list,
|
|
'simplesect': make_section,
|
|
'programlisting': make_codeblock,
|
|
'parameterlist': make_parameters,
|
|
'table': make_table,
|
|
}.get(child.tag)
|
|
if func:
|
|
cur_para = finish_paragraph(cur_para)
|
|
result.append(func(child, index))
|
|
elif child.tag == 'para':
|
|
cur_para = finish_paragraph(cur_para)
|
|
result.extend(make_blocks(child, index))
|
|
else:
|
|
cur_para.append(make_phrase(child, index))
|
|
if child.tail:
|
|
cur_para.append(remove_endlines(child.tail))
|
|
|
|
finish_paragraph(cur_para)
|
|
return result
|
|
|
|
def make_list(element, index):
|
|
items = []
|
|
for child in element:
|
|
assert child.tag == 'listitem'
|
|
items.append(make_blocks(child, index))
|
|
return List(element.get('type'), items)
|
|
|
|
def make_parameters(element, index):
|
|
result = []
|
|
descr = None
|
|
kind = element.get('kind')
|
|
for descr_block in element:
|
|
assert descr_block.tag == 'parameteritem'
|
|
descr = None
|
|
params = []
|
|
for item in descr_block:
|
|
if item.tag == 'parameterdescription':
|
|
assert descr == None
|
|
descr = item
|
|
continue
|
|
|
|
assert item.tag == 'parameternamelist'
|
|
|
|
name = item.find('parametername')
|
|
direction = name.get('direction') if name is not None else None
|
|
params.append(
|
|
ParameterItem(
|
|
text_with_refs(item.find('parametertype'), index),
|
|
text_with_refs(name, index),
|
|
direction))
|
|
|
|
assert descr is not None
|
|
result.append(ParameterDescription(make_blocks(descr, index), params))
|
|
return ParameterList(kind, result)
|
|
|
|
def make_section(element, index):
|
|
title = None
|
|
if len(element) and element[0].tag == 'title':
|
|
title = phrase_content(element[0], index)
|
|
title = Paragraph(title or [])
|
|
|
|
kind = element.get('kind')
|
|
|
|
parts = []
|
|
for child in element:
|
|
if child.tag == 'para':
|
|
parts.extend(make_blocks(child, index))
|
|
|
|
return Section(kind, title, parts)
|
|
|
|
def make_codeblock(element, index):
|
|
lines = []
|
|
for line in element:
|
|
assert line.tag == 'codeline'
|
|
text = ''
|
|
for hl in line:
|
|
assert hl.tag == 'highlight'
|
|
if hl.text:
|
|
text += hl.text
|
|
|
|
for part in hl:
|
|
if part.tag == 'sp':
|
|
text += ' '
|
|
elif part.tag == 'ref':
|
|
text += part.text or ''
|
|
if part.tail:
|
|
text += part.tail
|
|
if hl.tail:
|
|
text += hl.tail
|
|
lines.append(text)
|
|
|
|
return CodeBlock(lines)
|
|
|
|
def make_table(element, index):
|
|
cols = element.get('cols')
|
|
|
|
caption = None
|
|
if len(element) and element[0].tag == 'caption':
|
|
caption = phrase_content(element[0], index)
|
|
caption = Paragraph(caption or [])
|
|
|
|
rows = []
|
|
for row in element[(1 if caption else 0):]:
|
|
assert row.tag == 'row'
|
|
cells = []
|
|
for cell in row:
|
|
cells.append(Cell(
|
|
make_blocks(cell, index),
|
|
col_span=cell.get('colspan'),
|
|
row_span=cell.get('rowspan'),
|
|
is_header=cell.get('thead'),
|
|
horizontal_align=cell.get('align'),
|
|
vertical_align=cell.get('valign'),
|
|
width=cell.get('width'),
|
|
role=cell.get('class'),
|
|
))
|
|
rows.append(cells)
|
|
|
|
return Table(cols, rows, caption)
|
|
|
|
def phrase_content(element, index, allow_missing_refs=False):
|
|
if element is None:
|
|
return []
|
|
|
|
result = []
|
|
if element.text:
|
|
result.append(remove_endlines(element.text))
|
|
|
|
for child in element:
|
|
result.append(
|
|
make_phrase(child, index, allow_missing_refs=allow_missing_refs))
|
|
if child.tail:
|
|
result.append(remove_endlines(child.tail))
|
|
|
|
return result
|
|
|
|
def make_phrase(element, index, allow_missing_refs=False):
|
|
func = {
|
|
'bold': make_strong,
|
|
'computeroutput': make_monospaced,
|
|
'verbatim': make_monospaced,
|
|
'emphasis': make_emphasised,
|
|
'ulink': make_url_link,
|
|
'linebreak': make_linebreak,
|
|
'ref': make_entity_reference,
|
|
'mdash': EmDash,
|
|
'ndash': EnDash,
|
|
}[element.tag]
|
|
return func(element, index, allow_missing_refs=allow_missing_refs)
|
|
|
|
def make_strong(element, index, allow_missing_refs=False):
|
|
return Strong(
|
|
phrase_content(element, index, allow_missing_refs=allow_missing_refs))
|
|
|
|
def make_monospaced(element, index, allow_missing_refs=False):
|
|
return Monospaced(
|
|
phrase_content(element, index, allow_missing_refs=allow_missing_refs))
|
|
|
|
def make_emphasised(element, index, allow_missing_refs=False):
|
|
return Emphasised(
|
|
phrase_content(element, index, allow_missing_refs=allow_missing_refs))
|
|
|
|
def make_url_link(element, index, allow_missing_refs=False):
|
|
return UrlLink(
|
|
element.get('url'),
|
|
phrase_content(element, index, allow_missing_refs=allow_missing_refs))
|
|
|
|
def make_linebreak(element, index, allow_missing_refs=None):
|
|
return Linebreak()
|
|
|
|
def make_entity_reference(
|
|
element, index, refid=None, allow_missing_refs=False
|
|
):
|
|
refid = refid or element.get('refid')
|
|
assert refid
|
|
|
|
target = index.get(refid)
|
|
if target:
|
|
return EntityRef(
|
|
target,
|
|
phrase_content(
|
|
element, index, allow_missing_refs=allow_missing_refs))
|
|
|
|
if allow_missing_refs:
|
|
return Phrase(phrase_content(element, index, allow_missing_refs=True))
|
|
|
|
def text_with_refs(element, index):
|
|
return Phrase(phrase_content(element, index, allow_missing_refs=True))
|
|
|
|
def resolve_type(element, index):
|
|
result = text_with_refs(element, index)
|
|
if (
|
|
result
|
|
and isinstance(result[0], str)
|
|
and result[0].startswith('constexpr')
|
|
):
|
|
result[0] = result[0][len('constexpr'):].lstrip()
|
|
return result
|
|
|
|
_chartable = {
|
|
ord('\r'): None,
|
|
ord('\n'): None,
|
|
}
|
|
def remove_endlines(s):
|
|
return s.translate(_chartable)
|
|
|
|
_noexcept_pattern = re.compile(r'(?<=\bnoexcept\()')
|
|
def parse_noexcept_condition(argstring):
|
|
match = _noexcept_pattern.search(argstring)
|
|
if match:
|
|
parens = 1
|
|
start = match.start(0)
|
|
end = -1
|
|
for i in range(start, len(argstring)):
|
|
if not parens:
|
|
end = i
|
|
break
|
|
if argstring[i] == '(':
|
|
parens += 1
|
|
elif argstring[i]== ')':
|
|
parens -= 1
|
|
assert parens == 0
|
|
return argstring[start:end]
|
|
|
|
class Location():
|
|
def __init__(self, elem):
|
|
self.file = elem.get('file')
|
|
|
|
self.line = elem.get('line')
|
|
if self.line:
|
|
self.line = int(self.line)
|
|
|
|
self.column = elem.get('column')
|
|
if self.column :
|
|
self.column = int(self.column)
|
|
|
|
|
|
class Entity():
|
|
access = Access.public
|
|
|
|
def __init__(self, element, scope, index=dict()):
|
|
self.id = element.get('id')
|
|
assert self.id
|
|
|
|
self.scope = scope
|
|
|
|
self.name = ''.join( element.find(self.nametag).itertext() )
|
|
|
|
self.groups = []
|
|
|
|
loc = element.find('location')
|
|
self._location = Location(loc) if (loc is not None) else None
|
|
|
|
self._brief = element.find('briefdescription')
|
|
self._description = element.find('detaileddescription')
|
|
|
|
self.index = index
|
|
index[self.id] = self
|
|
|
|
@property
|
|
def location(self):
|
|
return (
|
|
self._location
|
|
or (self.scope.location if self.scope else None)
|
|
)
|
|
|
|
@property
|
|
def fully_qualified_name(self):
|
|
return '::'.join((c.name for c in self.path))
|
|
|
|
@property
|
|
def path(self):
|
|
result = self.scope.path if self.scope else []
|
|
result.append(self)
|
|
return result
|
|
|
|
def lookup(self, qname):
|
|
name_parts = qname.split('::')
|
|
if not name_parts:
|
|
return
|
|
|
|
scope = self
|
|
while scope is not None:
|
|
if scope.name == name_parts[0]:
|
|
break
|
|
else:
|
|
found = None
|
|
for entity in scope.members.values():
|
|
if entity.name == name_parts[0]:
|
|
found = entity
|
|
break
|
|
if found is None:
|
|
scope = scope.scope
|
|
else:
|
|
break
|
|
if not scope:
|
|
return
|
|
|
|
for part in name_parts:
|
|
if scope.name != part:
|
|
found = None
|
|
for entity in scope.members.values():
|
|
if entity.name == part:
|
|
found = entity
|
|
break
|
|
scope = found
|
|
if found is None:
|
|
break
|
|
|
|
return scope
|
|
|
|
def resolve_references(self):
|
|
self.brief = make_blocks(self._brief, self.index)
|
|
delattr(self, '_brief')
|
|
|
|
self.description = make_blocks(self._description, self.index)
|
|
delattr(self, '_description')
|
|
|
|
def update_scopes(self):
|
|
pass
|
|
|
|
def __lt__(self, other):
|
|
return self.name < other.name
|
|
|
|
|
|
class Compound(Entity):
|
|
nametag = 'compoundname'
|
|
|
|
def __init__(self, element, scope, index=dict()):
|
|
super().__init__(element, scope, index)
|
|
|
|
self.members = {}
|
|
self._nested = []
|
|
for section in element:
|
|
if section.tag in ('innerclass', 'innernamespace', 'innergroup'):
|
|
self._nested.append((
|
|
section.get('prot', Access.public),
|
|
section.get('refid')))
|
|
|
|
def update_scopes(self):
|
|
super().update_scopes()
|
|
|
|
for access, refid in self._nested:
|
|
entity = self.index[refid]
|
|
entity.scope = self
|
|
entity.access = access
|
|
|
|
assert entity.name not in self.members
|
|
self.members[entity.name] = entity
|
|
delattr(self, '_nested')
|
|
|
|
def resolve_references(self):
|
|
super().resolve_references()
|
|
if getattr(self, '_nested', None):
|
|
self.update_scopes()
|
|
|
|
|
|
class Member(Entity):
|
|
nametag = 'name'
|
|
|
|
def __init__(self, element, scope, index=dict()):
|
|
super().__init__(element, scope, index)
|
|
self.access = element.get('prot') or Access.public
|
|
|
|
|
|
class Group(Compound):
|
|
nametag = 'title'
|
|
|
|
def __init__(self, element, index=dict()):
|
|
super().__init__(element, None, index)
|
|
|
|
def adopt(self, entity, access):
|
|
entity.groups.append(self)
|
|
|
|
|
|
class Templatable(Entity):
|
|
def __init__(self, element, scope, index=dict()):
|
|
super().__init__(element, scope, index)
|
|
self._template_parameters = element.find('templateparamlist')
|
|
self.is_specialization = (
|
|
(self.name.find('<') > 0) and (self.name.find('>') > 0))
|
|
|
|
def resolve_references(self):
|
|
super().resolve_references()
|
|
params = (
|
|
self._template_parameters
|
|
if self._template_parameters is not None
|
|
and len(self._template_parameters)
|
|
else []
|
|
)
|
|
self.template_parameters = [Parameter(elem, self) for elem in params]
|
|
delattr(self, '_template_parameters')
|
|
|
|
|
|
class Type(Templatable):
|
|
declarator = None
|
|
objects = []
|
|
|
|
|
|
class Scope(Entity):
|
|
def __init__(self, element, scope, index=dict()):
|
|
super().__init__(element, scope, index)
|
|
|
|
nesting = 0
|
|
name = ''
|
|
colon = False
|
|
for c in self.name:
|
|
if colon:
|
|
colon = False
|
|
if c == ':':
|
|
name = ''
|
|
continue
|
|
else:
|
|
name += ':'
|
|
|
|
if nesting:
|
|
if c == '<':
|
|
nesting += 1
|
|
elif c == '>':
|
|
nesting -= 1
|
|
name += c
|
|
elif c == ':':
|
|
colon = True
|
|
else:
|
|
if c == '<':
|
|
nesting += 1
|
|
name += c
|
|
if colon:
|
|
name.append(':')
|
|
self.name = name
|
|
|
|
self.members = dict()
|
|
for section in element:
|
|
if not section.tag == 'sectiondef':
|
|
continue
|
|
|
|
for member_def in section:
|
|
assert member_def.tag == 'memberdef'
|
|
|
|
kind = member_def.get('kind')
|
|
if (kind == 'friend'
|
|
and member_def.find('type').text == 'class'):
|
|
# ignore friend classes
|
|
continue
|
|
|
|
factory = {
|
|
'function': OverloadSet.create,
|
|
'friend': OverloadSet.create,
|
|
'variable': Variable,
|
|
'typedef': TypeAlias,
|
|
'enum': Enum,
|
|
}[kind]
|
|
member = factory(member_def, section, self, index)
|
|
if type(member) is OverloadSet:
|
|
key = (member.name, member.access, member.kind)
|
|
self.members[key] = member
|
|
else:
|
|
if member and member.name in self.members:
|
|
member = work_around_repeated_member(self, member)
|
|
if member is None:
|
|
continue
|
|
assert member.name not in self.members
|
|
self.members[member.name] = member
|
|
|
|
|
|
def resolve_references(self):
|
|
super().resolve_references()
|
|
bad_keys = []
|
|
for key, member in self.members.items():
|
|
if isinstance(member, OverloadSet):
|
|
good_funcs = [
|
|
func for func in member
|
|
if self.index.get(func.id) is func
|
|
]
|
|
if good_funcs:
|
|
member.funcs = good_funcs
|
|
else:
|
|
bad_keys.append(key)
|
|
elif self.index[member.id] is not member:
|
|
bad_keys.append(key)
|
|
|
|
for key in bad_keys:
|
|
del self.members[key]
|
|
|
|
class Namespace(Scope, Compound):
|
|
declarator = 'namespace'
|
|
|
|
def __init__(self, element, index=dict()):
|
|
super().__init__(element, None, index)
|
|
|
|
def resolve_references(self):
|
|
super().resolve_references()
|
|
for member in self.members.values():
|
|
if isinstance(member, OverloadSet):
|
|
for func in member:
|
|
func.is_free = True
|
|
|
|
|
|
class Class(Scope, Compound, Type):
|
|
declarator = 'class'
|
|
|
|
def __init__(self, element, index=dict()):
|
|
super().__init__(element, None, index)
|
|
self._bases = element.findall('basecompoundref')
|
|
|
|
def resolve_references(self):
|
|
super().resolve_references()
|
|
self.bases = [Generalization(entry, self) for entry in self._bases]
|
|
delattr(self, '_bases')
|
|
|
|
|
|
class Generalization():
|
|
def __init__(self, element, derived):
|
|
self.is_virtual = element.get('virt') == 'virtual'
|
|
self.access = element.get('prot')
|
|
assert self.access
|
|
|
|
refid = element.get('refid')
|
|
if not refid:
|
|
entity = derived.lookup( ''.join(element.itertext()).strip() )
|
|
if entity is not None:
|
|
refid = entity.id
|
|
|
|
if refid:
|
|
self.base = Phrase(
|
|
[make_entity_reference(element, derived.index, refid)])
|
|
else:
|
|
self.base = text_with_refs(element, derived.index)
|
|
|
|
|
|
class Struct(Class):
|
|
declarator = 'struct'
|
|
|
|
|
|
class Union(Class):
|
|
declarator = 'union'
|
|
|
|
|
|
class Enum(Scope, Type, Member):
|
|
def __init__(self, element, section, parent, index=dict()):
|
|
super().__init__(element, parent, index)
|
|
self.is_scoped = element.get('strong') == 'yes'
|
|
self._underlying_type = element.find('type')
|
|
|
|
self.objects = []
|
|
for child in element.findall('enumvalue'):
|
|
enumerator = Enumerator(child, section, self, index)
|
|
self.objects.append(enumerator)
|
|
assert enumerator.name not in enumerator.scope.members
|
|
enumerator.scope.members[enumerator.name] = enumerator
|
|
|
|
@property
|
|
def declarator(self):
|
|
return 'enum class' if self.is_scoped else 'enum'
|
|
|
|
def resolve_references(self):
|
|
super().resolve_references()
|
|
self.underlying_type = text_with_refs(
|
|
self._underlying_type, self.index)
|
|
delattr(self, '_underlying_type')
|
|
|
|
|
|
class Value(Member, Templatable):
|
|
def __init__(self, element, parent, index=dict()):
|
|
super().__init__(element, parent, index)
|
|
self.is_static = element.get('static') == 'yes'
|
|
self.is_constexpr = element.get('constexpr') == 'yes'
|
|
self.is_volatile = element.get('volatile') == 'yes'
|
|
self.is_const = (
|
|
element.get('const') == 'yes'
|
|
or element.get('mutable') == 'no'
|
|
or False)
|
|
self.is_inline = element.get('inline') == 'yes'
|
|
|
|
|
|
class Function(Value):
|
|
def __init__(self, element, section, parent, index=dict()):
|
|
super().__init__(element, parent, index)
|
|
self.is_explicit = element.get('explicit') == 'yes'
|
|
self.refqual = element.get('refqual')
|
|
self.virtual_kind = element.get('virt', VirtualKind.nonvirtual)
|
|
self.is_friend = section.get('kind') == 'friend'
|
|
self.is_free = section.get('kind') == 'related'
|
|
self.is_constructor = self.name == parent.name
|
|
self.is_destructor = self.name == '~' + parent.name
|
|
noexcept = element.get('noexcept')
|
|
if noexcept:
|
|
self.is_noexcept = noexcept == 'yes'
|
|
else:
|
|
self.is_noexcept = self.is_destructor
|
|
|
|
args = element.find('argsstring').text or ''
|
|
self.is_deleted = args.endswith('=delete')
|
|
self.is_defaulted = args.endswith('=default')
|
|
|
|
self.overload_set = None
|
|
|
|
self._return_type = element.find('type')
|
|
assert (
|
|
self.is_constructor
|
|
or self.is_destructor
|
|
or self._return_type is not None)
|
|
|
|
self._parameters = element.findall('param')
|
|
|
|
self.noexcept_condition = None
|
|
if self.is_noexcept:
|
|
self.noexcept_condition = element.get('noexceptexpression')
|
|
if not self.noexcept_condition:
|
|
self.noexcept_condition = parse_noexcept_condition(args)
|
|
|
|
@property
|
|
def kind(self):
|
|
if self.is_friend:
|
|
return FunctionKind.friend
|
|
if self.is_free:
|
|
return FunctionKind.free
|
|
if self.is_static:
|
|
return FunctionKind.static
|
|
return FunctionKind.nonstatic
|
|
|
|
@property
|
|
def is_sole_overload(self):
|
|
return len(self.overload_set) == 1
|
|
|
|
@property
|
|
def overload_index(self):
|
|
if self.overload_set is None or self.is_sole_overload:
|
|
return -1
|
|
|
|
for n, overload in enumerate(self.overload_set):
|
|
if self == overload:
|
|
return n
|
|
|
|
return -1
|
|
|
|
def resolve_references(self):
|
|
super().resolve_references()
|
|
|
|
self.return_type = resolve_type(self._return_type, self.index)
|
|
delattr(self, '_return_type')
|
|
|
|
self.parameters = [Parameter(elem, self) for elem in self._parameters]
|
|
delattr(self, '_parameters')
|
|
|
|
def __lt__(self, other):
|
|
if not isinstance(other, Function):
|
|
return self.name < other.name
|
|
|
|
if self.name == other.name:
|
|
if self.scope == other.scope:
|
|
return self.overload_index < other.overload_index
|
|
return self.scope < other.scope
|
|
|
|
# if self.is_constructor:
|
|
# return True
|
|
# if other.is_constructor:
|
|
# return False
|
|
# if self.is_destructor:
|
|
# return True
|
|
# if other.is_destructor:
|
|
# return False
|
|
return self.name < other.name
|
|
|
|
|
|
class Parameter():
|
|
def __init__(self, element, parent):
|
|
self.type = text_with_refs(element.find('type'), parent.index)
|
|
self.default_value = text_with_refs(
|
|
element.find('defval'), parent.index)
|
|
|
|
self.description = element.find('briefdescription')
|
|
if self.description is not None:
|
|
self.description = make_blocks(self.description, parent)
|
|
else:
|
|
self.description = []
|
|
|
|
self.name = element.find('declname')
|
|
if self.name is not None:
|
|
self.name = self.name.text
|
|
|
|
self.array = text_with_refs(element.find('array'), parent.index)
|
|
if self.array:
|
|
assert isinstance(self.type[-1], str)
|
|
assert self.type[-1].endswith('(&)')
|
|
self.type[-1] = self.type[-1][:-3]
|
|
|
|
self.args = text_with_refs(element.find('argsstring'), parent.index)
|
|
if self.args:
|
|
assert isinstance(self.type[-1], str)
|
|
assert isinstance(self.args[0], str)
|
|
assert self.type[-1].endswith('(*')
|
|
assert self.args[0].startswith(')(')
|
|
self.type[-1] = self.type[-1][:-2]
|
|
self.args[0] = self.args[0][1:]
|
|
|
|
|
|
class OverloadSet():
|
|
@staticmethod
|
|
def create(element, section, parent, index):
|
|
if (index.get( element.get('id') )
|
|
and (section.get('kind') != 'related'
|
|
or not isinstance(parent, Class))
|
|
):
|
|
return None
|
|
|
|
func = Function(element, section, parent, index)
|
|
|
|
key = (func.name, func.access, func.kind)
|
|
if key in parent.members:
|
|
overload_set = parent.members[key]
|
|
overload_set.append(func)
|
|
else:
|
|
overload_set = OverloadSet([func])
|
|
func.overload_set = overload_set
|
|
return overload_set
|
|
|
|
def __init__(self, funcs):
|
|
self.funcs = funcs
|
|
assert len(funcs)
|
|
self._resort()
|
|
|
|
def append(self, func):
|
|
self.funcs.append(func)
|
|
self._resort()
|
|
|
|
def __getattr__(self, name):
|
|
return getattr(self.funcs[0], name)
|
|
|
|
@property
|
|
def brief(self):
|
|
return [func.brief for func in self.funcs]
|
|
|
|
def __getitem__(self, pos):
|
|
return self.funcs[pos]
|
|
|
|
def __len__(self):
|
|
return len(self.funcs)
|
|
|
|
def __lt__(self, other):
|
|
if isinstance(other, OverloadSet):
|
|
return self.funcs[0] < other.funcs[0]
|
|
return self.name < other.name
|
|
|
|
def _resort(self):
|
|
funcs_backwards = []
|
|
briefs_backwards = []
|
|
for func in self.funcs:
|
|
brief = (
|
|
''.join(func._brief.itertext())
|
|
if func._brief is not None
|
|
else ''
|
|
)
|
|
try:
|
|
n = briefs_backwards.index(brief)
|
|
except:
|
|
n = 0
|
|
briefs_backwards.insert(n, brief)
|
|
funcs_backwards.insert(n, func)
|
|
funcs_backwards.reverse()
|
|
self.funcs = funcs_backwards
|
|
|
|
|
|
class Variable(Value):
|
|
def __init__(self, element, section, parent, index=dict()):
|
|
super().__init__(element, parent, index)
|
|
self._value = element.find('initializer')
|
|
self._type = element.find('type')
|
|
self._args = element.find('argsstring')
|
|
|
|
def resolve_references(self):
|
|
super().resolve_references()
|
|
|
|
self.value = text_with_refs(self._value, self.index)
|
|
delattr(self, '_value')
|
|
|
|
self.type = resolve_type(self._type, self.index)
|
|
delattr(self, '_type')
|
|
|
|
self.args = text_with_refs(self._args, self.index)
|
|
delattr(self, '_args')
|
|
if (self.args
|
|
and self.type[-1].endswith('(*')
|
|
and self.args[0].startswith(')(')
|
|
):
|
|
self.type[-1] = self.type[-1][:-2]
|
|
self.args[0] = self.args[0][1:]
|
|
|
|
|
|
class Enumerator(Variable):
|
|
def __init__(self, element, section, parent, index=dict()):
|
|
super().__init__(element, section, parent, index)
|
|
self.is_constexpr = True
|
|
self.is_const = True
|
|
self.is_static = True
|
|
self.enum = parent
|
|
self.scope = parent if parent.is_scoped else parent.scope
|
|
assert self.scope
|
|
|
|
def resolve_references(self):
|
|
super().resolve_references()
|
|
self.type = Phrase([EntityRef(self.enum, [self.enum.name])])
|
|
|
|
|
|
class TypeAlias(Member, Type):
|
|
declarator = 'using'
|
|
|
|
def __init__(self, element, section, parent, index=dict()):
|
|
super().__init__(element, parent, index)
|
|
|
|
self._aliased = element.find('type')
|
|
assert self._aliased is not None
|
|
|
|
def resolve_references(self):
|
|
super().resolve_references()
|
|
self.aliased = text_with_refs(self._aliased, self.index)
|
|
delattr(self, '_aliased')
|
|
|
|
|
|
def work_around_repeated_member(scope, member):
|
|
scope_loc = scope.location
|
|
member_loc = member.location
|
|
old_member = scope.members[member.name]
|
|
old_member_loc = old_member.location
|
|
if (scope.is_specialization and
|
|
scope_loc and
|
|
member_loc and
|
|
old_member_loc and
|
|
member_loc.file == scope_loc.file and
|
|
member_loc.line >= scope_loc.line and
|
|
(old_member_loc.file != scope_loc.file or
|
|
old_member_loc.line < scope_loc.line)):
|
|
# the new member's declaration is in the same file as the
|
|
# specialization's declaration and follows it; the old member's
|
|
# declarattion is either in a different file or preceeds the
|
|
# specialization's declaration
|
|
del scope.members[member.name]
|
|
return member
|
|
return None
|
|
|
|
|
|
class AcceptOneorNone(argparse.Action):
|
|
def __init__(self, option_strings, dest, **kwargs):
|
|
if kwargs.get('nargs') is not None:
|
|
raise ValueError("nargs not allowed")
|
|
if kwargs.get('const') is not None:
|
|
raise ValueError("const not allowed")
|
|
if kwargs.get('default') is not None:
|
|
raise ValueError("default not allowed")
|
|
|
|
super().__init__(option_strings, dest, **kwargs)
|
|
|
|
def __call__(self, parser, namespace, values, option_string=None):
|
|
if getattr(namespace, self.dest) is not None:
|
|
raise argparse.ArgumentError(self, "multiple values")
|
|
|
|
setattr(namespace, self.dest, values)
|
|
|
|
def parse_args(args):
|
|
parser = argparse.ArgumentParser(
|
|
prog=args[0],
|
|
description='Produces API reference in QuickBook markup')
|
|
parser.add_argument(
|
|
'-i',
|
|
'--input',
|
|
action=AcceptOneorNone,
|
|
help='Doxygen XML index file; STDIN by default')
|
|
parser.add_argument(
|
|
'-o',
|
|
'--output',
|
|
action=AcceptOneorNone,
|
|
help='Output file; STDOUT by default')
|
|
parser.add_argument(
|
|
'-c', '--config',
|
|
action='append',
|
|
default=[],
|
|
help='Configuration files')
|
|
parser.add_argument(
|
|
'-T', '--template',
|
|
action=AcceptOneorNone,
|
|
help='Jinja2 template to use for output')
|
|
parser.add_argument(
|
|
'-E', '--extension',
|
|
action='append',
|
|
default=[],
|
|
help='Extension module')
|
|
parser.add_argument(
|
|
'-I', '--include',
|
|
action='append',
|
|
default=[],
|
|
help='Directory with template partials')
|
|
parser.add_argument(
|
|
'-D', '--directory',
|
|
action=AcceptOneorNone,
|
|
help=(
|
|
'Directory with additional data files; '
|
|
'by default INPUT parent directory if that is provided, '
|
|
'otherwise PWD'))
|
|
return parser.parse_args(args[1:])
|
|
|
|
def open_input(stdin, args, cwd):
|
|
data_dir = args.directory
|
|
if args.input:
|
|
file = open(args.input, 'r', encoding='utf-8')
|
|
ctx = file
|
|
data_dir = data_dir or os.path.dirname(args.input)
|
|
else:
|
|
file = stdin
|
|
ctx = Nullcontext()
|
|
data_dir = data_dir or cwd
|
|
return (file, ctx, data_dir)
|
|
|
|
def open_output(stdout, args):
|
|
if args.output:
|
|
file = open(args.output, 'w', encoding='utf-8')
|
|
ctx = file
|
|
else:
|
|
file = stdout
|
|
ctx = Nullcontext()
|
|
return (file, ctx)
|
|
|
|
def load_configs(args):
|
|
result = {
|
|
'include_private': False,
|
|
'legacy_behavior': True,
|
|
}
|
|
for file_name in args.config:
|
|
with open(file_name, 'r', encoding='utf-8') as file:
|
|
result.update( json.load(file) )
|
|
if 'allowed_prefixes' not in result:
|
|
allowed_prefixes = ['']
|
|
default_ns = result.get('default_namespace')
|
|
if default_ns:
|
|
allowed_prefixes[0] = default_ns + '::'
|
|
result['allowed_prefixes'] = allowed_prefixes
|
|
|
|
return result
|
|
|
|
def collect_compound_refs(file):
|
|
tree = ET.parse(file)
|
|
root = tree.getroot()
|
|
for child in root:
|
|
if child.tag != 'compound':
|
|
continue
|
|
|
|
kind = child.get('kind')
|
|
assert kind
|
|
if kind in ('file', 'dir'):
|
|
continue
|
|
|
|
refid = child.get('refid')
|
|
assert refid
|
|
yield refid
|
|
|
|
def collect_data(parent_dir, refs):
|
|
result = dict()
|
|
for refid in refs:
|
|
file_name = os.path.join(parent_dir, refid) + '.xml'
|
|
with open(file_name, 'r', encoding='utf-8') as file:
|
|
tree = ET.parse(file)
|
|
root = tree.getroot()
|
|
assert len(root) == 1
|
|
|
|
element = root[0]
|
|
assert element.tag == 'compounddef'
|
|
|
|
factory = {
|
|
'class': Class,
|
|
'namespace': Namespace,
|
|
'struct': Struct,
|
|
'union': Union,
|
|
'group': Group
|
|
}.get(element.get('kind'))
|
|
if not factory:
|
|
continue
|
|
factory(element, result)
|
|
|
|
for entity in result.values():
|
|
assert entity is not None
|
|
entity.update_scopes()
|
|
|
|
for entity in result.values():
|
|
entity.resolve_references();
|
|
|
|
return result
|
|
|
|
def docca_include_dir(script):
|
|
return os.path.join(os.path.dirname(script), 'include')
|
|
|
|
def template_file_name(includes, args):
|
|
return (args.template
|
|
or os.path.join(includes, 'docca/quickbook.jinja2'))
|
|
|
|
def collect_include_dirs(template, include_dir, args):
|
|
result = [os.path.dirname(template), include_dir]
|
|
result.extend(args.include)
|
|
return result
|
|
|
|
def construct_environment(loader, config):
|
|
env = jinja2.Environment(
|
|
loader=loader,
|
|
autoescape=False,
|
|
undefined=jinja2.StrictUndefined,
|
|
extensions=[
|
|
'jinja2.ext.do',
|
|
'jinja2.ext.loopcontrols',
|
|
],
|
|
)
|
|
|
|
env.globals['Access'] = Access
|
|
env.globals['FunctionKind'] = FunctionKind
|
|
env.globals['VirtualKind'] = VirtualKind
|
|
env.globals['Section'] = Section
|
|
env.globals['ParameterList'] = ParameterList
|
|
env.globals['Config'] = config
|
|
env.globals['re'] = re
|
|
|
|
env.tests['Entity'] = lambda x: isinstance(x, Entity)
|
|
env.tests['Templatable'] = lambda x: isinstance(x, Templatable)
|
|
env.tests['Type'] = lambda x: isinstance(x, Type)
|
|
env.tests['Scope'] = lambda x: isinstance(x, Scope)
|
|
env.tests['Namespace'] = lambda x: isinstance(x, Namespace)
|
|
env.tests['TypeAlias'] = lambda x: isinstance(x, TypeAlias)
|
|
env.tests['Class'] = lambda x: isinstance(x, Class)
|
|
env.tests['Struct'] = lambda x: isinstance(x, Struct)
|
|
env.tests['Union'] = lambda x: isinstance(x, Union)
|
|
env.tests['Enum'] = lambda x: isinstance(x, Enum)
|
|
env.tests['Value'] = lambda x: isinstance(x, Value)
|
|
env.tests['Variable'] = lambda x: isinstance(x, Variable)
|
|
env.tests['Enumerator'] = lambda x: isinstance(x, Enumerator)
|
|
env.tests['Function'] = lambda x: isinstance(x, Function)
|
|
env.tests['OverloadSet'] = lambda x: isinstance(x, OverloadSet)
|
|
env.tests['Parameter'] = lambda x: isinstance(x, Parameter)
|
|
|
|
env.tests['Phrase'] = lambda x: isinstance(x, Phrase)
|
|
env.tests['Linebreak'] = lambda x: isinstance(x, Linebreak)
|
|
env.tests['Emphasised'] = lambda x: isinstance(x, Emphasised)
|
|
env.tests['Strong'] = lambda x: isinstance(x, Strong)
|
|
env.tests['Monospaced'] = lambda x: isinstance(x, Monospaced)
|
|
env.tests['EntityRef'] = lambda x: isinstance(x, EntityRef)
|
|
env.tests['UrlLink'] = lambda x: isinstance(x, UrlLink)
|
|
env.tests['EmDash'] = lambda x: isinstance(x, EmDash)
|
|
env.tests['EnDash'] = lambda x: isinstance(x, EnDash)
|
|
|
|
env.tests['Block'] = lambda x: isinstance(x, Block)
|
|
env.tests['Paragraph'] = lambda x: isinstance(x, Paragraph)
|
|
env.tests['List'] = lambda x: isinstance(x, List)
|
|
env.tests['ListItem'] = lambda x: isinstance(x, ListItem)
|
|
env.tests['Section'] = lambda x: isinstance(x, Section)
|
|
env.tests['CodeBlock'] = lambda x: isinstance(x, CodeBlock)
|
|
env.tests['Table'] = lambda x: isinstance(x, Table)
|
|
env.tests['Cell'] = lambda x: isinstance(x, Cell)
|
|
env.tests['ParameterList'] = lambda x: isinstance(x, ParameterList)
|
|
env.tests['ParameterDescription'] = lambda x: isinstance(x, ParameterDescription)
|
|
env.tests['ParameterItem'] = lambda x: isinstance(x, ParameterItem)
|
|
|
|
return env
|
|
|
|
def load_extensions(files):
|
|
result = []
|
|
counter = 0
|
|
for file in files:
|
|
module = None
|
|
if not os.path.exists(file):
|
|
raise RuntimeError('Could not find module %s' % file)
|
|
|
|
name = 'docca._ext' + str(counter)
|
|
spec = importlib.util.spec_from_file_location(name, file)
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[name] = module
|
|
spec.loader.exec_module(module)
|
|
result.append(module)
|
|
counter += 1
|
|
|
|
return result
|
|
|
|
def install_extensions(env, exts):
|
|
for ext in exts:
|
|
ext.install_docca_extension(env)
|
|
return env
|
|
|
|
def render(env, file_name, output, data):
|
|
template = env.get_template(os.path.basename(file_name))
|
|
template.stream(entities=data).dump(output)
|
|
|
|
def main(args, stdin, stdout, script):
|
|
args = parse_args(args)
|
|
|
|
file, ctx, data_dir = open_input(stdin, args, os.getcwd())
|
|
with ctx:
|
|
refs = list(collect_compound_refs(file))
|
|
data = collect_data(data_dir, refs)
|
|
|
|
config = load_configs(args)
|
|
|
|
file, ctx = open_output(stdout, args)
|
|
with ctx:
|
|
include_dir = docca_include_dir(script)
|
|
template = template_file_name(include_dir, args)
|
|
|
|
include_dirs = collect_include_dirs(template, include_dir, args)
|
|
|
|
env = construct_environment(
|
|
jinja2.FileSystemLoader(include_dirs), config)
|
|
|
|
exts = load_extensions(args.extension)
|
|
env = install_extensions(env, exts)
|
|
|
|
render(env, template, file, data)
|
|
|
|
if __name__ == '__main__':
|
|
main(sys.argv, sys.stdin, sys.stdout, os.path.realpath(__file__))
|