diff --git a/muppet/output/__init__.py b/muppet/output/__init__.py index d03a471fdfd4b9c0bc57d9e8b3b69a27187d940c..611ac4c94cfc8cc382a24f59b112ec78c1401696 100644 --- a/muppet/output/__init__.py +++ b/muppet/output/__init__.py @@ -4,7 +4,6 @@ Generate all output files. The primary entry point of this module is the class :class:`PuppetEnvironment`. """ -from dataclasses import dataclass, field import html import logging import os.path @@ -17,10 +16,6 @@ from typing import ( Sequence, cast, ) -from jinja2 import ( - Environment, - FileSystemLoader, -) from muppet.syntax_highlight import highlight from muppet.puppet.strings import ( @@ -43,15 +38,22 @@ from muppet.breadcrumbs import Breadcrumbs, breadcrumbs from muppet.util import group_by, partition from muppet.cache import AbstractCache +from jinja2 import ( + Environment, + FileSystemLoader, +) from .docstring import format_docstring from .puppet_source import hyperlink_puppet_source -from .util import HtmlSerializable +from .toc import ( + Toc, + # TocEntry, + TocHeader, + type_aliases_index, + resource_type_index, + index_item, -jinja = Environment( - loader=FileSystemLoader('templates'), - autoescape=False, ) @@ -139,7 +141,7 @@ class Templates: def module_index(self, *, # content: list[], # something with to_html_list and to_html - content: Sequence[HtmlSerializable], + content: Sequence[Toc], module_author: Optional[str], module_name: str, doc_files: list[tuple[str, str]], @@ -186,188 +188,6 @@ class Templates: right_sidebar=right_sidebar) -@dataclass -class ResourceTypeOutput: - - title: str - module_name: str - children: list[HtmlSerializable] = field(default_factory=list) - link: Optional[str] = None - summary: Optional[str] = None - - def base(self) -> str: - """ - Return base text of the node. - - If the node has a link, create a hyperlink, otherwise return - it's title directly. - """ - # TODO make links absolute to root, to allow inclusion anywhere - if self.link: - return f'<a href="{ self.link }">{ self.title }</a>' - else: - return self.title - - def to_html(self) -> str: - """Return HTML string.""" - # self.__class__.__name__ - return jinja \ - .get_template('snippets/ResourceType-index-entry.html') \ - .render(item=self, - module_name=self.module_name, - # TODO don't hardcode prefix - prefix='/code/muppet-strings/output') - - def to_html_list(self) -> str: - """Return HTML suitable for a list.""" - return jinja \ - .get_template('snippets/ResourceType-list-entry.html') \ - .render(item=self) - - -@dataclass -class IndexItem: - """ - A concrete type, on an index page. - - This will be something like a class or resource type. - - :param name: - Name of the resource or similar - :param file: - Relative path to the resource. - :param summary: - One line summary of the resource, will be displayed in the UI. - """ - - name: str - file: str - summary: Optional[str] = None - - def base(self) -> str: - """Return link to self.""" - # TODO make links absolute to root, to allow inclusion anywhere - return f'<a href="{self.file}">{ self.name }</a>' - - def to_html(self) -> str: - """Convert item to an HTML string.""" - out: str = '' - out += f'<dt>{self.base()}</dt>' - - if self.summary: - out += f"<dd>{self.summary}</dd>" - - return out - - def to_html_list(self) -> str: - """Convert itom to an HTML string sutibale for a list.""" - out: str = '' - out += f'<li>{self.base()}</li>' - return out - - -@dataclass -class IndexSubcategory: - """ - Subheading on index page. - - Will most likely be 'Public' or 'Private' objects for the given - top heading. - """ - - title: str - list: list[IndexItem] - - def to_html(self) -> str: - """Convert subcategory to an HTML string.""" - out: str = '' - out += f'<h3>{html.escape(self.title)}</h3>' - out += '<dl class="overview-list">' - - for item in self.list: - out += item.to_html() - - out += '</dl>' - - return out - - def to_html_list(self) -> str: - """Convert itom to an html string suitable for a list.""" - pass - out: str = '' - out += f'<li>{html.escape(self.title)}<ul>' - for item in self.list: - out += item.to_html_list() - out += '</ul></li>' - - return out - - -@dataclass -class IndexCategory: - """ - Top level heading in index. - - This should be something like 'Classes', 'Types', ... - Each entry contains a set of subentries, which can either be - distinct entries, or sometimes subcategories (such as 'Public' and - 'Private'). - """ - - title: str - list: list[IndexSubcategory] - - def base(self) -> str: - """Return HTML escaped title.""" - return html.escape(self.title) - - def to_html(self) -> str: - """Return class as an HTML string.""" - out: str = '' - out += f'<h2>{self.base()}</h2>' - - for item in self.list: - out += item.to_html() - - return out - - def to_html_list(self) -> str: - """Return class as an HTML string suitable for a list.""" - out: str = '' - out += f'<li>{self.base()}<ul>' - for item in self.list: - out += item.to_html_list() - out += '</ul></li>' - - return out - - -@dataclass -class ResourceIndex: - - title: str - children: list[HtmlSerializable] - - def to_html(self) -> str: - """Return something.""" - out: str = '' - out += f'<h2>{self.title}</h2>' - out += '<dl>' - for child in self.children: - out += child.to_html() - out += '</dl>' - return out - - def to_html_list(self) -> str: - """Return something.""" - out: str = '' - out += f'<li>{self.title}<ul>' - for child in self.children: - out += child.to_html_list() - out += '</ul></li>' - return out - - class PuppetModule: """ A representation of an entire Puppet module. @@ -425,10 +245,10 @@ class PuppetModule: self.module_toc: str = ''.join([ '<ul class="toc">', - *(e.to_html_list() for e in self._build_module_toc()), + *(e.to_html_list(path_base=self.output_prefix) for e in self._build_module_toc()), '</ul>']) - def _build_module_toc(self) -> Sequence[ResourceIndex | IndexCategory]: + def _build_module_toc(self) -> Sequence[Toc]: """ Build the TOC of the module. @@ -440,41 +260,38 @@ class PuppetModule: :returns: A list of categories. """ - content: list[ResourceIndex | IndexCategory] = [] + content: list[Toc] = [] if puppet_classes := self.strings_output.puppet_classes: - content.append(class_index(puppet_classes)) + content.append(class_index(self.name, puppet_classes)) # data_types if _ := self.strings_output.data_types: - content.append(IndexCategory( - title='Data types not yet implmented', - list=[])) + content.append(TocHeader( + name='Data types not yet implmented')) if data_type_aliases := self.strings_output.data_type_aliases: - content.append(type_aliases_index(data_type_aliases)) + content.append(type_aliases_index(self.name, data_type_aliases)) if defined_types := self.strings_output.defined_types: - content.append(defined_types_index(defined_types)) + content.append(defined_types_index(self.name, defined_types)) if resource_types := self.strings_output.resource_types: - content.append(ResourceIndex( - title='Resource Types', + content.append(TocHeader( + name='Resource Types', children=resource_type_index( resource_types, self.name))) # providers if _ := self.strings_output.providers: - content.append(IndexCategory( - title='Providers not yet implmented', - list=[])) + content.append(TocHeader( + name='Providers not yet implmented')) # puppet_functions if _ := self.strings_output.puppet_functions: - content.append(IndexCategory( - title='Puppet Functions not yet implmented', - list=[])) + content.append(TocHeader( + name='Puppet Functions not yet implmented')) # templates/ # files/ @@ -488,16 +305,13 @@ class PuppetModule: # puppet_tasks if _ := self.strings_output.puppet_tasks: - content.append(IndexCategory( - title='Puppet Tasks not yet implmented', - list=[])) + content.append(TocHeader( + name='Puppet Tasks not yet implmented')) # puppet_plans if _ := self.strings_output.puppet_plans: - content.append(IndexCategory( - title='Puppet Plans not yet implmented', - list=[], - )) + content.append(TocHeader( + name='Puppet Plans not yet implmented')) return content @@ -770,29 +584,29 @@ class PuppetEnvironment: module.output(destination) -def class_index(class_list: list[PuppetClass]) -> IndexCategory: +def class_index(module_name: str, class_list: list[PuppetClass]) -> Toc: """Prepage class index list.""" publics, privates = partition(isprivate, class_list) - lst: list[IndexSubcategory] = [] + lst: list[TocHeader] = [] if publics: - lst.append(IndexSubcategory( - title='Public Classes', - list=[index_item(i) for i in publics])) + lst.append(TocHeader( + name='Public Classes', + children=[index_item(module_name, i) for i in publics])) if privates: - lst.append(IndexSubcategory( - title='Private Classes', - list=[index_item(i) for i in privates])) + lst.append(TocHeader( + name='Private Classes', + children=[index_item(module_name, i) for i in privates])) - return IndexCategory( - title='Classes', - list=lst + return TocHeader( + name='Classes', + children=lst ) -def defined_types_index(defined_list: list[DefinedType]) -> IndexCategory: +def defined_types_index(module_name: str, defined_list: list[DefinedType]) -> TocHeader: """ Prepare defined types index list. @@ -801,107 +615,26 @@ def defined_types_index(defined_list: list[DefinedType]) -> IndexCategory: """ groups = group_by(isprivate, defined_list) - lst: list[IndexSubcategory] = [] - - if publics := groups.get(False): - lst.append(IndexSubcategory( - title='Public Defined Types', - list=[index_item(i) for i in publics], - )) - - if privates := groups.get(True): - lst.append(IndexSubcategory( - title='Private Defined Types', - list=[index_item(i) for i in privates], - )) - - return IndexCategory( - title='Defined Types', - list=lst - ) - + lst: list[TocHeader] = [] -def type_aliases_index(alias_list: list[DataTypeAlias]) -> IndexCategory: - """Prepare type alias index list.""" - groups = group_by(isprivate, alias_list) - lst: list[IndexSubcategory] = [] if publics := groups.get(False): - lst.append(IndexSubcategory( - title='Public Type Aliases', - list=[IndexItem(name=i.name, - file=os.path.splitext(i.file)[0]) - for i in publics], + lst.append(TocHeader( + name='Public Defined Types', + children=[index_item(module_name, i) for i in publics], )) if privates := groups.get(True): - lst.append(IndexSubcategory( - title='Private Type Aliases', - list=[IndexItem(name=i.name, - file=os.path.splitext(i.file)[0]) - for i in privates], + lst.append(TocHeader( + name='Private Defined Types', + children=[index_item(module_name, i) for i in privates], )) - return IndexCategory( - title='Type Aliases', - list=lst, + return TocHeader( + name='Defined Types', + children=lst ) -def resource_type_index(resource_types: list[ResourceType], - module_name: str) -> list[HtmlSerializable]: - """ - Generate index of all known resource types. - """ - lst: list[HtmlSerializable] = [] - - for resource_type in resource_types: - # resource_type['file'] - # resource_type['docstring'] - - items: list[HtmlSerializable] = [] - if providers := resource_type.providers: - for provider in providers: - # TODO summary tag? - # TODO, instead, render markdown and take first paragraph - documentation = provider.docstring.text.split('\n')[0] - items.append(ResourceTypeOutput( - title=provider.name, - link=provider.file, - module_name=module_name, - summary=documentation)) - - lst.append(ResourceTypeOutput(title=resource_type.name, - module_name=module_name, - children=items)) - - return lst - - -def index_item(obj: PuppetClass | DefinedType) -> IndexItem: - """ - Format a puppet type declaration into an index entry. - - :param obj: - A dictionary at least containing the keys 'name' and 'file', - and optionally containing 'docstring'. If docstring is present - then a summary tag is searched for, and added to the resulting - object. - """ - name = obj.name - - out: IndexItem = IndexItem( - file=os.path.splitext(obj.file)[0], - name=name, - ) - - for tag in obj.docstring.tags: - if tag.tag_name == 'summary': - out.summary = markdown(tag.text) - break - - return out - - def format_class(d_type: DefinedType | PuppetClass) -> tuple[str, str]: """Format Puppet class.""" out = '' diff --git a/muppet/output/toc.py b/muppet/output/toc.py new file mode 100644 index 0000000000000000000000000000000000000000..517a017c79f622d9789961ffac5e38d74bebfb20 --- /dev/null +++ b/muppet/output/toc.py @@ -0,0 +1,307 @@ +""" +Procedures for generating a table of contents. + +This is both used for the main page, containing everything, including +summary of them. As well as the sidebar summary which only contains +(hyperlinked) names. + +A table of contents here is defined as a list of :class:`TocHeader` +objects, where each one is a top level entry. +Each entry may then contain subheading, or :class:`TocEntry` entries, +which represent concrete data which may have summaries (in our cases, +puppet classes, functions, ...) +""" + +from dataclasses import dataclass, field +from typing import ( + Any, + TypeAlias, + Union, + cast, +) +import html +import os.path + +from muppet.puppet.strings import ( + DataTypeAlias, + DefinedType, + PuppetClass, + ResourceType, + isprivate, +) +from muppet.util import group_by +from muppet.markdown import markdown + +from jinja2 import ( + Environment, + FileSystemLoader, +) + + +jinja = Environment( + loader=FileSystemLoader('templates'), + autoescape=False, +) + + +""" +--- +- name: Classes + children: + - name: Public classes + children: + - name: apt + url: apt/manifests/init + summary: "Main class, includes all other classes." +- name: Resource Types + children: + - name: apt_key + url: lib/puppet/type/apt_key + summary: "A summary of apt_key would have gone here" + children: + - name: apt_key + url: lib/puppet/provider/apt_key/apt_key + summary: "Summary of this specific provider" + +""" + + +def html_tag(tag: str, content: str, **attributes: str) -> str: + """ + Generate the string of an HTML tag. + + .. code-block:: python + :caption: Example invocation and output + + >>> html_tag('a', 'A link', href='example.com') + '<a href="example.com">A link</a>' + + + *NOTE* none of the arguments will be escaped. + + :param tag: + The containing tag. + + :param content: + The body of the HTML. Note that this is NOT escaped, to allow + nesting. Use ``html.escape`` if escaping is wanted. + + :param attributes: + Remaining key-value attributes are passed along as HTML + attributes. + """ + out = f'<{tag}' + for key, value in attributes.items(): + out += f' {key}="{value}"' + out += '>' + out += content + out += f'</{tag}>' + return out + + +Toc: TypeAlias = Union['TocHeader', 'TocEntry'] +TocList: TypeAlias = list['TocHeader'] | list['TocEntry'] + + +@dataclass +class TocHeader: + """ + A header entry in a TOC list. + + This is an entry which which will in "rich" form be generated with + a header tag. Either way, it can't contain further data, except + for children. + + :param name: + The header of the entry. Think of this as the ``<h[2-6]>`` tag. + + :param children: + A list of either only other TocHeader objects, or only + TocEntry objects, the distinction since I don't think it makes + sense to allow both at the same level. + """ + + name: str + # children: list['Toc'] = field(default_factory=list) + children: TocList = field(default_factory=lambda: cast(TocList, [])) + + def to_html(self, *, path_base: str, level: int, **args: Any) -> str: + """Generate an HTML representation suitable for a standalone toc page.""" + out = [] + out.append(html_tag(f'h{level}', html.escape(self.name))) + if ch := self.children: + if isinstance(ch[0], TocHeader): + out += [c.to_html(path_base=path_base, level=level + 1, **args) for c in ch] + elif isinstance(ch[0], TocEntry): + out.append(html_tag( + 'dl', ''.join(c.to_html(path_base=path_base, **args) for c in ch))) + return ''.join(out) + + def to_html_list(self, path_base: str) -> str: + """Generate an HTML representation suitable for a compact list.""" + out = '<li>' + out += html.escape(self.name) + if ch := self.children: + out += html_tag('ul', ''.join(c.to_html_list(path_base=path_base) for c in ch)) + out += '</li>' + return out + + +@dataclass +class TocEntry: + """ + An entry in a TOC list which may contain data. + + In Puppet's case, this is anything which actually links to a file + (or similar). + + :param name: + Name of the object, should be something like a class name. + + :param url: + Relative URL to where the documentation for that object will be located. + + :param summary: + An (optional) short summary of the object. Will most likely be + extracted from an ``@summary`` tag. + + :param children: + Even these can have children. Mostly used by resource types for their providers. + """ + + name: str + url: str + summary: str = '' + children: list['TocEntry'] = field(default_factory=list) + + def to_html(self, *, path_base: str, **_: Any) -> str: + """ + Generate an HTML representation suitable for a standalone toc page. + + Varargs are taken to be compatible with other implementations + actually taking arguments. + """ + out = [] + out.append(html_tag('dt', + html_tag('a', html.escape(self.name), + href=f'{path_base}/{self.url}'))) + out.append('<dd>') + out.append(self.summary) + if ch := self.children: + out.append(html_tag( + 'dl', ''.join(c.to_html(path_base=path_base) for c in ch))) + out.append('</dd>') + return ''.join(out) + + def to_html_list(self, *, path_base: str) -> str: + """Generate an HTML representation suitable for a compact list.""" + out = '<li>' + out += html_tag('a', html.escape(self.name), href=f'{path_base}/{self.url}') + # Summary intentionally ignored + if ch := self.children: + out += html_tag('ul', ''.join(c.to_html_list(path_base=path_base) for c in ch)) + out += '</li>' + return out + + +def type_aliases_index(module_name: str, alias_list: list[DataTypeAlias]) -> TocHeader: + """ + Prepare type alias index list. + + A concrete type, on an index page. + + This will be something like a class or resource type. + + Subheading on index page. + + Will most likely be 'Public' or 'Private' objects for the given + top heading. + + Top level heading in index. + + This should be something like 'Classes', 'Types', ... + Each entry contains a set of subentries, which can either be + distinct entries, or sometimes subcategories (such as 'Public' and + 'Private'). + + :param name: + Name of the resource or similar + :param file: + Relative path to the resource. + :param summary: + One line summary of the resource, will be displayed in the UI. + """ + groups = group_by(isprivate, alias_list) + lst: list[TocHeader] = [] + if publics := groups.get(False): + lst.append(TocHeader( + name='Public Type Aliases', + children=[TocEntry(name=i.name, + url=f"{module_name}/{os.path.splitext(i.file)[0]}") + for i in publics], + )) + + if privates := groups.get(True): + lst.append(TocHeader( + name='Private Type Aliases', + children=[TocEntry(name=i.name, + url=f"{module_name}/{os.path.splitext(i.file)[0]}") + for i in privates], + )) + + return TocHeader( + name='Type Aliases', + children=lst, + ) + + +def resource_type_index(resource_types: list[ResourceType], + module_name: str) -> list[TocEntry]: + """Generate index of all known resource types.""" + lst: list[TocEntry] = [] + + for resource_type in resource_types: + # resource_type['file'] + # resource_type['docstring'] + + items: list[TocEntry] = [] + if providers := resource_type.providers: + for provider in providers: + # TODO summary tag? + # TODO, instead, render markdown and take first paragraph + documentation = provider.docstring.text.split('\n')[0] + items.append(TocEntry( + name=provider.name, + url=provider.file, + summary=documentation)) + + lst.append(TocEntry(name=resource_type.name, + url='#TODO', + children=items)) + + return lst + + +def index_item(module_name: str, obj: PuppetClass | DefinedType) -> TocEntry: + """ + Format a puppet type declaration into an index entry. + + :param obj: + A dictionary at least containing the keys 'name' and 'file', + and optionally containing 'docstring'. If docstring is present + then a summary tag is searched for, and added to the resulting + object. + """ + name = obj.name + + out: TocEntry = TocEntry( + name=name, + url=f'{module_name}/{os.path.splitext(obj.file)[0]}', + ) + + for tag in obj.docstring.tags: + if tag.tag_name == 'summary': + out.summary = markdown(tag.text) + break + + return out diff --git a/muppet/output/util.py b/muppet/output/util.py index 2c3c3a3256e30d97fde053b6cc109c96f0b58909..cdb0fa8798d563db269d4fc98d3759df11d95ad6 100644 --- a/muppet/output/util.py +++ b/muppet/output/util.py @@ -7,7 +7,7 @@ useful than other. The aim is to only have pure functions here. """ -from typing import Protocol +from typing import Any, Protocol from muppet.parser_combinator import ( MatchCompound, MatchObject, @@ -48,10 +48,10 @@ def inner_text(obj: MatchObject | list[MatchObject]) -> str: class HtmlSerializable(Protocol): """Classes which can be serialized as HTML.""" - def to_html(self) -> str: # pragma: no cover + def to_html(self, **_: Any) -> str: # pragma: no cover """Return HTML string.""" ... - def to_html_list(self) -> str: # pragma: no cover + def to_html_list(self, **_: Any) -> str: # pragma: no cover """Return HTML suitable for a list.""" ... diff --git a/templates/module_index.html b/templates/module_index.html index d00e73a455625103f85c45d53d32cb4772a6e9c8..05d92e45c1bca4eccd524176ed6c080ae9117fec 100644 --- a/templates/module_index.html +++ b/templates/module_index.html @@ -35,7 +35,7 @@ Parameters: </ul> {% for entry in content %} - {{ entry.to_html() }} + {{ entry.to_html(path_base=path_base, level=2) }} {% endfor %} {% endblock %} diff --git a/templates/snippets/ResourceType-index-entry.html b/templates/snippets/ResourceType-index-entry.html deleted file mode 100644 index 45501f46e1de01820cd1b57bdfc482108c0de86f..0000000000000000000000000000000000000000 --- a/templates/snippets/ResourceType-index-entry.html +++ /dev/null @@ -1,20 +0,0 @@ -{# -:param item: An instance of ResourceTypeOutput -:param prefix: Prefix for HTTP output path, - (e.g. '/code/muppet-strings/output') -:param module_name: - -#} -<dt><a href="{{ prefix }}/{{ module_name }}/lib/puppet/types/{{ item.base() }}.rb">{{ item.base() }}</a></dt> -<dd> - {% if item.summary %} - <!-- TODO docstring.text --> - {{ item.summary }} - {% endif %} - <dl> - {% for provider in item.children %} - {{ provider.to_html() }} - {% endfor %} - </dl> -</dd> -{# ft:jinja2 #} diff --git a/templates/snippets/ResourceType-list-entry.html b/templates/snippets/ResourceType-list-entry.html deleted file mode 100644 index dedb1a04dcd04b84b319d45a9eafd4939ed411c3..0000000000000000000000000000000000000000 --- a/templates/snippets/ResourceType-list-entry.html +++ /dev/null @@ -1,10 +0,0 @@ -{# -#} -<li><a href="#">{{ item.base() }}</a> - <ul> - {% for provider in item.children %} - {{ provider.to_html_list() }} - {% endfor %} - </ul> -</li> -{# ft:jinja2 #}