diff --git a/python/openassetio_traitgen/generators/__init__.py b/python/openassetio_traitgen/generators/__init__.py index ee256a0..d2aa14b 100644 --- a/python/openassetio_traitgen/generators/__init__.py +++ b/python/openassetio_traitgen/generators/__init__.py @@ -62,6 +62,7 @@ def generate( from . import helpers from . import python from . import cpp +from . import markdown # All known language generators -ALL = ("python", "cpp") +ALL = ("python", "cpp", "markdown") diff --git a/python/openassetio_traitgen/generators/markdown.py b/python/openassetio_traitgen/generators/markdown.py new file mode 100644 index 0000000..2028692 --- /dev/null +++ b/python/openassetio_traitgen/generators/markdown.py @@ -0,0 +1,142 @@ +""" +A traitgen generator that outputs a markdown document based on the +openassetio_traitgen PackageDefinition model. +""" + +import logging +import os +import re + +import jinja2 + +from ..datamodel import PackageDeclaration, PropertyType + + +__all__ = ["generate"] + + +def generate( + package_declaration: PackageDeclaration, + globals_: dict, + output_directory: str, + creation_callback, + logger: logging.Logger, +): + """ + Generates markdown documents for the supplied definition under outputDirPath. + """ + env = _create_jinja_env(globals_, logger) + + def render_template(name: str, path: str, variables: dict): + """ + A convenience to render a named template into its corresponding + file and call the creationCallback. + """ + # pylint: disable=line-too-long + # NB: Jinja assumes '/' on all plaftorms: + template = env.get_template(f"markdown/{name}.md.in") + with open(path, "w", encoding="utf-8", newline="\n") as f: + f.write(template.render(variables)) + creation_callback(path) + + def create_dir_with_path_components(*args) -> str: + """ + A convenience to create a directory from the supplied path + components, calling the creationCallback and returning its path + as a string. + """ + path = os.path.join(*args) + os.makedirs(path, exist_ok=True) + creation_callback(path) + return path + + output_dir_path = create_dir_with_path_components( + output_directory, f"{package_declaration.id}-md" + ) + + trait_namespaces = [] + specification_namespaces= [] + + for kind in ("traits", "specifications"): + namespaces = getattr(package_declaration, kind, None) + if namespaces: + if kind == "traits": + trait_namespaces.extend(namespaces) + else: + specification_namespaces.extend(namespaces) + + render_template( + kind, + os.path.join(output_dir_path, f"{kind}.md"), + { + "package": package_declaration, + "namespaces": namespaces, + }, + ) + + render_template( + "index", + os.path.join(output_dir_path, "index.md"), + { + "package": package_declaration, + "trait_namespaces": trait_namespaces, + "specification_namespaces": specification_namespaces, + }, + ) + + +# +## Jinja setup +# + + +def _create_jinja_env(env_globals, logger): + """ + Creates a custom Jinja2 environment with: + - A package a loader that automatically finds templates within a + 'templates' directory in the openassetio_traitgen python package. + - Updated globals. + - Custom filters. + """ + env = jinja2.Environment(loader=jinja2.PackageLoader("openassetio_traitgen")) + env.globals.update(env_globals) + _install_custom_filters(env, logger) + return env + + +# Custom filters + + +def _install_custom_filters(environment, logger): + """ + Installs custom filters in to the Jinja template environment that allow + data from the model to be conformed to markdown-specific standards. + """ + def cleanup_md_string(string: str): + """ + Tidies up a string for markdown. + """ + # Some instances of lists may not be formatted correctly for markdown + new_string = re.sub("(:\s*)([-\*])", r":\n \2", string) + # Specifc area in traits.yaml that doesn't work so well + new_string = re.sub('asset - `"seq003"`', 'asset\n- `"seq003"`', new_string) + return new_string + + type_map = { + PropertyType.STRING: "string", + PropertyType.INTEGER: "integer", + PropertyType.FLOAT: "float", + PropertyType.BOOL: "boolean", + PropertyType.DICT: "dict", # This must be InfoDictionary, but this isn't bound + } + + def to_type_pretty_name(declaration_type): + """ + Returns a pretty name for the property declaration (PropertyType). + """ + if declaration_type == PropertyType.DICT: + raise TypeError("Dictionary types are not yet supported as trait properties") + return type_map[declaration_type] + + environment.filters["cleanup_md_string"] = cleanup_md_string + environment.filters["to_type_pretty_name"] = to_type_pretty_name diff --git a/python/openassetio_traitgen/templates/markdown/index.md.in b/python/openassetio_traitgen/templates/markdown/index.md.in new file mode 100644 index 0000000..4a0fa0f --- /dev/null +++ b/python/openassetio_traitgen/templates/markdown/index.md.in @@ -0,0 +1,16 @@ +# Index of Traits and Specifications +## Traits +{% for namespace in trait_namespaces %} +- {{ namespace.id | title}} +{% for trait in namespace.members %} + - {{ trait.name }} Trait +{% endfor %} +{% endfor %} + +## Specifications +{% for namespace in specification_namespaces %} +- {{ namespace.id | title}} +{% for specification in namespace.members %} + - {{ specification.id }} Specification +{% endfor %} +{% endfor %} diff --git a/python/openassetio_traitgen/templates/markdown/specifications.md.in b/python/openassetio_traitgen/templates/markdown/specifications.md.in new file mode 100644 index 0000000..d4a0a6d --- /dev/null +++ b/python/openassetio_traitgen/templates/markdown/specifications.md.in @@ -0,0 +1,19 @@ +{% for namespace in namespaces %} +# {{ namespace.id[0] | upper }}{{ namespace.id[1:] }} +{{ namespace.description | cleanup_md_string }} + +{% for specification in namespace.members %} +## {{ specification.id }} Specification +{% if specification.usage %} +**Usage:** {{ specification.usage | join(', ') }} +{% endif %} + +### Description +{{ specification.description | cleanup_md_string }} + +### Trait Set +{% for trait in specification.trait_set %} +- {{ trait.namespace }}.{{ trait.name }} +{% endfor %} +{% endfor %} +{% endfor %} diff --git a/python/openassetio_traitgen/templates/markdown/traits.md.in b/python/openassetio_traitgen/templates/markdown/traits.md.in new file mode 100644 index 0000000..4c6741e --- /dev/null +++ b/python/openassetio_traitgen/templates/markdown/traits.md.in @@ -0,0 +1,24 @@ +{% for namespace in namespaces %} +# {{ namespace.id[0] | upper }}{{ namespace.id[1:] }} +{{ namespace.description | cleanup_md_string}} + +{% for trait in namespace.members %} +## {{ trait.name }} Trait +**kId:** {{ trait.id }} +{% if trait.usage -%} +
**Usage:** {{ trait.usage | join(', ') }} +{% endif %} + +### Description +{{ trait.description | cleanup_md_string }} + +{% if trait.properties %} +### Properties +{% for property in trait.properties %} +**{{ property.id }} ({{ property.type | to_type_pretty_name }})** +
{{ property.description | cleanup_md_string }} +{% endfor %} +{% endif %} + +{% endfor %} +{% endfor %}