Uzantı Oluşturma
Uzantı, özel etiketler, filtreler, işlevler, sağlayıcılar vb. tanımlayabilen yeniden kullanılabilir bir sınıftır.
Latte özelleştirmelerimizi farklı projelerde yeniden kullanmak veya başkalarıyla paylaşmak istediğimizde uzantılar oluştururuz. Her web projesi için proje şablonlarında kullanmak istediğiniz tüm özel etiketleri ve filtreleri içerecek bir uzantı oluşturmak da yararlıdır.
Uzatma Sınıfı
Uzantı, Latte\Extension adresinden miras alınan bir
sınıftır. addExtension()
kullanılarak (veya yapılandırma dosyası aracılığıyla) Latte'ye
kaydedilir:
$latte = new Latte\Engine;
$latte->addExtension(new MyLatteExtension);
Birden fazla uzantı kaydederseniz ve bunlar aynı adlı etiketleri, filtreleri veya işlevleri tanımlarsa, son eklenen uzantı kazanır. Bu aynı zamanda uzantılarınızın yerel etiketleri/filtreleri/işlevleri geçersiz kılabileceği anlamına gelir.
Bir sınıfta değişiklik yaptığınızda ve otomatik yenileme kapatılmadığında, Latte şablonlarınızı otomatik olarak yeniden derleyecektir.
Bir sınıf aşağıdaki yöntemlerden herhangi birini uygulayabilir:
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;
}
Uzantının neye benzediğine dair bir fikir edinmek için yerleşik "CoreExtension:https://github.com/…xtension.php"a bir göz atın.
beforeCompile(Latte\Engine $engine): void
Şablon derlenmeden önce çağrılır. Bu yöntem, örneğin derlemeyle ilgili başlatmalar için kullanılabilir.
getTags(): array
Şablon derlendiğinde çağrılır. Etiket ayrıştırma işlevleri olan etiket adı ⇒ çağrılabilir bir ilişkisel dizi döndürür.
public function getTags(): array
{
return [
'foo' => [FooNode::class, 'create'],
'bar' => [BarNode::class, 'create'],
'n:baz' => [NBazNode::class, 'create'],
// ...
];
}
n:baz
etiketi saf bir n:özniteliği temsil eder, yani yalnızca bir öznitelik olarak yazılabilen bir
etikettir.
foo
ve bar
etiketleri söz konusu olduğunda, Latte bunların çift olup olmadığını otomatik
olarak tanıyacaktır ve eğer öyleyse, n:inner-foo
ve n:tag-foo
öneklerine sahip varyantlar da dahil
olmak üzere n:attributes kullanılarak otomatik olarak yazılabilirler.
Bu tür n:özniteliklerin yürütülme sırası getTags()
tarafından döndürülen dizideki sıralarına göre
belirlenir. Bu nedenle, nitelikler HTML etiketinde ters sırada listelenmiş olsa bile n:foo
her zaman
n:bar
adresinden önce yürütülür. <div n:bar="..." n:foo="...">
.
Birden fazla uzantıda n:niteliklerinin sırasını belirlemeniz gerekiyorsa, order()
yardımcı yöntemini
kullanın; burada before
xor after
parametresi hangi etiketlerin etiketten önce veya sonra
sıralanacağını belirler.
public function getTags(): array
{
return [
'foo' => self::order([FooNode::class, 'create'], before: 'bar')]
'bar' => self::order([BarNode::class, 'create'], after: ['block', 'snippet'])]
];
}
getPasses(): array
Şablon derlendiğinde çağrılır. AST'yi dolaşan ve değiştiren sözde derleyici geçişlerini temsil eden işlevler olan name pass ⇒ callable ilişkisel bir dizi döndürür.
Yine order()
yardımcı yöntemi kullanılabilir. before
veya after
parametrelerinin
değeri before/after all anlamında *
olabilir.
public function getPasses(): array
{
return [
'optimize' => [Passes::class, 'optimizePass'],
'sandbox' => self::order([$this, 'sandboxPass'], before: '*'),
// ...
];
}
beforeRender(Latte\Engine $engine): void
Her şablon render işleminden önce çağrılır. Yöntem, örneğin oluşturma sırasında kullanılan değişkenleri başlatmak için kullanılabilir.
getFilters(): array
Şablon işlenmeden önce çağrılır. Filtreleri ilişkisel bir dizi olarak döndürür filter name ⇒ callable.
public function getFilters(): array
{
return [
'batch' => [$this, 'batchFilter'],
'trim' => [$this, 'trimFilter'],
// ...
];
}
getFunctions(): array
Şablon işlenmeden önce çağrılır. İşlevleri bir ilişkisel dizi olarak döndürür işlev adı ⇒ çağrılabilir.
public function getFunctions(): array
{
return [
'clamp' => [$this, 'clampFunction'],
'divisibleBy' => [$this, 'divisibleByFunction'],
// ...
];
}
getProviders(): array
Şablon işlenmeden önce çağrılır. Genellikle çalışma zamanında etiketleri kullanan nesneler olan sağlayıcılardan
oluşan bir dizi döndürür. Bunlara $this->global->...
üzerinden erişilir.
public function getProviders(): array
{
return [
'myFoo' => $this->foo,
'myBar' => $this->bar,
// ...
];
}
getCacheKey(Latte\Engine $engine): mixed
Şablon oluşturulmadan önce çağrılır. Dönüş değeri, hash'i derlenen şablon dosyasının adında bulunan anahtarın bir parçası haline gelir. Böylece, farklı geri dönüş değerleri için Latte farklı önbellek dosyaları oluşturacaktır.
Latte Nasıl Çalışır?
Özel etiketlerin veya derleyici geçişlerinin nasıl tanımlanacağını anlamak için, Latte'nin kaputun altında nasıl çalıştığını anlamak önemlidir.
Latte'de şablon derleme basitçe şu şekilde çalışır:
- İlk olarak, lexer şablon kaynak kodunu daha kolay işlenebilmesi için küçük parçalara (token) ayırır
- Daha sonra, ayrıştırıcı belirteç akışını anlamlı bir düğüm ağacına (Soyut Sözdizimi Ağacı, AST) dönüştürür
- Son olarak, derleyici AST'den şablonu işleyen ve önbelleğe alan bir PHP sınıfı oluşturur.
Aslında, derleme biraz daha karmaşıktır. Latte iki sözlükleyici ve ayrıştırıcıya sahiptir: biri HTML şablonu için diğeri de etiketlerin içindeki PHP benzeri kod için. Ayrıca, ayrıştırma tokenizasyondan sonra çalışmaz, ancak lexer ve parser iki „thread“ içinde paralel olarak çalışır ve koordine olur. Bu roket bilimi :-)
Ayrıca, tüm etiketlerin kendi ayrıştırma rutinleri vardır. Ayrıştırıcı bir etiketle karşılaştığında, ayrıştırma işlevini çağırır ( Extension::getTags() döndürür). Görevleri, etiket argümanlarını ve eşleştirilmiş etiketler söz konusu olduğunda iç içeriği ayrıştırmaktır. AST'nin bir parçası haline gelen bir node döndürür. Ayrıntılar için Etiket ayrıştırma işlevine bakın.
Ayrıştırıcı işini bitirdiğinde, şablonu temsil eden eksiksiz bir AST'ye sahip oluruz. Kök düğüm
Latte\Compiler\Nodes\TemplateNode
'dur. Ağacın içindeki tek tek düğümler yalnızca etiketleri değil, aynı
zamanda HTML öğelerini, bunların niteliklerini, etiketlerin içinde kullanılan ifadeleri vb. temsil eder.
Bundan sonra, AST'yi değiştiren fonksiyonlar ( Extension::getPasses() tarafından döndürülen) olan Derleyici geçişleri devreye girer.
Şablon içeriğinin yüklenmesinden ayrıştırılmasına ve sonuç dosyasının oluşturulmasına kadar tüm süreç, deneyebileceğiniz ve ara sonuçları dökebileceğiniz bu kodla sıralanabilir:
$latte = new Latte\Engine;
$source = $latte->getLoader()->getContent($file);
$ast = $latte->parse($source);
$latte->applyPasses($ast);
$code = $latte->generate($ast, $file);
AST örneği
AST hakkında daha iyi bir fikir edinmek için bir örnek ekliyoruz. Bu kaynak şablonudur:
{foreach $category->getItems() as $item}
<li>{$item->name|upper}</li>
{else}
no items found
{/foreach}
Bu da onun AST biçimindeki temsilidir:
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') ) ) ) )
Özel Etiketler
Yeni bir etiket tanımlamak için üç adım gereklidir:
- etiket ayrıştırma işlevini tanımlama (etiketi bir düğüme ayrıştırmaktan sorumlu)
- bir düğüm sınıfı oluşturma ( PHP kodu üretmekten ve AST geçişinden sorumlu)
- Extension::getTags() kullanarak etiketi kaydetme
Etiket Ayrıştırma İşlevi
Etiketlerin ayrıştırılması kendi ayrıştırma fonksiyonu ( Extension::getTags() tarafından
döndürülen) tarafından gerçekleştirilir. Görevi, etiket içindeki tüm argümanları ayrıştırmak ve kontrol etmektir
(bunu yapmak için TagParser kullanır). Ayrıca, etiket bir çift ise, TemplateParser'dan iç içeriği ayrıştırmasını ve
döndürmesini isteyecektir. Fonksiyon, genellikle Latte\Compiler\Nodes\StatementNode
'un bir çocuğu olan bir
düğüm oluşturur ve döndürür ve bu AST'nin bir parçası olur.
Şimdi yapacağımız gibi her düğüm için bir sınıf oluşturacağız ve ayrıştırma işlevini statik bir fabrika olarak
zarif bir şekilde içine yerleştireceğiz. Örnek olarak, tanıdık {foreach}
etiketini oluşturmayı
deneyelim:
use Latte\Compiler\Nodes\StatementNode;
class ForeachNode extends StatementNode
{
// a parsing function that just creates a node for now
public static function create(Latte\Compiler\Tag $tag): self
{
$node = $tag->node = new self;
return $node;
}
public function print(Latte\Compiler\PrintContext $context): string
{
// kod daha sonra eklenecektir
}
public function &getIterator(): \Generator
{
// kod daha sonra eklenecektir
}
}
Ayrıştırma işlevi create()
, etiketle ilgili temel bilgileri (klasik bir etiket mi yoksa n:attribute mu
olduğu, hangi satırda olduğu, vb) taşıyan ve esas olarak $tag->parser
içindeki Latte\Compiler\TagParser adresine erişen bir Latte\Compiler\Tag nesnesi aktarır.
Etiketin argümanlara sahip olması gerekiyorsa, $tag->expectArguments()
adresini çağırarak bunların
varlığını kontrol edin. $tag->parser
nesnesinin yöntemleri bunları ayrıştırmak için kullanılabilir:
- PHP benzeri bir ifade için
parseExpression(): ExpressionNode
(örn.10 + 3
) parseUnquotedStringOrExpression(): ExpressionNode
bir ifade veya tırnaksız dize içinparseArguments(): ArrayNode
dizinin içeriği (örn.10, true, foo => bar
)parseModifier(): ModifierNode
bir değiştirici için (örn.|upper|truncate:10
)- typehint için
parseType(): expressionNode
(örn.int|string
veyaFoo\Bar[]
)
ve doğrudan jetonlarla çalışan düşük seviyeli bir Latte\Compiler\TokenStream:
$tag->parser->stream->consume(...): Token
$tag->parser->stream->tryConsume(...): ?Token
Latte, PHP sözdizimini küçük yollarla genişletir, örneğin değiştiriciler, kısaltılmış üçlü operatörler
ekleyerek veya basit alfanümerik dizelerin tırnak işaretleri olmadan yazılmasına izin vererek. Bu nedenle PHP yerine PHP
benzeri terimini kullanıyoruz. Bu nedenle, örneğin parseExpression()
yöntemi foo
adresini
'foo'
olarak ayrıştırır. Ayrıca, unquoted-string tırnak içine alınması gerekmeyen, ancak aynı
zamanda alfanümerik olması da gerekmeyen bir dizginin özel bir durumudur. Örneğin, {include ../file.latte}
etiketindeki bir dosyanın yoludur. Bunu ayrıştırmak için parseUnquotedStringOrExpression()
yöntemi
kullanılır.
Latte'nin bir parçası olan düğüm sınıflarını incelemek, ayrıştırma sürecinin tüm incelikli ayrıntılarını öğrenmenin en iyi yoludur.
{foreach}
etiketine geri dönelim. İçinde, aşağıdaki gibi ayrıştırdığımız
expression + 'as' + second expression
biçiminde argümanlar bekliyoruz:
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;
}
}
$expression
ve $value
değişkenlerine yazdığımız ifadeler alt düğümleri temsil
etmektedir.
Alt düğümleri olan değişkenleri public olarak tanımlayın, böylece gerekirse sonraki işlem adımlarında değiştirilebilirler. Ayrıca, bunları çaprazlama için ulaşılabilir kılmak da gereklidir.
Bizimki gibi eşleştirilmiş etiketler için, yöntem TemplateParser'ın etiketin iç içeriğini ayrıştırmasına da izin
vermelidir. Bu işlem, [iç içerik, son etiket] çiftini döndüren yield
tarafından gerçekleştirilir. İç
içeriği $node->content
değişkeninde saklıyoruz.
public AreaNode $content;
public static function create(Latte\Compiler\Tag $tag): \Generator
{
// ...
[$node->content, $endTag] = yield;
return $node;
}
yield
anahtar sözcüğü create()
yönteminin sonlanmasına neden olarak kontrolü TemplateParser'a
geri döndürür ve TemplateParser end etiketine ulaşana kadar içeriği ayrıştırmaya devam eder. Daha sonra kontrolü,
kaldığı yerden devam eden create()
yöntemine geri aktarır. yield
, yöntemi kullanıldığında
otomatik olarak Generator
döndürülür.
Ayrıca yield
adresine, son etiketten önce gelirlerse ayrıştırmayı durdurmak istediğiniz bir dizi etiket
adı da iletebilirsiniz. Bu bize şu yöntemi uygulamamıza yardımcı olur {foreach}...{else}...{/foreach}
yapı.
Eğer {else}
oluşursa, ondan sonraki içeriği $node->elseContent
olarak ayrıştırırız:
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;
}
Dönen düğüm etiket ayrıştırmasını tamamlar.
PHP Kodu Oluşturma
Her düğüm print()
yöntemini uygulamalıdır. Şablonun verilen bölümünü işleyen PHP kodunu döndürür
(çalışma zamanı kodu). Parametre olarak bir Latte\Compiler\PrintContext nesnesi aktarılır; bu
nesne, elde edilen kodun montajını basitleştiren kullanışlı bir format()
yöntemine sahiptir.
format(string $mask, ...$args)
yöntemi maskede aşağıdaki yer tutucuları kabul eder:
%node
düğüm yazdırır%dump
değeri PHP'ye aktarır%raw
metni herhangi bir dönüştürme yapmadan doğrudan ekler%args
fonksiyon çağrısına argüman olarak ArrayNode yazdırır%line
satır numarasıyla birlikte bir yorum yazdırır%escape(...)
içerikten kaçar%modify(...)
bir değiştirici uygular%modifyContent(...)
bloklara bir değiştirici uygular
print()
fonksiyonumuz aşağıdaki gibi görünebilir (basitlik için else
dalını ihmal
ediyoruz):
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,
);
}
$this->position
değişkeni Latte\Compiler\Node sınıfı tarafından zaten
tanımlanmıştır ve ayrıştırıcı tarafından ayarlanır. Etiketin kaynak koddaki konumunu satır ve sütun numarası
şeklinde gösteren bir Latte\Compiler\Position nesnesi
içerir.
Çalışma zamanı kodu yardımcı değişkenler kullanabilir. Şablonun kendisi tarafından kullanılan değişkenlerle
çakışmayı önlemek için, bunların önüne $ʟ__
karakterlerinin eklenmesi gelenekseldir.
Ayrıca, çalışma zamanında Extension::getProviders() yöntemi kullanılarak şablona
sağlayıcılar şeklinde aktarılan rastgele değerler de kullanabilir. Bunlara $this->global->...
adresini
kullanarak erişir.
AST Çaprazlama
AST ağacını derinlemesine dolaşmak için getIterator()
yöntemini uygulamak gerekir. Bu, alt düğümlere
erişim sağlayacaktır:
public function &getIterator(): \Generator
{
yield $this->expression;
yield $this->value;
yield $this->content;
if ($this->elseContent) {
yield $this->elseContent;
}
}
getIterator()
adresinin bir referans döndürdüğüne dikkat edin. Bu, düğüm ziyaretçilerinin tek tek
düğümleri diğer düğümlerle değiştirmesini sağlar.
Bir düğümün alt düğümleri varsa, bu yöntemi uygulamak ve tüm alt düğümleri kullanılabilir hale getirmek gerekir. Aksi takdirde bir güvenlik açığı oluşabilir. Örneğin, sandbox modu alt düğümleri kontrol edemeyecek ve izin verilmeyen yapıların içlerinde çağrılmamasını sağlayamayacaktır.
Alt düğümleri olmasa bile yield
anahtar sözcüğünün yöntem gövdesinde bulunması gerektiğinden,
aşağıdaki gibi yazın:
public function &getIterator(): \Generator
{
if (false) {
yield;
}
}
AuxiliaryNode
Latte için yeni bir etiket oluşturuyorsanız, bunun için AST ağacında temsil edecek özel bir düğüm sınıfı
oluşturmanız tavsiye edilir (yukarıdaki örnekte ForeachNode
sınıfına bakın). Bazı durumlarda,
print()
yönteminin gövdesini ve getIterator()
yöntemi tarafından erişilebilir kılınan
düğümlerin listesini yapıcı parametreleri olarak geçirmenize olanak tanıyan önemsiz yardımcı düğüm sınıfı AuxiliaryNode 'u yararlı
bulabilirsiniz:
// 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],
);
Derleyici Geçişleri
Derleyici Geçişleri, AST'leri değiştiren veya içlerindeki bilgileri toplayan işlevlerdir. Extension::getPasses() yöntemi tarafından döndürülürler.
Düğüm Çaprazlayıcı
AST ile çalışmanın en yaygın yolu bir Latte\Compiler\NodeTraverser kullanmaktır:
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
$ast = (new NodeTraverser)->traverse(
$ast,
enter: fn(Node $node) => ...,
leave: fn(Node $node) => ...,
);
Enter* fonksiyonu (yani ziyaretçi) bir düğümle ilk karşılaşıldığında, alt düğümleri işlenmeden önce çağrılır. Tüm alt düğümler ziyaret edildikten sonra leave fonksiyonu çağrılır. Yaygın bir model, enter işlevinin bazı bilgileri toplamak için kullanılması ve ardından leave işlevinin buna dayalı değişiklikler yapmasıdır. leave* çağrıldığında, düğüm içindeki tüm kodlar zaten ziyaret edilmiş ve gerekli bilgiler toplanmış olacaktır.
AST nasıl değiştirilir? En kolay yol basitçe düğümlerin özelliklerini değiştirmektir. İkinci yol ise yeni bir
düğüm döndürerek düğümü tamamen değiştirmektir. Örnek: Aşağıdaki kod AST'deki tüm tamsayıları string olarak
değiştirecektir (örneğin 42, '42'
olarak değiştirilecektir).
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);
}
},
);
Bir AST kolayca binlerce düğüm içerebilir ve hepsinin üzerinden geçmek yavaş olabilir. Bazı durumlarda, tam bir çaprazlamadan kaçınmak mümkündür.
Bir ağaçta tüm Html\ElementNode
adreslerini arıyorsanız, Php\ExpressionNode
adresini gördükten
sonra tüm alt düğümlerini de kontrol etmenin bir anlamı olmadığını bilirsiniz, çünkü HTML ifadelerin içinde olamaz.
Bu durumda, traverser'a sınıf düğümüne geri dönmemesi talimatını verebilirsiniz:
$ast = (new NodeTraverser)->traverse(
$ast,
enter: function (Node $node) {
if ($node instanceof Php\ExpressionNode) {
return NodeTraverser::DontTraverseChildren;
}
// ...
},
);
Yalnızca belirli bir düğümü arıyorsanız, bulduktan sonra geçişi tamamen iptal etmek de mümkündür.
$ast = (new NodeTraverser)->traverse(
$ast,
enter: function (Node $node) {
if ($node instanceof Nodes\ParametersNode) {
return NodeTraverser::StopTraversal;
}
// ...
},
);
Düğüm Yardımcıları
Latte\Compiler\NodeHelpers sınıfı, belirli bir geri çağrıyı vb. karşılayan AST düğümlerini bulabilen bazı yöntemler sağlar. Birkaç örnek gösterilmiştir:
Latte\Compiler\NodeHelpers kullanın;
// tüm HTML öğesi düğümlerini bulur
$elements = NodeHelpers::find($ast, fn(Node $node) => $node instanceof Nodes\Html\ElementNode);
// ilk metin düğümünü bulur
$text = NodeHelpers::findFirst($ast, fn(Node $node) => $node instanceof Nodes\TextNode);
// PHP değer düğümünü gerçek değere dönüştürür
$value = NodeHelpers::toValue($node);
// statik metinsel düğümü dizeye dönüştürür
$text = NodeHelpers::toText($node);