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]