Having some brain farts...
<?php
namespace Elkarte\Messages\Formatters;
class ForumML
{
    const KNOWN_NODE_TYPES = [
        'TEXT'          => 1,
        'NEW_LINE'      => 2,
        'EMPTY_LINE'    => 3,
        'TAG'           => 4,
        'EMOJI'         => 5,
        'LINK'          => 6,
    ];
    public function format($message)
    {
        foreach ($message as $node) {
            if (!$this->isKnownType($node->getType())) {
                throw new InvalidNodeTypeException;
            }
            $this->formatChildren($node);
        }
    }
    public function isKnownType($type)
    {
        $knownTypes = self::KNOWN_NODE_TYPES;
        return isset($knownTypes[$type]);
    }
    protected function formatChildren($node)
    {
        if ($node->hasChildren()) {
            foreach ($node->getChildren() as $childNode) {
                $this->format($childNode);
            }
        }
    }
}
class NodeFactory
{
    public function newNode($node, AbstractNode $parent = null)
    {
        if (!isset($node['type']) || !$this->nodeFactory->isKnownType($node['type'])) {
            throw new InvalidNodeTypeException;
        }
        $newNode = null;
        switch ($node['type']) {
            case NodeFactory::TEXT:
                $newNode = new TextNode($node, $this, $parent);
                break;
        }
        return $newNode;
    }
}
abstract class AbstractNode
{
    protected $type;
    protected $factory;
    protected $parent;
    protected $value;
    protected $children = [];
    protected $attributes = [];
    abstract public function render();
    abstract public function getType();
    /**
     * AbstractNode constructor.
     * @param array $node the unserialized array
     * @param NodeFactory $nodeFactory
     * @param AbstractNode|null $parent
     */
    public function __construct(array $node, NodeFactory $nodeFactory, AbstractNode $parent = null)
    {
        $this->nodeFactory = $nodeFactory;
        $this->type = $this->getType();
        if (isset($node['attributes'])) {
            $this->attributes = $node['attributes'];
        }
        if (isset($node['children'])) {
            $this->setChildren($node['children']);
        }
        if (isset($node['value'])) {
            $this->value = $node['value'];
        }
    }
    public function __toString()
    {
        $this->render();
    }
    public function __sleep()
    {
        return array_filter([
            'type'          => $this->type,
            'value'         => $this->value,
            'attributes'    => $this->attributes,
            'children'      => array_reduce($this->children, function ($children, $child) {
                $children[] = $child->__sleep;
                return $children;
            }, []),
        ]);
    }
    public function hasParent()
    {
        return $this->parent instanceof AbstractNode;
    }
    public function getParent()
    {
        return $this->parent;
    }
    public function hasParentOfType($type)
    {
        return $this->hasParent() && (
            $this->getParent()->getType() === $type
            || $this->getParent()->hasParentOfType($type));
    }
    public function hasChildren()
    {
        return !empty($this->children);
    }
    public function getChildren()
    {
        return $this->children;
    }
    public function hasAttributes()
    {
        return !empty($this->attributes);
    }
    public function getAttributes()
    {
        return $this->attributes;
    }
    protected function setParent(AbstractNode $parent)
    {
        $this->parent = $parent;
        return $this;
    }
    protected function setChildren(array $nodes)
    {
        $this->children = [];
        foreach ($nodes as $node) {
            $this->children[] = $this->factory->newNode($node, $this);
        }
        return $this;
    }
}
// namespace Nodes;
class Tag extends AbstractNode
{
    const TYPE = 'TAG';
    public function getType()
    {
        return self::TYPE;
    }
}