diff --git a/muppet/data/__init__.py b/muppet/data/__init__.py index 9e685be9229845736a9327092935bf1480478c1e..6666e5bff2ff87264b85e38cb5bd8b85ed379265 100644 --- a/muppet/data/__init__.py +++ b/muppet/data/__init__.py @@ -22,6 +22,7 @@ from typing import ( Markup: TypeAlias = Union[str, 'Tag', + 'Declaration', 'Link', 'ID', 'Documentation', @@ -49,6 +50,20 @@ class Tag: return f'tag({repr(self.item)}, tags={self.tags})' +@dataclass +class Declaration(Tag): + """ + Superset of tag, containing declaration statements. + + Mostly used for class and resource parameters. + + :param variable: + Name of the variable being declared. + """ + + variable: str + + @dataclass class Link: """An item which should link somewhere.""" @@ -103,6 +118,11 @@ def tag(item: Markup | Sequence[Markup], *tags: str) -> Tag: return Tag(item, tags=tags) +def declaration(item: Markup | Sequence[Markup], *tags: str, variable: str) -> Declaration: + """Mark name of variable in declaration.""" + return Declaration(item, tags=tags, variable=variable) + + def link(item: Markup, target: str) -> Link: """Create a new link element.""" return Link(item, target) diff --git a/muppet/data/html.py b/muppet/data/html.py index 67edef91c2d06f15cf37d51dc38a991f459d6046..df4f0f2d25db0d54571eb675961ab9776146dae5 100644 --- a/muppet/data/html.py +++ b/muppet/data/html.py @@ -2,6 +2,7 @@ from . import ( Tag, + Declaration, Link, ID, Documentation, @@ -11,10 +12,21 @@ from . import ( ) from collections.abc import Sequence import html +from dataclasses import dataclass, field +@dataclass class HTMLRenderer(Renderer): - """Render the document into HTML.""" + """ + Render the document into HTML. + + :param param_documentation: + A dictionary containing (rendered) documentation for each + parameter of the class or resource type currently being + rendered. + """ + + param_documentation: dict[str, str] = field(default_factory=dict) def render_tag(self, tag: Tag) -> str: """Attaches all tags as classes in a span.""" @@ -26,8 +38,18 @@ class HTMLRenderer(Renderer): else: inner = render(self, tag.item) + out = '' + if isinstance(tag, Declaration): + if comment := self.param_documentation.get(tag.variable): + if isinstance(tag.item, list) \ + and tag.item \ + and isinstance(tag.item[0], Indentation): + out += render(self, tag.item[0]) + out += f'<span class="comment">{comment.strip()}</span>\n' + tags = ' '.join(tag.tags) - return f'<span class="{tags}">{inner}</span>' + out += f'<span class="{tags}">{inner}</span>' + return out def render_link(self, link: Link) -> str: """Wrap the value in an anchor tag.""" diff --git a/muppet/format.py b/muppet/format.py index c9bce7ccacfe568f0ca20c881b6cc7bcc4fdd48a..67aa5550530f35ed799c4dc049c11c9607583718 100644 --- a/muppet/format.py +++ b/muppet/format.py @@ -10,6 +10,7 @@ from commonmark import commonmark from subprocess import CalledProcessError import html import sys +import re from typing import ( Any, Literal, @@ -29,6 +30,7 @@ from .data import ( id, link, tag, + declaration, render, ) from .data.html import ( @@ -260,18 +262,20 @@ def parse(form: Any, indent: int, context: list[str]) -> Tag: if 'params' in rest: items += ['(', '\n'] for name, data in rest['params'].items(): - items += [ind(indent+1)] + decls: list[Markup] = [] + decls += [ind(indent+1)] if 'type' in data: tt = parse(data['type'], indent+1, context) - items += [tag(tt, 'type'), + decls += [tag(tt, 'type'), ' '] - items += [tag(f'${name}', 'var')] + decls += [declare_var(name)] if 'value' in data: - items += [ + decls += [ ' ', '=', ' ', # TODO this is a declaration parse(data.get('value'), indent+1, context), ] + items += [declaration(decls, 'declaration', variable=name)] items += [',', '\n'] items += [ind(indent), ')', ' ', '{', '\n'] else: @@ -320,18 +324,20 @@ def parse(form: Any, indent: int, context: list[str]) -> Tag: if params := rest.get('params'): items += ['(', '\n'] for name, data in params.items(): - items += [ind(indent+1)] + decl: list[Markup] = [] + decl += [ind(indent+1)] if 'type' in data: - items += [tag(parse(data['type'], indent, context), - 'type'), - ' '] + decl += [tag(parse(data['type'], indent, context), + 'type'), + ' '] # print(f'<span class="var">${name}</span>', end='') - items += [declare_var(name)] + decl += [declare_var(name)] if 'value' in data: - items += [ + decl += [ ' ', '=', ' ', parse(data.get('value'), indent, context), ] + items += [declaration(decl, 'declaration', variable=name)] items += [',', '\n'] items += [ind(indent), ')', ' '] @@ -1050,8 +1056,20 @@ def print_docstring(name: str, docstring: dict[str, Any]) -> str: for t in tags: text = html.escape(t.get('text') or '') if t['tag_name'] == 'example': - out += f'<h3>{t["name"]}</h3>\n' - out += f'<pre><code class="puppet">{text}</code></pre>\n' + if name := t.get('name'): + out += f'<h3>{name}</h3>\n' + out += f'<pre class="example"><code class="puppet">{text}</code></pre>\n' + + # out += '<dl>' + # for t in tags: + # if t['tag_name'] == 'param': + # out += f"<dt>{t['name']}</dt>" + # if text := t.get('text'): + # text = re.sub(r'(NOTE|TODO)', + # r'<mark>\1</mark>', + # commonmark(text)) + # out += f"<dd>{text}</dd>" + # out += '</dl>' if 'text' in docstring: out += '<div>' @@ -1061,19 +1079,45 @@ def print_docstring(name: str, docstring: dict[str, Any]) -> str: return out -renderer = HTMLRenderer() +def build_param_dict(docstring: dict[str, Any]) -> dict[str, str]: + """ + Extract all parameter documentation from a docstring dict. + + :param docstring: + The object present under 'docstring' in the information about + a single object (class, resource, ...) in the output of + `puppet strings`. + + :returns: + A dictionary where the keys are the variables which have + documentation, and the value is the (formatted) documentation + for that key. Undocumented keys (even those with the tag, but + no text) are ommitted from the resulting dictionary. + """ + if tags := docstring.get('tags'): + obj = {} + for t in tags: + if t['tag_name'] == 'param': + if text := t.get('text'): + obj[t['name']] = re.sub(r'(NOTE|TODO)', + r'<mark>\1</mark>', + commonmark(text)) + return obj + else: + return {} def format_class(d_type: dict[str, Any]) -> str: """Format Puppet class.""" + t = parse_puppet(d_type['source']) + data = parse(t, 0, ['root']) + renderer = HTMLRenderer(build_param_dict(d_type['docstring'])) out = '' name = d_type['name'] # print(name, file=sys.stderr) - print_docstring(name, d_type['docstring']) + out += print_docstring(name, d_type['docstring']) out += '<pre><code class="puppet">' - t = parse_puppet(d_type['source']) - data = parse(t, 0, ['root']) out += render(renderer, data) out += '</code></pre>' return out @@ -1086,6 +1130,7 @@ def format_type() -> str: def format_type_alias(d_type: dict[str, Any]) -> str: """Format Puppet type alias.""" + renderer = HTMLRenderer() out = '' name = d_type['name'] # print(name, file=sys.stderr) @@ -1101,6 +1146,7 @@ def format_type_alias(d_type: dict[str, Any]) -> str: def format_defined_type(d_type: dict[str, Any]) -> str: """Format Puppet defined type.""" + renderer = HTMLRenderer(build_param_dict(d_type['docstring'])) out = '' name = d_type['name'] # print(name, file=sys.stderr) diff --git a/static/highlight.css b/static/highlight.css index 4949f3bca1b46cdfb86d2c543c362fcfc77ff60f..b3aa5d0258fc31d8225722eb7df395cd9a3fb304 100644 --- a/static/highlight.css +++ b/static/highlight.css @@ -43,3 +43,7 @@ .string { color: olive; } + +.comment { + color: grey; +} diff --git a/static/style.css b/static/style.css index 43e414463086ecfc7359477e81074ee8f7d06ab9..fb19678a0ed02564100d9635f7bfad42d1827b0e 100644 --- a/static/style.css +++ b/static/style.css @@ -57,3 +57,26 @@ code.json { .overview-list p { display: inline; } + +.example { + background: lightgray; + padding: 1em; + border-radius: 1ex; +} + +.comment { + border-left: 1ex; + border-left-style: dotted; + display: inline-block; + padding-left: 1em; + font-family: sans; + font-size: 80%; +} + +.comment p:first-child { + margin-top: 0; +} + +.comment p:last-child { + margin-bottom: 0; +}