diff --git a/.flake8 b/.flake8
index 4dbcc0f56f7ebf92d2a5e5b4ba1bc38a2c9d2a66..cc60e253e58bc42af1148249e461fbfb3fb1eaeb 100644
--- a/.flake8
+++ b/.flake8
@@ -1,4 +1,5 @@
 [flake8]
+extend-ignore = D105
 per-file-ignores =
 	tests/*: D103
 max-line-length = 99
diff --git a/highlight.css b/highlight.css
new file mode 100644
index 0000000000000000000000000000000000000000..4949f3bca1b46cdfb86d2c543c362fcfc77ff60f
--- /dev/null
+++ b/highlight.css
@@ -0,0 +1,45 @@
+/*
+.case { color: ; }
+.splat { color: ; }
+.array { color: ; }
+.parse-error { color: ; }
+.parameter { color: ; }
+.string { color: ; }
+.regex { color: ; }
+.invoke { color: ; }
+.default { color: ; }
+.call { color: ; }
+.qr { color: ; }
+.lambda { color: ; }
+.number { color: ; }
+.regex-body { color: ; }
+.call-method { color: ; }
+*/
+
+.literal { color: green; }
+.true {}
+.false {}
+.undef {}
+
+.keyword { color: orange; }
+.class {}
+.or {}
+.define {}
+.unless {}
+.if {}
+.else {}
+.function {}
+.and {}
+.in {}
+
+.type { color: darkgreen; }
+.qn { color: darkgreen; }
+
+.var { color: blue; }
+.str-var { color: blue; }
+
+.name { color: red; }
+
+.string {
+	color: olive;
+}
diff --git a/main.py b/main.py
index 5e8ca79f7b377b119376c0fb7df384fc2ba589fa..baec3d648a21bf54b9edbd8ca3559a8c7bb023d7 100644
--- a/main.py
+++ b/main.py
@@ -14,18 +14,26 @@ import html
 import json
 import sys
 from typing import (
-    Union,
-    Literal,
     Any,
-    TypeAlias,
+    Literal,
     Tuple,
+    TypeAlias,
+    TypeVar,
+    Union,
 )
+from collections.abc import Sequence, Generator
+from dataclasses import dataclass
 
 
 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:
     case [_, d, *_]:
         filename = d
@@ -40,28 +48,71 @@ data = info
 param_doc: dict[str, str] = {}
 
 
+@dataclass
+class Tag:
+    """An item with basic metadata."""
+
+    # item: Any  # str | 'Tag' | Sequence[str | 'Tag']
+    # item: str | 'Tag' | Sequence[str | 'Tag']
+    item: Any
+    tags: Sequence[str]
+
+    def __str__(self) -> str:
+        inner: str
+        if isinstance(self.item, str):
+            inner = self.item
+        elif isinstance(self.item, Tag):
+            inner = str(self.item)
+        else:
+            inner = ''.join(str(i) for i in self.item)
+
+        tags = ' '.join(self.tags)
+        return f'<span class="{tags}">{inner}</span>'
+
+
+all_tags: set[str] = set()
+
+
+def tag(item: str | Tag | Sequence[str | Tag], *tags: str) -> Tag:
+    """Tag item with tags."""
+    global all_tags
+    all_tags |= set(tags)
+    return Tag(item, tags=tags)
+
+
+def ind(level: int) -> str:
+    """Returnu a string for indentation."""
+    return ' '*level*2
+
+
 def print_hash(hash: list[HashEntry],
                indent: int,
-               context: list[str]) -> None:
+               context: Context) -> Tag:
     """Print the contents of a puppet hash literal."""
     if not hash:
-        return
+        return tag('')
     # namelen = 0
+    items: list[str | Tag] = []
     for item in hash:
         match item:
             case ['=>', key, value]:
-                print(' '*indent*2, end='')
-                parse(key, indent, context)
-                # print(' =&gt; ', end='')
-                print(' ⇒ ', end='')
-                parse(value, indent, context)
+                items += [
+                    ind(indent),
+                    parse(key, indent, context),
+                    ' ⇒ ',
+                    parse(value, indent, context),
+                ]
             case ['splat-hash', value]:
-                print(' '*indent*2, end='')
-                print('* =&gt; ', end='')
-                parse(value, indent, context)
+                items += [
+                    ind(indent),
+                    '* ⇒ ',
+                    parse(value, indent, context),
+                ]
             case _:
-                print(f'<span class="parse-error">[|[{item}]|]</span>')
-        print(',')
+                items += [tag(f'[|[{item}]|]', 'parse-error'), '\n']
+        items += [',', '\n']
+
+    return tag(items)
 
 
 def ops_namelen(ops: list[HashEntry]) -> int:
@@ -82,9 +133,6 @@ def ops_namelen(ops: list[HashEntry]) -> int:
 
 def print_array(arr: list[Any], indent: int, context: list[str]) -> None:
     """Print a puppet array literal."""
-    if not arr:
-        print('[]', end='')
-        return
     print('[')
     for item in arr:
         print(' '*(indent+1)*2, end='')
@@ -93,7 +141,7 @@ def print_array(arr: list[Any], indent: int, context: list[str]) -> None:
     print(' '*indent*2 + ']', end='')
 
 
-def print_var(x: str, dollar: bool = True) -> None:
+def print_var(x: str, dollar: bool = True) -> Tag:
     """
     Print the given variable.
 
@@ -105,10 +153,10 @@ def print_var(x: str, dollar: bool = True) -> None:
     """
     dol = '$' if dollar else ''
     if doc := param_doc.get(x):
