Rewriting
Rewriters
Rewriters are the primary mechanism for modifying Forte documents. Rewriters operate at the document level; because documents are immutable, rewriters always create a new document instance. To improve performance and reduce the number of intermediate documents required for rewriter chains or pipelines, Forte uses a queue and commits pending changes to a document when needed.
#Implementing Rewriters
The vast majority of rewriters are implemented using a familiar visitor pattern. Here is a basic rewriter implementation:
<?php
use Forte\Rewriting\NodePath;
use Forte\Rewriting\Visitor;
class MyRewriter extends Visitor
{
public function enter(NodePath $path): void
{
echo 'Entering '.$path->nodeIndex().': '.get_class($path->node()).PHP_EOL;
}
public function leave(NodePath $path): void
{
echo 'Leaving '.$path->nodeIndex().': '.get_class($path->node()).PHP_EOL;
}
}
You can apply the visitor to a document by wrapping it in a Rewriter:
<?php
use Forte\Facades\Forte;
use Forte\Rewriting\Rewriter;
$blade = <<<'BLADE'
<div class="mt-4">
@if ($count > 0)
<p>Hello, world.</p>
@endif
</div>
BLADE;
$doc = Forte::parse($blade);
$rewriter = new Rewriter;
$rewriter->addVisitor(new MyRewriter);
$newDoc = $rewriter->rewrite($doc);
When the above example executes, you would see the following output:
Entering 1: Forte\Ast\Elements\ElementNode
Entering 10: Forte\Ast\TextNode
Leaving 10: Forte\Ast\TextNode
Entering 11: Forte\Ast\DirectiveBlockNode
Entering 12: Forte\Ast\DirectiveNode
Entering 13: Forte\Ast\TextNode
Leaving 13: Forte\Ast\TextNode
Entering 14: Forte\Ast\Elements\ElementNode
Entering 17: Forte\Ast\TextNode
Leaving 17: Forte\Ast\TextNode
Leaving 14: Forte\Ast\Elements\ElementNode
Entering 20: Forte\Ast\TextNode
Leaving 20: Forte\Ast\TextNode
Leaving 12: Forte\Ast\DirectiveNode
Entering 21: Forte\Ast\DirectiveNode
Leaving 21: Forte\Ast\DirectiveNode
Leaving 11: Forte\Ast\DirectiveBlockNode
Entering 22: Forte\Ast\TextNode
Leaving 22: Forte\Ast\TextNode
Leaving 1: Forte\Ast\Elements\ElementNode
While this example rewriter doesn't actually change the document, it demonstrates how to implement a visitor. Notice that the enter and leave methods do not return a result, and all interactions happen through a NodePath object.
The node path handles interactions between all rewriters and the underlying document system.
Another important behavior to notice is that the rewriters do not visit a node's internal children directly. An example of an internal child is the class="mt-4" attribute on the first ElementNode.
#Enter vs Leave
Visitors have two methods that are called during traversal:
enter(NodePath)is called before visiting a node's children. Use this for most transformations.leave(NodePath)is called after visiting a node's children. Use this when you need the children to be processed first.
<?php
use Forte\Rewriting\NodePath;
use Forte\Rewriting\Visitor;
class LogTraversal extends Visitor
{
public function enter(NodePath $path): void
{
// Called top-down
if ($path->isElement()) {
echo "Entering: " . $path->asElement()->tagNameText() . "\n";
}
}
public function leave(NodePath $path): void
{
// Called bottom-up
if ($path->isElement()) {
echo "Leaving: " . $path->asElement()->tagNameText() . "\n";
}
}
}
#Inline Visitors
For quick one-off transformations, you can use an anonymous class instead of creating a dedicated visitor:
<?php
use Forte\Rewriting\NodePath;
use Forte\Rewriting\Visitor;
$rewriter = new class extends Visitor {
public function enter(NodePath $path): void
{
if ($path->isTag('div') && $path->hasAttribute('data-legacy')) {
$path->removeAttribute('data-legacy');
}
}
};
#Callback Visitors
For even simpler cases, CallbackVisitor lets you create a visitor from plain callables without declaring a class. The static factories onEnter and onLeave each accept a single callback:
<?php
use Forte\Facades\Forte;
use Forte\Rewriting\CallbackVisitor;
use Forte\Rewriting\NodePath;
use Forte\Rewriting\Rewriter;
$doc = Forte::parse('<div>Hello</div>');
$visitor = CallbackVisitor::onEnter(function (NodePath $path) {
if ($path->isTag('div')) {
$path->addClass('container');
}
});
$rewriter = new Rewriter;
$rewriter->addVisitor($visitor);
$newDoc = $rewriter->rewrite($doc);
// '<div class="container">Hello</div>'
$newDoc->render();
The onLeave factory works the same way but fires after a node's children have been visited:
<?php
use Forte\Facades\Forte;
use Forte\Rewriting\CallbackVisitor;
use Forte\Rewriting\NodePath;
use Forte\Rewriting\Rewriter;
$doc = Forte::parse('<ul><li>One</li><li>Two</li></ul>');
$visitor = CallbackVisitor::onLeave(function (NodePath $path) {
if ($path->isTag('li')) {
$path->addClass('list-item');
}
});
$rewriter = new Rewriter;
$rewriter->addVisitor($visitor);
$newDoc = $rewriter->rewrite($doc);
// '<ul><li class="list-item">One</li><li class="list-item">Two</li></ul>'
$newDoc->render();
You can also provide both enter and leave callbacks through the constructor:
<?php
use Forte\Facades\Forte;
use Forte\Rewriting\CallbackVisitor;
use Forte\Rewriting\NodePath;
use Forte\Rewriting\Rewriter;
$doc = Forte::parse('<section><p>Hello</p></section>');
$visitor = new CallbackVisitor(
enter: function (NodePath $path) {
if ($path->isTag('section')) {
$path->addClass('prose');
}
},
leave: function (NodePath $path) {
if ($path->isTag('p')) {
$path->addClass('mb-4');
}
}
);
$rewriter = new Rewriter;
$rewriter->addVisitor($visitor);
$newDoc = $rewriter->rewrite($doc);
// '<section class="prose"><p class="mb-4">Hello</p></section>'
$newDoc->render();
The rewriteWith method on Document uses CallbackVisitor under the hood, making it the quickest way to apply a one-off transformation:
<?php
use Forte\Facades\Forte;
use Forte\Rewriting\NodePath;
$doc = Forte::parse('<div class="old">Hello</div>');
$newDoc = $doc->rewriteWith(function (NodePath $path) {
if ($path->isTag('div')) {
$path->addClass('new');
}
});
// '<div class="old new">Hello</div>'
$newDoc->render();
#Wrapping Nodes with Directives
Visitors can use the Builder class to insert Blade directives around existing nodes. The following rewriter wraps every element that has a class attribute inside a @foreach block:
<?php
use Forte\Rewriting\Builders\Builder;
use Forte\Rewriting\NodePath;
use Forte\Rewriting\Visitor;
class MyRewriter extends Visitor
{
public function enter(NodePath $path): void
{
if (! $path->isElement() || ! $path->hasAttribute('class')) {
return;
}
$path->wrapIn(
Builder::directive('foreach', '($users as $user)'),
Builder::directive('endforeach')
);
}
}
Applying it to a document:
<?php
use Forte\Facades\Forte;
use Forte\Rewriting\Rewriter;
$blade = <<<'BLADE'
<div class="mt-4">
@if ($count > 0)
<p>Hello, world.</p>
@endif
</div>
BLADE;
$doc = Forte::parse($blade);
$rewriter = new Rewriter;
$rewriter->addVisitor(new MyRewriter);
$newDoc = $rewriter->rewrite($doc);
echo $newDoc->render();
would result in the following output (formatting has been applied to the output):
@foreach ($users as $user)
<div class="mt-4">
@if ($count > 0)
<p>Hello, world.</p>
@endif
</div>
@endforeach
#Node Paths
The NodePath is how all rewriters should interact with the document and rewriting system when performing modifications. In addition to providing mutation methods, the node path also provides convenience methods for working with various node types. Forte queues operations and commits them to a document when necessary, helping to prevent excessive intermediate document instances.
#Inspection
The node path provides methods for inspecting the current node and its position within the document tree. You can access the underlying node, its parent, and the source document:
$path->node(); // Node (the current node)
$path->parent(); // ?Node (parent node)
$path->parentPath(); // ?NodePath (parent's path)
$path->document(); // Document (the source document)
Position and depth information is also available:
$path->indexInParent(); // int (position among siblings)
$path->nodeIndex(); // int (index in flat storage)
$path->depth(); // int (nesting depth, 0 for root)
$path->isRoot(); // bool (is root-level node?)
#Type Checking and Casting
The node path exposes the same type-check and safe-cast methods available on all nodes (see Basic Nodes for the full list). Additionally, NodePath provides convenience methods for checking specific tag names and directive names:
$path->isTag('div'); // is element with tag name 'div'?
$path->isDirectiveNamed('if'); // is directive/block named 'if'?
$path->isElement(); // bool
$path->asElement(); // ?ElementNode
#Navigation
The node path allows you to navigate the document tree relative to the current node. Sibling navigation lets you access adjacent nodes:
$path->siblings(); // array<Node> (siblings excluding self)
$path->previousSibling(); // ?Node
$path->nextSibling(); // ?Node
You can also walk up the tree to find ancestor nodes:
$path->ancestors(); // array<Node> (root-to-parent order)
$path->findAncestor(fn(Node $n) => $n instanceof ElementNode); // ?Node
Ordering difference
The NodePath::ancestors() method returns an array in root-to-parent order. The Node::ancestors() method used in Traversal yields in the opposite order (parent-to-root).
You can also inspect element attributes directly from the node path:
$path->hasAttribute('data-id'); // bool
$path->getAttribute('data-id'); // ?string
#Replacing Nodes
The replaceWith method swaps the current node for one or more new nodes. It accepts builders, raw source strings, or arrays of either:
// Replace with a builder
$path->replaceWith(Builder::element('section')->text('New content'));
// Replace with raw source
$path->replaceWith('<span>Replaced</span>');
// Replace with multiple nodes
$path->replaceWith([
Builder::comment('Replaced element'),
Builder::element('div')->text('New'),
]);
#Removing Nodes
To remove a node and all of its children from the document, call remove on the node path:
$path->remove();
#Inserting Nodes
You can insert new nodes adjacent to the current node without replacing it:
// Insert before this node
$path->insertBefore(Builder::comment('Before'));
$path->insertBefore('<hr>');
// Insert after this node
$path->insertAfter(Builder::element('br')->selfClosing());
#Wrapping and Unwrapping
Forte provides several methods for wrapping nodes in new structures or removing existing wrappers. You can wrap a node inside a new parent element using wrapWith, or remove an element's wrapper while keeping its children using unwrap:
$path->wrapWith(Builder::element('div')->class('wrapper'));
$path->unwrap();
The wrapIn method inserts nodes before and after the current node without replacing it, which is particularly useful for adding Blade directive pairs:
$path->wrapIn(
Builder::directive('if', '($show)'),
Builder::directive('endif')
);
For more complex transformations, surroundWith replaces the current node and inserts nodes before and after the replacement:
$path->surroundWith(
Builder::text('Before '),
Builder::element('span')->text('Middle'),
Builder::text(' After')
);
#Modifying Children
When working with container nodes like elements or directive blocks, you can modify their children directly:
// Replace all children
$path->replaceChildren(Builder::text('New content only'));
// Prepend children
$path->prependChildren(Builder::comment('Added at start'));
// Append children
$path->appendChild(Builder::element('footer')->text('End'));
#Element Attributes
These methods are only valid when the current node is an ElementNode. Attribute names are matched by their raw name, including any Blade prefix. Use :class to target a bound attribute or ::class for an escaped attribute:
// Set/overwrite an attribute
$path->setAttribute('data-id', '42');
// Remove a static attribute
$path->removeAttribute('style');
// Remove a bound Blade attribute
$path->removeAttribute(':class');
// Rename the tag
$path->renameTag('section');
#CSS Classes
The node path includes convenience methods for working with CSS classes on element nodes. You can add, remove, and toggle classes:
<?php
use Forte\Facades\Forte;
use Forte\Rewriting\NodePath;
$doc = Forte::parse('<div class="hidden">Hello</div>');
$newDoc = $doc->rewriteWith(function (NodePath $path) {
if ($path->isTag('div')) {
$path->addClass('active');
$path->removeClass('hidden');
$path->toggleClass('visible');
$path->toggleClass('active', true); // Force add (no-op, already present)
$path->toggleClass('hidden', false); // Force remove (no-op, already removed)
}
});
$newDoc->render(); // '<div class="active visible">Hello</div>'
You can also query an element's current classes:
<?php
use Forte\Facades\Forte;
use Forte\Rewriting\NodePath;
$doc = Forte::parse('<div class="container mx-auto">Hello</div>');
$doc->rewriteWith(function (NodePath $path) {
if ($path->isTag('div')) {
$path->hasClass('container'); // true
$path->hasClass('hidden'); // false
$path->getClasses(); // ['container', 'mx-auto']
}
});
#Traversal Control
You can control the visitor's traversal behavior from within your enter or leave methods. The skipChildren method prevents the visitor from descending into the current node's children, while stopTraversal halts the entire rewriting process:
$path->skipChildren();
$path->stopTraversal();
#The Builder API
Rewriters use the Builder class to construct synthetic nodes for replacements, insertions, and wrapping. The builder provides factory methods for elements, directives, echoes, comments, PHP tags, and raw source. For the full API reference, see Node Builders.
<?php
use Forte\Rewriting\Builders\Builder;
Builder::element('div')->class('wrapper')->text('Hello');
Builder::directive('if', '($condition)');
Builder::echo('$name'); // {{ $name }}
Builder::comment('A note'); // <!-- A note -->
Builder::raw('<hr>');
#Applying Rewriters
You can apply rewriters to documents in a few different ways. If you already have a document instance, call the apply method with the rewriters you want to use:
<?php
use Forte\Facades\Forte;
$newDoc = Forte::parse('....')
->apply(
new RewriterOne,
new RewriterTwo,
new RewriterThree
);
#Applying Multiple Rewriters
If you'd like to create reusable custom rewriter pipelines, you may use the RewritePipeline class:
<?php
use Forte\Rewriting\RewritePipeline;
use Forte\Facades\Forte;
$pipeline = new RewritePipeline(
new RewriterOne,
new RewriterTwo,
new RewriterThree
);
$newDoc = $pipeline->rewrite(
Forte::parse('...')
);
#Using Custom Rewriters with Enclaves
If you'd like to apply a custom rewriter to an Enclave, you may call the apply method on that Enclave instance:
<?php
use Forte\Facades\Forte;
Forte::app()
->apply(
new RewriterOne,
new RewriterTwo,
new RewriterThree
);
#Declarative Rewriting
The rewrite method on Document provides a declarative API for common mutations without implementing a full visitor. It accepts a callback that receives a RewriteBuilder, which provides methods for selecting nodes and applying changes:
<?php
use Forte\Facades\Forte;
use Forte\Rewriting\RewriteBuilder;
$doc = Forte::parse('<div><span>text</span></div>');
$newDoc = $doc->rewrite(function (RewriteBuilder $builder) {
$builder->findAll('div')->addClass('container');
$builder->findAll('span')->setAttribute('data-new', 'true');
});
$newDoc->render(); // '<div class="container"><span data-new="true">text</span></div>'
#Selecting Nodes
The builder provides several ways to select nodes. The find method returns a Selection containing the first matching element or component, while findAll returns all matches:
<?php
$builder->find('div'); // Selection (first div)
$builder->findAll('div'); // Selection (all divs)
You can also select nodes using XPath expressions:
<?php
$builder->xpath('//div[@class]'); // Selection (first match)
$builder->xpathAll('//div[@class]'); // Selection (all matches)
For direct node selection when you already have a reference, use select or selectAll:
<?php
$builder->select($node); // Selection (single node)
$builder->selectAll([$nodeA, $nodeB]); // Selection (multiple nodes)
#Selection Methods
Each selection method returns a Selection object with a fluent API for queuing mutations. Attribute and class methods operate on element nodes within the selection:
<?php
use Forte\Facades\Forte;
use Forte\Rewriting\RewriteBuilder;
$doc = Forte::parse('<div class="old"><p>Keep</p><span>Remove</span></div>');
$newDoc = $doc->rewrite(function (RewriteBuilder $builder) {
$builder->find('div')->removeClass('old')->addClass('new');
$builder->findAll('span')->remove();
});
$newDoc->render(); // '<div class="new"><p>Keep</p></div>'
The full set of mutation methods on Selection:
<?php
$selection->addClass('name'); // add a CSS class
$selection->removeClass('name'); // remove a CSS class
$selection->setAttribute('key', 'value'); // set an attribute
$selection->removeAttribute('key'); // remove an attribute
$selection->remove(); // remove selected nodes
$selection->replaceWith('<new>content</new>'); // replace with content
$selection->wrapWith('div.wrapper'); // wrap in an element
$selection->insertBefore('<!-- before -->'); // insert before
$selection->insertAfter('<!-- after -->'); // insert after
The wrapWith method accepts a tag name with an optional dot-separated class shorthand (e.g., 'div.wrapper'), plus an optional attributes array:
<?php
use Forte\Facades\Forte;
use Forte\Rewriting\RewriteBuilder;
$doc = Forte::parse('<span>content</span>');
$newDoc = $doc->rewrite(function (RewriteBuilder $builder) {
$builder->find('span')->wrapWith('div.wrapper');
});
$newDoc->render(); // '<div class="wrapper"><span>content</span></div>'
Selections also support iteration and filtering with each, filter, first, last, count, isEmpty, and isNotEmpty.
#Practical Examples
The following examples demonstrate common rewriting patterns. Each visitor is self-contained and can be applied to any document by wrapping it in a Rewriter instance and calling rewrite, as shown in Implementing Rewriters.
#Add a Class to All Divs
This visitor adds the container class to every div element in the document:
<?php
use Forte\Rewriting\NodePath;
use Forte\Rewriting\Visitor;
class AddContainerClass extends Visitor
{
public function enter(NodePath $path): void
{
if ($path->isTag('div')) {
$path->addClass('container');
}
}
}
#Replace Deprecated Tags
This visitor replaces deprecated HTML tags with their modern equivalents, adding appropriate utility classes where needed:
<?php
use Forte\Rewriting\NodePath;
use Forte\Rewriting\Visitor;
class ReplaceDeprecatedTags extends Visitor
{
public function enter(NodePath $path): void
{
if ($path->isTag('center')) {
$path->renameTag('div');
$path->addClass('text-center');
}
if ($path->isTag('font')) {
$path->renameTag('span');
}
}
}
#Remove All HTML Comments
This visitor strips all HTML comments from the document:
<?php
use Forte\Rewriting\NodePath;
use Forte\Rewriting\Visitor;
class RemoveComments extends Visitor
{
public function enter(NodePath $path): void
{
if ($path->isComment()) {
$path->remove();
}
}
}
#Wrap Components in Auth Checks
This visitor wraps admin panel components in authorization checks using Blade's @can directive:
<?php
use Forte\Rewriting\Builders\Builder;
use Forte\Rewriting\NodePath;
use Forte\Rewriting\Visitor;
class WrapInAuthChecks extends Visitor
{
public function enter(NodePath $path): void
{
if ($path->isTag('x-admin-panel')) {
$path->wrapIn(
Builder::directive('can', "('admin')"),
Builder::directive('endcan')
);
}
}
}
#Strip All Blade Attributes
This visitor removes all bound (Blade) attributes from elements, leaving only standard HTML attributes. Use rawName to include the : prefix when targeting bound attributes:
<?php
use Forte\Rewriting\NodePath;
use Forte\Rewriting\Visitor;
class StripBladeAttributes extends Visitor
{
public function enter(NodePath $path): void
{
if ($element = $path->asElement()) {
foreach ($element->attributes() as $attr) {
if ($attr->isBound()) {
$path->removeAttribute($attr->rawName());
}
}
}
}
}
#See also
- Node Builders: Factory methods for creating synthetic nodes
- Traversal: Navigate and query the document tree
- Documents: Parse templates and apply rewriters
- Enclaves: Use rewriters with isolated compilation environments