diff --git a/muppet/__main__.py b/muppet/__main__.py
index 46aecc412d8bfca086ba60a5250c10bc0f254fdb..cd2cfefe5284b0467bf9582cfb95b67d74b97dbc 100644
--- a/muppet/__main__.py
+++ b/muppet/__main__.py
@@ -16,12 +16,18 @@ logger.setLevel(logging.DEBUG)
 
 
 ch = colorlog.StreamHandler()
-ch.setLevel(logging.INFO)
+ch.setLevel(logging.DEBUG)
 # formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
-formatter = colorlog.ColoredFormatter('%(log_color)s%(name)s - %(levelname)s - %(message)s')
+formatter = colorlog.ColoredFormatter('%(log_color)s%(name)s:%(lineno)s - %(message)s')
 ch.setFormatter(formatter)
 logger.addHandler(ch)
 
+formatter2 = colorlog.ColoredFormatter('%(log_color)s%(name)s - %(file)s - %(message)s')
+ch2 = colorlog.StreamHandler()
+ch2.setLevel(logging.DEBUG)
+ch2.setFormatter(formatter2)
+logging.getLogger('muppet.puppet-string').addHandler(ch2)
+
 
 def __main() -> None:
     parser = argparse.ArgumentParser(
diff --git a/muppet/format.py b/muppet/format.py
index 26c0b8fd1c32fb982059158161e4485654ef7ee0..4726258cbced4df7d96df1314b6b1a88da2d632a 100644
--- a/muppet/format.py
+++ b/muppet/format.py
@@ -29,8 +29,8 @@ from .puppet.strings import (
     DocStringExampleTag,
 )
 from muppet.puppet.ast import build_ast
-from muppet.puppet.format import serialize
-from muppet.puppet.format.html import HTMLFormatter
+from muppet.puppet.format import to_string
+from muppet.puppet.format.parser import ParserFormatter, ParseError
 
 
 logger = logging.getLogger(__name__)
@@ -127,7 +127,7 @@ def format_class(d_type: DefinedType | PuppetClass) -> Tuple[str, str]:
     """Format Puppet class."""
     out = ''
     name = d_type.name
-    logger.debug("Formatting class %s", name)
+    logger.info("Formatting class %s", name)
     # print(name, file=sys.stderr)
     name, body = format_docstring(name, d_type.docstring)
     out += body
@@ -140,7 +140,11 @@ def format_class(d_type: DefinedType | PuppetClass) -> Tuple[str, str]:
     # out += render(renderer, data)
     # ------ New ---------------------------------------
     ast = build_ast(puppet_parser(d_type.source))
-    out += serialize(ast, HTMLFormatter)
+    try:
+        out += to_string(ParserFormatter(d_type.source).serialize(ast))
+    except ParseError as e:
+        logger.error("Parsing %(name)s failed: %(err)s",
+                     {'name': d_type.name, 'err': e})
     out += '</code></pre>'
     return name, out
 
@@ -154,7 +158,7 @@ def format_type_alias(d_type: DataTypeAlias) -> Tuple[str, str]:
     """Format Puppet type alias."""
     out = ''
     name = d_type.name
-    logger.debug("Formatting type alias %s", name)
+    logger.info("Formatting type alias %s", name)
     # print(name, file=sys.stderr)
     title, body = format_docstring(name, d_type.docstring)
     out += body
@@ -162,7 +166,7 @@ def format_type_alias(d_type: DataTypeAlias) -> Tuple[str, str]:
     out += '<pre class="highlight-muppet"><code class="puppet">'
     t = puppet_parser(d_type.alias_of)
     data = build_ast(t)
-    out += serialize(data, HTMLFormatter)
+    out += to_string(ParserFormatter(d_type.alias_of).serialize(data))
     out += '</code></pre>\n'
     return title, out
 
@@ -172,13 +176,14 @@ def format_defined_type(d_type: DefinedType) -> Tuple[str, str]:
     # renderer = HTMLRenderer(build_param_dict(d_type.docstring))
     out = ''
     name = d_type.name
-    logger.debug("Formatting defined type %s", name)
+    logger.info("Formatting defined type %s", name)
     # print(name, file=sys.stderr)
     title, body = format_docstring(name, d_type.docstring)
     out += body
 
     out += '<pre class="highlight-muppet"><code class="puppet">'
-    out += serialize(build_ast(puppet_parser(d_type.source)), HTMLFormatter)
+    ast = build_ast(puppet_parser(d_type.source))
+    out += to_string(ParserFormatter(d_type.source).serialize(ast))
     out += '</code></pre>\n'
     return title, out
 
@@ -186,7 +191,7 @@ def format_defined_type(d_type: DefinedType) -> Tuple[str, str]:
 def format_resource_type(r_type: ResourceType) -> str:
     """Format Puppet resource type."""
     name = r_type.name
-    logger.debug("Formatting resource type %s", name)
+    logger.info("Formatting resource type %s", name)
     out = ''
     out += f'<h2>{name}</h2>\n'
     out += str(r_type.docstring)
@@ -224,7 +229,7 @@ def format_puppet_function(function: Function) -> str:
     """Format Puppet function."""
     out = ''
     name = function.name
-    logger.debug("Formatting puppet function %s", name)
+    logger.info("Formatting puppet function %s", name)
     out += f'<h2>{name}</h2>\n'
     t = function.type
     # docstring = function.docstring
@@ -242,7 +247,8 @@ def format_puppet_function(function: Function) -> str:
         try:
             # source = parse_puppet(function.source)
             # out += str(build_ast(source))
-            out += serialize(build_ast(puppet_parser(function.source)), HTMLFormatter)
+            ast = build_ast(puppet_parser(function.source))
+            out += to_string(ParserFormatter(function.source).serialize(ast))
         except CalledProcessError as e:
             print(e, file=sys.stderr)
             print(f"Failed on function: {name}", file=sys.stderr)
diff --git a/muppet/output.py b/muppet/output.py
index c3a24cab6240a380da99d172a91803caa4f37f0d..fe1be7d8ea3f68b1c697309109410e383e926673 100644
--- a/muppet/output.py
+++ b/muppet/output.py
@@ -500,7 +500,7 @@ def setup_module(base: str, module: ModuleEntry, *, path_base: str) -> None:
     data = module.strings_output
 
     for puppet_class in data.puppet_classes + data.defined_types:
-        logger.debug('Formamting %s', puppet_class.name)
+        logger.info('Formamting %s', puppet_class.name)
         # localpath = puppet_class['name'].split('::')
         localpath, _ = os.path.splitext(puppet_class.file)
         dir = os.path.join(path, localpath)
diff --git a/muppet/parser_combinator.py b/muppet/parser_combinator.py
new file mode 100644
index 0000000000000000000000000000000000000000..439b3ef7c783a359be0aa400893826d86f6d52c5
--- /dev/null
+++ b/muppet/parser_combinator.py
@@ -0,0 +1,507 @@
+"""A basic parser combinator for Python."""
+
+# import html
+from dataclasses import dataclass, field
+from typing import (
+    Any,
+    Callable,
+    Optional,
+    Sequence,
+    TypeAlias,
+    Union,
+)
+
+
+@dataclass
+class MatchObject:
+    """A matched item, similar to a regex match."""
+
+    # start: int
+    # end: int
+    type: str
+    matched: str | list['MatchObject']
+
+    def __init__(self, type: str, matched: str | list['MatchObject']):
+        self.type = type
+        self.matched = matched
+        # logger.debug(repr(self))
+
+    # def __str__(self) -> str:
+    #     match self.matched:
+    #         case str(s):
+    #             return s
+    #         case xs:
+    #             return ''.join(str(s) for s in xs)
+
+    # def __repr__(self) -> str:
+    #     match self:
+    #         case MatchObject('', str(s)):
+    #             return repr(s)
+    #         case _:
+    #             return f'MatchedObject({repr(self.type)}, {repr(self.matched)})'
+
+    # def serialize(self) -> str:
+    #     """Seralize into HTML."""
+    #     match self.matched:
+    #         case str(s):
+    #             if self.type:
+    #                 return f'<span class="{self.type}">{html.escape(s)}</span>'
+    #             else:
+    #                 return html.escape(s)
+    #         case _:
+    #             out = ''
+    #             if self.type:
+    #                 out += f'<span class="{self.type}">'
+    #             for item in self.matched:
+    #                 out += item.serialize()
+    #             if self.type:
+    #                 out += '</span>'
+    #             return out
+
+
+def stringify_match(items: list[MatchObject]) -> str:
+    """Serialize a list of match objects back into their string form."""
+    out: str = ''
+    for item in items:
+        match item.matched:
+            case str(s):
+                out += s
+            case other:
+                out += stringify_match(other)
+    return out
+
+
+@dataclass
+class ParseError(Exception):
+    """
+    Errors encountered while parsing.
+
+    This should only appear with optional fields, since we don't know
+    if the next token is the "expected" one. It should be captured
+    internally by all exported procedures and methods.
+    """
+
+    msg: Optional[str] = None
+
+    stk: list['Items'] = field(default_factory=list)
+
+    def __str__(self) -> str:
+        s = f"{self.msg}\nTraceback of called parsers:\n"
+        for item in self.stk:
+            s += f'• {item}\n'
+        return s
+
+
+class ParseDirective:
+    """
+    Common type for special parsing directives.
+
+    This is used for optional parsers, alternative parsers, and the like.
+    """
+
+    def run(self, parser: 'ParserCombinator') -> list[MatchObject]:
+        """
+        Execute this directive.
+
+        :param parser:
+            The formatter which holds the context to use.
+        """
+        raise NotImplementedError(f'Missing run() for {self.__class__.__name__}')
+
+    def __and__(self, other: Any) -> 'ParseDirective':
+        return and_(self, other)
+
+    def __or__(self, other: Any) -> 'ParseDirective':
+        return or_(self, other)
+
+
+Items: TypeAlias = Union[
+    str,
+    None,
+    ParseDirective,
+    Callable[[], list[MatchObject]],
+    Sequence['Items']
+]
+
+
+@dataclass
+class s(ParseDirective):
+    """
+    Wraps a thingy in a parser directive.
+
+    This directive exactly matches the given string, and is mostly
+    here to wrap strings to allow infix operators.
+    """
+
+    s: Items
+
+    def run(self, parser: 'ParserCombinator') -> list[MatchObject]:  # noqa: D102):
+        return parser.get(self.s)
+
+
+@dataclass
+class name(ParseDirective):
+    """Attach a name to a parser, purely for debugging."""
+
+    name: str
+    form: 'Items'
+
+    def run(self, parser: 'ParserCombinator') -> list[MatchObject]:  # noqa: D102
+        return parser.get(self.form)
+
+    def __repr__(self) -> str:
+        return f'{self.name}'
+
+    def __str__(self) -> str:
+        return repr(self)
+
+
+@dataclass
+class optional(ParseDirective):
+    """An optional parameter."""
+
+    form: 'Items'
+
+    def run(self, parser: 'ParserCombinator') -> list[MatchObject]:  # noqa: D102
+        try:
+            return parser.get(self.form)
+        except ParseError:
+            return []
+
+    def __repr__(self) -> str:
+        return f'optional({repr(self.form)})'
+
+
+@dataclass
+class and_(ParseDirective):
+    """Parse a group in order."""
+
+    items: list['Items']
+
+    def __init__(self, *items: 'Items'):
+        self.items = list(items)
+
+    def run(self, parser: 'ParserCombinator') -> list[MatchObject]:  # noqa: D102
+        snapshot = parser.snapshot()
+        out = []
+        try:
+            for item in self.items:
+                out += parser.get(item)
+        except ParseError as e:
+            parser.restore(snapshot)
+            raise e
+        return out
+
+    def __repr__(self) -> str:
+        return f'and_({", ".join(repr(x) for x in self.items)})'
+
+
+@dataclass
+class or_(ParseDirective):
+    """
+    A set of multiple parse forms to try in order.
+
+    The first matched one will be used.
+
+    (Alias Variant)
+    """
+
+    alternatives: list['Items']
+
+    def __init__(self, *alternatives: Items):
+        """Retrun a new OptionParse object."""
+        self.alternatives = list(alternatives)
+
+    def run(self, parser: 'ParserCombinator') -> list[MatchObject]:  # noqa: D102
+        save = parser.snapshot()
+        for alternative in self.alternatives:
+            try:
+                return parser.get(alternative)
+            except ParseError:
+                parser.restore(save)
+        else:
+            msg = f"No alternative suceeded, cases={self.alternatives}, seek={parser.seek}"
+            raise ParseError(msg)
+
+    def __repr__(self) -> str:
+        return f'or_({", ".join(repr(x) for x in self.alternatives)})'
+
+
+@dataclass
+class count(ParseDirective):
+    """Run the given parser between min and max times."""
+
+    min: int
+    max: int
+    parser: Items
+
+    def __init__(self,
+                 parser: Items,
+                 arg: int,
+                 *args: int,
+                 **kwargs: int):
+        """
+        Run parser between ``min`` and ``max`` times.
+
+        If only one numeric argument is given, then that's used as the
+        maximum count, but if two values are given, then the minimum count
+        preceeds the maximum.
+
+        Both max and min can also be given as keyword arguments.
+
+        :param parser:
+        :param min:
+        :param max:
+
+        .. code-block:: python
+
+            count(p, min, max)
+
+        .. code-block:: python
+
+            count(p, max)
+        """
+        min_ = 0
+        max_ = 1
+
+        match args:
+            case []:
+                max_ = arg
+            case [v]:
+                min_ = arg
+                max_ = v
+
+        if 'min' in kwargs:
+            min_ = kwargs['min']
+        if 'max' in kwargs:
+            max_ = kwargs['max']
+
+        self.min = min_
+        self.max = max_
+        self.parser = parser
+
+    def run(self, parser: 'ParserCombinator') -> list[MatchObject]:  # noqa: D102
+        snapshot = parser.snapshot()
+        out = []
+        try:
+            # If any of these fails, then the whole operation was a fail.
+            for _ in range(self.min):
+                out += parser.get(self.parser)
+
+            # These are optional, so an error here is "expected"
+            for _ in range(self.min, self.max):
+                try:
+                    out += parser.get(self.parser)
+                except ParseError:
+                    break
+        except Exception as e:
+            out = []
+            parser.restore(snapshot)
+            raise e
+        return out
+
+    def __repr__(self) -> str:
+        return f'count({repr(self.parser)}, {self.min}, {self.max})'
+
+
+@dataclass
+class CharParser(ParseDirective):
+    """Parse a single character."""
+
+    def run(self, parser: 'ParserCombinator') -> list[MatchObject]:  # noqa: D102
+        out = [MatchObject('', parser.__source[parser.seek])]
+        parser.seek += 1
+        return out
+
+    def __repr__(self) -> str:
+        return 'char'
+
+
+char = CharParser()
+
+
+@dataclass
+class many(ParseDirective):
+    """Many a parser as many times as possible."""
+
+    parser: 'Items'
+
+    def run(self, parser: 'ParserCombinator') -> list[MatchObject]:  # noqa: D102
+        out = []
+        try:
+            while True:
+                entry = parser.get(self.parser)
+                if not entry:
+                    raise ParseError()
+                # logger.info("seek = %s, parser = %s",
+                #             parser.seek, self.parser)
+                out += entry
+        except ParseError:
+            return out
+
+    def __repr__(self) -> str:
+        return f'many({repr(self.parser)})'
+
+
+@dataclass
+class complement(ParseDirective):
+    """Parses any character which isn't given."""
+
+    chars: str
+
+    def run(self, parser: 'ParserCombinator') -> list[MatchObject]:  # noqa: D102
+        match parser.peek(char):
+            case [MatchObject(matched=str(c))]:
+                if c not in self.chars:
+                    return parser.get(char)
+                else:
+                    raise ParseError()
+            case _:
+                raise ParseError()
+
+    def __repr__(self) -> str:
+        return f'complement({repr(self.chars)})'
+
+
+@dataclass
+class tag(ParseDirective):
+    """Tag value returned by parser."""
+
+    tag: str
+    parser: 'Items'
+
+    def run(self, parser: 'ParserCombinator') -> list[MatchObject]:  # noqa: D102
+        result = parser.get(self.parser)
+        return [MatchObject(self.tag, result)]
+
+
+@dataclass
+class delimited(ParseDirective):
+    """
+    Read an infix delimited list of items.
+
+    If a optional trailing "comma" is wanted:
+
+    .. code-block:: python
+
+        and_(delimited(and_(ws, ',', ws),
+                       ITEM),
+            optional(ws, ','))
+    """
+
+    delim: 'Items'
+    parser: 'Items'
+
+    def run(self, parser: 'ParserCombinator') -> list[MatchObject]:  # noqa: D102
+        return parser.get(and_(self.parser, many(and_(self.delim, self.parser))))
+
+
+hexdig = name('hexdig',
+              or_(*([chr(x + ord('0')) for x in range(0, 10)]
+                    + [chr(x + ord('A')) for x in range(0, 6)])))
+
+space = or_(' ', '\t', '\n', '\r')
+
+ws = tag('ws',
+         name('ws',
+              # TODO tagging space with 'ws' locks us into an infinite loop
+              many(or_(many(tag('space', space)),
+                       and_(tag('comment', and_('#', many(complement('\n')))),
+                            '\n')))))
+
+
+class ParserCombinatorSnapshot:
+    """
+    An opaque object storing the state of the ParserCombinator.
+
+    This allows undoing any number of operations. Each parser snapshot
+    can only be used with the parser formatter which created it.
+
+    A snapshot can be restored multiple times.
+    """
+
+    def __init__(self, seek: int, creator: 'ParserCombinator'):
+        self._seek = seek
+        self._creator = creator
+
+
+class ParserCombinator:
+    """
+    A basic parser combinator for Python.
+
+    :param source:
+        The string which should be parsed.
+    :param file:
+        Optional name of the file being parsed.
+    """
+
+    # Required fields
+    # __source: str
+    # __seek: int = 0
+
+    # # Fields useful for better debugging info
+    # file: Optional[str] = None
+
+    def __init__(self, source: str, file: Optional[str] = None):
+        self.__source = source
+        self.seek = 0
+        self.file = file
+
+    def peek(self, item: Items) -> list[MatchObject]:
+        """Run the parser without updating the state."""
+        snapshot = self.snapshot()
+        result = self.get(item)
+        self.restore(snapshot)
+        return result
+
+    def get(self, item: Items) -> list[MatchObject]:
+        """Like get, but dosn't seek through whitespace or comments."""
+        out: list[MatchObject] = []
+        try:
+            match item:
+                case [*entries]:
+                    for entry in entries:
+                        out += self.get(entry)
+                case str(s):
+                    substr = self.__source[self.seek:][:len(s)]
+                    # Always case fold when matching
+                    if substr.lower() == s.lower():
+                        self.seek += len(s)
+                        out += [MatchObject('', s)]
+                    else:
+                        raise ParseError(f'Expected {item!r}, got {substr!r} (char {self.seek})')
+                case None:
+                    pass
+                case ParseDirective():
+                    out += item.run(self)
+                # TODO case Puppet():
+                case other:
+                    if callable(item):
+                        out += item()
+                    else:
+                        raise ValueError(f"Unexpected item: {other}")
+        except ParseError as e:
+            e.stk.append(item)
+            raise e
+        return out
+
+    def snapshot(self) -> ParserCombinatorSnapshot:
+        """Create a snapshot of the parsers current state."""
+        return ParserCombinatorSnapshot(
+                seek=self.seek,
+                creator=self)
+
+    def restore(self, snapshot: ParserCombinatorSnapshot) -> None:
+        """Restore a snapshot, altering the parsers state."""
+        assert snapshot._creator == self
+        self.seek = snapshot._seek
+
+    def peek_string(self, max_len: int) -> str:
+        """
+        Return the upcomming string, for debugging purposes.
+
+        :param max_len:
+            Maximum length to return, may be shorter if there isn't
+            enough characters left
+        """
+        return self.__source[self.seek:][:max_len]
diff --git a/muppet/puppet/ast.py b/muppet/puppet/ast.py
index b527c23d1c8c1535a8fec20fd763c1d38046ef29..63127b94b7d14bdd5c8c552c886b3a1f6c109159 100644
--- a/muppet/puppet/ast.py
+++ b/muppet/puppet/ast.py
@@ -159,6 +159,7 @@ class PuppetClass(Puppet):
     name: str
     params: Optional[list[PuppetDeclarationParameter]] = None
     body: list[Puppet] = field(default_factory=list)
+    parent: Optional[str] = None
 
 
 @dataclass
@@ -181,25 +182,28 @@ class PuppetCollect(Puppet):
 
 
 @dataclass
-class PuppetIf(Puppet):
+class PuppetIfChain(Puppet):
     """
-    A puppet if expression.
+    Puppet if expressions.
 
     .. code-block:: puppet
 
-        if condition {
-            consequent
+        if a {
+            1
+        } elsif b {
+            2
         } else {
-            alretnative
+            3
         }
 
-    ``elsif`` is parsed as an else block with a single if expression
-    in it.
+    .. code-block:: python
+
+        [('a', [1]),
+         ('b', [2]),
+         ('else', [3])]
     """
 
-    condition: Puppet
-    consequent: list[Puppet]
-    alternative: Optional[list[Puppet]] = None
+    clauses: list[tuple[Puppet | Literal['else'], list[Puppet]]]
 
 
 @dataclass
@@ -361,6 +365,7 @@ class PuppetNumber(Puppet):
     """A puppet numeric literal."""
 
     x: int | float
+    radix: Optional[int] = None
 
 
 @dataclass
@@ -402,6 +407,12 @@ class PuppetResourceOverride(Puppet):
     """
     A resource override.
 
+    The resource attribute will most likely be a PuppetAccess object.
+
+    .. code-block:: python
+
+        PuppetAccess(how=PuppetQr(name='File'), args=[PuppetString(s='name')])
+
     .. code-block:: puppet
 
         File["name"] {
@@ -462,6 +473,14 @@ class PuppetNode(Puppet):
     body: list[Puppet]
 
 
+@dataclass
+class PuppetTypeAlias(Puppet):
+    """A type alias."""
+
+    name: str
+    implementation: Puppet
+
+
 @dataclass
 class PuppetParseError(Puppet):
     """Anything we don't know how to handle."""
@@ -493,8 +512,8 @@ def parse_puppet_declaration_params(data: dict[str, dict[str, Any]]) \
         value: Optional[Puppet] = None
         if t := definition.get('type'):
             type = build_ast(t)
-        if v := definition.get('value'):
-            value = build_ast(v)
+        if 'value' in definition:
+            value = build_ast(definition['value'])
 
         parameters.append(PuppetDeclarationParameter(k=name, v=value, type=type))
 
@@ -592,6 +611,9 @@ def build_ast(form: Any) -> Puppet:
             if b := rest.get('body'):
                 args['body'] = [build_ast(x) for x in b]
 
+            if p := rest.get('parent'):
+                args['parent'] = p
+
             # This is only valid for 'function', and simply will
             # never be true for the other cases.
             if r := rest.get('returns'):
@@ -636,13 +658,19 @@ def build_ast(form: Any) -> Puppet:
             return build_ast(form)
 
         case ['if', {'test': test, **rest}]:
-            consequent = []
-            alternative = None
-            if then := rest.get('then'):
-                consequent = [build_ast(x) for x in then]
+            clauses = []
+
+            clauses.append((build_ast(test),
+                            [build_ast(x) for x in rest.get('then', [])]))
+
             if els := rest.get('else'):
-                alternative = [build_ast(x) for x in els]
-            return PuppetIf(build_ast(test), consequent, alternative)
+                match [build_ast(x) for x in els]:
+                    case [PuppetIfChain(cls)]:
+                        clauses += cls
+                    case xs:
+                        clauses.append(('else', xs))
+
+            return PuppetIfChain(clauses)
 
         case ['unless', {'test': test, 'then': forms}]:
             return PuppetUnless(build_ast(test), [build_ast(x) for x in forms])
@@ -713,9 +741,15 @@ def build_ast(form: Any) -> Puppet:
             return PuppetNode([build_ast(x) for x in matches],
                               [build_ast(x) for x in body])
 
+        case ['type-alias', str(name), implementation]:
+            return PuppetTypeAlias(name, build_ast(implementation))
+
         case str(s):
             return PuppetString(s)
 
+        case ['int', {'radix': radix, 'value': value}]:
+            return PuppetNumber(value, radix)
+
         case int(x): return PuppetNumber(x)
         case float(x): return PuppetNumber(x)
 
diff --git a/muppet/puppet/format/__init__.py b/muppet/puppet/format/__init__.py
index d40f9b4252cdd252593b75dadd4673269d422447..d30eb7d3977dde8d3e852a9e412753bfe661c5ef 100644
--- a/muppet/puppet/format/__init__.py
+++ b/muppet/puppet/format/__init__.py
@@ -1,8 +1,9 @@
 """Fromat Puppet AST's into something useful."""
 
 from .base import Serializer
+from muppet.parser_combinator import MatchObject
 from muppet.puppet.ast import Puppet
-from typing import TypeVar
+from typing import Any, TypeVar
 
 
 T = TypeVar('T')
@@ -10,4 +11,17 @@ T = TypeVar('T')
 
 def serialize(ast: Puppet, serializer: type[Serializer[T]]) -> T:
     """Run the given serializer on the given data."""
-    return serializer().serialize(ast, 0)
+    return serializer().serialize(ast)
+
+
+def to_string(t: Any) -> str:
+    """Turn a serialized structure into a string."""
+    match t:
+        case str(s):
+            return s
+        case MatchObject():
+            return t.serialize()
+        case [*xs]:
+            return ''.join(to_string(x) for x in xs)
+        case _:
+            raise ValueError()
diff --git a/muppet/puppet/format/base.py b/muppet/puppet/format/base.py
index 9afad2b92e0bae75a79db04ba44fe332b199a6ea..b24fd3346f3f2729f3d3e3604be44306466294d6 100644
--- a/muppet/puppet/format/base.py
+++ b/muppet/puppet/format/base.py
@@ -14,14 +14,14 @@ from muppet.puppet.ast import (
     PuppetUnaryOperator, PuppetArray, PuppetCallMethod,
     PuppetCase, PuppetDeclarationParameter,
     PuppetInstanciationParameter, PuppetClass, PuppetConcat,
-    PuppetCollect, PuppetIf, PuppetUnless, PuppetKeyword,
+    PuppetCollect, PuppetIfChain, PuppetUnless, PuppetKeyword,
     PuppetExportedQuery, PuppetVirtualQuery, PuppetFunction,
     PuppetHash, PuppetHeredoc, PuppetLiteralHeredoc, PuppetVar,
     PuppetLambda,  PuppetQn, PuppetQr, PuppetRegex,
     PuppetResource, PuppetDefine, PuppetString,
     PuppetNumber, PuppetInvoke, PuppetResourceDefaults,
     PuppetResourceOverride, PuppetDeclaration, PuppetSelector,
-    PuppetBlock, PuppetNode,
+    PuppetBlock, PuppetNode, PuppetTypeAlias,
     PuppetCall, PuppetParenthesis, PuppetNop,
 
     # HashEntry,
@@ -33,6 +33,9 @@ T = TypeVar('T')
 logger = logging.getLogger(__name__)
 
 
+# TODO flag everything as @abstractmethod
+
+
 class Serializer(Generic[T]):
     """
     Base class for serialization.
@@ -44,253 +47,221 @@ class Serializer(Generic[T]):
     by each instance.
     """
 
-    @classmethod
-    def _puppet_literal(cls, it: PuppetLiteral, indent: int) -> T:
-        raise NotImplementedError("puppet_literal must be implemented by subclass")
+    def __raise(self, proc: str) -> T:
+        msg = f"{proc} must be implemented by subclass {self.__class__.__name__}"
+        raise NotImplementedError(msg)
+
+    def _puppet_literal(self, it: PuppetLiteral) -> T:
+        return self.__raise("puppet_literal")
+
+    def _puppet_access(self, it: PuppetAccess) -> T:
+        return self.__raise("puppet_access")
 
-    @classmethod
-    def _puppet_access(cls, it: PuppetAccess, indent: int) -> T:
-        raise NotImplementedError("puppet_access must be implemented by subclass")
+    def _puppet_binary_operator(self, it: PuppetBinaryOperator) -> T:
+        return self.__raise("puppet_binary_operator")
 
-    @classmethod
-    def _puppet_binary_operator(cls, it: PuppetBinaryOperator, indent: int) -> T:
-        raise NotImplementedError("puppet_binary_operator must be implemented by subclass")
+    def _puppet_unary_operator(self, it: PuppetUnaryOperator) -> T:
+        return self.__raise("puppet_unary_operator")
 
-    @classmethod
-    def _puppet_unary_operator(cls, it: PuppetUnaryOperator, indent: int) -> T:
-        raise NotImplementedError("puppet_unary_operator must be implemented by subclass")
+    def _puppet_array(self, it: PuppetArray) -> T:
+        return self.__raise("puppet_array")
 
-    @classmethod
-    def _puppet_array(cls, it: PuppetArray, indent: int) -> T:
-        raise NotImplementedError("puppet_array must be implemented by subclass")
+    def _puppet_call(self, it: PuppetCall) -> T:
+        return self.__raise("puppet_call")
 
-    @classmethod
-    def _puppet_call(cls, it: PuppetCall, indent: int) -> T:
-        raise NotImplementedError("puppet_call must be implemented by subclass")
+    def _puppet_call_method(self, it: PuppetCallMethod) -> T:
+        return self.__raise("puppet_call_method")
 
-    @classmethod
-    def _puppet_call_method(cls, it: PuppetCallMethod, indent: int) -> T:
-        raise NotImplementedError("puppet_call_method must be implemented by subclass")
+    def _puppet_case(self, it: PuppetCase) -> T:
+        return self.__raise("puppet_case")
 
-    @classmethod
-    def _puppet_case(cls, it: PuppetCase, indent: int) -> T:
-        raise NotImplementedError("puppet_case must be implemented by subclass")
+    def _puppet_declaration_parameter(self, it: PuppetDeclarationParameter) -> T:
+        return self.__raise("puppet_declaration_parameter")
 
-    @classmethod
-    def _puppet_declaration_parameter(cls, it: PuppetDeclarationParameter, indent: int) -> T:
-        raise NotImplementedError("puppet_declaration_parameter must be implemented by subclass")
+    def _puppet_instanciation_parameter(self, it: PuppetInstanciationParameter) -> T:
+        return self.__raise("puppet_instanciation_parameter")
 
-    @classmethod
-    def _puppet_instanciation_parameter(cls, it: PuppetInstanciationParameter, indent: int) -> T:
-        raise NotImplementedError("puppet_instanciation_parameter must be implemented by subclass")
+    def _puppet_class(self, it: PuppetClass) -> T:
+        return self.__raise("puppet_class")
 
-    @classmethod
-    def _puppet_class(cls, it: PuppetClass, indent: int) -> T:
-        raise NotImplementedError("puppet_class must be implemented by subclass")
+    def _puppet_concat(self, it: PuppetConcat) -> T:
+        return self.__raise("puppet_concat")
 
-    @classmethod
-    def _puppet_concat(cls, it: PuppetConcat, indent: int) -> T:
-        raise NotImplementedError("puppet_concat must be implemented by subclass")
+    def _puppet_collect(self, it: PuppetCollect) -> T:
+        return self.__raise("puppet_collect")
 
-    @classmethod
-    def _puppet_collect(cls, it: PuppetCollect, indent: int) -> T:
-        raise NotImplementedError("puppet_collect must be implemented by subclass")
+    def _puppet_if_chain(self, it: PuppetIfChain) -> T:
+        return self.__raise("puppet_if_chain")
 
-    @classmethod
-    def _puppet_if(cls, it: PuppetIf, indent: int) -> T:
-        raise NotImplementedError("puppet_if must be implemented by subclass")
+    def _puppet_unless(self, it: PuppetUnless) -> T:
+        return self.__raise("puppet_unless")
 
-    @classmethod
-    def _puppet_unless(cls, it: PuppetUnless, indent: int) -> T:
-        raise NotImplementedError("puppet_unless must be implemented by subclass")
+    def _puppet_keyword(self, it: PuppetKeyword) -> T:
+        return self.__raise("puppet_keyword")
 
-    @classmethod
-    def _puppet_keyword(cls, it: PuppetKeyword, indent: int) -> T:
-        raise NotImplementedError("puppet_keyword must be implemented by subclass")
+    def _puppet_exported_query(self, it: PuppetExportedQuery) -> T:
+        return self.__raise("puppet_exported_query")
 
-    @classmethod
-    def _puppet_exported_query(cls, it: PuppetExportedQuery, indent: int) -> T:
-        raise NotImplementedError("puppet_exported_query must be implemented by subclass")
+    def _puppet_virtual_query(self, it: PuppetVirtualQuery) -> T:
+        return self.__raise("puppet_virtual_query")
 
-    @classmethod
-    def _puppet_virtual_query(cls, it: PuppetVirtualQuery, indent: int) -> T:
-        raise NotImplementedError("puppet_virtual_query must be implemented by subclass")
+    def _puppet_function(self, it: PuppetFunction) -> T:
+        return self.__raise("puppet_function")
 
-    @classmethod
-    def _puppet_function(cls, it: PuppetFunction, indent: int) -> T:
-        raise NotImplementedError("puppet_function must be implemented by subclass")
+    def _puppet_hash(self, it: PuppetHash) -> T:
+        return self.__raise("puppet_hash")
 
-    @classmethod
-    def _puppet_hash(cls, it: PuppetHash, indent: int) -> T:
-        raise NotImplementedError("puppet_hash must be implemented by subclass")
+    def _puppet_heredoc(self, it: PuppetHeredoc) -> T:
+        return self.__raise("puppet_heredoc")
 
-    @classmethod
-    def _puppet_heredoc(cls, it: PuppetHeredoc, indent: int) -> T:
-        raise NotImplementedError("puppet_heredoc must be implemented by subclass")
+    def _puppet_literal_heredoc(self, it: PuppetLiteralHeredoc) -> T:
+        return self.__raise("puppet_literal_heredoc")
 
-    @classmethod
-    def _puppet_literal_heredoc(cls, it: PuppetLiteralHeredoc, indent: int) -> T:
-        raise NotImplementedError("puppet_literal_heredoc must be implemented by subclass")
+    def _puppet_var(self, it: PuppetVar) -> T:
+        return self.__raise("puppet_var")
 
-    @classmethod
-    def _puppet_var(cls, it: PuppetVar, indent: int) -> T:
-        raise NotImplementedError("puppet_var must be implemented by subclass")
+    def _puppet_lambda(self, it: PuppetLambda) -> T:
+        return self.__raise("puppet_lambda")
 
-    @classmethod
-    def _puppet_lambda(cls, it: PuppetLambda, indent: int) -> T:
-        raise NotImplementedError("puppet_lambda must be implemented by subclass")
+    def _puppet_qn(self, it: PuppetQn) -> T:
+        return self.__raise("puppet_qn")
 
-    @classmethod
-    def _puppet_qn(cls, it: PuppetQn, indent: int) -> T:
-        raise NotImplementedError("puppet_qn must be implemented by subclass")
+    def _puppet_qr(self, it: PuppetQr) -> T:
+        return self.__raise("puppet_qr")
 
-    @classmethod
-    def _puppet_qr(cls, it: PuppetQr, indent: int) -> T:
-        raise NotImplementedError("puppet_qr must be implemented by subclass")
+    def _puppet_regex(self, it: PuppetRegex) -> T:
+        return self.__raise("puppet_regex")
 
-    @classmethod
-    def _puppet_regex(cls, it: PuppetRegex, indent: int) -> T:
-        raise NotImplementedError("puppet_regex must be implemented by subclass")
+    def _puppet_resource(self, it: PuppetResource) -> T:
+        return self.__raise("puppet_resource")
 
-    @classmethod
-    def _puppet_resource(cls, it: PuppetResource, indent: int) -> T:
-        raise NotImplementedError("puppet_resource must be implemented by subclass")
+    def _puppet_define(self, it: PuppetDefine) -> T:
+        return self.__raise("puppet_define")
 
-    @classmethod
-    def _puppet_define(cls, it: PuppetDefine, indent: int) -> T:
-        raise NotImplementedError("puppet_define must be implemented by subclass")
+    def _puppet_string(self, it: PuppetString) -> T:
+        return self.__raise("puppet_string")
 
-    @classmethod
-    def _puppet_string(cls, it: PuppetString, indent: int) -> T:
-        raise NotImplementedError("puppet_string must be implemented by subclass")
+    def _puppet_number(self, it: PuppetNumber) -> T:
+        return self.__raise("puppet_number")
 
-    @classmethod
-    def _puppet_number(cls, it: PuppetNumber, indent: int) -> T:
-        raise NotImplementedError("puppet_number must be implemented by subclass")
+    def _puppet_invoke(self, it: PuppetInvoke) -> T:
+        return self.__raise("puppet_invoke")
 
-    @classmethod
-    def _puppet_invoke(cls, it: PuppetInvoke, indent: int) -> T:
-        raise NotImplementedError("puppet_invoke must be implemented by subclass")
+    def _puppet_resource_defaults(self, it: PuppetResourceDefaults) -> T:
+        return self.__raise("puppet_resource_defaults")
 
-    @classmethod
-    def _puppet_resource_defaults(cls, it: PuppetResourceDefaults, indent: int) -> T:
-        raise NotImplementedError("puppet_resource_defaults must be implemented by subclass")
+    def _puppet_resource_override(self, it: PuppetResourceOverride) -> T:
+        return self.__raise("puppet_resource_override")
 
-    @classmethod
-    def _puppet_resource_override(cls, it: PuppetResourceOverride, indent: int) -> T:
-        raise NotImplementedError("puppet_resource_override must be implemented by subclass")
+    def _puppet_declaration(self, it: PuppetDeclaration) -> T:
+        return self.__raise("puppet_declaration")
 
-    @classmethod
-    def _puppet_declaration(cls, it: PuppetDeclaration, indent: int) -> T:
-        raise NotImplementedError("puppet_declaration must be implemented by subclass")
+    def _puppet_selector(self, it: PuppetSelector) -> T:
+        return self.__raise("puppet_selector")
 
-    @classmethod
-    def _puppet_selector(cls, it: PuppetSelector, indent: int) -> T:
-        raise NotImplementedError("puppet_selector must be implemented by subclass")
+    def _puppet_block(self, it: PuppetBlock) -> T:
+        return self.__raise("puppet_block")
 
-    @classmethod
-    def _puppet_block(cls, it: PuppetBlock, indent: int) -> T:
-        raise NotImplementedError("puppet_block must be implemented by subclass")
+    def _puppet_node(self, it: PuppetNode) -> T:
+        return self.__raise("puppet_node")
 
-    @classmethod
-    def _puppet_node(cls, it: PuppetNode, indent: int) -> T:
-        raise NotImplementedError("puppet_node must be implemented by subclass")
+    def _puppet_type_alias(self, it: PuppetTypeAlias) -> T:
+        return self.__raise("type_alias")
 
-    @classmethod
-    def _puppet_parenthesis(cls, it: PuppetParenthesis, indent: int) -> T:
-        raise NotImplementedError("puppet_parenthesis must be implemented by subclass")
+    def _puppet_parenthesis(self, it: PuppetParenthesis) -> T:
+        return self.__raise("puppet_parenthesis")
 
-    @classmethod
-    def _puppet_nop(cls, it: PuppetNop, indent: int) -> T:
-        raise NotImplementedError("puppet_nop must be implemented by subclass")
+    def _puppet_nop(self, it: PuppetNop) -> T:
+        return self.__raise("puppet_nop")
 
     @final
-    @classmethod
-    def serialize(cls, form: Puppet, indent: int) -> T:
+    def serialize(self, form: Puppet) -> T:
         """Dispatch depending on type."""
         match form:
             case PuppetLiteral():
-                return cls._puppet_literal(form, indent)
+                return self._puppet_literal(form)
             case PuppetAccess():
-                return cls._puppet_access(form, indent)
+                return self._puppet_access(form)
             case PuppetBinaryOperator():
-                return cls._puppet_binary_operator(form, indent)
+                return self._puppet_binary_operator(form)
             case PuppetUnaryOperator():
-                return cls._puppet_unary_operator(form, indent)
+                return self._puppet_unary_operator(form)
             case PuppetUnaryOperator():
-                return cls._puppet_unary_operator(form, indent)
+                return self._puppet_unary_operator(form)
             case PuppetArray():
-                return cls._puppet_array(form, indent)
+                return self._puppet_array(form)
             case PuppetCall():
-                return cls._puppet_call(form, indent)
+                return self._puppet_call(form)
             case PuppetCallMethod():
-                return cls._puppet_call_method(form, indent)
+                return self._puppet_call_method(form)
             case PuppetCase():
-                return cls._puppet_case(form, indent)
+                return self._puppet_case(form)
             case PuppetDeclarationParameter():
-                return cls._puppet_declaration_parameter(form, indent)
+                return self._puppet_declaration_parameter(form)
             case PuppetInstanciationParameter():
-                return cls._puppet_instanciation_parameter(form, indent)
+                return self._puppet_instanciation_parameter(form)
             case PuppetClass():
-                return cls._puppet_class(form, indent)
+                return self._puppet_class(form)
             case PuppetConcat():
-                return cls._puppet_concat(form, indent)
+                return self._puppet_concat(form)
             case PuppetCollect():
-                return cls._puppet_collect(form, indent)
-            case PuppetIf():
-                return cls._puppet_if(form, indent)
+                return self._puppet_collect(form)
+            case PuppetIfChain():
+                return self._puppet_if_chain(form)
             case PuppetUnless():
-                return cls._puppet_unless(form, indent)
+                return self._puppet_unless(form)
             case PuppetKeyword():
-                return cls._puppet_keyword(form, indent)
+                return self._puppet_keyword(form)
             case PuppetExportedQuery():
-                return cls._puppet_exported_query(form, indent)
+                return self._puppet_exported_query(form)
             case PuppetVirtualQuery():
-                return cls._puppet_virtual_query(form, indent)
+                return self._puppet_virtual_query(form)
             case PuppetFunction():
-                return cls._puppet_function(form, indent)
+                return self._puppet_function(form)
             case PuppetHash():
-                return cls._puppet_hash(form, indent)
+                return self._puppet_hash(form)
             case PuppetHeredoc():
-                return cls._puppet_heredoc(form, indent)
+                return self._puppet_heredoc(form)
             case PuppetLiteralHeredoc():
-                return cls._puppet_literal_heredoc(form, indent)
+                return self._puppet_literal_heredoc(form)
             case PuppetVar():
-                return cls._puppet_var(form, indent)
+                return self._puppet_var(form)
             case PuppetLambda():
-                return cls._puppet_lambda(form, indent)
+                return self._puppet_lambda(form)
             case PuppetQn():
-                return cls._puppet_qn(form, indent)
+                return self._puppet_qn(form)
             case PuppetQr():
-                return cls._puppet_qr(form, indent)
+                return self._puppet_qr(form)
             case PuppetRegex():
-                return cls._puppet_regex(form, indent)
+                return self._puppet_regex(form)
             case PuppetResource():
-                return cls._puppet_resource(form, indent)
+                return self._puppet_resource(form)
             case PuppetDefine():
-                return cls._puppet_define(form, indent)
+                return self._puppet_define(form)
             case PuppetString():
-                return cls._puppet_string(form, indent)
+                return self._puppet_string(form)
             case PuppetNumber():
-                return cls._puppet_number(form, indent)
+                return self._puppet_number(form)
             case PuppetInvoke():
-                return cls._puppet_invoke(form, indent)
+                return self._puppet_invoke(form)
             case PuppetResourceDefaults():
-                return cls._puppet_resource_defaults(form, indent)
+                return self._puppet_resource_defaults(form)
             case PuppetResourceOverride():
-                return cls._puppet_resource_override(form, indent)
+                return self._puppet_resource_override(form)
             case PuppetDeclaration():
-                return cls._puppet_declaration(form, indent)
+                return self._puppet_declaration(form)
             case PuppetSelector():
-                return cls._puppet_selector(form, indent)
+                return self._puppet_selector(form)
             case PuppetBlock():
-                return cls._puppet_block(form, indent)
+                return self._puppet_block(form)
             case PuppetNode():
-                return cls._puppet_node(form, indent)
+                return self._puppet_node(form)
             case PuppetParenthesis():
-                return cls._puppet_parenthesis(form, indent)
+                return self._puppet_parenthesis(form)
+            case PuppetTypeAlias():
+                return self._puppet_type_alias(form)
             case PuppetNop():
-                return cls._puppet_nop(form, indent)
+                return self._puppet_nop(form)
             case _:
                 logger.warn("Unexpected form: %s", form)
                 raise ValueError(f'Unexpected: {form}')
diff --git a/muppet/puppet/format/html.py b/muppet/puppet/format/html.py
index 9719b534910ec6d72f5fd36be7231a202c236a1b..d7b887b485821b8d458db95a9ec5f5f0bff87563 100644
--- a/muppet/puppet/format/html.py
+++ b/muppet/puppet/format/html.py
@@ -17,7 +17,7 @@ from muppet.puppet.ast import (
     PuppetUnaryOperator, PuppetArray, PuppetCallMethod,
     PuppetCase, PuppetDeclarationParameter,
     PuppetInstanciationParameter, PuppetClass, PuppetConcat,
-    PuppetCollect, PuppetIf, PuppetUnless, PuppetKeyword,
+    PuppetCollect, PuppetIfChain, PuppetUnless, PuppetKeyword,
     PuppetExportedQuery, PuppetVirtualQuery, PuppetFunction,
     PuppetHash, PuppetHeredoc, PuppetLiteralHeredoc, PuppetVar,
     PuppetLambda,  PuppetQn, PuppetQr, PuppetRegex,
@@ -34,7 +34,6 @@ import html
 from .text import (
     override,
     find_heredoc_delimiter,
-    ind,
     string_width,
 )
 
@@ -80,25 +79,39 @@ def number(x: str) -> str:
 class HTMLFormatter(Serializer[str]):
     """AST formatter returning source code."""
 
-    @classmethod
+    def __init__(self, indent: int = 0):
+        self.__indent = indent
+
+    def indent(self, change: int) -> 'HTMLFormatter':
+        """Return the current context, with an updated indentation level."""
+        return self.__class__(indent=(self.__indent + change))
+
+    def ind(self, change: int = 0) -> str:
+        """
+        Return indentation for current context.
+
+        :param change:
+            Extra indentation level to add to this output.
+        """
+        return (self.__indent + change) * 2 * ' '
+
     def format_declaration_parameter(
-            cls,
+            self,
             param: PuppetDeclarationParameter,
-            indent: int) -> str:
+            ) -> str:
         """Format a single declaration parameter."""
         out: str = ''
         if param.type:
-            out += f'{cls.serialize(param.type, indent + 1)} '
+            out += f'{self.indent(1).serialize(param.type)} '
         out += var(f'${param.k}')
         if param.v:
-            out += f' = {cls.serialize(param.v, indent + 1)}'
+            out += f' = {self.indent(1).serialize(param.v)}'
         return out
 
-    @classmethod
     def format_declaration_parameters(
-            cls,
+            self,
             lst: list[PuppetDeclarationParameter],
-            indent: int) -> str:
+            ) -> str:
         """
         Print declaration parameters.
 
@@ -109,121 +122,112 @@ class HTMLFormatter(Serializer[str]):
 
         out = ' (\n'
         for param in lst:
-            out += ind(indent + 1) + cls.format_declaration_parameter(param, indent + 1) + ',\n'
-        out += ind(indent) + ')'
+            out += self.ind(1) + self.indent(1).format_declaration_parameter(param) + ',\n'
+        out += self.ind() + ')'
         return out
 
-    @classmethod
     def serialize_hash_entry(
-            cls,
+            self,
             entry: HashEntry,
-            indent: int) -> str:
+            ) -> str:
         """Return a hash entry as a string."""
-        return f'{cls.serialize(entry.k, indent + 1)} => {cls.serialize(entry.v, indent + 2)}'
+        out = f'{self.indent(1).serialize(entry.k)}'
+        out += f' => {self.indent(2).serialize(entry.v)}'
+        return out
 
     @override
-    @classmethod
-    def _puppet_literal(cls, it: PuppetLiteral, indent: int) -> str:
+    def _puppet_literal(self, it: PuppetLiteral) -> str:
         return literal(it.literal)
 
     @override
-    @classmethod
-    def _puppet_access(cls, it: PuppetAccess, indent: int) -> str:
-        args = ', '.join(cls.serialize(x, indent) for x in it.args)
+    def _puppet_access(self, it: PuppetAccess) -> str:
+        args = ', '.join(self.serialize(x) for x in it.args)
 
-        return f'{cls.serialize(it.how, indent)}[{args}]'
+        return f'{self.serialize(it.how)}[{args}]'
 
     @override
-    @classmethod
-    def _puppet_binary_operator(cls, it: PuppetBinaryOperator, indent: int) -> str:
-        out = cls.serialize(it.lhs, indent)
+    def _puppet_binary_operator(self, it: PuppetBinaryOperator) -> str:
+        out = self.serialize(it.lhs)
         out += f' {op(it.op)} '
-        out += cls.serialize(it.rhs, indent)
+        out += self.serialize(it.rhs)
         return out
 
     @override
-    @classmethod
-    def _puppet_unary_operator(cls, it: PuppetUnaryOperator, indent: int) -> str:
-        return f'{op(it.op)} {cls.serialize(it.x, indent)}'
+    def _puppet_unary_operator(self, it: PuppetUnaryOperator) -> str:
+        return f'{op(it.op)} {self.serialize(it.x)}'
 
     @override
-    @classmethod
-    def _puppet_array(cls, it: PuppetArray, indent: int) -> str:
+    def _puppet_array(self, it: PuppetArray) -> str:
         if not it.items:
             return '[]'
         else:
             out = '[\n'
             for item in it.items:
-                out += ind(indent + 1) + cls.serialize(item, indent + 2) + ',\n'
-            out += ind(indent) + ']'
+                out += self.ind(1) + self.indent(2).serialize(item) + ',\n'
+            out += self.ind() + ']'
             return out
 
     @override
-    @classmethod
-    def _puppet_call(cls, it: PuppetCall, indent: int) -> str:
-        args = ', '.join(cls.serialize(x, indent) for x in it.args)
-        return f'{cls.serialize(it.func, indent)}({args})'
+    def _puppet_call(self, it: PuppetCall) -> str:
+        args = ', '.join(self.serialize(x) for x in it.args)
+        return f'{self.serialize(it.func)}({args})'
 
     @override
-    @classmethod
-    def _puppet_call_method(cls, it: PuppetCallMethod, indent: int) -> str:
-        out: str = cls.serialize(it.func, indent)
+    def _puppet_call_method(self, it: PuppetCallMethod) -> str:
+        out: str = self.serialize(it.func)
 
         if it.args:
-            args = ', '.join(cls.serialize(x, indent) for x in it.args)
+            args = ', '.join(self.serialize(x) for x in it.args)
             out += f' ({args})'
 
         if it.block:
-            out += cls.serialize(it.block, indent)
+            out += self.serialize(it.block)
 
         return out
 
     @override
-    @classmethod
-    def _puppet_case(cls, it: PuppetCase, indent: int) -> str:
-        out: str = f'{keyword("case")} {cls.serialize(it.test, indent)} {{\n'
+    def _puppet_case(self, it: PuppetCase) -> str:
+        out: str = f'{keyword("case")} {self.serialize(it.test)} {{\n'
         for (when, body) in it.cases:
-            out += ind(indent + 1)
-            out += ', '.join(cls.serialize(x, indent + 1) for x in when)
+            out += self.ind(1)
+            out += ', '.join(self.indent(1).serialize(x) for x in when)
             out += ': {\n'
             for item in body:
-                out += ind(indent + 2) + cls.serialize(item, indent + 2) + '\n'
-            out += ind(indent + 1) + '}\n'
-        out += ind(indent) + '}'
+                out += self.ind(2) + self.indent(2).serialize(item) + '\n'
+            out += self.ind(1) + '}\n'
+        out += self.ind() + '}'
         return out
 
     @override
-    @classmethod
-    def _puppet_declaration_parameter(cls, it: PuppetDeclarationParameter, indent: int) -> str:
+    def _puppet_declaration_parameter(
+            self, it: PuppetDeclarationParameter) -> str:
         out: str = ''
         if it.type:
-            out += f'{cls.serialize(it.type, indent + 1)} '
+            out += f'{self.indent(1).serialize(it.type)} '
         out += var(f'${it.k}')
         if it.v:
-            out += f' = {cls.serialize(it.v, indent + 1)}'
+            out += f' = {self.indent(1).serialize(it.v)}'
         return out
 
     @override
-    @classmethod
-    def _puppet_instanciation_parameter(cls, it: PuppetInstanciationParameter, indent: int) -> str:
-        return f'{it.k} {it.arrow} {cls.serialize(it.v, indent)}'
+    def _puppet_instanciation_parameter(
+            self, it: PuppetInstanciationParameter) -> str:
+        return f'{it.k} {it.arrow} {self.serialize(it.v)}'
 
     @override
-    @classmethod
-    def _puppet_class(cls, it: PuppetClass, indent: int) -> str:
+    def _puppet_class(self, it: PuppetClass) -> str:
         out: str = f'{keyword("class")} {it.name}'
         if it.params:
-            out += cls.format_declaration_parameters(it.params, indent)
+            out += self.format_declaration_parameters(it.params)
 
         out += ' {\n'
         for form in it.body:
-            out += ind(indent+1) + cls.serialize(form, indent+1) + '\n'
-        out += ind(indent) + '}'
+            out += self.ind(1) + self.indent(1).serialize(form) + '\n'
+        out += self.ind() + '}'
         return out
 
     @override
-    @classmethod
-    def _puppet_concat(cls, it: PuppetConcat, indent: int) -> str:
+    def _puppet_concat(self, it: PuppetConcat) -> str:
         out = '"'
         for item in it.fragments:
             match item:
@@ -232,97 +236,94 @@ class HTMLFormatter(Serializer[str]):
                 case PuppetVar(x):
                     out += var(f"${{{x}}}")
                 case puppet:
-                    out += f"${{{cls.serialize(puppet, indent)}}}"
+                    out += f"${{{self.serialize(puppet)}}}"
         out += '"'
         # Don't escape `out`, since it contains sub-expressions
         return f'<span class="string">{out}</span>'
 
     @override
-    @classmethod
-    def _puppet_collect(cls, it: PuppetCollect, indent: int) -> str:
-        return f'{cls.serialize(it.type, indent)} {cls.serialize(it.query, indent + 1)}'
-
-    @override
-    @classmethod
-    def _puppet_if(cls, it: PuppetIf, indent: int) -> str:
-        out: str = f'{keyword("if")} {cls.serialize(it.condition, indent)} {{\n'
-        for item in it.consequent:
-            out += ind(indent+1) + cls.serialize(item, indent+1) + '\n'
-        out += ind(indent) + '}'
-        if alts := it.alternative:
-            # TODO elsif
-            out += f' {keyword("else")} {{\n'
-            for item in alts:
-                out += ind(indent+1) + cls.serialize(item, indent+1) + '\n'
-            out += ind(indent) + '}'
+    def _puppet_collect(self, it: PuppetCollect) -> str:
+        return f'{self.indent(1).serialize(it.type)} {self.serialize(it.query)}'
+
+    @override
+    def _puppet_if(self, it: PuppetIfChain) -> str:
+        (test1, body1), *rest = it.clauses
+        assert test1 != 'else'
+        out: str = f'{keyword("if")} {self.serialize(test1)} {{\n'
+        for item in body1:
+            out += self.ind(1) + self.indent(1).serialize(item) + '\n'
+        out += self.ind() + '}'
+        for (testn, bodyn) in rest:
+            out += ' '
+            if testn == 'else':
+                out += keyword('else')
+            else:
+                out += f'{keyword("elsif")} {self.serialize(testn)}'
+            out += ' {'
+            for item in bodyn:
+                out += self.ind(1) + self.indent(1).serialize(item) + '\n'
+            out += self.ind() + '}'
         return out
 
     @override
-    @classmethod
-    def _puppet_unless(cls, it: PuppetUnless, indent: int) -> str:
-        out: str = f'{keyword("unless")} {cls.serialize(it.condition, indent)} {{\n'
+    def _puppet_unless(self, it: PuppetUnless) -> str:
+        out: str = f'{keyword("unless")} {self.serialize(it.condition)} {{\n'
         for item in it.consequent:
-            out += ind(indent+1) + cls.serialize(item, indent+1) + '\n'
-        out += ind(indent) + '}'
+            out += self.ind(1) + self.indent(1).serialize(item) + '\n'
+        out += self.ind() + '}'
         return out
 
     @override
-    @classmethod
-    def _puppet_keyword(cls, it: PuppetKeyword, indent: int) -> str:
+    def _puppet_keyword(self, it: PuppetKeyword) -> str:
         return it.name
 
     @override
-    @classmethod
-    def _puppet_exported_query(cls, it: PuppetExportedQuery, indent: int) -> str:
+    def _puppet_exported_query(self, it: PuppetExportedQuery) -> str:
         out: str = op('<<|')
         if f := it.filter:
-            out += ' ' + cls.serialize(f, indent)
+            out += ' ' + self.serialize(f)
         out += ' ' + op('|>>')
         return out
 
     @override
-    @classmethod
-    def _puppet_virtual_query(cls, it: PuppetVirtualQuery, indent: int) -> str:
+    def _puppet_virtual_query(self, it: PuppetVirtualQuery) -> str:
         out: str = op('<|')
         if f := it.q:
-            out += ' ' + cls.serialize(f, indent)
+            out += ' ' + self.serialize(f)
         out += ' ' + op('|>')
         return out
 
     @override
-    @classmethod
-    def _puppet_function(cls, it: PuppetFunction, indent: int) -> str:
+    def _puppet_function(self, it: PuppetFunction) -> str:
         out: str = f'{keyword("function")} {it.name}'
         if it.params:
-            out += cls.format_declaration_parameters(it.params, indent)
+            out += self.format_declaration_parameters(it.params)
 
         if ret := it.returns:
-            out += f' {op(">>")} {cls.serialize(ret, indent + 1)}'
+            out += f' {op(">>")} {self.indent(1).serialize(ret)}'
 
         out += ' {\n'
         for item in it.body:
-            out += ind(indent + 1) + cls.serialize(item, indent + 1) + '\n'
-        out += ind(indent) + '}'
+            out += self.ind(1) + self.indent(1).serialize(item) + '\n'
+        out += self.ind() + '}'
 
         return out
 
     @override
-    @classmethod
-    def _puppet_hash(cls, it: PuppetHash, indent: int) -> str:
+    def _puppet_hash(self, it: PuppetHash) -> str:
         if not it.entries:
             return '{}'
         else:
             out: str = '{\n'
             for item in it.entries:
-                out += ind(indent + 1)
-                out += cls.serialize_hash_entry(item, indent + 1)
+                out += self.ind(1)
+                out += self.indent(1).serialize_hash_entry(item)
                 out += ',\n'
-            out += ind(indent) + '}'
+            out += self.ind() + '}'
             return out
 
     @override
-    @classmethod
-    def _puppet_heredoc(cls, it: PuppetHeredoc, indent: int) -> str:
+    def _puppet_heredoc(self, it: PuppetHeredoc) -> str:
         """
         Serialize heredoc with interpolation.
 
@@ -347,7 +348,7 @@ class HTMLFormatter(Serializer[str]):
                 case PuppetVar(x):
                     body += f'${{{x}}}'
                 case p:
-                    body += cls.serialize(p, indent + 2)
+                    body += self.indent(2).serialize(p)
 
         # Check if string ends with a newline
         match it.fragments[-1]:
@@ -362,8 +363,7 @@ class HTMLFormatter(Serializer[str]):
         return f'@("EOF"{syntax}/$rt)\n{body}\n|{eol_marker} EOF'
 
     @override
-    @classmethod
-    def _puppet_literal_heredoc(cls, it: PuppetLiteralHeredoc, indent: int) -> str:
+    def _puppet_literal_heredoc(self, it: PuppetLiteralHeredoc) -> str:
         syntax: str = ''
         if it.syntax:
             syntax = f':{it.syntax}'
@@ -371,7 +371,7 @@ class HTMLFormatter(Serializer[str]):
         out: str = ''
         if not it.content:
             out += f'@(EOF{syntax})\n'
-            out += ind(indent) + '|- EOF'
+            out += self.ind() + '|- EOF'
             return out
 
         delimiter = find_heredoc_delimiter(it.content)
@@ -385,9 +385,9 @@ class HTMLFormatter(Serializer[str]):
             eol = True
 
         for line in lines:
-            out += ind(indent + 1) + line + '\n'
+            out += self.ind(1) + line + '\n'
 
-        out += ind(indent + 1) + '|'
+        out += self.ind(1) + '|'
 
         if not eol:
             out += '-'
@@ -397,153 +397,135 @@ class HTMLFormatter(Serializer[str]):
         return out
 
     @override
-    @classmethod
-    def _puppet_var(cls, it: PuppetVar, indent: int) -> str:
+    def _puppet_var(self, it: PuppetVar) -> str:
         return var(f'${it.name}')
 
     @override
-    @classmethod
-    def _puppet_lambda(cls, it: PuppetLambda, indent: int) -> str:
+    def _puppet_lambda(self, it: PuppetLambda) -> str:
         out: str = '|'
         for item in it.params:
             out += 'TODO'
         out += '| {'
         for form in it.body:
-            out += ind(indent + 1) + cls.serialize(form, indent + 1)
-        out += ind(indent) + '}'
+            out += self.ind(1) + self.indent(1).serialize(form)
+        out += self.ind() + '}'
         return out
 
     @override
-    @classmethod
-    def _puppet_qn(cls, it: PuppetQn, indent: int) -> str:
+    def _puppet_qn(self, it: PuppetQn) -> str:
         return span('qn', it.name)
 
     @override
-    @classmethod
-    def _puppet_qr(cls, it: PuppetQr, indent: int) -> str:
+    def _puppet_qr(self, it: PuppetQr) -> str:
         return span('qn', it.name)
 
     @override
-    @classmethod
-    def _puppet_regex(cls, it: PuppetRegex, indent: int) -> str:
+    def _puppet_regex(self, it: PuppetRegex) -> str:
         return span('regex', f'/{it.s}/')
 
     @override
-    @classmethod
-    def _puppet_resource(cls, it: PuppetResource, indent: int) -> str:
-        out = f'{cls.serialize(it.type, indent + 1)} {{'
+    def _puppet_resource(self, it: PuppetResource) -> str:
+        out = f'{self.indent(1).serialize(it.type)} {{'
         match it.bodies:
             case [(name, values)]:
-                out += f' {cls.serialize(name, indent + 1)}:\n'
+                out += f' {self.indent(1).serialize(name)}:\n'
                 for v in values:
-                    out += ind(indent + 1) + cls.serialize(v, indent + 2) + ',\n'
+                    out += self.ind(1) + self.indent(2).serialize(v) + ',\n'
             case bodies:
                 out += '\n'
                 for (name, values) in bodies:
-                    out += f'{ind(indent + 1)}{cls.serialize(name, indent + 1)}:\n'
+                    out += f'{self.ind(1)}{self.indent(1).serialize(name)}:\n'
                     for v in values:
-                        out += ind(indent + 2) + cls.serialize(v, indent + 3) + ',\n'
-                    out += ind(indent + 2) + ';\n'
-        out += ind(indent) + '}'
+                        out += self.ind(2) + self.indent(3).serialize(v) + ',\n'
+                    out += self.ind(2) + ';\n'
+        out += self.ind() + '}'
         return out
 
     @override
-    @classmethod
-    def _puppet_define(cls, it: PuppetDefine, indent: int) -> str:
+    def _puppet_define(self, it: PuppetDefine) -> str:
         out: str = f'{keyword("define")} {it.name}'
         if params := it.params:
-            out += cls.format_declaration_parameters(params, indent)
+            out += self.format_declaration_parameters(params)
 
         out += ' {\n'
         for form in it.body:
-            out += ind(indent + 1) + cls.serialize(form, indent + 1) + '\n'
-        out += ind(indent) + '}'
+            out += self.ind(1) + self.indent(1).serialize(form) + '\n'
+        out += self.ind() + '}'
         return out
 
     @override
-    @classmethod
-    def _puppet_string(cls, it: PuppetString, indent: int) -> str:
+    def _puppet_string(self, it: PuppetString) -> str:
         # TODO escaping
         return string(f"'{it.s}'")
 
     @override
-    @classmethod
-    def _puppet_number(cls, it: PuppetNumber, indent: int) -> str:
+    def _puppet_number(self, it: PuppetNumber) -> str:
         return number(str(it.x))
 
     @override
-    @classmethod
-    def _puppet_invoke(cls, it: PuppetInvoke, indent: int) -> str:
-        invoker = f'{cls.serialize(it.func, indent)}'
+    def _puppet_invoke(self, it: PuppetInvoke) -> str:
+        invoker = f'{self.serialize(it.func)}'
         out: str = invoker
         template: str
         if invoker == keyword('include'):
             template = ' {}'
         else:
             template = '({})'
-        out += template.format(', '.join(cls.serialize(x, indent + 1) for x in it.args))
+        out += template.format(', '.join(self.indent(1).serialize(x) for x in it.args))
         return out
 
     @override
-    @classmethod
-    def _puppet_resource_defaults(cls, it: PuppetResourceDefaults, indent: int) -> str:
-        out: str = f'{cls.serialize(it.type, indent)} {{\n'
+    def _puppet_resource_defaults(self, it: PuppetResourceDefaults) -> str:
+        out: str = f'{self.serialize(it.type)} {{\n'
         for op in it.ops:
-            out += ind(indent + 1) + cls.serialize(op, indent + 1) + ',\n'
-        out += ind(indent) + '}'
+            out += self.ind(1) + self.indent(1).serialize(op) + ',\n'
+        out += self.ind() + '}'
         return out
 
     @override
-    @classmethod
-    def _puppet_resource_override(cls, it: PuppetResourceOverride, indent: int) -> str:
-        out: str = f'{cls.serialize(it.resource, indent)} {{\n'
+    def _puppet_resource_override(self, it: PuppetResourceOverride) -> str:
+        out: str = f'{self.serialize(it.resource)} {{\n'
         for op in it.ops:
-            out += ind(indent + 1) + cls.serialize(op, indent + 1) + ',\n'
-        out += ind(indent) + '}'
+            out += self.ind(1) + self.indent(1).serialize(op) + ',\n'
+        out += self.ind() + '}'
         return out
 
     @override
-    @classmethod
-    def _puppet_declaration(cls, it: PuppetDeclaration, indent: int) -> str:
-        return f'{cls.serialize(it.k, indent)} = {cls.serialize(it.v, indent)}'
+    def _puppet_declaration(self, it: PuppetDeclaration) -> str:
+        return f'{self.serialize(it.k)} = {self.serialize(it.v)}'
 
     @override
-    @classmethod
-    def _puppet_selector(cls, it: PuppetSelector, indent: int) -> str:
-        out: str = f'{cls.serialize(it.resource, indent)} ? {{\n'
-        rendered_cases = [(cls.serialize(test, indent + 1),
-                           cls.serialize(body, indent + 2))
+    def _puppet_selector(self, it: PuppetSelector) -> str:
+        out: str = f'{self.serialize(it.resource)} ? {{\n'
+        rendered_cases = [(self.indent(1).serialize(test),
+                           self.indent(2).serialize(body))
                           for (test, body) in it.cases]
-        case_width = max(string_width(c[0], indent + 1) for c in rendered_cases)
+        case_width = max(string_width(c[0], self.__indent + 1) for c in rendered_cases)
         for (test, body) in rendered_cases:
-            out += ind(indent + 1) + test
-            out += ' ' * (case_width - string_width(test, indent + 1))
+            out += self.ind(1) + test
+            out += ' ' * (case_width - string_width(test, self.__indent + 1))
             out += f' => {body},\n'
-        out += ind(indent) + '}'
+        out += self.ind() + '}'
         return out
 
     @override
-    @classmethod
-    def _puppet_block(cls, it: PuppetBlock, indent: int) -> str:
-        return '\n'.join(cls.serialize(x, indent) for x in it.entries)
+    def _puppet_block(self, it: PuppetBlock) -> str:
+        return '\n'.join(self.serialize(x) for x in it.entries)
 
     @override
-    @classmethod
-    def _puppet_node(cls, it: PuppetNode, indent: int) -> str:
+    def _puppet_node(self, it: PuppetNode) -> str:
         out: str = keyword('node') + ' '
-        out += ', '.join(cls.serialize(x, indent) for x in it.matches)
+        out += ', '.join(self.serialize(x) for x in it.matches)
         out += ' {\n'
         for item in it.body:
-            out += ind(indent + 1) + cls.serialize(item, indent + 1) + '\n'
-        out += ind(indent) + '}'
+            out += self.ind(1) + self.indent(1).serialize(item) + '\n'
+        out += self.ind() + '}'
         return out
 
     @override
-    @classmethod
-    def _puppet_parenthesis(cls, it: PuppetParenthesis, indent: int) -> str:
-        return f'({cls.serialize(it.form, indent)})'
+    def _puppet_parenthesis(self, it: PuppetParenthesis) -> str:
+        return f'({self.serialize(it.form)})'
 
     @override
-    @classmethod
-    def _puppet_nop(cls, it: PuppetNop, indent: int) -> str:
+    def _puppet_nop(self, it: PuppetNop) -> str:
         return ''
diff --git a/muppet/puppet/format/parser.py b/muppet/puppet/format/parser.py
new file mode 100644
index 0000000000000000000000000000000000000000..ad4819137dfd05b9583b50776824d5f46e49ebb1
--- /dev/null
+++ b/muppet/puppet/format/parser.py
@@ -0,0 +1,566 @@
+"""
+"Parser" which instead "parses" the source code, and annotates it.
+
+This is basically a parser combinator, implementing the grammar for
+the exact file we were created from. While this might seem worthless,
+this allows *really* good syntax highlighting.
+"""
+
+import logging
+from .base import Serializer
+from muppet.puppet.ast import (
+    Puppet,
+    PuppetAccess,
+    PuppetArray,
+    PuppetBinaryOperator,
+    PuppetBlock,
+    PuppetCall,
+    PuppetCallMethod,
+    PuppetCase,
+    PuppetClass,
+    PuppetCollect,
+    PuppetConcat,
+    PuppetDeclaration,
+    PuppetDefine,
+    PuppetExportedQuery,
+    PuppetFunction,
+    PuppetHash,
+    PuppetHeredoc,
+    PuppetIfChain,
+    PuppetInstanciationParameter,
+    PuppetInvoke,
+    PuppetKeyword,
+    PuppetLambda,
+    PuppetLiteral,
+    PuppetLiteralHeredoc,
+    PuppetNode,
+    PuppetNop,
+    PuppetNumber,
+    PuppetParenthesis,
+    PuppetParseError,
+    PuppetQn,
+    PuppetQr,
+    PuppetRegex,
+    PuppetResource,
+    PuppetResourceDefaults,
+    PuppetResourceOverride,
+    PuppetSelector,
+    PuppetString,
+    PuppetUnaryOperator,
+    PuppetUnless,
+    PuppetVar,
+    PuppetVirtualQuery,
+
+    PuppetDeclarationParameter,
+)
+
+from muppet.parser_combinator import (
+    MatchObject,
+    ParseError,
+    ParseDirective,
+    # Items,
+    # name,
+    optional,
+    count,
+    # char,
+    many,
+    complement,
+    tag,
+    hexdig,
+    # space,
+    ws,
+    ParserCombinator,
+    s,
+    stringify_match,
+)
+
+from typing import (
+    Callable,
+    TypeVar,
+    Optional,
+)
+
+from dataclasses import dataclass
+
+
+F = TypeVar('F', bound=Callable[..., object])
+
+# TODO replace this decorator with
+# from typing import override
+# once the target python version is changed to 3.12
+
+
+def override(f: F) -> F:
+    """
+    Return function unchanged.
+
+    Placeholder @override annotator if the actual annotation isn't
+    implemented in the current python version.
+    """
+    return f
+
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class rich_char(ParseDirective):
+    """A single character character in a string with full escaping."""
+
+    c: str
+
+    def run(self, parser: 'ParserCombinator') -> list[MatchObject]:  # noqa: D102
+        snapshot = parser.snapshot()
+        try:
+            return parser.get(s(rf"\u{ord(self.c):04X}") | [r'\u{', count(hexdig, 2, 6), '}'])
+        except ParseError:
+            parser.restore(snapshot)
+
+        match self.c:
+            case '\\':
+                return parser.get(r'\\')
+            case '\n':
+                return parser.get(s('\n') | r'\n')
+            case '\r':
+                return parser.get(s('\r') | r'\r')
+            case '\t':
+                return parser.get(s('\t') | r'\t')
+            case ' ':
+                return parser.get(s(' ') | r'\s')
+            case '"':
+                return parser.get(r'\"')
+            case "'":
+                return parser.get(s("'") | r"\'")
+            case '$':
+                return parser.get(s('$') | r'\$')
+            case _:
+                return parser.get(self.c)
+
+
+@dataclass
+class known_array(ParseDirective):
+    """Parse an array where all the values are known beforehand."""
+
+    delim: str
+    in_items: list[Puppet]
+
+    def run(self, parser: 'ParserCombinator') -> list[MatchObject]:  # noqa: D102
+        """
+        Read a delimted, comma separated, array.
+
+        Reads the starting delimiter, a comma separated list of Puppet
+        items, an optional ending comma, and the ending delimiter.
+
+        :param delim:
+            A string of length two, containing the starting and ending delimiter.
+        :param in_items:
+        """
+        assert len(self.delim) == 2, "Delimiter should be the start and end character used."
+        items: list[MatchObject] = []
+        items += parser.get(ws & self.delim[0])
+        match self.in_items:
+            case []:
+                items += parser.get(ws & self.delim[1])
+            case [x, *xs]:
+                items += parser.get(ws & x)
+                for item in xs:
+                    parser.get(ws & ',' & ws & item)
+                items += parser.get(ws & optional(',') & ws & self.delim[1])
+        return items
+
+
+class ParserFormatter(Serializer[MatchObject]):
+    """
+    Reserialize AST by highlighting the original source code.
+
+    :param source:
+        The original source code. *Must* be the exact same source as
+        used to construct the corresponinding Puppet ast object.
+    :param seek:
+        Current parsing position in the string.
+
+        TODO make this private.
+    """
+
+    parser: ParserCombinator
+
+    def __init__(self, source: str, file: Optional[str] = None):
+        self.parser = ParserCombinator(source=source, file=file)
+
+    def get_declaration_parameter(self, item: PuppetDeclarationParameter) -> list[MatchObject]:
+        """Parse a single declaration parameter."""
+        return self.parser.get(ws & optional(item.type) &
+                               ws & '$' & item.k &
+                               optional(ws & '=' & ws & item.v))
+
+    def get_declaration_parameters(
+            self,
+            delim: str,
+            in_items: list[PuppetDeclarationParameter] | None) -> list[MatchObject]:
+        """See get_arr."""
+        items: list[MatchObject] = []
+        items += self.parser.get(ws & delim[0])
+        match in_items:
+            case [] | None:
+                items += self.parser.get(ws & delim[1])
+            case [x, *xs]:
+                items += self.parser.get_declaration_parameter(x)
+                for item in xs:
+                    items += self.parser.get(ws & ',')
+                    items += self.parser.get_declaration_parameter(item)
+                items += self.parser.get(ws & optional(',') & ws & delim[1])
+        return items
+
+    # --------------------------------------------------
+
+    @override
+    def _puppet_access(self, it: PuppetAccess) -> MatchObject:
+        return MatchObject('access', self.parser.get(
+            ws & it.how & ws & known_array('[]', it.args)))
+
+    @override
+    def _puppet_array(self, it: PuppetArray) -> MatchObject:
+        return MatchObject('array', self.parser.get(
+            ws & known_array('[]', it.items)))
+
+    @override
+    def _puppet_binary_operator(self, it: PuppetBinaryOperator) -> MatchObject:
+        return MatchObject('', self.parser.get(
+            ws & it.lhs & ws & it.op & ws & it.rhs))
+
+    @override
+    def _puppet_block(self, it: PuppetBlock) -> MatchObject:
+        return MatchObject('', self.parser.get(ws & it.entries))
+
+    @override
+    def _puppet_call(self, it: PuppetCall) -> MatchObject:
+        return MatchObject('', self.parser.get(
+            ws & it.func &
+            ws & known_array('()', it.args)))
+
+    @override
+    def _puppet_call_method(self, it: PuppetCallMethod) -> MatchObject:
+        return MatchObject('', self.parser.get(
+            ws & it.func &
+            optional(ws & known_array('()', it.args)) &
+            optional(ws & it.block)))
+
+    @override
+    def _puppet_case(self, it: PuppetCase) -> MatchObject:
+        items: list[MatchObject] = []
+        items += self.parser.get(
+                ws & tag('keyword', 'case') & ws & it.test & ws & '{')
+
+        for ((x, *xs), body) in it.cases:
+            items += self.parser.get(ws & x)
+            for x in xs:
+                items += self.parser.get(ws & ',' & ws & x)
+            items += self.parser.get(ws & ':' & ws & '{' & ws & body & ws & '}')
+        items += self.parser.get(ws & '}')
+        return MatchObject('', items)
+
+    @override
+    def _puppet_class(self, it: PuppetClass) -> MatchObject:
+        return MatchObject('', self.parser.get(
+            ws & tag('keyword', 'class') & ws & it.name &
+            optional(ws & (lambda: self.get_declaration_parameters('()', it.params))) &
+            optional(ws & tag('inherits', it.parent)) &
+            ws & '{' & ws & it.body & ws & '}'))
+
+    @override
+    def _puppet_collect(self, it: PuppetCollect) -> MatchObject:
+        return MatchObject('', self.parser.get(
+            ws & it.type & ws & it.query))
+
+    @override
+    def _puppet_concat(self, it: PuppetConcat) -> MatchObject:
+        out = []
+        out += self.parser.get(ws & '"')
+        for fragment in it.fragments:
+            match fragment:
+                case PuppetVar(x):
+                    f = ws & '$' & optional('{') & ws & optional('$') & x & ws & optional('}')
+                    out += self.parser.get(f)
+                case PuppetString(st):
+                    try:
+                        out += self.parser.get(st)
+                    except ParseError:
+                        for c in st:
+                            out += self.parser.get(rich_char(c))
+                case _:
+                    # TODO "${x[10][20]}"
+                    out += self.parser.get(ws & "${" & ws & fragment & ws & "}")
+        out += self.parser.get(s('"') & ws)
+        return MatchObject('string', out)
+
+    @override
+    def _puppet_declaration(self, it: PuppetDeclaration) -> MatchObject:
+        return MatchObject('', self.parser.get(
+            ws & it.k & ws & '=' & ws & it.v))
+
+    @override
+    def _puppet_define(self, it: PuppetDefine) -> MatchObject:
+        return MatchObject('', self.parser.get(
+            ws & tag('keyword', 'define') & ws & it.name &
+            optional(ws & (lambda: self.get_declaration_parameters('()', it.params))) &
+            ws & '{' & ws & it.body & ws & '}'))
+
+    @override
+    def _puppet_exported_query(self, it: PuppetExportedQuery) -> MatchObject:
+        return MatchObject('', self.parser.get(
+            ws & '<<|' & ws & it.filter & ws & '|>>'))
+
+    @override
+    def _puppet_function(self, it: PuppetFunction) -> MatchObject:
+        return MatchObject('', self.parser.get(
+            ws & tag('keyword', 'function') & ws & it.name &
+            optional(ws & (lambda: self.get_declaration_parameters('()', it.params))) &
+            optional(ws & '>>' & it.returns) &
+            ws & '{' & ws & it.body & ws & '}'))
+
+    @override
+    def _puppet_hash(self, it: PuppetHash) -> MatchObject:
+        out = []
+        out += self.parser.get(ws & '{')
+        for entry in it.entries:
+            out += self.parser.get(
+                    ws & entry.k &
+                    ws & '=>' &
+                    ws & entry.v &
+                    optional(ws & ','))
+        out += self.parser.get(ws & '}')
+        return MatchObject('', out)
+
+    @override
+    def _puppet_if_chain(self, it: PuppetIfChain) -> MatchObject:
+        logger.debug(it)
+        logger.debug("remaining = %a…", self.parser.peek_string(100))
+        (test1, body1), *rest = it.clauses
+        out = []
+        out += self.parser.get(ws & 'if' & ws & test1 & ws & '{' & ws & body1 & ws & '}')
+        while True:
+            logger.debug("seek = %s, rem = %a…, len = %s",
+                         self.parser.seek, self.parser.peek_string(100), len(rest))
+            match rest:
+                case []:
+                    break
+                case [('else', body), *xs]:
+                    out += self.parser.get(
+                            ws & tag('keyword', 'else') & ws & '{' & ws & body & ws & '}')
+                    rest = xs
+                case [(test, body), *xs]:
+                    out += self.parser.get(ws)
+                    try:
+                        out += self.parser.get(
+                                ws & tag('keyword', 'elsif') &
+                                ws & test & ws & '{' & ws & body & '}')
+                        rest = xs
+                    except ParseError:
+                        out += self.parser.get(
+                                ws & tag('keyword', 'else') &
+                                ws & '{' & ws & PuppetIfChain(rest) & ws & '}')
+                        rest = []
+        return MatchObject('', out)
+
+    @override
+    def _puppet_instanciation_parameter(self, it: PuppetInstanciationParameter) -> MatchObject:
+        return MatchObject('', self.parser.get(
+            ws & it.k & ws & it.arrow & ws & it.v & optional(ws & ' &')))
+
+    @override
+    def _puppet_invoke(self, it: PuppetInvoke) -> MatchObject:
+        out = self.parser.get(ws & it.func & optional(ws & '('))
+        match it.args:
+            case [x, *xs]:
+                out += self.parser.get(ws & x)
+                for x in xs:
+                    out += self.parser.get(ws & ',' & ws & x)
+        out += self.parser.get(optional(ws & ')'))
+        return MatchObject('', out)
+
+    @override
+    def _puppet_keyword(self, it: PuppetKeyword) -> MatchObject:
+        return MatchObject('keyword', self.parser.get(ws & it.name))
+
+    @override
+    def _puppet_lambda(self, it: PuppetLambda) -> MatchObject:
+        return MatchObject('lambda', self.parser.get(
+            s(lambda: self.get_declaration_parameters('||', it.params)) &
+            '{' & it.body & '}'))
+
+    @override
+    def _puppet_literal(self, it: PuppetLiteral) -> MatchObject:
+        return MatchObject('literal', self.parser.get(ws & it.literal))
+
+    @override
+    def _puppet_heredoc(self, it: PuppetHeredoc) -> MatchObject:
+        # TODO
+        return MatchObject('', [])
+
+    @override
+    def _puppet_literal_heredoc(self, it: PuppetLiteralHeredoc) -> MatchObject:
+        out: list[MatchObject] = []
+        out += self.parser.get(ws & '@(' & ws)
+        escape_switches = s('/') & many(s('n') | 'r' | 't' | 's' | '$' | 'u' | 'L')
+        try:
+            # Delimiter
+            out += self.parser.get('"')
+            delim_parts = self.parser.get(many(complement('"')))
+            out += delim_parts
+            out += self.parser.get('"')
+            # Syntax note
+            if it.syntax:
+                out += self.parser.get(ws & ':' & ws & it.syntax)
+            # escape switches
+            out += self.parser.get(optional(ws & escape_switches))
+            # end delimiter
+            out += self.parser.get(ws & ')')
+
+        except ParseError:
+            # Delimiter
+            delim_parts = self.parser.get(many(complement("):/")))
+            out += delim_parts
+            if it.syntax:
+                out += self.parser.get(ws & ':' & ws & it.syntax)
+            # escape switches
+            out += self.parser.get(optional(ws & escape_switches))
+            # end delimiter
+            out += self.parser.get(ws & ')')
+
+        delim = stringify_match(delim_parts)
+
+        for line in it.content.split('\n'):
+            out += self.parser.get(ws & line.lstrip() & '\n')
+        out += self.parser.get(ws & '|' & optional('-') & ws & delim)
+
+        # get_until("|-? *{delim}")
+        return MatchObject('', out)
+
+    @override
+    def _puppet_node(self, it: PuppetNode) -> MatchObject:
+        return MatchObject('', self.parser.get(
+           ws & 'node' &
+           # TODO non-wrapped list with optional trailing comma
+           ws & "{" & ws & it.body & "}"))
+
+    @override
+    def _puppet_nop(self, it: PuppetNop) -> MatchObject:
+        # Should match nothing
+        return MatchObject('', [])
+
+    @override
+    def _puppet_number(self, it: PuppetNumber) -> MatchObject:
+        out: list[MatchObject] = self.parser.get(ws)
+        match (it.x, it.radix):
+            case int(x), 8:
+                out += self.parser.get(s('0') & oct(x)[2:])
+            case int(x), 16:
+                out += self.parser.get(s('0') & 'x' & hex(x)[2:])
+            case x, None:
+                out += self.parser.get(str(it.x))
+            case _:
+                raise ValueError(f"Unexpected radix: {it.radix}")
+
+        return MatchObject('', out)
+
+    @override
+    def _puppet_parenthesis(self, it: PuppetParenthesis) -> MatchObject:
+        return MatchObject('', self.parser.get(ws & '(' & ws & it.form & ws & ')'))
+
+    @override
+    def _puppet_qn(self, it: PuppetQn) -> MatchObject:
+        return MatchObject('qn', self.parser.get(ws & it.name))
+
+    @override
+    def _puppet_qr(self, it: PuppetQr) -> MatchObject:
+        return MatchObject('qr', self.parser.get(ws & it.name))
+
+    @override
+    def _puppet_regex(self, it: PuppetRegex) -> MatchObject:
+        return MatchObject('rx', self.parser.get(ws & '/' & it.s.replace('/', r'\/') & '/'))
+
+    @override
+    def _puppet_resource(self, it: PuppetResource) -> MatchObject:
+        out = self.parser.get(ws & it.type & ws & '{')
+        for key, body in it.bodies:
+            out += self.parser.get(ws & key & ws & ':' & ws & body & ws & optional(';'))
+        out = self.parser.get(ws & '}')
+        return MatchObject('', out)
+
+    @override
+    def _puppet_resource_defaults(self, it: PuppetResourceDefaults) -> MatchObject:
+        return MatchObject('', self.parser.get(ws & it.type & ws & '{' & ws & it.ops & ws & '}'))
+
+    @override
+    def _puppet_resource_override(self, it: PuppetResourceOverride) -> MatchObject:
+        return MatchObject('', self.parser.get(
+            ws & it.resource & ws & '{' & ws & it.ops & ws & '}'))
+
+    @override
+    def _puppet_selector(self, it: PuppetSelector) -> MatchObject:
+        out = self.parser.get(ws & it.resource & ws & '?' & ws & '{')
+        for key, body in it.cases:
+            out += self.parser.get(ws & key & ws & '=>' & ws & body & ws & optional(','))
+        out += self.parser.get('}')
+        return MatchObject('', out)
+
+    @override
+    def _puppet_string(self, it: PuppetString) -> MatchObject:
+        # get one char to find delimiter
+        # Then read chars until matching delimiter (or parse expected
+        # string)
+        out: list[MatchObject] = []
+        try:
+            out += self.parser.get(ws & it.s)
+        except ParseError:
+            out += self.parser.get(ws)
+            match self.parser.get(s('"') | "'"):
+                case [MatchObject(matched="'") as match]:
+                    # Single quoted string
+                    out.append(match)
+                    for c in it.s:
+                        match c:
+                            case "'":
+                                out += self.parser.get(r"\'")
+                            case '\\':
+                                out += self.parser.get(s(r'\\') | '\\')
+                            case _:
+                                out += self.parser.get(c)
+                        # print([str(x) for x in out])
+                    out += self.parser.get("'")
+
+                case [MatchObject(matched='"') as match]:
+                    # Double quoted string
+                    out.append(match)
+                    for c in it.s:
+                        out += self.parser.get(rich_char(c))
+
+                    out += self.parser.get('"')
+                case err:
+                    logger.error("Unknown match object: %s", err)
+
+        return MatchObject('string', out)
+
+    @override
+    def _puppet_unary_operator(self, it: PuppetUnaryOperator) -> MatchObject:
+        return MatchObject('', self.parser.get(ws & it.op & ws & it.x))
+
+    @override
+    def _puppet_unless(self, it: PuppetUnless) -> MatchObject:
+        return MatchObject('', self.parser.get(
+            ws & 'unless' & ws & it.condition & ws & '{' &
+            ws & it.consequent & ws & '}'))
+
+    @override
+    def _puppet_var(self, it: PuppetVar) -> MatchObject:
+        return MatchObject('', self.parser.get(ws & '$' & it.name))
+
+    @override
+    def _puppet_virtual_query(self, it: PuppetVirtualQuery) -> MatchObject:
+        return MatchObject('', self.parser.get(ws & '<|' & ws & it.q & ws & '|>'))
+
+    @override
+    def _puppet_parse_error(self, it: PuppetParseError) -> MatchObject:
+        logger.fatal(it)
+        raise Exception(it)
+        # return MatchObject('', self.parser.get())
diff --git a/muppet/puppet/format/text.py b/muppet/puppet/format/text.py
index a3e772dfcba5c2a3073e79c7becf6b85626e6024..9d89042ea44ee0e207e57d573028a26beda84687 100644
--- a/muppet/puppet/format/text.py
+++ b/muppet/puppet/format/text.py
@@ -13,7 +13,7 @@ from muppet.puppet.ast import (
     PuppetUnaryOperator, PuppetArray, PuppetCallMethod,
     PuppetCase, PuppetDeclarationParameter,
     PuppetInstanciationParameter, PuppetClass, PuppetConcat,
-    PuppetCollect, PuppetIf, PuppetUnless, PuppetKeyword,
+    PuppetCollect, PuppetIfChain, PuppetUnless, PuppetKeyword,
     PuppetExportedQuery, PuppetVirtualQuery, PuppetFunction,
     PuppetHash, PuppetHeredoc, PuppetLiteralHeredoc, PuppetVar,
     PuppetLambda,  PuppetQn, PuppetQr, PuppetRegex,
@@ -90,11 +90,6 @@ def find_heredoc_delimiter(
             return delim
 
 
-def ind(level: int) -> str:
-    """Return indentation string of given depth."""
-    return ' ' * level * 2
-
-
 def string_width(s: str, indent: int) -> int:
     """
     Return the width of a rendered puppet expression.
@@ -129,25 +124,39 @@ def string_width(s: str, indent: int) -> int:
 class TextFormatter(Serializer[str]):
     """AST formatter returning source code."""
 
-    @classmethod
+    def __init__(self, indent: int = 0):
+        self.__indent: int = indent
+
+    def indent(self, change: int) -> 'TextFormatter':
+        """Return the current context, with an updated indentation level."""
+        return self.__class__(indent=self.__indent + change)
+
+    def ind(self, change: int = 0) -> str:
+        """
+        Return indentation for current context.
+
+        :param change:
+            Extra indentation level to add to this output.
+        """
+        return (self.__indent + change) * 2 * ' '
+
     def format_declaration_parameter(
-            cls,
+            self,
             param: PuppetDeclarationParameter,
-            indent: int) -> str:
+            ) -> str:
         """Format a single declaration parameter."""
         out: str = ''
         if param.type:
-            out += f'{cls.serialize(param.type, indent + 1)} '
+            out += f'{self.indent(1).serialize(param.type)} '
         out += f'${param.k}'
         if param.v:
-            out += f' = {cls.serialize(param.v, indent + 1)}'
+            out += f' = {self.indent(1).serialize(param.v)}'
         return out
 
-    @classmethod
     def format_declaration_parameters(
-            cls,
+            self,
             lst: list[PuppetDeclarationParameter],
-            indent: int) -> str:
+            ) -> str:
         """
         Print declaration parameters.
 
@@ -158,118 +167,107 @@ class TextFormatter(Serializer[str]):
 
         out = ' (\n'
         for param in lst:
-            out += ind(indent + 1) + cls.format_declaration_parameter(param, indent + 1) + ',\n'
-        out += ind(indent) + ')'
+            out += self.ind(1) + self.indent(1).format_declaration_parameter(param) + ',\n'
+        out += self.ind() + ')'
         return out
 
-    @classmethod
     def serialize_hash_entry(
-            cls,
+            self,
             entry: HashEntry,
-            indent: int) -> str:
+            ) -> str:
         """Return a hash entry as a string."""
-        return f'{cls.serialize(entry.k, indent + 1)} => {cls.serialize(entry.v, indent + 2)}'
+        return f'{self.indent(1).serialize(entry.k)} => {self.indent(2).serialize(entry.v)}'
 
     @override
-    @classmethod
-    def _puppet_literal(cls, it: PuppetLiteral, indent: int) -> str:
+    def _puppet_literal(self, it: PuppetLiteral) -> str:
         return it.literal
 
     @override
-    @classmethod
-    def _puppet_access(cls, it: PuppetAccess, indent: int) -> str:
-        args = ', '.join(cls.serialize(x, indent) for x in it.args)
+    def _puppet_access(self, it: PuppetAccess) -> str:
+        args = ', '.join(self.serialize(x) for x in it.args)
 
-        return f'{cls.serialize(it.how, indent)}[{args}]'
+        return f'{self.serialize(it.how)}[{args}]'
 
     @override
-    @classmethod
-    def _puppet_binary_operator(cls, it: PuppetBinaryOperator, indent: int) -> str:
-        return f'{cls.serialize(it.lhs, indent)} {it.op} {cls.serialize(it.rhs, indent)}'
+    def _puppet_binary_operator(self, it: PuppetBinaryOperator) -> str:
+        return f'{self.serialize(it.lhs)} {it.op} {self.serialize(it.rhs)}'
 
     @override
-    @classmethod
-    def _puppet_unary_operator(cls, it: PuppetUnaryOperator, indent: int) -> str:
-        return f'{it.op} {cls.serialize(it.x, indent)}'
+    def _puppet_unary_operator(self, it: PuppetUnaryOperator) -> str:
+        return f'{it.op} {self.serialize(it.x)}'
 
     @override
-    @classmethod
-    def _puppet_array(cls, it: PuppetArray, indent: int) -> str:
+    def _puppet_array(self, it: PuppetArray) -> str:
         if not it.items:
             return '[]'
         else:
             out = '[\n'
             for item in it.items:
-                out += ind(indent + 1) + cls.serialize(item, indent + 2) + ',\n'
-            out += ind(indent) + ']'
+                out += self.ind(1) + self.indent(2).serialize(item) + ',\n'
+            out += self.ind() + ']'
             return out
 
     @override
-    @classmethod
-    def _puppet_call(cls, it: PuppetCall, indent: int) -> str:
-        args = ', '.join(cls.serialize(x, indent) for x in it.args)
-        return f'{cls.serialize(it.func, indent)}({args})'
+    def _puppet_call(self, it: PuppetCall) -> str:
+        args = ', '.join(self.serialize(x) for x in it.args)
+        return f'{self.serialize(it.func)}({args})'
 
     @override
-    @classmethod
-    def _puppet_call_method(cls, it: PuppetCallMethod, indent: int) -> str:
-        out: str = cls.serialize(it.func, indent)
+    def _puppet_call_method(self, it: PuppetCallMethod) -> str:
+        out: str = self.serialize(it.func)
 
         if it.args:
-            args = ', '.join(cls.serialize(x, indent) for x in it.args)
+            args = ', '.join(self.serialize(x) for x in it.args)
             out += f' ({args})'
 
         if it.block:
-            out += cls.serialize(it.block, indent)
+            out += self.serialize(it.block)
 
         return out
 
     @override
-    @classmethod
-    def _puppet_case(cls, it: PuppetCase, indent: int) -> str:
-        out: str = f'case {cls.serialize(it.test, indent)} {{\n'
+    def _puppet_case(self, it: PuppetCase) -> str:
+        out: str = f'case {self.serialize(it.test)} {{\n'
         for (when, body) in it.cases:
-            out += ind(indent + 1)
-            out += ', '.join(cls.serialize(x, indent + 1) for x in when)
+            out += self.ind(1)
+            out += ', '.join(self.indent(1).serialize(x) for x in when)
             out += ': {\n'
             for item in body:
-                out += ind(indent + 2) + cls.serialize(item, indent + 2) + '\n'
-            out += ind(indent + 1) + '}\n'
-        out += ind(indent) + '}'
+                out += self.ind(2) + self.indent(2).serialize(item) + '\n'
+            out += self.ind(1) + '}\n'
+        out += self.ind() + '}'
         return out
 
     @override
-    @classmethod
-    def _puppet_declaration_parameter(cls, it: PuppetDeclarationParameter, indent: int) -> str:
+    def _puppet_declaration_parameter(
+            self, it: PuppetDeclarationParameter) -> str:
         out: str = ''
         if it.type:
-            out += f'{cls.serialize(it.type, indent + 1)} '
+            out += f'{self.indent(1).serialize(it.type)} '
         out += f'${it.k}'
         if it.v:
-            out += f' = {cls.serialize(it.v, indent + 1)}'
+            out += f' = {self.indent(1).serialize(it.v)}'
         return out
 
     @override
-    @classmethod
-    def _puppet_instanciation_parameter(cls, it: PuppetInstanciationParameter, indent: int) -> str:
-        return f'{it.k} {it.arrow} {cls.serialize(it.v, indent)}'
+    def _puppet_instanciation_parameter(
+            self, it: PuppetInstanciationParameter) -> str:
+        return f'{it.k} {it.arrow} {self.serialize(it.v)}'
 
     @override
-    @classmethod
-    def _puppet_class(cls, it: PuppetClass, indent: int) -> str:
+    def _puppet_class(self, it: PuppetClass) -> str:
         out: str = f'class {it.name}'
         if it.params:
-            out += cls.format_declaration_parameters(it.params, indent)
+            out += self.format_declaration_parameters(it.params)
 
         out += ' {\n'
         for form in it.body:
-            out += ind(indent+1) + cls.serialize(form, indent+1) + '\n'
-        out += ind(indent) + '}'
+            out += self.ind(1) + self.indent(1).serialize(form) + '\n'
+        out += self.ind() + '}'
         return out
 
     @override
-    @classmethod
-    def _puppet_concat(cls, it: PuppetConcat, indent: int) -> str:
+    def _puppet_concat(self, it: PuppetConcat) -> str:
         out = '"'
         for item in it.fragments:
             match item:
@@ -278,96 +276,93 @@ class TextFormatter(Serializer[str]):
                 case PuppetVar(x):
                     out += f"${{{x}}}"
                 case puppet:
-                    out += f"${{{cls.serialize(puppet, indent)}}}"
+                    out += f"${{{self.serialize(puppet)}}}"
         out += '"'
         return out
 
     @override
-    @classmethod
-    def _puppet_collect(cls, it: PuppetCollect, indent: int) -> str:
-        return f'{cls.serialize(it.type, indent)} {cls.serialize(it.query, indent + 1)}'
-
-    @override
-    @classmethod
-    def _puppet_if(cls, it: PuppetIf, indent: int) -> str:
-        out: str = f'if {cls.serialize(it.condition, indent)} {{\n'
-        for item in it.consequent:
-            out += ind(indent+1) + cls.serialize(item, indent+1) + '\n'
-        out += ind(indent) + '}'
-        if alts := it.alternative:
-            # TODO elsif
-            out += ' else {\n'
-            for item in alts:
-                out += ind(indent+1) + cls.serialize(item, indent+1) + '\n'
-            out += ind(indent) + '}'
+    def _puppet_collect(self, it: PuppetCollect) -> str:
+        return f'{self.indent(1).serialize(it.type)} {self.serialize(it.query)}'
+
+    @override
+    def _puppet_if_chain(self, it: PuppetIfChain) -> str:
+        (test1, body1), *rest = it.clauses
+        assert test1 != 'else'
+        out: str = f'if {self.serialize(test1)} {{\n'
+        for item in body1:
+            out += self.ind(1) + self.indent(1).serialize(item) + '\n'
+        out += self.ind() + '}'
+        for (testn, bodyn) in rest:
+            out += ' '
+            if testn == 'else':
+                out += 'else'
+            else:
+                out += f'elsif {self.serialize(testn)}'
+            out += ' {'
+            for item in bodyn:
+                out += self.ind(1) + self.indent(1).serialize(item) + '\n'
+            out += self.ind() + '}'
         return out
 
     @override
-    @classmethod
-    def _puppet_unless(cls, it: PuppetUnless, indent: int) -> str:
-        out: str = f'unless {cls.serialize(it.condition, indent)} {{\n'
+    def _puppet_unless(self, it: PuppetUnless) -> str:
+        out: str = f'unless {self.serialize(it.condition)} {{\n'
         for item in it.consequent:
-            out += ind(indent+1) + cls.serialize(item, indent+1) + '\n'
-        out += ind(indent) + '}'
+            out += self.ind(1) + self.indent(1).serialize(item) + '\n'
+        out += self.ind() + '}'
         return out
 
     @override
-    @classmethod
-    def _puppet_keyword(cls, it: PuppetKeyword, indent: int) -> str:
+    def _puppet_keyword(self, it: PuppetKeyword) -> str:
         return it.name
 
     @override
-    @classmethod
-    def _puppet_exported_query(cls, it: PuppetExportedQuery, indent: int) -> str:
+    def _puppet_exported_query(self, it: PuppetExportedQuery) -> str:
         out: str = '<<|'
         if f := it.filter:
-            out += ' ' + cls.serialize(f, indent)
+            out += ' ' + self.serialize(f)
         out += ' |>>'
         return out
 
     @override
-    @classmethod
-    def _puppet_virtual_query(cls, it: PuppetVirtualQuery, indent: int) -> str:
+    def _puppet_virtual_query(self, it: PuppetVirtualQuery) -> str:
         out: str = '<|'
         if f := it.q:
-            out += ' ' + cls.serialize(f, indent)
+            out += ' ' + self.serialize(f)
         out += ' |>'
         return out
 
     @override
-    @classmethod
-    def _puppet_function(cls, it: PuppetFunction, indent: int) -> str:
+    def _puppet_function(self, it: PuppetFunction) -> str:
         out: str = f'function {it.name}'
         if it.params:
-            out += cls.format_declaration_parameters(it.params, indent)
+            out += self.format_declaration_parameters(it.params)
 
         if ret := it.returns:
-            out += f' >> {cls.serialize(ret, indent + 1)}'
+            out += f' >> {self.indent(1).serialize(ret)}'
 
         out += ' {\n'
         for item in it.body:
-            out += ind(indent + 1) + cls.serialize(item, indent + 1) + '\n'
-        out += ind(indent) + '}'
+            out += self.ind(1) + self.indent(1).serialize(item) + '\n'
+        out += self.ind() + '}'
 
         return out
 
     @override
-    @classmethod
-    def _puppet_hash(cls, it: PuppetHash, indent: int) -> str:
+    def _puppet_hash(self, it: PuppetHash) -> str:
         if not it.entries:
             return '{}'
         else:
             out: str = '{\n'
             for item in it.entries:
-                out += ind(indent + 1)
-                out += cls.serialize_hash_entry(item, indent + 1)
+                out += self.ind(1)
+                out += self.indent(1).serialize_hash_entry(item)
                 out += ',\n'
-            out += ind(indent) + '}'
+            out += self.ind() + '}'
             return out
 
     @override
-    @classmethod
-    def _puppet_heredoc(cls, it: PuppetHeredoc, indent: int) -> str:
+    def _puppet_heredoc(self, it: PuppetHeredoc) -> str:
         """
         Serialize heredoc with interpolation.
 
@@ -392,7 +387,7 @@ class TextFormatter(Serializer[str]):
                 case PuppetVar(x):
                     body += f'${{{x}}}'
                 case p:
-                    body += cls.serialize(p, indent + 2)
+                    body += self.indent(2).serialize(p)
 
         # Check if string ends with a newline
         match it.fragments[-1]:
@@ -407,8 +402,7 @@ class TextFormatter(Serializer[str]):
         return f'@("EOF"{syntax}/$rt)\n{body}\n|{eol_marker} EOF'
 
     @override
-    @classmethod
-    def _puppet_literal_heredoc(cls, it: PuppetLiteralHeredoc, indent: int) -> str:
+    def _puppet_literal_heredoc(self, it: PuppetLiteralHeredoc) -> str:
         syntax: str = ''
         if it.syntax:
             syntax = f':{it.syntax}'
@@ -416,7 +410,7 @@ class TextFormatter(Serializer[str]):
         out: str = ''
         if not it.content:
             out += f'@(EOF{syntax})\n'
-            out += ind(indent) + '|- EOF'
+            out += self.ind() + '|- EOF'
             return out
 
         delimiter = find_heredoc_delimiter(it.content)
@@ -430,9 +424,9 @@ class TextFormatter(Serializer[str]):
             eol = True
 
         for line in lines:
-            out += ind(indent + 1) + line + '\n'
+            out += self.ind(1) + line + '\n'
 
-        out += ind(indent + 1) + '|'
+        out += self.ind(1) + '|'
 
         if not eol:
             out += '-'
@@ -442,153 +436,135 @@ class TextFormatter(Serializer[str]):
         return out
 
     @override
-    @classmethod
-    def _puppet_var(cls, it: PuppetVar, indent: int) -> str:
+    def _puppet_var(self, it: PuppetVar) -> str:
         return f'${it.name}'
 
     @override
-    @classmethod
-    def _puppet_lambda(cls, it: PuppetLambda, indent: int) -> str:
+    def _puppet_lambda(self, it: PuppetLambda) -> str:
         out: str = '|'
         for item in it.params:
             out += 'TODO'
         out += '| {'
         for form in it.body:
-            out += ind(indent + 1) + cls.serialize(form, indent + 1)
-        out += ind(indent) + '}'
+            out += self.ind(1) + self.indent(1).serialize(form)
+        out += self.ind() + '}'
         return out
 
     @override
-    @classmethod
-    def _puppet_qn(cls, it: PuppetQn, indent: int) -> str:
+    def _puppet_qn(self, it: PuppetQn) -> str:
         return it.name
 
     @override
-    @classmethod
-    def _puppet_qr(cls, it: PuppetQr, indent: int) -> str:
+    def _puppet_qr(self, it: PuppetQr) -> str:
         return it.name
 
     @override
-    @classmethod
-    def _puppet_regex(cls, it: PuppetRegex, indent: int) -> str:
+    def _puppet_regex(self, it: PuppetRegex) -> str:
         return f'/{it.s}/'
 
     @override
-    @classmethod
-    def _puppet_resource(cls, it: PuppetResource, indent: int) -> str:
-        out = f'{cls.serialize(it.type, indent + 1)} {{'
+    def _puppet_resource(self, it: PuppetResource) -> str:
+        out = f'{self.indent(1).serialize(it.type)} {{'
         match it.bodies:
             case [(name, values)]:
-                out += f' {cls.serialize(name, indent + 1)}:\n'
+                out += f' {self.indent(1).serialize(name)}:\n'
                 for v in values:
-                    out += ind(indent + 1) + cls.serialize(v, indent + 2) + ',\n'
+                    out += self.ind(1) + self.indent(2).serialize(v) + ',\n'
             case bodies:
                 out += '\n'
                 for (name, values) in bodies:
-                    out += f'{ind(indent + 1)}{cls.serialize(name, indent + 1)}:\n'
+                    out += f'{self.ind(1)}{self.indent(1).serialize(name)}:\n'
                     for v in values:
-                        out += ind(indent + 2) + cls.serialize(v, indent + 3) + ',\n'
-                    out += ind(indent + 2) + ';\n'
-        out += ind(indent) + '}'
+                        out += self.ind(2) + self.indent(3).serialize(v) + ',\n'
+                    out += self.ind(2) + ';\n'
+        out += self.ind() + '}'
         return out
 
     @override
-    @classmethod
-    def _puppet_define(cls, it: PuppetDefine, indent: int) -> str:
+    def _puppet_define(self, it: PuppetDefine) -> str:
         out: str = f'define {it.name}'
         if params := it.params:
-            out += cls.format_declaration_parameters(params, indent)
+            out += self.format_declaration_parameters(params)
 
         out += ' {\n'
         for form in it.body:
-            out += ind(indent + 1) + cls.serialize(form, indent + 1) + '\n'
-        out += ind(indent) + '}'
+            out += self.ind(1) + self.indent(1).serialize(form) + '\n'
+        out += self.ind() + '}'
         return out
 
     @override
-    @classmethod
-    def _puppet_string(cls, it: PuppetString, indent: int) -> str:
+    def _puppet_string(self, it: PuppetString) -> str:
         # TODO escaping
         return f"'{it.s}'"
 
     @override
-    @classmethod
-    def _puppet_number(cls, it: PuppetNumber, indent: int) -> str:
+    def _puppet_number(self, it: PuppetNumber) -> str:
         return str(it.x)
 
     @override
-    @classmethod
-    def _puppet_invoke(cls, it: PuppetInvoke, indent: int) -> str:
-        invoker = f'{cls.serialize(it.func, indent)}'
+    def _puppet_invoke(self, it: PuppetInvoke) -> str:
+        invoker = f'{self.serialize(it.func)}'
         out: str = invoker
         template: str
         if invoker == 'include':
             template = ' {}'
         else:
             template = '({})'
-        out += template.format(', '.join(cls.serialize(x, indent + 1) for x in it.args))
+        out += template.format(', '.join(self.indent(1).serialize(x) for x in it.args))
         return out
 
     @override
-    @classmethod
-    def _puppet_resource_defaults(cls, it: PuppetResourceDefaults, indent: int) -> str:
-        out: str = f'{cls.serialize(it.type, indent)} {{\n'
+    def _puppet_resource_defaults(self, it: PuppetResourceDefaults) -> str:
+        out: str = f'{self.serialize(it.type)} {{\n'
         for op in it.ops:
-            out += ind(indent + 1) + cls.serialize(op, indent + 1) + ',\n'
-        out += ind(indent) + '}'
+            out += self.ind(1) + self.indent(1).serialize(op) + ',\n'
+        out += self.ind() + '}'
         return out
 
     @override
-    @classmethod
-    def _puppet_resource_override(cls, it: PuppetResourceOverride, indent: int) -> str:
-        out: str = f'{cls.serialize(it.resource, indent)} {{\n'
+    def _puppet_resource_override(self, it: PuppetResourceOverride) -> str:
+        out: str = f'{self.serialize(it.resource)} {{\n'
         for op in it.ops:
-            out += ind(indent + 1) + cls.serialize(op, indent + 1) + ',\n'
-        out += ind(indent) + '}'
+            out += self.ind(1) + self.indent(1).serialize(op) + ',\n'
+        out += self.ind() + '}'
         return out
 
     @override
-    @classmethod
-    def _puppet_declaration(cls, it: PuppetDeclaration, indent: int) -> str:
-        return f'{cls.serialize(it.k, indent)} = {cls.serialize(it.v, indent)}'
+    def _puppet_declaration(self, it: PuppetDeclaration) -> str:
+        return f'{self.serialize(it.k)} = {self.serialize(it.v)}'
 
     @override
-    @classmethod
-    def _puppet_selector(cls, it: PuppetSelector, indent: int) -> str:
-        out: str = f'{cls.serialize(it.resource, indent)} ? {{\n'
-        rendered_cases = [(cls.serialize(test, indent + 1),
-                           cls.serialize(body, indent + 2))
+    def _puppet_selector(self, it: PuppetSelector) -> str:
+        out: str = f'{self.serialize(it.resource)} ? {{\n'
+        rendered_cases = [(self.indent(1).serialize(test),
+                           self.indent(2).serialize(body))
                           for (test, body) in it.cases]
-        case_width = max(string_width(c[0], indent + 1) for c in rendered_cases)
+        case_width = max(string_width(c[0], self.__indent + 1) for c in rendered_cases)
         for (test, body) in rendered_cases:
-            out += ind(indent + 1) + test
-            out += ' ' * (case_width - string_width(test, indent + 1))
+            out += self.ind(1) + test
+            out += ' ' * (case_width - string_width(test, self.__indent + 1))
             out += f' => {body},\n'
-        out += ind(indent) + '}'
+        out += self.ind() + '}'
         return out
 
     @override
-    @classmethod
-    def _puppet_block(cls, it: PuppetBlock, indent: int) -> str:
-        return '\n'.join(cls.serialize(x, indent) for x in it.entries)
+    def _puppet_block(self, it: PuppetBlock) -> str:
+        return '\n'.join(self.serialize(x) for x in it.entries)
 
     @override
-    @classmethod
-    def _puppet_node(cls, it: PuppetNode, indent: int) -> str:
+    def _puppet_node(self, it: PuppetNode) -> str:
         out: str = 'node '
-        out += ', '.join(cls.serialize(x, indent) for x in it.matches)
+        out += ', '.join(self.serialize(x) for x in it.matches)
         out += ' {\n'
         for item in it.body:
-            out += ind(indent + 1) + cls.serialize(item, indent + 1) + '\n'
-        out += ind(indent) + '}'
+            out += self.ind(1) + self.indent(1).serialize(item) + '\n'
+        out += self.ind() + '}'
         return out
 
     @override
-    @classmethod
-    def _puppet_parenthesis(cls, it: PuppetParenthesis, indent: int) -> str:
-        return f'({cls.serialize(it.form, indent)})'
+    def _puppet_parenthesis(self, it: PuppetParenthesis) -> str:
+        return f'({self.serialize(it.form)})'
 
     @override
-    @classmethod
-    def _puppet_nop(cls, it: PuppetNop, indent: int) -> str:
+    def _puppet_nop(self, it: PuppetNop) -> str:
         return ''
diff --git a/muppet/puppet/strings/__init__.py b/muppet/puppet/strings/__init__.py
index fabaf14c048874d76b4c28040fb0f05f1c1cdba0..42d558d3d43fabda37404a23ca3db61c65639f81 100644
--- a/muppet/puppet/strings/__init__.py
+++ b/muppet/puppet/strings/__init__.py
@@ -35,6 +35,7 @@ from dataclasses import dataclass, field
 import logging
 from .internal import Deserializable
 import re
+# from muppet.util import ILoggerAdapter
 
 
 logger = logging.getLogger(__name__)
@@ -377,6 +378,9 @@ def puppet_strings(path: str) -> bytes:
     import tempfile
 
     tmpfile = tempfile.NamedTemporaryFile()
+    logger = logging.getLogger('muppet.puppet-strings')
+    # logger = ILoggerAdapter(logging.getLogger('muppet.puppet-strings'),
+    #                         extra={'file': path})
 
     cmd = subprocess.Popen(
             ['puppet', 'strings', 'generate',
@@ -397,6 +401,7 @@ def puppet_strings(path: str) -> bytes:
         # enum found here:
         # https://github.com/puppetlabs/puppet-strings/blob/afe75151f8b47ce33433c488e22ca508aa48ac7c/spec/unit/puppet-strings/yard/handlers/ruby/rsapi_handler_spec.rb#L105
         # Expected formatting from observing the output.
+        # TODO document this special logger
         if m := re.match(r'^\[(\w+)]: (.*)', line):
             match m[1]:
                 case "debug":  logger.debug(m[2])
diff --git a/muppet/util.py b/muppet/util.py
index c12d15d48ad5d5696f78a481b3ef153384be5b1d..874ce8de0f8ba1b4ed5a92c0076d65458c906d9f 100644
--- a/muppet/util.py
+++ b/muppet/util.py
@@ -3,12 +3,16 @@
 from typing import (
     TypeVar,
     Callable,
+    Iterable,
 )
 
 from collections.abc import (
     Sequence,
 )
 
+# from logging import LoggerAdapter
+# import copy
+
 
 T = TypeVar('T')
 U = TypeVar('U')
@@ -26,3 +30,29 @@ def group_by(proc: Callable[[T], U], seq: Sequence[T]) -> dict[U, list[T]]:
         key = proc(item)
         d[key] = (d.get(key) or []) + [item]
     return d
+
+
+def concatenate(lstlst: Iterable[Iterable[T]]) -> list[T]:
+    """Concatenate a list of lists into a flat(er) list."""
+    out: list[T] = []
+    for lst in lstlst:
+        out += lst
+    return out
+
+
+# class ILoggerAdapter(LoggerAdapter):
+#     """Wrapper around a logger, allowing extra keys to added once."""
+#
+#     def __init__(self, logger, extra):
+#         super().__init__(logger, extra)
+#         self.env = extra
+#
+#     def process(self, msg: str, kwargs):
+#         """Overridden method from upstream."""
+#         msg, kwargs = super().process(msg, kwargs)
+#         result = copy.deepcopy(kwargs)
+#         default_kwargs_key = ['exc_info', 'stack_info', 'extra']
+#         custom_key = [k for k in result.keys() if k not in default_kwargs_key]
+#         result['extra'].update({k: result.pop(k) for k in custom_key})
+#
+#         return msg, result