-        print(f'<span class="var">{dol}{x}<div class="documentation">{doc}</div></span>',
-              end='')
+        s = f'{dol}{x}<div class="documentation">{doc}</div>'
+        return tag(s, 'var')
     else:
-        print(f'<span class="var">{dol}{x}</span>', end='')
+        return tag(f'{dol}{x}', 'var')
 
 # TODO strip leading colons when looking up documentation
 
@@ -127,7 +175,40 @@ symbols: dict[str, str] = {
 }
 
 
-def parse(form: Any, indent: int, context: list[str]) -> None:
+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:
+    """Handle case body when parsing AST."""
+    ret: list[Tag | str] = []
+    for form in forms:
+        when = form['when']
+        then = form['then']
+        ret += [ind(indent+1)]
+        # cases = []
+
+        ret += list(intersperse(', ', list(parse(item, indent+1, context)
+                                           for item in when)))
+
+        ret += [':', ' ', '{', '\n']
+
+        for item in then:
+            ret += [ind(indent+2), parse(item, indent+2, context), '\n']
+        ret += [ind(indent+1), '},', '\n']
+
+    return tag(ret)
+
+
+def parse(form: Any, indent: int, context: list[str]) -> Tag:
     """
     Print everything from a puppet parse tree.
 
@@ -140,227 +221,240 @@ def parse(form: Any, indent: int, context: list[str]) -> None:
     # Sorted per `sort -V`
     match form:
         case None:
-            print('<span class="undef">undef</span>', end='')
+            return tag('undef', 'literal', 'undef')
 
         case True:
-            print('<span class="true">true</span>', end='')
+            return tag('true', 'literal', 'true')
 
         case False:
-            print('<span class="false">false</span>', end='')
+            return tag('false', 'literal', 'false')
 
         case ['access', how, *args]:
-            print('<span class="compound-type">', end='')
-            parse(how, indent, context)
-            print('[', end='')
-            first = True
-            for arg in args:
-                if not first:
-                    print(', ', end='')
-                # TODO newlines?
-                parse(arg, indent, context)
-                first = False
-            print(']', end='')
-            print('</span>', end='')
+            # TODO newlines?
+            t = [parse(arg, indent, context) for arg in args]
+            return tag([
+                parse(how, indent, context),
+                '[',
+                *intersperse(', ', t),
+                ']',
+            ], 'access')
 
         case ['and', a, b]:
-            parse(a, indent, context)
-            print(' and ', end='')
-            parse(b, indent, context)
+            return tag([
+                parse(a, indent, context),
+                ' ',
+                tag('and', 'keyword', 'and'),
+                ' ',
+                parse(b, indent, context),
+            ])
+
+        case ['array']:
+            return tag('[]', 'array')
 
         case ['array', *items]:
-            print_array(items, indent+1, context)
+            return tag([
+                '[',
+                *([ind(indent+2),
+                   parse(item, indent+1, context),
+                   ','] for item in items),
+                ind(indent),
+                ']',
+            ], 'array')
 
         case ['call', {'functor': func,
                        'args': args}]:
-            print('<span class="call">', end='')
-            parse(func, indent, context)
-            print('(', end='')
-            first = True
-            for arg in args:
-                if not first:
-                    print(', ', end='')
-                first = False
-                parse(arg, indent, context)
-            print(')', end='')
-            print('</span>', end='')
+            return tag([
+                parse(func, indent, context),
+                '(',
+                *intersperse(', ', [parse(arg, indent, context)
+                                    for arg in args]),
+                ')',
+            ], 'call')
 
         case ['call-method', func]:
-            print('<span class="call-method">', end='')
-            parse(func['functor'], indent, context)
+            items = [
+                parse(func['functor'], indent, context)
+            ]
 
-            first = True
             if not ('block' in func and func['args'] == []):
-                print('(', end='')
-                for x in func['args']:
-                    if not first:
-                        print(', ', end='')
-                    first = False
-                    parse(x, indent, context)
-                # print(', '.join(parse(x, indent, context) for x in func['args']), end='')
-                print(')', end='')
+                items += [
+                    '(',
+                    *intersperse(', ', [parse(x, indent, context)
+                                        for x in func['args']]),
+                    ')',
+                ]
 
             if 'block' in func:
-                parse(func['block'], indent+1, context)
+                items += [parse(func['block'], indent+1, context)]
 
-            print('</span>', end='')
-
-            # print()
+            return tag(items, 'call-method')
 
         case ['case', test, forms]:
-            print('case ', end='')
-            parse(test, indent, context)
-            print(' {')
-            for form in forms:
-                when = form['when']
-                then = form['then']
-                print(' '*(indent+1)*2, end='')
-                # print('{', end='')
-                cases = []
-                first = True
-                for item in when:
-                    if not first:
-                        print(', ', end='')
-                    first = False
-                    parse(item, indent+1, context)
-                print(': {')
-                for item in then:
-                    print(' '*(indent+2)*2, end='')
-                    parse(item, indent+2, context)
-                    print()
-                print(' '*(indent+1)*2+'},')
-            print(' '*indent*2+'}', end='')
+            items = [
+                tag('case', 'keyword', 'case'),
+                ' ',
+                parse(test, indent, context),
+                ' ', '{', '\n',
+                handle_case_body(forms, indent, context),
+                ind(indent),
+                '}',
+            ]
+
+            return tag(items)
 
         case ['class', {'name': name,
                         'body': body,
                         **rest}]:
-            print(' '*indent*2 +
-                  f'<span class="class">class</span> <span class="name">{name}</span> ', end='')
+            items = []
+            items += [
+                ind(indent),
+                tag('class', 'keyword', 'class'),
+                ' ',
+                tag(name, 'name'),
+                ' ',
+            ]
+
             if 'params' in rest:
-                print('(')
+                items += ['(', '\n']
                 for name, data in rest['params'].items():
-                    print(' '*(indent+1)*2, end='')
+                    items += [ind(indent+1)]
                     if 'type' in data:
-                        print('<span class="type">', end='')
-                        parse(data['type'], indent, context)
-                        print('</span> ', end='')
-                    print(f'<span class="var">${name}</span>', end='')
+                        tt = parse(data['type'], indent, context)
+                        # print('type =', tt, file=sys.stderr)
+                        items += [tag(tt, 'type'),
+                                  ' ']
+                    items += [tag(f'${name}', 'var')]
                     if 'value' in data:
-                        print(' = ', end='')
-                        parse(data.get('value'), indent, context)
-                        print(',', end='')
-                    print()
-                print(' '*indent*2 + ') {')
+                        items += [
+                            ' = ',
+                            parse(data.get('value'), indent, context),
+                        ]
+                    items += [',', '\n']
+                items += [ind(indent), ')', ' ', '{', '\n']
             else:
-                print('{')
+                items += ['{', '\n']
 
             for entry in body:
-                print(' '*(indent+1)*2, end='')
-                parse(entry, indent+1, context)
-                print()
-
-            print(' '*indent*2+'}')
+                items += [ind(indent+1),
+                          parse(entry, indent+1, context),
+                          '\n']
+            items += [ind(indent), '}' + '\n']
+            return tag(items)
 
         case ['concat', *args]:
-            print('<span class="string">"', end='')
+            items = ['"']
             for item in args:
                 match item:
                     case ['str', thingy]:
-                        print('<span class="str-var">${', end='')
-                        # print_var(x, dollar=False)
-                        parse(thingy, indent, ['str'] + context)
-                        print('}</span>', end='')
+                        content = parse(thingy, indent, ['str'] + context)
+                        items += [tag(f'${content}', 'str-var')]
                     case s:
-                        # print(s, file=sys.stderr)
-                        print(s
-                              .replace('"', '\\"')
-                              .replace('\n', '\\n'),
-                              end='')
-
-            print('"</span>', end='')
+                        items += [s
+                                  .replace('"', '\\"')
+                                  .replace('\n', '\\n')]
+            items += '"'
+            return tag(items, 'string')
 
         case ['collect', {'type': t,
                           'query': q}]:
-            parse(t, indent, context)
-            print(' ', end='')
-            parse(q, indent, context)
+            return tag([parse(t, indent, context),
+                        ' ',
+                        parse(q, indent, context)])
 
         case ['default']:
-            print('<span class="default">default</span>', end='')
+            return tag('default', 'keyword', 'default')
 
         case ['define', {'name': name,
                          'body': body,
                          **rest}]:
-            print(' '*indent*2 +
-                  f'<span class="define">define</span> <span class="name">{name}</span> ', end='')
+            items = [ind(indent),
+                     tag('define', 'keyword', 'define'),
+                     ' ',
+                     tag(name, 'name'),
+                     ' ']
 
             if params := rest.get('params'):
-                print('(')
+                items += ['(', '\n']
                 for name, data in params.items():
-                    print(' '*(indent+1)*2, end='')
+                    items += [ind(indent+1)]
                     if 'type' in data:
-                        print('<span class="type">', end='')
-                        parse(data['type'], indent, context)
-                        print('</span> ', end='')
+                        items += [tag(parse(data['type'], indent, context),
+                                      'type'),
+                                  ' ']
                     # print(f'<span class="var">${name}</span>', end='')
-                    print_var(name)
+                    items += [print_var(name)]
                     if 'value' in data:
-                        print(' = ', end='')
-                        parse(data.get('value'), indent, context)
-                        print(',', end='')
-                    print()
+                        items += [
+                            ' = ',
+                            parse(data.get('value'), indent, context),
+                            ',',
+                        ]
+                    items += ['\n']
 
-                print(' '*indent*2 + ') ', end='')
-            print('{')
+                items += [ind(indent), ')']
+
+            items += ['{', '\n']
 
             for entry in body:
-                print(' '*(indent+1)*2, end='')
-                parse(entry, indent+1, context)
-                print()
+                items += [ind(indent+1),
+                          parse(entry, indent+1, context),
+                          '\n']
+
+            items += [ind(indent), '}']
 
-            print(' '*indent*2 + '}', end='')
+            return tag(items)
 
         case ['exported-query']:
-            print('<<| |>>', end='')
+            return tag(['<<|', ' ', '|>>'])
 
         case ['exported-query', arg]:
-            print('<<| ', end='')
-            parse(arg, indent, context)
-            print(' |>>', end='')
+            return tag(['<<|', ' ',
+                        parse(arg, indent, context),
+                        ' ', '|>>'])
 
         case ['function', {'name': name,
                            'body': body,
                            **rest}]:
-            print(f'function {name} (')
+            items = []
+            items += [tag('function', 'keyword', 'function'),
+                      ' ', name, ' ', '(', '\n']
             if 'params' in rest:
                 for name, attributes in rest['params'].items():
-                    print(' '*(indent+1)*2, end='')
+                    items += [ind(indent+1)]
                     if 'type' in attributes:
-                        parse(attributes['type'], indent, context)
-                        print(' ', end='')
-                    print(f'${name}', end='')
+                        items += [parse(attributes['type'], indent, context),
+                                  ' ']
+                    items += [f'${name}']
                     if 'value' in attributes:
-                        print(' = ', end='')
-                        parse(attributes['value'], indent, context)
-                    print(',')
-            print(')', end='')
+                        items += [
+                            ' = ',
+                            parse(attributes['value'], indent, context),
+                        ]
+                    items += [',', '\n']
+            items += [')']
             if 'returns' in rest:
-                print(' >> ', end='')
-                parse(rest['returns'], indent, context)
-            print(' {')
+                items += [' >> ',
+                          parse(rest['returns'], indent, context)]
+            items += [' ', '{', '\n']
             for item in body:
-                print(' '*(indent+1)*2, end='')
-                parse(item, indent+1, context)
-                print()
-            print('}')
+                items += [
+                    ind(indent+1),
+                    parse(item, indent+1, context),
+                    '\n',
+                ]
+            items += ['}', '\n']
+            return tag(items)
+
+        case ['hash']:
+            return tag('{}')
 
         case ['hash', *hash]:
-            if not hash:
-                print('{}', end='')
-            else:
-                print('{')
-                print_hash(hash, indent+1, context)
-                print(' '*indent*2, end='')
-                print('}', end='')
+            return tag([
+                '{', '\n',
+                print_hash(hash, indent+1, context),
+                ind(indent),
+                '}',
+            ])
 
         case ['heredoc', {'text': text}]:
             # TODO Should variables be interploated?
@@ -372,110 +466,136 @@ def parse(form: Any, indent: int, context: list[str]) -> None:
             # NOTE text can be a number of types
             # It can be an explicit "concat",
             # it can be an untagged string
-            print('@("EOF")')
-            parse(text, indent + 1, ['heredoc'] + context)
-            print(' '*(indent+1)*2 + '| EOF', end='')
+            tag(['@("EOF")', '\n',
+                 parse(text, indent + 1, ['heredoc'] + context),
+                 ind(indent+1),
+                 '|', ' ', 'EOF',
+                 ])
 
         case ['if', {'test': test,
                      **rest}]:
-            print('if ', end='')
-            parse(test, indent, context)
-            print(' {')
+            items = []
+            items += [
+                tag('if', 'keyword', 'if'),
+                ' ',
+                parse(test, indent, context),
+                ' ', '{', '\n',
+            ]
             if 'then' in rest:
                 for item in rest['then']:
-                    print(' '*(indent+1)*2, end='')
-                    parse(item, indent+1, context)
-                    print()
-            print(' '*indent*2 + '} ', end='')
+                    items += [
+                        ind(indent+1),
+                        parse(item, indent+1, context),
+                        '\n',
+                    ]
+            items += [ind(indent), '}', ' ']
 
             if 'else' in rest:
+                items = []
                 match rest['else']:
                     case [['if', *rest]]:
-                        print('els', end='')
-                        parse(['if', *rest], indent, context)
+                        # TODO propper tagging
+                        items += ['els',
+                                  parse(['if', *rest], indent, context)]
                     case el:
-                        print('else {')
+                        items += [tag('else', 'keyword', 'else'),
+                                  ' ', '{', '\n']
                         for item in el:
-                            print(' '*(indent+1)*2, end='')
-                            parse(item, indent+1, context)
-                            print()
-                        print(' '*indent*2+'}', end='')
+                            items += [
+                                ind(indent+1),
+                                parse(item, indent+1, context),
+                                '\n',
+                            ]
+                        items += [
+                            ind(indent),
+                            '}',
+                        ]
+            return tag(items)
 
         case ['in', needle, stack]:
-            parse(needle, indent, context)
-            print(' in ', end='')
-            parse(stack, indent, context)
+            return tag([
+                parse(needle, indent, context),
+                ' ', tag('in', 'keyword', 'in'), ' ',
+                parse(stack, indent, context),
+            ])
 
         case ['invoke', {'functor': func,
                          'args': args}]:
-            print('<span class="invoke">', end='')
-            parse(func, indent, context)
-            print(' ', end='')
+            items = [
+                parse(func, indent, context),
+                ' ',
+            ]
             if len(args) == 1:
-                parse(args[0], indent+1, context)
+                items += [parse(args[0], indent+1, context)]
             else:
-                print(args)
-                print('(', end='')
-                first = True
-                for arg in args:
-                    if not first:
-                        print(', ', end='')
-                    first = False
-                    parse(arg, indent+1, context)
-                # print(' '*indent*2, end='')
-                print(')', end='')
-            # print()
-            print('</span>', end='')
+                items += [
+                    args,
+                    '\n',
+                    '(',
+                    *intersperse(', ', [parse(arg, indent+1, context)
+                                        for arg in args]),
+                    ')',
+                ]
+            return tag(items, 'invoke')
 
         case ['nop']:
-            # print()
-            pass
+            return tag('')
 
         case ['lambda', {'params': params,
                          'body': body}]:
-            print('<span class="lambda">', end='')
-            args = ', '.join(f'${x}' for x in params.keys())
-            print(f' |{args}| {{')
+            items = []
+            args = [', '.join(f'${x}' for x in params.keys())]
+            items += [f' |{args}| {{', '\n']
             for entry in body:
-                print(' '*indent*2, end='')
-                parse(entry, indent, context)
-                print()
-            print(' '*(indent-1)*2 + '}', end='')
-            print('</span>', end='')
+                items += [
+                    ind(indent),
+                    parse(entry, indent, context),
+                    '\n',
+                ]
+            items += [ind(indent-1), '}']
+            return tag(items, 'lambda')
+
         case ['and', a, b]:
-            parse(a, indent, context)
-            print(' and ', end='')
-            parse(b, indent, context)
+            return tag([
+                parse(a, indent, context),
+                ' ', tag('and', 'keyword', 'and'), ' ',
+                parse(b, indent, context),
+            ])
 
         case ['or', a, b]:
-            parse(a, indent, context)
-            print(' or ', end='')
-            parse(b, indent, context)
+            return tag([
+                parse(a, indent, context),
+                ' ', tag('or', 'keyword', 'or'), ' ',
+                parse(b, indent, context),
+            ])
 
         case ['paren', *forms]:
-            print('(', end='')
-            for form in forms:
-                parse(form, indent+1, context)
-            print(')', end='')
+            return tag([
+                '(',
+                *(parse(form, indent+1, context)
+                  for form in forms),
+                ')',
+            ])
 
         # Qualified name?
         case ['qn', x]:
-            print(f'<span class="qn">{x}</span>', end='')
+            return tag(x, 'qn')
 
         # Qualified resource?
         case ['qr', x]:
-            print(f'<span class="qr">{x}</span>', end='')
+            return tag(x, 'qr')
 
         case ['regexp', s]:
-            print(f'<span class="regex">/<span class="regex-body">{s}</span>/</span>',
-                  end='')
+            return tag(['/', tag(s, 'regex-body'), '/'], 'regex')
 
         case ['resource', {'type': t,
                            'bodies': [body]}]:
-            parse(t, indent, context)
-            print(' { ', end='')
-            parse(body['title'], indent, context)
-            print(':')
+            items = [
+                parse(t, indent, context),
+                ' ', '{', ' ',
+                parse(body['title'], indent, context),
+                ':', '\n',
+            ]
             ops = body['ops']
 
             namelen = ops_namelen(ops)
@@ -483,298 +603,401 @@ def parse(form: Any, indent: int, context: list[str]) -> None:
             for item in ops:
                 match item:
                     case ['=>', key, value]:
-                        print(' '*(indent+1)*2, end='')
                         pad = namelen - len(key)
-                        print(f'<span class="parameter">{key}</span>', end='')
-                        # print(' '*pad + ' => ', end='')
-                        print(' '*pad + ' ⇒ ', end='')
-                        parse(value, indent+1, context)
-                        print(',')
+                        items += [
+                            ind(indent+1),
+                            tag(key, 'parameter'),
+                            ' '*pad, ' ⇒ ',
+                            parse(value, indent+1, context),
+                            ',', '\n',
+                        ]
 
                     case ['splat-hash', value]:
-                        print(' '*(indent+1)*2, end='')
-                        print('<span class="parameter splat">*</span>', end='')
-                        # print(' '*(namelen - 1) + ' => ', end='')
-                        print(' '*(namelen - 1) + ' ⇒ ', end='')
-                        parse(value, indent+1, context)
-                        print(',')
+                        items += [
+                                ind(indent+1),
+                                tag('*', 'parameter', 'splat'),
+                                ' '*(namelen-1),
+                                ' ⇒ ',
+                                parse(value, indent+1, context),
+                                ',', '\n',
+                        ]
 
                     case _:
                         raise Exception("Unexpected item in resource:", item)
-            print(' '*indent*2+'}', end='')
 
-        case ['resource', {'type': t,
-                           'bodies': bodies}]:
-            parse(t, indent, context)
-            print(' {')
-            for body in bodies:
-                print(' '*(indent+1)*2, end='')
-                parse(body['title'], indent, context)
-                print(':')
-                ops = body['ops']
-
-                namelen = ops_namelen(ops)
-
-                for item in ops:
-                    match item:
-                        case ['=>', key, value]:
-                            print(' '*(indent+2)*2, end='')
-                            pad = namelen - len(key)
-                            print(f'<span class="parameter">{key}</span>', end='')
-                            # print(' '*pad + ' => ', end='')
-                            print(' '*pad + ' ⇒ ', end='')
-                            parse(value, indent+2, context)
-                            print(',')
-
-                        case ['splat-hash', value]:
-                            print(' '*(indent+2)*2, end='')
-                            print('<span class="parameter splat">*</span>', end='')
-                            # print(' '*(namelen - 1) + ' => ', end='')
-                            print(' '*(namelen - 1) + ' ⇒ ', end='')
-                            parse(value, indent+2, context)
-                            print(',')
-
-                        case _:
-                            raise Exception("Unexpected item in resource:", item)
-
-                print(' '*(indent+1)*2 + ';')
-            print(' '*indent*2+'}', end='')
+            items += [
+                ind(indent),
+                '}',
+            ]
+
+            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)
 
         case ['resource-defaults', {'type': t,
                                     'ops': ops}]:
-            parse(t, indent, context)
-            print(' {')
+            items = [
+                parse(t, indent, context),
+                ' ', '{', '\n',
+            ]
             namelen = ops_namelen(ops)
             for op in ops:
                 match op:
                     case ['=>', key, value]:
-                        print(' '*(indent+1)*2, end='')
                         pad = namelen - len(key)
-                        print(f'<span class="parameter">{key}</span>', end='')
-                        # print(' '*pad + ' => ', end='')
-                        print(' '*pad + ' ⇒ ', end='')
-                        parse(value, indent+3, context)
-                        print(',')
+                        items += [
+                            ind(indent+1),
+                            tag(key, 'parameter'),
+                            ' '*pad,
+                            ' ⇒ ',
+                            parse(value, indent+3, context),
+                            ',', '\n',
+                        ]
 
                     case ['splat-hash', value]:
-                        print(' '*(indent+1)*2, end='')
                         pad = namelen - 1
-                        print('<span class="parameter splat">*</span>', end=' '*pad)
-                        print(' '*(namelen - 1) + ' ⇒ ', end='')
-                        parse(value, indent+2, context)
-                        print(',')
+                        items += [
+                            ind(indent+1),
+                            tag('*', 'parameter', 'splat'),
+                            ' '*pad,
+                            ' '*(namelen-1),
+                            ' ⇒ ',
+                            parse(value, indent+2, context),
+                            ',', '\n',
+                        ]
 
                     case x:
                         raise Exception('Unexpected item in resource defaults:', x)
-            print(' '*indent*2 + '}', end='')
+
+            items += [ind(indent),
+                      '}']
+
+            return tag(items)
 
         case ['resource-override', {'resources': resources,
                                     'ops': ops}]:
-            parse(resources, indent, context)
-            print(' {')
+            items = [
+                parse(resources, indent, context),
+                ' ', '{', '\n',
+            ]
             namelen = ops_namelen(ops)
             for op in ops:
                 match op:
                     case ['=>', key, value]:
-                        print(' '*(indent+1)*2, end='')
                         pad = namelen - len(key)
-                        print(f'<span class="parameter">{key}</span>', end='')
-                        # print(' '*pad + ' => ', end='')
-                        print(' '*pad + ' ⇒ ', end='')
-                        parse(value, indent+3, context)
-                        print(',')
+                        items += [
+                            ind(indent+1),
+                            tag(key, 'parameter'),
+                            ' '*pad,
+                            ' ⇒ ',
+                            parse(value, indent+3, context),
+                            ',', '\n',
+                        ]
 
                     case ['+>', key, value]:
-                        print(' '*(indent+2)*2, end='')
                         pad = namelen - len(key)
-                        print(f'<span class="parameter">{key}</span>', end='')
-                        # print(' '*pad + ' => ', end='')
-                        print(' '*pad + ' +> ', end='')
-                        parse(value, indent+2, context)
-                        print(',')
+                        items += [
+                            ind(indent+2),
+                            tag(key, 'parameter'),
+                            ' '*pad,
+                            ' +> ',
+                            parse(value, indent+2, context),
+                            ',', '\n',
+                        ]
 
                     case ['splat-hash', value]:
-                        print(' '*(indent+1)*2, end='')
                         pad = namelen - 1
-                        print('<span class="parameter splat">*</span>', end=' '*pad)
-                        print(' '*(namelen - 1) + ' ⇒ ', end='')
-                        parse(value, indent+2, context)
-                        print(',')
+                        items += [
+                            ind(indent+1),
+                            tag('*', 'parameter', 'splat'),
+                            ' '*pad,
+                            ' '*(namelen-1),
+                            ' ⇒ ',
+                            parse(value, indent+2, context),
+                            ',', '\n',
+                        ]
 
                     case _:
                         raise Exception('Unexpected item in resource override:',
                                         op)
-            print(' '*indent*2 + '}', end='')
+
+            items += [
+                ind(indent),
+                '}',
+            ]
+
+            return tag(items)
 
         case ['unless', {'test': test,
                          'then': then}]:
-            print('unless ', end='')
-            parse(test, indent, context)
-            print(' {')
+            items = [
+                tag('unless', 'keyword', 'unless'),
+                ' ',
+                parse(test, indent, context),
+                ' ', '{', '\n',
+            ]
             for item in then:
-                print(' '*(indent+1)*2, end='')
-                parse(item, indent+1, context)
-                print()
+                items += [
+                    ind(indent+1),
+                    parse(item, indent+1, context),
+                    '\n',
+                ]
 
-            print(' '*indent*2 + '}', end='')
+            items += [
+                ind(indent),
+                '}',
+            ]
 
         case ['var', x]:
             # TODO how does this work with deeply nested expressions
             # in strings?
-            print_var(x, context[0] != 'str')
+            return print_var(x, context[0] != 'str')
 
         case ['virtual-query', q]:
-            print('<| ', end='')
-            parse(q, indent, context)
-            print(' |>', end='')
+            return tag([
+                '<|', ' ',
+                parse(q, indent, context),
+                ' ', '|>',
+            ])
 
         case ['virtual-query']:
-            print('<| |>', end='')
+            return tag(['<|', ' ', '|>'])
 
         # TODO unary splat
 
         case ['!', x]:
-            # print('! ', end='')
-            print('¬ ', end='')
-            parse(x, indent, context)
+            return tag([
+                '¬', ' ',
+                parse(x, indent, context),
+            ])
 
         case ['!=', a, b]:
-            parse(a, indent, context)
-            # print(' != ', end='')
-            print(' ≠ ', end='')
-            parse(b, indent, context)
+            return tag([
+                parse(a, indent, context),
+                ' ≠ ',
+                parse(b, indent, context),
+            ])
 
         case ['+', a, b]:
-            parse(a, indent, context)
-            print(' + ', end='')
-            parse(b, indent, context)
+            return tag([
+                parse(a, indent, context),
+                ' + ',
+                parse(b, indent, context),
+            ])
 
         case ['-', a, b]:
-            parse(a, indent, context)
-            print(' - ', end='')
-            parse(b, indent, context)
+            return tag([
+                parse(a, indent, context),
+                ' - ',
+                parse(b, indent, context),
+            ])
 
         case ['-', a]:
-            print('- ', end='')
-            parse(a)
+            return tag([
+                '- ',
+                parse(a),
+            ])
 
         case ['*', a, b]:
-            parse(a, indent, context)
-            print(' × ', end='')
-            parse(b, indent, context)
+            return tag([
+                parse(a, indent, context),
+                ' × ',
+                parse(b, indent, context),
+            ])
 
         case ['%', a, b]:
-            parse(a, indent, context)
-            print(' % ', end='')
-            parse(b, indent, context)
+            return tag([
+                parse(a, indent, context),
+                ' % ',
+                parse(b, indent, context),
+            ])
 
         case ['<<', a, b]:
-            parse(a, indent, context)
-            print(' << ', end='')
-            parse(b, indent, context)
+            return tag([
+                parse(a, indent, context),
+                '  << ',
+                parse(b, indent, context),
+            ])
 
         case ['>>', a, b]:
-            parse(a, indent, context)
-            print(' >> ', end='')
-            parse(b, indent, context)
+            return tag([
+                parse(a, indent, context),
+                ' >> ',
+                parse(b, indent, context),
+            ])
 
         case ['>=', a, b]:
-            parse(a, indent, context)
-            print(' ≥ ', end='')
-            parse(b, indent, context)
+            return tag([
+                parse(a, indent, context),
+                ' ≥ ',
+                parse(b, indent, context),
+            ])
 
         case ['<=', a, b]:
-            parse(a, indent, context)
-            print(' ≤ ', end='')
-            parse(b, indent, context)
+            return tag([
+                parse(a, indent, context),
+                ' ≤ ',
+                parse(b, indent, context),
+            ])
 
         case ['>', a, b]:
-            parse(a, indent, context)
-            print(' > ', end='')
-            parse(b, indent, context)
+            return tag([
+                parse(a, indent, context),
+                ' > ',
+                parse(b, indent, context),
+            ])
 
         case ['<', a, b]:
-            parse(a, indent, context)
-            print(' < ', end='')
-            parse(b, indent, context)
+            return tag([
+                parse(a, indent, context),
+                ' < ',
+                parse(b, indent, context),
+            ])
 
         case ['~>', left, right]:
-            parse(left, indent, context)
-            print(f'\n{" "*indent*2}⤳ ', end='')
-            # print(f'\n{" "*indent*2}~&gt; ', end='')
-            parse(right, indent, context)
+            return tag([
+                parse(left, indent, context),
+                '\n',
+                ind(indent),
+                '⤳', ' ',
+                parse(right, indent, context)
+            ])
 
         case ['->', left, right]:
-            parse(left, indent, context)
-            # print(f'\n{" "*indent*2}-&gt; ', end='')
-            print(f'\n{" "*indent*2}→ ', end='')
-            parse(right, indent, context)
+            return tag([
+                parse(left, indent, context),
+                '\n',
+                ind(indent),
+                '→ ',
+                parse(right, indent, context),
+            ])
 
         case ['.', left, right]:
-            parse(left, indent, context)
-            print()
-            print(' '*indent*2, end='.')
-            parse(right, indent+1, context)
+            return tag([
+                parse(left, indent, context),
+                '\n',
+                ind(indent),
+                '.',
+                parse(right, indent+1, context),
+            ])
 
         case ['/', a, b]:
-            parse(a, indent, context)
-            print(' / ', end='')
-            parse(b, indent, context)
+            return tag([
+                parse(a, indent, context),
+                ' / ',
+                parse(b, indent, context),
+            ])
 
         case ['=', field, value]:
-            # print('  ', end='')
-            parse(field, indent, context)
-            print(' = ', end='')
-            parse(value, indent, context)
+            return tag([
+                parse(field, indent, context),
+                ' = ',
+                parse(value, indent, context),
+            ])
 
         case ['==', a, b]:
-            parse(a, indent, context)
-            # print(' == ', end='')
-            print(' ≡ ', end='')
-            parse(b, indent, context)
+            return tag([
+                parse(a, indent, context),
+                ' ≡ ',
+                parse(b, indent, context),
+            ])
 
         case ['=~', a, b]:
-            parse(a, indent, context)
-            print(' =~ ', end='')
-            parse(b, indent, context)
+            return tag([
+                parse(a, indent, context),
+                ' =~ ',
+                parse(b, indent, context),
+            ])
 
         case ['!~', a, b]:
-            parse(a, indent, context)
-            print(' ≁ ', end='')
-            parse(b, indent, context)
+            return tag([
+                parse(a, indent, context),
+                ' ≁ ',
+                parse(b, indent, context),
+            ])
 
         case ['?', condition, cases]:
-            print('<span class="case">', end='')
-            parse(condition, indent, context)
-            print(' ? {')
-            print_hash(cases, indent+1, context)
-            print(' '*indent*2 + '}', end='')
-            print('</span>', end='')
+            return tag([
+                parse(condition, indent, context),
+                ' ? {',
+                '\n',
+                print_hash(cases, indent+1, context),
+                ind(indent),
+                '}',
+                ], 'case')
 
         case form:
-            if type(form) == str:
-                print('<span class="string">', end='')
+            if isinstance(form, str):
                 if context[0] == 'heredoc':
-                    ind = ' '*indent*2
                     lines: list[str]
                     match form.split('\n'):
                         case [*_lines, '']:
                             lines = _lines
                         case _lines:
                             lines = _lines
+
+                    items = []
                     for line in lines:
-                        print(ind + line)
+                        items += [ind(indent), line, '\n']
+
+                    return tag(items, 'literal', 'string')
                 else:
                     s = form.replace('\n', r'\n')
-                    print(f"'{s}'", end='')
-                print('</span>', end='')
-            elif type(form) == int or type(form) == float:
-                print(f'<span class="number">{form}</span>', end='')
+                    s = f"'{s}'"
+                    return tag(s, 'literal', 'string')
+
+            elif isinstance(form, int) or isinstance(form, float):
+                return tag(str(form), 'literal', 'number')
             else:
-                print(f'<span class="parse-error">[|[{form}]|]</span>', end='')
+                return tag(f'[|[{form}]|]', 'parse-error')
 
 
-def print_docstring(docstring: dict[str, Any]) -> None:
+def print_docstring(name: str, docstring: dict[str, Any]) -> None:
     """
     Format docstrings as they appear in some puppet types.
 
