diff --git a/intersperse.py b/intersperse.py new file mode 100644 index 0000000000000000000000000000000000000000..ed1351c3d32d0aee45bbf3ba3db955d13aaf0339 --- /dev/null +++ b/intersperse.py @@ -0,0 +1,19 @@ +"""Intersperse.""" + +from typing import TypeVar +from collections.abc import Sequence, Generator + + +T = TypeVar('T') +U = TypeVar('U') + + +def intersperse(inset: U, sequence: Sequence[T]) -> Generator[U | T, None, None]: + """Intersperse the inset between each element in sequence.""" + if not sequence: + return + + yield sequence[0] + for item in sequence[1:]: + yield inset + yield item diff --git a/main.py b/main.py index 0999a5819165b183a1d903517baf2ef74d64c2c1..942db7351e7a9421ed241afa93ab3b192c1c1831 100644 --- a/main.py +++ b/main.py @@ -18,20 +18,22 @@ from typing import ( Literal, Tuple, TypeAlias, - TypeVar, Union, ) -from collections.abc import Sequence, Generator +from collections.abc import Sequence from dataclasses import dataclass +from jinja2 import Environment, PackageLoader +from intersperse import intersperse +env = Environment( + loader=PackageLoader('main'), + autoescape=False, +) HashEntry: TypeAlias = Union[Tuple[Literal['=>'], str, Any], Tuple[Literal['+>'], str, Any], Tuple[Literal['splat-hash'], Any]] -T = TypeVar('T') -U = TypeVar('U') - Context: TypeAlias = list['str'] match sys.argv: @@ -165,16 +167,6 @@ symbols: dict[str, str] = { } -def intersperse(inset: U, sequence: Sequence[T]) -> Generator[U | T, None, None]: - """Intersperse the inset between each element in sequence.""" - if not sequence: - return - - yield sequence[0] - for item in sequence[1:]: - yield inset - yield item - def handle_case_body(forms: list[dict[str, Any]], indent: int, context: Context) -> Tag: @@ -245,14 +237,16 @@ def parse(form: Any, indent: int, context: list[str]) -> Tag: return tag('[]', 'array') case ['array', *items]: - return tag([ - '[', - *([ind(indent+2), - parse(item, indent+1, context), - ','] for item in items), - ind(indent), - ']', - ], 'array') + out = ['[', '\n'] + for item in items: + out += [ + ind(indent+2), + parse(item, indent+1, context), + ',' + '\n', + ] + out += [ind(indent), ']'] + return tag(out, 'array') case ['call', {'functor': func, 'args': args}]: @@ -311,7 +305,7 @@ def parse(form: Any, indent: int, context: list[str]) -> Tag: for name, data in rest['params'].items(): items += [ind(indent+1)] if 'type' in data: - tt = parse(data['type'], indent, context) + tt = parse(data['type'], indent+1, context) # print('type =', tt, file=sys.stderr) items += [tag(tt, 'type'), ' '] @@ -319,7 +313,7 @@ def parse(form: Any, indent: int, context: list[str]) -> Tag: if 'value' in data: items += [ ' ', '=', ' ', - parse(data.get('value'), indent, context), + parse(data.get('value'), indent+1, context), ] items += [',', '\n'] items += [ind(indent), ')', ' ', '{', '\n'] @@ -339,7 +333,7 @@ def parse(form: Any, indent: int, context: list[str]) -> Tag: match item: case ['str', thingy]: content = parse(thingy, indent, ['str'] + context) - items += [tag(f'${content}', 'str-var')] + items += [tag(f'${{{content}}}', 'str-var')] case s: items += [s .replace('"', '\\"') @@ -483,7 +477,6 @@ def parse(form: Any, indent: int, context: list[str]) -> Tag: items += [ind(indent), '}', ' '] if 'else' in rest: - items = [] match rest['else']: case [['if', *rest]]: # TODO propper tagging @@ -583,6 +576,7 @@ def parse(form: Any, indent: int, context: list[str]) -> Tag: case ['regexp', s]: return tag(['/', tag(s, 'regex-body'), '/'], 'regex') + # Resource instansiation with exactly one instance case ['resource', {'type': t, 'bodies': [body]}]: items = [ @@ -627,52 +621,53 @@ def parse(form: Any, indent: int, context: list[str]) -> Tag: return tag(items) - # case ['resource', {'type': t, - # 'bodies': bodies}]: - # items = [] - # items += [ - # parse(t, indent, context), - # ' {', - # ] - # for body in bodies: - # items += [ - # ind(indent+1), - # parse(body['title'], indent, context), - # ':', '\n', - # ] - - # ops = body['ops'] - # namelen = ops_namelen(ops) - - # for item in ops: - # match item: - # case ['=>', key, value]: - # pad = namelen - len(key) - # items += [ - # ind(indent+2), - # tag(key, 'parameter'), - # ' '*pad, - # ' ⇒ ', - # parse(value, indent+2, context), - # ',', '\n', - # ] - - # case ['splat-hash', value]: - # items += [ - # ind(indent+2), - # tag('*', 'parameter', 'splat'), - # ' '*(namelen - 1), - # ' ⇒ ', - # parse(value, indent+2, context), - # ',', '\n', - # ] - - # case _: - # raise Exception("Unexpected item in resource:", item) - - # items += [ind(indent+1), ';', '\n'] - # items += [ind(indent), '}'] - # return tag(items) + # Resource instansiation with any number of instances + case ['resource', {'type': t, + 'bodies': bodies}]: + items = [] + items += [ + parse(t, indent, context), + ' ', '{', + ] + for body in bodies: + items += [ + '\n', ind(indent+1), + parse(body['title'], indent, context), + ':', '\n', + ] + + ops = body['ops'] + namelen = ops_namelen(ops) + + for item in ops: + match item: + case ['=>', key, value]: + pad = namelen - len(key) + items += [ + ind(indent+2), + tag(key, 'parameter'), + ' '*pad, + ' ', '⇒', ' ', + parse(value, indent+2, context), + ',', '\n', + ] + + case ['splat-hash', value]: + items += [ + ind(indent+2), + tag('*', 'parameter', 'splat'), + ' '*(namelen - 1), + ' ', '⇒', ' ', + parse(value, indent+2, context), + ',', '\n', + ] + + case _: + raise Exception("Unexpected item in resource:", item) + + items += [ind(indent+1), ';'] + items += ['\n', ind(indent), '}'] + return tag(items) case ['resource-defaults', {'type': t, 'ops': ops}]: @@ -994,7 +989,7 @@ def parse(form: Any, indent: int, context: list[str]) -> Tag: return tag(f'[|[{form}]|]', 'parse-error') -def print_docstring(name: str, docstring: dict[str, Any]) -> None: +def print_docstring(name: str, docstring: dict[str, Any]) -> str: """ Format docstrings as they appear in some puppet types. @@ -1005,6 +1000,8 @@ def print_docstring(name: str, docstring: dict[str, Any]) -> None: """ global param_doc + out = '' + if 'tags' in docstring: param_doc = {tag['name']: tag.get('text') or '' for tag in docstring['tags'] @@ -1018,146 +1015,268 @@ def print_docstring(name: str, docstring: dict[str, Any]) -> None: # param_defaults = d_type['defaults'] - print(f'<h2><code>{name}</code></h2>') + out += f'<h2><code>{name}</code></h2>\n' for tag in tags: text = html.escape(tag.get('text') or '') if tag['tag_name'] == 'summary': - print('<em class="summary">', end='') - print(text) - print('</em>') + out += '<em class="summary">' + out += text + out += '</em>' for tag in tags: text = html.escape(tag.get('text') or '') if tag['tag_name'] == 'example': - print(f'<h3>{tag["name"]}</h3>') - print(f'<pre><code class="puppet">{text}</code></pre>') + out += f'<h3>{tag["name"]}</h3>\n' + out += f'<pre><code class="puppet">{text}</code></pre>\n' if 'text' in docstring: - print('<div>') - print(commonmark(docstring['text'])) - print('</div>') + out += '<div>' + out += commonmark(docstring['text']) + out += '</div>' + + return out + + +@dataclass +class Tab: + """ + A single tab part of a tab group. + + :parameters: + title - Display name of the tab + id - Internal reference ID, must be globaly unique + content - contents of the tab + """ + + title: str + id: str + content: str + + +@dataclass +class TabGroup: + """A collection of tabs.""" + + id: str + tabs: list[Tab] + + +def tab_widget(tabgroup: TabGroup) -> str: + """ + Render a HTML tab widget. + + The argument is the list of tabs, nothing is returned, but instead + written to stdout. + """ + template = env.get_template('tabset.html') + return template.render(tabset=tabgroup) + + +__counter = 0 + + +def next_id(prefix: str = 'id') -> str: + """Return a new unique id.""" + global __counter + __counter += 1 + return f'{prefix}-{__counter}' + + +def tabs(panes: dict[str, str]) -> str: + """ + Build a tab widget from given dictionary. + + Keys are used as tab names, values as tab content. + Id's are generated. + """ + tabs = [] + for title, content in panes.items(): + tabs.append(Tab(title=title, content=content, id=next_id('tab'))) + + return tab_widget(TabGroup(id=next_id('tabgroup'), tabs=tabs)) + + +def format_class(d_type: dict[str, Any]) -> str: + """Format Puppet class.""" + out = '' + name = d_type['name'] + # print(name, file=sys.stderr) + print_docstring(name, d_type['docstring']) + + out += '<pre><code class="puppet">' + tree = parse_puppet(d_type['source']) + t = traverse(tree) + out += str(parse(t, 0, ['root'])) + out += '</code></pre>' + return out + + +def format_type() -> str: + """Format Puppet type.""" + return 'TODO format_type not implemented' + + +def format_type_alias(d_type: dict[str, Any]) -> str: + """Format Puppet type alias.""" + out = '' + name = d_type['name'] + # print(name, file=sys.stderr) + out += print_docstring(name, d_type['docstring']) + out += '\n' + out += '<pre><code class="puppet">' + tree = parse_puppet(d_type['alias_of']) + t = traverse(tree) + out += str(parse(t, 0, ['root'])) + out += '</code></pre>\n' + return out + + +def format_defined_type(d_type: dict[str, Any]) -> str: + """Format Puppet defined type.""" + out = '' + name = d_type['name'] + # print(name, file=sys.stderr) + out += print_docstring(name, d_type['docstring']) + + out += '<pre><code class="puppet">' + tree = parse_puppet(d_type['source']) + t = traverse(tree) + out += str(parse(t, 0, ['root'])) + out += '</code></pre>\n' + return out + + +def format_resource_type(r_type: dict[str, Any]) -> str: + """Format Puppet resource type.""" + name = r_type['name'] + out = '' + out += f'<h2>{name}</h2>\n' + out += str(r_type['docstring']) + if 'properties' in r_type: + out += '<h3>Properties</h3>\n' + out += '<ul>\n' + for property in r_type['properties']: + out += f'<li>{property["name"]}</li>\n' + # description, values, default + out += '</ul>\n' + + out += '<h3>Parameters</h3>\n' + out += '<ul>\n' + for parameter in r_type['parameters']: + out += f'<li>{parameter["name"]}</li>\n' + # description + # Optional[isnamevar] + out += '</ul>\n' + + if 'providers' in r_type: + out += '<h3>Providers</h3>\n' + for provider in r_type['providers']: + out += f'<h4>{provider["name"]}</h4>\n' + # TODO + + return out + + +def format_puppet_function(function: dict[str, Any]) -> str: + """Format Puppet function.""" + out = '' + name = function['name'] + out += f'<h2>{name}</h2>\n' + t = function['type'] + # docstring = function['docstring'] + for signature in function['signatures']: + signature['signature'] + signature['docstring'] + if t in ['ruby3x', 'ruby4x']: + out += f'<pre><code class="ruby">{function["source"]}</code></pre>\n' + elif t == 'puppet': + out += '<pre><code class="puppet">' + try: + tree = parse_puppet(function['source']) + t = traverse(tree) + out += str(parse(t, 0, ['root'])) + except CalledProcessError as e: + print(e, file=sys.stderr) + print(f"Failed on function: {name}", file=sys.stderr) + + out += '</code></pre>\n' + + return out + + +def format_puppet_task() -> str: + """Format Puppet task.""" + return 'TODO format_puppet_task not implemented' + + +def format_puppet_plan() -> str: + """Format Puppet plan.""" + return 'TODO format_puppet_plan not implemented' def main() -> None: """Entry point of program.""" - print('''<!doctype html> - <html> - <head> - <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <link type="text/css" rel="stylesheet" href="style.css"/> - <link type="text/css" rel="stylesheet" href="highlight.css"/> - </head> - <body> - ''') - - print('<h1>Puppet Classes</h1>') - for d_type in data['puppet_classes']: - name = d_type['name'] - # print(name, file=sys.stderr) - print_docstring(name, d_type['docstring']) - - print('<pre><code class="puppet">') - tree = parse_puppet(d_type['source']) - t = traverse(tree) - print(parse(t, 0, ['root'])) - print('</code></pre>') + out = '' - print('<hr/>') + out += '<h1>Puppet Classes</h1>' + for d_type in data['puppet_classes']: + out += f'<h2>{d_type["name"]}</h2>' + out += tabs({ + 'Formatted': format_class(d_type), + 'JSON': '<pre><code class="json">' + json.dumps(d_type, indent=2) + '</code></pre>', + 'Source': '<pre><code class="puppet">' + d_type['source'] + '</code></pre>', + }) + out += '<hr/>' - print('<h1>Data Types</h1>') + out += '<h1>Data Types</h1>' # TODO - print('<h1>Data Type Aliases</h1>') + out += '<h1>Data Type Aliases</h1>' for d_type in data['data_type_aliases']: - name = d_type['name'] - # print(name, file=sys.stderr) - print_docstring(name, d_type['docstring']) - print('<pre><code class="puppet">') - tree = parse_puppet(d_type['alias_of']) - t = traverse(tree) - print(parse(t, 0, ['root'])) - print('</code></pre>') - - print('<hr/>') - - print('<h1>Defined Types</h1>') - for d_type in data['defined_types']: - name = d_type['name'] - # print(name, file=sys.stderr) - print_docstring(name, d_type['docstring']) + out += tabs({ + 'Formatted': format_type_alias(d_type), + 'JSON': '<pre><code class="json">' + json.dumps(d_type, indent=2) + '</code></pre>', + }) - print('<pre><code class="puppet">') - tree = parse_puppet(d_type['source']) - t = traverse(tree) - print(parse(t, 0, ['root'])) - print('</code></pre>') + out += '<hr/>' - print('<hr/>') + out += '<h1>Defined Types</h1>' + for d_type in data['defined_types']: + out += tabs({ + 'Formatted': format_defined_type(d_type), + 'JSON': '<pre><code class="json">' + json.dumps(d_type, indent=2) + '</code></pre>', + 'Source': '<pre><code class="puppet">' + d_type['source'] + '</code></pre>', + }) + + out += '<hr/>' - print('<h1>Resource Types</h1>') + out += '<h1>Resource Types</h1>' for r_type in data['resource_types']: - name = r_type['name'] - print(f'<h2>{name}</h2>') - print(r_type['docstring']) - if 'properties' in r_type: - print('<h3>Properties</h3>') - print('<ul>') - for property in r_type['properties']: - print(f'<li>{property["name"]}</li>') - # description, values, default - print('</ul>') - - print('<h3>Parameters</h3>') - print('<ul>') - for parameter in r_type['parameters']: - print(f'<li>{parameter["name"]}</li>') - # description - # Optional[isnamevar] - print('</ul>') - - if 'providers' in r_type: - print('<h3>Providers</h3>') - for provider in r_type['providers']: - print(f'<h4>{provider["name"]}</h4>') - # TODO - - print('<h1>Puppet Functions</h1>') + out += tabs({ + 'Formatted': format_resource_type(r_type), + 'JSON': '<pre><code class="json">' + json.dumps(r_type, indent=2) + '</code></pre>', + }) + + out += '<h1>Puppet Functions</h1>' for function in data['puppet_functions']: - name = function['name'] - print(f'<h2>{name}</h2>') - t = function['type'] - # docstring = function['docstring'] - for signature in function['signatures']: - signature['signature'] - signature['docstring'] - if t in ['ruby3x', 'ruby4x']: - print(f'<pre><code class="ruby">{function["source"]}</code></pre>') - elif t == 'puppet': - print('<pre><code class="puppet">') - try: - tree = parse_puppet(function['source']) - t = traverse(tree) - print(parse(t, 0, ['root'])) - except CalledProcessError as e: - print(e) - print('</code></pre>') - - print('<h1>Puppet Tasks</h1>') + out += tabs({ + 'Formatted': format_puppet_function(function), + 'JSON': '<pre><code class="json">' + json.dumps(function, indent=2) + '</code></pre>', + 'Source': '<pre><code class="puppet">' + function['source'] + '</code></pre>', + }) + + out += '<h1>Puppet Tasks</h1>' # TODO - print('<h1>Puppet Plans</h1>') + out += '<h1>Puppet Plans</h1>' # TODO - print('</body></html>') - - -# for t in all_tags: -# print(t, file=sys.stderr) - + template = env.get_template('base.html') + print(template.render(content=out)) # TODO apache::* + if __name__ == '__main__': main() diff --git a/style.css b/style.css index 089626f4c77d0eca8101a59a4eb90a3b3ce7e5a7..a62934bd1cebd76b0cf731a9a28b55792a8df1ca 100644 --- a/style.css +++ b/style.css @@ -41,3 +41,11 @@ h2 { display: block; } */ + +.noscript { + display: none; +} + +code.json { + font-size: 80%; +} diff --git a/tabs.js b/tabs.js new file mode 100644 index 0000000000000000000000000000000000000000..6ae290c717782d6c24c7153c251a919e5e9638ca --- /dev/null +++ b/tabs.js @@ -0,0 +1,14 @@ +window.addEventListener('load', function () { + for (let tabgroup of document.getElementsByClassName('tabs')) { + let tab_labels = tabgroup.querySelectorAll('[data-tab]') + let tabs = tabgroup.getElementsByClassName('tab') + for (let tab_label of tab_labels) { + tab_label.addEventListener('click', function () { + for (let tab of tabs) { + tab.classList.remove('selected') + } + document.getElementById(tab_label.dataset.tab).classList.add('selected') + }) + } + } +}); diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000000000000000000000000000000000000..3ee0ef577e0649ba4fe95d9d94ecb5b7d053152d --- /dev/null +++ b/templates/base.html @@ -0,0 +1,24 @@ +<!doctype html> +<html> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <link type="text/css" rel="stylesheet" href="style.css"/> + <link type="text/css" rel="stylesheet" href="highlight.css"/> + <link type="text/css" rel="stylesheet" href="style2.css"/> + <script src="tabs.js"></script> + <noscript> + <style> +.noscript { + display: initial; +} + +.yesscript { + display: none; +} + </style> + </noscript> + </head> + <body>{{ content }}</body> +</html> +{# ft: jinja #} diff --git a/templates/tabset.html b/templates/tabset.html new file mode 100644 index 0000000000000000000000000000000000000000..d29f85e970a4f6a241d8d506fb8d13d34c2616bc --- /dev/null +++ b/templates/tabset.html @@ -0,0 +1,33 @@ +{# https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-automatic/ #} +<div class="tabs" data-tabset="{{ tabset.id }}"> + <menu class="yesscript" + role="tablist" > + {% for tab in tabset.tabs %} + <li data-tab="{{ tab.id }}"> + <button type="button" + role="tab" + aria-controls="{{ tab.id }}" + {% if first %} + aria-selected="true" + {% else %} + tabindex="-1" + {% endif %} + >{{ tab.title }}</button> + </li> + {% endfor %} + </menu> + {% for tab in tabset.tabs %} + <div class="tab" + id="{{ tab.id }}" + role="tabpanel" + tabindex="0" + aria-laballedby="{# TODO id of button#}" + > + <div class="noscript"> + <h2>{{ tab.title }}</h2> + </div> + <div>{{ tab.content }}</div> + </div> + {% endfor %} +</div> +{# ft:jinja2 #} diff --git a/tests/test_intersperse.py b/tests/test_intersperse.py index a8811db36dcec3c6b8ffd77a22b0f4bbc148ab75..7df28325319396774611ea84872e9344bf97954f 100644 --- a/tests/test_intersperse.py +++ b/tests/test_intersperse.py @@ -1,4 +1,4 @@ -from main import intersperse +from intersperse import intersperse def test_intersperse(): assert list(intersperse(1, [2, 3, 4])) == [2, 1, 3, 1, 4]