Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Asciidoc generator #161

Draft
wants to merge 3 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docca.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,10 @@ def __init__(self, element, scope, index=dict()):
self.index = index
index[self.id] = self

def __repr__(self) -> str:
return f'{type(self).__name__}({self.fully_qualified_name})'


@property
def location(self):
return (
Expand Down Expand Up @@ -895,6 +899,20 @@ def overload_index(self):
return -1

def resolve_references(self):
assert self._description is not None
node = self._description.find('.//xrefsect/..')
if node is not None:
subnode = node.find('xrefsect')
assert subnode is not None
xreftitle = subnode.find('xreftitle')
assert xreftitle is not None
assert xreftitle.text == 'overload_specific'
xrefdescription = subnode.find('xrefdescription')
assert xrefdescription is not None
self.overload_specific = make_blocks(xrefdescription, self.index)
node.remove(subnode)
else:
self.overload_specific = []
super().resolve_references()

self.return_type = resolve_type(self._return_type, self.index)
Expand Down Expand Up @@ -964,6 +982,9 @@ def __init__(self, funcs):
self.funcs = funcs
assert len(funcs)
self._resort()

def __repr__(self) -> str:
return f'OverloadSet({self.fully_qualified_name})'

def append(self, func):
self.funcs.append(func)
Expand Down
174 changes: 174 additions & 0 deletions docca_asciidoc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import sys
import os

sys.path.append(os.path.abspath(os.path.dirname(__file__)))

import argparse
import jinja2
from typing import List
from docca import (
AcceptOneorNone,
open_input,
collect_compound_refs,
collect_data,
load_configs,
construct_environment,
Class,
Namespace,
Access,
Compound,
OverloadSet,
TypeAlias,
Enum,
Variable,
Entity,
Type,
FunctionKind
)

DEFAULT_TPLT_DIR = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'include/docca/asciidoc'))

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(
'-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 render(env: jinja2.Environment, template_file: str, output_dir: str, output_file: str, **kwargs):
output_path = os.path.join(output_dir, output_file)
template = env.get_template(template_file)
os.makedirs(os.path.dirname(output_path), exist_ok=True)
with open(output_path, 'wt', encoding='utf-8') as f:
template.stream(**kwargs).dump(f)


def render_entities(env: jinja2.Environment, template_file: str, output_dir: str, entities):
for e in entities:
print(f'Rendering {e}')
render(env, template_file, output_dir, asciidoc_file(e), entity=e)


def _sanitize_path_segment(s: str) -> str:
return s.replace("[", "-lb") \
.replace("]", "-rb")\
.replace("(", "-lp")\
.replace(")", "-rp")\
.replace("<=>", "-spshp")\
.replace("->", "-arrow")\
.replace("operator>", "operator-gt")\
.replace("operator~", "operator-bnot")\
.replace("=", "-eq")\
.replace("!", "-not")\
.replace("+", "-plus")\
.replace("&", "-and")\
.replace("|", "-or")\
.replace("^", "-xor")\
.replace("*", "-star")\
.replace("/", "-slash")\
.replace("%", "-mod")\
.replace("<", "-lt")\
.replace(">", "-gt")\
.replace("~", "dtor-")\
.replace(",", "-comma")\
.replace(":", "-")\
.replace(" ", "-")


def asciidoc_file(e: Entity) -> str:
file_path = '/'.join(_sanitize_path_segment(elm.name) for elm in e.path)
return f'{file_path}.adoc'


def main():
args = parse_args(sys.argv)

file, ctx, data_dir = open_input(sys.stdin, args, os.getcwd())
with ctx:
refs = list(collect_compound_refs(file))
data = collect_data(data_dir, refs)

output_dir: str = args.output

config = load_configs(args)

env = construct_environment(
jinja2.FileSystemLoader(DEFAULT_TPLT_DIR), config)
env.filters['adocfile'] = asciidoc_file

# Namespaces (not rendered directly)
namespaces = [e for e in data.values() if isinstance(e, Namespace)]

# Classes (including member types)
classes = [e for e in data.values()
if isinstance(e, Class) and e.access == Access.public and
not e.is_specialization]
render_entities(env, 'class.jinja2', output_dir, classes)

# Functions
fns = []
for e in [e2 for e2 in data.values() if isinstance(e2, Compound)]:
fns += [mem for mem in e.members.values() if isinstance(mem, OverloadSet) and mem.access == Access.public]
render_entities(env, 'overload-set.jinja2', output_dir, fns)

# Type aliases
type_alias = [e for e in data.values() if isinstance(e, TypeAlias)]
render_entities(env, 'type-alias.jinja2', output_dir, type_alias)

# Enums
enums = [e for e in data.values() if isinstance(e, Enum)]
render_entities(env, 'enum.jinja2', output_dir, enums)

# Constants
constants = []
for ns in namespaces:
constants += [e for e in ns.members.values() if isinstance(e, Variable)]
render_entities(env, 'variable.jinja2', output_dir, constants)

# Quickref
render(
env,
'quickref.jinja2',
output_dir,
'reference.adoc',
classes=classes,
enums=enums,
free_functions=[e for e in fns if isinstance(e.scope, Namespace)],
type_aliases=[e for e in type_alias if isinstance(e.scope, Namespace)],
constants=constants
)



if __name__ == '__main__':
main()
148 changes: 148 additions & 0 deletions include/docca/asciidoc/class.jinja2
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
{% from "common.jinja2" import description, link, template_parameters, phrase, header_file %}

== {{ entity.fully_qualified_name }}


{{ header_file(entity) }}

{{ description(entity.brief) }}

[source, cpp, subs="+macros", role="raw-code"]
----
{{ template_parameters(entity) }}
{{ entity.declarator }} {{ entity.name }}
{%- for entry in entity.bases -%}
{% if loop.first %}:{% else %},{% endif -%}
{{ ' ' ~ entry.access ~ ' ' }}
{%- if entry.is_virtual %}virtual {% endif -%}
{{ phrase(entry.base, in_code=True) }}
{%- endfor -%}
;
----

{{ description(entity.description) }}

{% for type in entity.members.values()
| select("Type")
| selectattr("access", "eq", Access.public) %}
{% if loop.first %}
=== Member types

[cols="1,2", grid=rows, frame=none, role="mt-0"]
|===
{% endif %}
.^|{{ link(type, '[link-to-entity bg-white]`' + type.name + '`') }}
.^|{{ description(type.brief) }}

{% if loop.last %}
|===
{% endif %}

{%- endfor -%}


{% for fn in entity.members.values()
| select("OverloadSet")
| selectattr("access", "eq", Access.public)
| selectattr("kind", "eq", FunctionKind.nonstatic) %}
{% if loop.first %}
=== Member functions

[cols="1,2", grid=rows, frame=none, role="mt-0"]
|===
{% endif %}
.^|{{ link(fn, '[link-to-entity bg-white]`' + fn.name + '`') }}
.^|{{ description(fn.brief[0]) }}

{% if loop.last %}
|===
{% endif %}

{%- endfor -%}



{% for fn in entity.members.values()
| select("OverloadSet")
| selectattr("access", "eq", Access.public)
| selectattr("kind", "eq", FunctionKind.static) %}
{% if loop.first %}
=== Static functions

[cols="1,2", grid=rows, frame=none, role="mt-0"]
|===
{% endif %}
.^|{{ link(fn, '[link-to-entity bg-white]`' + fn.name + '`') }}
.^|{{ description(fn.brief[0]) }}

{% if loop.last %}
|===
{% endif %}
{%- endfor -%}



{% for mem in entity.members.values()
| select("Variable")
| selectattr("access", "eq", Access.public)
| selectattr("is_static", "eq", False) %}

{% if loop.first %}
=== Data members

[cols="1", grid=none, frame=none, role="mt-0 space-rows"]
|===
{% endif %}
.^a|
[source, cpp, subs="+macros", id={{ mem.name }}]
----
{{ phrase(mem.type) }} {{ mem.name }}{%- if mem.value %} {{ phrase(mem.value) }} {%- endif -%};
----


{{ description(mem.brief) }}

{{ description(mem.description) }}

{% if loop.last %}
|===
{% endif %}
{%- endfor -%}


{# TODO: this is duplicate #}
{% for mem in entity.members.values()
| select("Variable")
| selectattr("access", "eq", Access.public)
| selectattr("is_static", "eq", True) %}

{% if loop.first %}
=== Static data members

[cols="1", grid=none, frame=none, role="mt-0 space-rows"]
|===
{% endif %}
.^a|
[source, cpp, subs="+macros", id={{ mem.name }}]
----
{{ template_parameters(mem) }}
{%- set qualifiers = ['static'] -%}
{%- if mem.is_constexpr -%}
{%- set qualifiers = qualifiers + ['constexpr'] -%}
{%- endif -%}
{%- if mem.is_inline -%}
{%- set qualifiers = qualifiers + ['inline'] -%}
{%- endif -%}

{{ qualifiers | join(' ') }} {{ phrase(mem.type) }} {{ mem.name }}{%- if mem.value %} {{ phrase(mem.value) }} {%- endif -%};
----


{{ description(mem.brief) }}

{{ description(mem.description) }}

{% if loop.last %}
|===
{% endif %}
{%- endfor -%}
Loading