Erstellen einer Erweiterung
Eine Erweiterung ist eine wiederverwendbare Klasse, die benutzerdefinierte Tags, Filter, Funktionen, Anbieter usw. definieren kann.
Wir erstellen Erweiterungen, wenn wir unsere Latte-Anpassungen in verschiedenen Projekten wiederverwenden oder sie mit anderen teilen wollen. Es ist auch nützlich, für jedes Webprojekt eine Erweiterung zu erstellen, die alle spezifischen Tags und Filter enthält, die Sie in den Projektvorlagen verwenden möchten.
Erweiterung Klasse
Extension ist eine Klasse, die von Latte\Extension erbt. Sie
wird bei Latte mit addExtension()
(oder über eine Konfigurationsdatei) registriert:
$latte = new Latte\Engine;
$latte->addExtension(new MyLatteExtension);
Wenn Sie mehrere Erweiterungen registrieren und diese identisch benannte Tags, Filter oder Funktionen definieren, gewinnt die zuletzt hinzugefügte Erweiterung. Dies bedeutet auch, dass Ihre Erweiterungen native Tags/Filter/Funktionen außer Kraft setzen können.
Immer wenn Sie eine Klasse ändern und die automatische Aktualisierung nicht ausgeschaltet ist, kompiliert Latte Ihre Vorlagen automatisch neu.
Eine Klasse kann jede der folgenden Methoden implementieren:
abstract class Extension
{
/**
* Initializes before template is compiler.
*/
public function beforeCompile(Engine $engine): void;
/**
* Returns a list of parsers for Latte tags.
* @return array<string, callable>
*/
public function getTags(): array;
/**
* Returns a list of compiler passes.
* @return array<string, callable>
*/
public function getPasses(): array;
/**
* Returns a list of |filters.
* @return array<string, callable>
*/
public function getFilters(): array;
/**
* Returns a list of functions used in templates.
* @return array<string, callable>
*/
public function getFunctions(): array;
/**
* Returns a list of providers.
* @return array<mixed>
*/
public function getProviders(): array;
/**
* Returns a value to distinguish multiple versions of the template.
*/
public function getCacheKey(Engine $engine): mixed;
/**
* Initializes before template is rendered.
*/
public function beforeRender(Template $template): void;
}
Um eine Vorstellung davon zu bekommen, wie die Erweiterung aussieht, sehen Sie sich die eingebaute CoreExtension an.
beforeCompile(Latte\Engine $engine): void
Wird aufgerufen, bevor die Vorlage kompiliert wird. Die Methode kann z. B. für kompilierungsbezogene Initialisierungen verwendet werden.
getTags(): array
Wird aufgerufen, wenn die Vorlage kompiliert wird. Gibt ein assoziatives Array Tagname ⇒ callable zurück, das Tag-Parsing-Funktionen enthält.
public function getTags(): array
{
return [
'foo' => [FooNode::class, 'create'],
'bar' => [BarNode::class, 'create'],
'n:baz' => [NBazNode::class, 'create'],
// ...
];
}
Das Tag n:baz
stellt ein reines n:Attribut dar, d. h. es ist ein Tag, das nur als Attribut geschrieben
werden kann.
Im Falle der Tags foo
und bar
erkennt Latte automatisch, ob es sich um Paare handelt, und wenn ja,
können sie automatisch mit n:Attributen geschrieben werden, einschließlich der Varianten mit den Präfixen
n:inner-foo
und n:tag-foo
.
Die Reihenfolge der Ausführung solcher n:Attribute wird durch ihre Reihenfolge in dem von getTags()
zurückgegebenen Array bestimmt. So wird n:foo
immer vor n:bar
ausgeführt, auch wenn die Attribute im
HTML-Tag in umgekehrter Reihenfolge aufgeführt sind als <div n:bar="..." n:foo="...">
.
Wenn Sie die Reihenfolge von n:Attributen über mehrere Erweiterungen hinweg bestimmen müssen, verwenden Sie die Hilfsmethode
order()
, bei der der Parameter before
xor after
bestimmt, welche Tags vor oder nach dem Tag
angeordnet werden.
public function getTags(): array
{
return [
'foo' => self::order([FooNode::class, 'create'], before: 'bar')]
'bar' => self::order([BarNode::class, 'create'], after: ['block', 'snippet'])]
];
}
getPasses(): array
Sie wird aufgerufen, wenn die Vorlage kompiliert wird. Gibt ein assoziatives Array Name pass ⇒ callable zurück, das Funktionen repräsentiert, die sogenannte Compiler-Passes darstellen, die den AST durchlaufen und verändern.
Auch hier kann die Hilfsmethode order()
verwendet werden. Der Wert der Parameter before
oder
after
kann *
mit der Bedeutung before/after all sein.
public function getPasses(): array
{
return [
'optimize' => [Passes::class, 'optimizePass'],
'sandbox' => self::order([$this, 'sandboxPass'], before: '*'),
// ...
];
}
beforeRender(Latte\Engine $engine): void
Sie wird vor jedem Rendering einer Vorlage aufgerufen. Die Methode kann z. B. dazu verwendet werden, Variablen zu initialisieren, die während des Renderings verwendet werden.
getFilters(): array
Sie wird aufgerufen, bevor die Vorlage gerendert wird. Gibt Filter als assoziatives Array zurück Filtername ⇒ aufrufbar.
public function getFilters(): array
{
return [
'batch' => [$this, 'batchFilter'],
'trim' => [$this, 'trimFilter'],
// ...
];
}
getFunctions(): array
Wird aufgerufen, bevor die Vorlage gerendert wird. Gibt Funktionen als assoziatives Array zurück Funktionsname ⇒ aufrufbar.
public function getFunctions(): array
{
return [
'clamp' => [$this, 'clampFunction'],
'divisibleBy' => [$this, 'divisibleByFunction'],
// ...
];
}
getProviders(): array
Sie wird aufgerufen, bevor die Vorlage gerendert wird. Gibt ein Array von Anbietern zurück, bei denen es sich in der Regel um
Objekte handelt, die Tags zur Laufzeit verwenden. Auf sie wird über $this->global->...
zugegriffen.
public function getProviders(): array
{
return [
'myFoo' => $this->foo,
'myBar' => $this->bar,
// ...
];
}
getCacheKey(Latte\Engine $engine): mixed
Sie wird aufgerufen, bevor die Vorlage gerendert wird. Der Rückgabewert wird Teil des Schlüssels, dessen Hash im Namen der kompilierten Vorlagendatei enthalten ist. Für unterschiedliche Rückgabewerte erzeugt Latte also unterschiedliche Cache-Dateien.
Wie funktioniert Latte?
Um zu verstehen, wie man benutzerdefinierte Tags oder Compilerübergänge definiert, ist es wichtig zu verstehen, wie Latte unter der Haube funktioniert.
Die Kompilierung von Vorlagen in Latte funktioniert vereinfacht gesagt wie folgt:
- Zuerst zerlegt der Lexer den Quellcode der Vorlage in kleine Stücke (Token), um die Verarbeitung zu erleichtern.
- Dann wandelt der Parser den Token-Strom in einen sinnvollen Baum von Knoten um (den Abstract Syntax Tree, AST)
- Schließlich generiert der Compiler aus dem AST eine PHP-Klasse, die die Vorlage wiedergibt und im Zwischenspeicher ablegt.
Eigentlich ist die Kompilierung ein bisschen komplizierter. Latte hat zwei Lexer und Parser: einen für die HTML-Vorlage und einen für den PHP-ähnlichen Code innerhalb der Tags. Außerdem läuft das Parsen nicht nach der Tokenisierung, sondern der Lexer und der Parser laufen parallel in zwei „Threads“ und koordinieren sich. Das ist Raketentechnik :-)
Außerdem haben alle Tags ihre eigenen Parsing-Routinen. Wenn der Parser auf ein Tag stößt, ruft er seine Parsing-Funktion auf (sie liefert Extension::getTags()). Ihre Aufgabe ist es, die Tag-Argumente und, im Falle von gepaarten Tags, den inneren Inhalt zu analysieren. Sie gibt einen Knoten zurück, der Teil des AST wird. Siehe Tag-Parsing-Funktion für Details.
Wenn der Parser seine Arbeit beendet hat, haben wir einen vollständigen AST, der die Vorlage repräsentiert. Der Wurzelknoten
ist Latte\Compiler\Nodes\TemplateNode
. Die einzelnen Knoten innerhalb des Baums repräsentieren dann nicht nur die
Tags, sondern auch die HTML-Elemente, ihre Attribute, alle Ausdrücke, die innerhalb der Tags verwendet werden, usw.
Danach kommen die so genannten Compiler-Passes ins Spiel, d. h. Funktionen (die von Extension::getPasses() zurückgegeben werden), die den AST verändern.
Der gesamte Prozess, vom Laden des Vorlageninhalts über das Parsen bis hin zur Erzeugung der resultierenden Datei, lässt sich mit diesem Code abbilden, mit dem Sie experimentieren und die Zwischenergebnisse ausgeben können:
$latte = new Latte\Engine;
$source = $latte->getLoader()->getContent($file);
$ast = $latte->parse($source);
$latte->applyPasses($ast);
$code = $latte->generate($ast, $file);
Beispiel für AST
Um eine bessere Vorstellung vom AST zu bekommen, fügen wir ein Beispiel hinzu. Dies ist die Quelltextvorlage:
{foreach $category->getItems() as $item}
<li>{$item->name|upper}</li>
{else}
no items found
{/foreach}
Und dies ist ihre Darstellung in Form von AST:
Latte\Compiler\Nodes\TemplateNode( Latte\Compiler\Nodes\FragmentNode( - Latte\Essential\Nodes\ForeachNode( expression: Latte\Compiler\Nodes\Php\Expression\MethodCallNode( object: Latte\Compiler\Nodes\Php\Expression\VariableNode('$category') name: Latte\Compiler\Nodes\Php\IdentifierNode('getItems') ) value: Latte\Compiler\Nodes\Php\Expression\VariableNode('$item') content: Latte\Compiler\Nodes\FragmentNode( - Latte\Compiler\Nodes\TextNode(' ') - Latte\Compiler\Nodes\Html\ElementNode('li')( content: Latte\Essential\Nodes\PrintNode( expression: Latte\Compiler\Nodes\Php\Expression\PropertyFetchNode( object: Latte\Compiler\Nodes\Php\Expression\VariableNode('$item') name: Latte\Compiler\Nodes\Php\IdentifierNode('name') ) modifier: Latte\Compiler\Nodes\Php\ModifierNode( filters: - Latte\Compiler\Nodes\Php\FilterNode('upper') ) ) ) ) else: Latte\Compiler\Nodes\FragmentNode( - Latte\Compiler\Nodes\TextNode('no items found') ) ) ) )
Benutzerdefinierte Tags
Zur Definition eines neuen Tags sind drei Schritte erforderlich:
- Definition der Tag-Parsing-Funktion (verantwortlich für das Parsen des Tags in einen Knoten)
- Erstellen einer Knotenklasse (verantwortlich für die Erzeugung von PHP-Code und AST-Traversierung)
- Registrierung des Tags mit Extension::getTags()
Tag-Parsing-Funktion
Das Parsen von Tags wird von der Parsing-Funktion durchgeführt (die von Extension::getTags()
zurückgegeben wird). Ihre Aufgabe ist es, alle Argumente innerhalb des Tags zu analysieren und zu überprüfen (sie verwendet
TagParser, um dies zu tun). Wenn das Tag ein Paar ist, bittet sie außerdem TemplateParser, den inneren Inhalt zu analysieren und
zurückzugeben. Die Funktion erstellt und gibt einen Knoten zurück, der in der Regel ein Kind von
Latte\Compiler\Nodes\StatementNode
ist und Teil des AST wird.
Wir erstellen eine Klasse für jeden Knoten, was wir jetzt tun werden, und platzieren die Parsing-Funktion elegant als
statische Factory in dieser Klasse. Versuchen wir als Beispiel, den bekannten Tag {foreach}
zu erstellen:
use Latte\Compiler\Nodes\StatementNode;
class ForeachNode extends StatementNode
{
// eine Parsing-Funktion, die vorerst nur einen Knoten erstellt
public static function create(Latte\Compiler\Tag $tag): self
{
$node = $tag->node = new self;
return $node;
}
public function print(Latte\Compiler\PrintContext $context): string
{
// Code wird später hinzugefügt
}
public function &getIterator(): \Generator
{
// Code wird später hinzugefügt
}
}
Der Parsing-Funktion create()
wird ein Objekt Latte\Compiler\Tag übergeben, das grundlegende Informationen
über das Tag enthält (ob es sich um ein klassisches Tag oder ein n:-Attribut handelt, in welcher Zeile es sich befindet usw.)
und hauptsächlich auf das Latte\Compiler\TagParser in
$tag->parser
zugreift.
Wenn das Tag Argumente haben muss, prüfen Sie deren Vorhandensein, indem Sie $tag->expectArguments()
aufrufen.
Zum Parsen stehen die Methoden des $tag->parser
Objekts zur Verfügung:
parseExpression(): ExpressionNode
für einen PHP-ähnlichen Ausdruck (z. B.10 + 3
)parseUnquotedStringOrExpression(): ExpressionNode
für einen Ausdruck oder eine unquoted-stringparseArguments(): ArrayNode
für den Inhalt des Arrays (z.B.10, true, foo => bar
)parseModifier(): ModifierNode
für einen Modifikator (z. B.|upper|truncate:10
)parseType(): expressionNode
für einen Typ-Hinweis (z. B.int|string
oderFoo\Bar[]
)
und ein low-level Latte\Compiler\TokenStream, das direkt mit Token arbeitet:
$tag->parser->stream->consume(...): Token
$tag->parser->stream->tryConsume(...): ?Token
Latte erweitert die PHP-Syntax in kleinen Schritten, z.B. durch Hinzufügen von Modifikatoren, verkürzten ternären Operatoren
oder der Möglichkeit, einfache alphanumerische Zeichenketten ohne Anführungszeichen zu schreiben. Aus diesem Grund verwenden wir
den Begriff PHP-ähnlich anstelle von PHP. Die Methode parseExpression()
analysiert also beispielsweise
foo
als 'foo'
. Darüber hinaus ist unquoted-string ein Spezialfall einer Zeichenkette, die
ebenfalls nicht in Anführungszeichen gesetzt werden muss, aber gleichzeitig nicht alphanumerisch sein muss. Dies ist zum Beispiel
der Pfad zu einer Datei im Tag {include ../file.latte}
. Die Methode parseUnquotedStringOrExpression()
wird verwendet, um ihn zu parsen.
Das Studium der Knotenklassen, die Teil von Latte sind, ist der beste Weg, um alle Feinheiten des Parsing-Prozesses zu lernen.
Kehren wir zum Tag {foreach}
zurück. In diesem Tag erwarten wir Argumente der Form
expression + 'as' + second expression
, die wir wie folgt parsen:
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\AreaNode;
class ForeachNode extends StatementNode
{
public ExpressionNode $expression;
public ExpressionNode $value;
public static function create(Latte\Compiler\Tag $tag): self
{
$tag->expectArguments();
$node = $tag->node = new self;
$node->expression = $tag->parser->parseExpression();
$tag->parser->stream->consume('as');
$node->value = $parser->parseExpression();
return $node;
}
}
Die Ausdrücke, die wir in die Variablen $expression
und $value
geschrieben haben, stellen
Unterknoten dar.
Definieren Sie Variablen mit Unterknoten als öffentlich, damit sie bei Bedarf in weiteren Verarbeitungsschritten geändert werden können. Außerdem ist es notwendig, sie für die Durchquerungverfügbar zu machen.
Bei gepaarten Tags, wie dem unseren, muss die Methode auch den TemplateParser den inneren Inhalt des Tags parsen lassen. Dies
wird von yield
erledigt, das ein Paar [innerer Inhalt, End-Tag] zurückgibt. Wir speichern den inneren Inhalt in der
Variablen $node->content
.
public AreaNode $content;
public static function create(Latte\Compiler\Tag $tag): \Generator
{
// ...
[$node->content, $endTag] = yield;
return $node;
}
Mit dem Schlüsselwort yield
wird die Methode create()
beendet und die Kontrolle an den
TemplateParser zurückgegeben, der das Parsen des Inhalts fortsetzt, bis er auf das End-Tag stößt. Dann übergibt er die
Kontrolle zurück an create()
, der dort weitermacht, wo er aufgehört hat. Die Verwendung der Methode
yield
gibt automatisch Generator
zurück.
Sie können auch ein Array von Tag-Namen an yield
übergeben, für die Sie das Parsen stoppen möchten, wenn sie
vor dem End-Tag auftreten. Dies hilft uns bei der Implementierung des {foreach}...{else}...{/foreach}
Konstrukts.
Wenn {else}
auftritt, parsen wir den Inhalt danach in $node->elseContent
:
public AreaNode $content;
public ?AreaNode $elseContent = null;
public static function create(Latte\Compiler\Tag $tag): \Generator
{
// ...
[$node->content, $nextTag] = yield ['else'];
if ($nextTag?->name === 'else') {
[$node->elseContent] = yield;
}
return $node;
}
Mit der Rückgabe des Knotens ist das Tag-Parsing abgeschlossen.
Erzeugen von PHP-Code
Jeder Knoten muss die Methode print()
implementieren. Gibt PHP-Code zurück, der den angegebenen Teil der Vorlage
wiedergibt (Laufzeitcode). Als Parameter wird ein Objekt Latte\Compiler\PrintContext übergeben, das über eine
nützliche Methode format()
verfügt, die das Zusammensetzen des resultierenden Codes vereinfacht.
Die Methode format(string $mask, ...$args)
akzeptiert die folgenden Platzhalter in der Maske:
%node
gibt Node aus%dump
exportiert den Wert nach PHP%raw
fügt den Text direkt ohne Transformation ein%args
gibt ArrayNode als Argumente für den Funktionsaufruf aus%line
gibt einen Kommentar mit einer Zeilennummer aus%escape(...)
entschlüsselt den Inhalt%modify(...)
wendet einen Modifikator an%modifyContent(...)
wendet einen Modifikator auf Blöcke an
Unsere Funktion print()
könnte wie folgt aussehen (der Einfachheit halber vernachlässigen wir den Zweig
else
):
public function print(Latte\Compiler\PrintContext $context): string
{
return $context->format(
<<<'XX'
foreach (%node as %node) %line {
%node
}
XX,
$this->expression,
$this->value,
$this->position,
$this->content,
);
}
Die Variable $this->position
ist bereits durch die Klasse Latte\Compiler\Node definiert und wird durch den Parser
gesetzt. Sie enthält ein Latte\Compiler\Position
Objekt mit der Position des Tags im Quellcode in Form einer Zeilen- und Spaltennummer.
Der Laufzeitcode kann Hilfsvariablen verwenden. Um Kollisionen mit Variablen zu vermeiden, die von der Vorlage selbst verwendet
werden, ist es üblich, ihnen die Zeichen $ʟ__
voranzustellen.
Es kann zur Laufzeit auch beliebige Werte verwenden, die dem Template in Form von Providern mit der Methode Extension::getProviders() übergeben werden. Der Zugriff auf sie erfolgt über
$this->global->...
.
AST-Traversierung
Um den AST-Baum in der Tiefe zu durchlaufen, ist es notwendig, die Methode getIterator()
zu implementieren. Dies
ermöglicht den Zugriff auf Unterknoten:
public function &getIterator(): \Generator
{
yield $this->expression;
yield $this->value;
yield $this->content;
if ($this->elseContent) {
yield $this->elseContent;
}
}
Beachten Sie, dass getIterator()
einen Verweis zurückgibt. Dies ermöglicht es den Besuchern von Knoten, einzelne
Knoten durch andere Knoten zu ersetzen.
Wenn ein Knoten Unterknoten hat, ist es notwendig, diese Methode zu implementieren und alle Unterknoten verfügbar zu machen. Andernfalls könnte eine Sicherheitslücke entstehen. So wäre der Sandbox-Modus beispielsweise nicht in der Lage, Unterknoten zu kontrollieren und sicherzustellen, dass in ihnen keine unerlaubten Konstrukte aufgerufen werden.
Da das Schlüsselwort yield
im Körper der Methode vorhanden sein muss, auch wenn sie keine Unterknoten hat,
schreiben Sie sie wie folgt:
public function &getIterator(): \Generator
{
if (false) {
yield;
}
}
AuxiliaryNode
Wenn Sie ein neues Tag für Latte erstellen, ist es ratsam, eine eigene Knotenklasse dafür zu erstellen, die es im AST-Baum
repräsentiert (siehe die Klasse ForeachNode
im obigen Beispiel). In einigen Fällen kann die triviale
Hilfsknotenklasse AuxiliaryNode nützlich sein,
die es Ihnen ermöglicht, den Körper der Methode print()
und die Liste der Knoten, die durch die Methode
getIterator()
zugänglich gemacht werden, als Konstruktorparameter zu übergeben:
// Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode
// or Latte\Compiler\Nodes\AuxiliaryNode
$node = new AuxiliaryNode(
// body of the print() method:
fn(PrintContext $context, $argNode) => $context->format('myFunc(%node)', $argNode),
// nodes accessed via getIterator() and also passed into the print() method:
[$argNode],
);
Compiler übergibt
Compiler-Passes sind Funktionen, die ASTs modifizieren oder Informationen in ihnen sammeln. Sie werden von der Methode Extension::getPasses() zurückgegeben.
Node Traverser
Die häufigste Art, mit dem AST zu arbeiten, ist die Verwendung eines Latte\Compiler\NodeTraverser:
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
$ast = (new NodeTraverser)->traverse(
$ast,
enter: fn(Node $node) => ...,
leave: fn(Node $node) => ...,
);
Die Funktion enter (d.h. visitor) wird aufgerufen, wenn ein Knoten zum ersten Mal gefunden wird, bevor seine Unterknoten verarbeitet werden. Die Funktion leave wird aufgerufen, nachdem alle Unterknoten besucht worden sind. Ein gängiges Muster ist, dass enter dazu verwendet wird, einige Informationen zu sammeln, und leave dann Änderungen auf der Grundlage dieser Informationen vornimmt. Zu dem Zeitpunkt, an dem leave aufgerufen wird, ist der gesamte Code innerhalb des Knotens bereits besucht und die notwendigen Informationen gesammelt worden.
Wie ändert man AST? Am einfachsten ist es, einfach die Eigenschaften der Knoten zu ändern. Die zweite Möglichkeit besteht
darin, den Knoten vollständig zu ersetzen, indem ein neuer Knoten zurückgegeben wird. Beispiel: Der folgende Code ändert alle
Ganzzahlen im AST in Strings (z.B. 42 wird in '42'
geändert).
use Latte\Compiler\Nodes\Php;
$ast = (new NodeTraverser)->traverse(
$ast,
leave: function (Node $node) {
if ($node instanceof Php\Scalar\IntegerNode) {
return new Php\Scalar\StringNode((string) $node->value);
}
},
);
Ein AST kann leicht Tausende von Knoten enthalten, und das Durchlaufen all dieser Knoten kann langsam sein. In einigen Fällen ist es möglich, eine vollständige Durchquerung zu vermeiden.
Wenn Sie nach allen Html\ElementNode
in einem Baum suchen, wissen Sie, dass es keinen Sinn macht, auch alle
untergeordneten Knoten zu überprüfen, sobald Sie Php\ExpressionNode
gesehen haben, da HTML nicht in Ausdrücken
enthalten sein kann. In diesem Fall können Sie den Traverser anweisen, nicht in den Klassenknoten zu rekursieren:
$ast = (new NodeTraverser)->traverse(
$ast,
enter: function (Node $node) {
if ($node instanceof Php\ExpressionNode) {
return NodeTraverser::DontTraverseChildren;
}
// ...
},
);
Wenn Sie nur nach einem bestimmten Knoten suchen, ist es auch möglich, den Traversal nach dem Auffinden des Knotens ganz abzubrechen.
$ast = (new NodeTraverser)->traverse(
$ast,
enter: function (Node $node) {
if ($node instanceof Nodes\ParametersNode) {
return NodeTraverser::StopTraversal;
}
// ...
},
);
Knoten-Helfer
Die Klasse Latte\Compiler\NodeHelpers bietet einige Methoden, mit denen AST-Knoten gefunden werden können, die entweder einen bestimmten Callback etc. erfüllen. Ein paar Beispiele werden gezeigt:
use Latte\Compiler\NodeHelpers;
// findet alle HTML-Element-Knoten
$elements = NodeHelpers::find($ast, fn(Node $node) => $node instanceof Nodes\Html\ElementNode);
// findet den ersten Textknoten
$text = NodeHelpers::findFirst($ast, fn(Node $node) => $node instanceof Nodes\TextNode);
// wandelt PHP-Wert-Knoten in realen Wert um
$value = NodeHelpers::toValue($node);
// konvertiert statische Textknoten in eine Zeichenkette
$text = NodeHelpers::toText($node);