@@ -819,119 +1042,125 @@ def print_docstring(docstring: dict[str, Any]) -> None:
         print('</div>')
 
 
-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"/>
-  </head>
-  <body>
-''')
+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('<h1>Puppet Classes</h1>')
-for d_type in data['puppet_classes']:
-    name = d_type['name']
-    print(name, file=sys.stderr)
-    print_docstring(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>')
 
-    print('<pre><code class="puppet">')
-    tree = parse_puppet(d_type['source'])
-    t = traverse(tree)
-    parse(t, 0, ['root'])
-    print('</code></pre>')
+        print('<hr/>')
 
-    print('<hr/>')
+    print('<h1>Data Types</h1>')
 
+# TODO
 
-print('<h1>Data Types</h1>')
+    print('<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>')
 
-# TODO
+        print('<hr/>')
 
-print('<h1>Data Type Aliases</h1>')
-for d_type in data['data_type_aliases']:
-    name = d_type['name']
-    print(name, file=sys.stderr)
-    print_docstring(d_type['docstring'])
-    print('<pre><code class="puppet">')
-    tree = parse_puppet(d_type['alias_of'])
-    t = traverse(tree)
-    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(d_type['docstring'])
-
-    print('<pre><code class="puppet">')
-    tree = parse_puppet(d_type['source'])
-    t = traverse(tree)
-    parse(t, 0, ['root'])
-    print('</code></pre>')
-
-    print('<hr/>')
-
-
-print('<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('<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'])
 
-    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>')
-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)
-            parse(t, 0, ['root'])
-        except CalledProcessError as e:
-            print(e)
+        tree = parse_puppet(d_type['source'])
+        t = traverse(tree)
+        print(parse(t, 0, ['root']))
         print('</code></pre>')
 
-print('<h1>Puppet Tasks</h1>')
+        print('<hr/>')
+
+    print('<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>')
+    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>')
 # TODO
-print('<h1>Puppet Plans</h1>')
+    print('<h1>Puppet Plans</h1>')
 # TODO
 
+    print('</body></html>')
 
-print('</body></html>')
+
+# for t in all_tags:
+#     print(t, file=sys.stderr)
 
 
 # TODO apache::*
+
+if __name__ == '__main__':
+    main()
diff --git a/style.css b/style.css
index a693aeb0f403e364db71cf215c25623ca21ec1a9..089626f4c77d0eca8101a59a4eb90a3b3ce7e5a7 100644
--- a/style.css
+++ b/style.css
@@ -1,42 +1,3 @@
-.qn {
-	color: green;
-}
-
-.var {
-	color: blue;
-}
-
-.define {
-	color: orange;
-}
-
-.name {
-	color: red;
-}
-
-.string {
-	color: olive;
-}
-
-.str-var {
-	color: pink;
-}
-
-.qr {
-	color: darkgreen;
-}
-
-.compound-type {
-	color: lightblue;
-}
-
-.undef {
-	color: lightgray;
-}
-
-.number {
-	color: red;
-}
 
 /* -------------------------------------------------- */
 
@@ -57,6 +18,11 @@ h2 {
 
 /* -------------------------------------------------- */
 
+.documentation {
+	display: none;
+}
+
+/*
 .var {
 	position: relative;
 }
@@ -74,3 +40,4 @@ h2 {
 .var:hover .documentation {
 	display: block;
 }
+*/
diff --git a/tests/test_intersperse.py b/tests/test_intersperse.py
new file mode 100644
index 0000000000000000000000000000000000000000..a8811db36dcec3c6b8ffd77a22b0f4bbc148ab75
--- /dev/null
+++ b/tests/test_intersperse.py
@@ -0,0 +1,7 @@
+from main import intersperse
+
+def test_intersperse():
+    assert list(intersperse(1, [2, 3, 4])) == [2, 1, 3, 1, 4]
+
+def test_intersperse_empty():
+    assert list(intersperse(1, [])) == []