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:

1<?php
2
3use Forte\Rewriting\NodePath;
4use Forte\Rewriting\Visitor;
5
6class MyRewriter extends Visitor
7{
8 public function enter(NodePath $path): void
9 {
10 echo 'Entering '.$path->nodeIndex().': '.get_class($path->node()).PHP_EOL;
11 }
12
13 public function leave(NodePath $path): void
14 {
15 echo 'Leaving '.$path->nodeIndex().': '.get_class($path->node()).PHP_EOL;
16 }
17}

You can apply the visitor to a document by wrapping it in a Rewriter:

1<?php
2
3use Forte\Facades\Forte;
4use Forte\Rewriting\Rewriter;
5
6$blade = <<<'BLADE'
7<div class="mt-4">
8 @if ($count > 0)
9 <p>Hello, world.</p>
10 @endif
11</div>
12BLADE;
13
14$doc = Forte::parse($blade);
15
16$rewriter = new Rewriter;
17$rewriter->addVisitor(new MyRewriter);
18$newDoc = $rewriter->rewrite($doc);

When the above example executes, you would see the following output:

1Entering 1: Forte\Ast\Elements\ElementNode
2Entering 10: Forte\Ast\TextNode
3Leaving 10: Forte\Ast\TextNode
4Entering 11: Forte\Ast\DirectiveBlockNode
5Entering 12: Forte\Ast\DirectiveNode
6Entering 13: Forte\Ast\TextNode
7Leaving 13: Forte\Ast\TextNode
8Entering 14: Forte\Ast\Elements\ElementNode
9Entering 17: Forte\Ast\TextNode
10Leaving 17: Forte\Ast\TextNode
11Leaving 14: Forte\Ast\Elements\ElementNode
12Entering 20: Forte\Ast\TextNode
13Leaving 20: Forte\Ast\TextNode
14Leaving 12: Forte\Ast\DirectiveNode
15Entering 21: Forte\Ast\DirectiveNode
16Leaving 21: Forte\Ast\DirectiveNode
17Leaving 11: Forte\Ast\DirectiveBlockNode
18Entering 22: Forte\Ast\TextNode
19Leaving 22: Forte\Ast\TextNode
20Leaving 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.
1<?php
2
3use Forte\Rewriting\NodePath;
4use Forte\Rewriting\Visitor;
5
6class LogTraversal extends Visitor
7{
8 public function enter(NodePath $path): void
9 {
10 // Called top-down
11 if ($path->isElement()) {
12 echo "Entering: " . $path->asElement()->tagNameText() . "\n";
13 }
14 }
15
16 public function leave(NodePath $path): void
17 {
18 // Called bottom-up
19 if ($path->isElement()) {
20 echo "Leaving: " . $path->asElement()->tagNameText() . "\n";
21 }
22 }
23}

#Inline Visitors

For quick one-off transformations, you can use an anonymous class instead of creating a dedicated visitor:

1<?php
2
3use Forte\Rewriting\NodePath;
4use Forte\Rewriting\Visitor;
5
6$rewriter = new class extends Visitor {
7 public function enter(NodePath $path): void
8 {
9 if ($path->isTag('div') && $path->hasAttribute('data-legacy')) {
10 $path->removeAttribute('data-legacy');
11 }
12 }
13};

#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:

1<?php
2
3use Forte\Facades\Forte;
4use Forte\Rewriting\CallbackVisitor;
5use Forte\Rewriting\NodePath;
6use Forte\Rewriting\Rewriter;
7
8$doc = Forte::parse('<div>Hello</div>');
9
10$visitor = CallbackVisitor::onEnter(function (NodePath $path) {
11 if ($path->isTag('div')) {
12 $path->addClass('container');
13 }
14});
15
16$rewriter = new Rewriter;
17$rewriter->addVisitor($visitor);
18$newDoc = $rewriter->rewrite($doc);
19
20// '<div class="container">Hello</div>'
21$newDoc->render();

The onLeave factory works the same way but fires after a node's children have been visited:

1<?php
2
3use Forte\Facades\Forte;
4use Forte\Rewriting\CallbackVisitor;
5use Forte\Rewriting\NodePath;
6use Forte\Rewriting\Rewriter;
7
8$doc = Forte::parse('<ul><li>One</li><li>Two</li></ul>');
9
10$visitor = CallbackVisitor::onLeave(function (NodePath $path) {
11 if ($path->isTag('li')) {
12 $path->addClass('list-item');
13 }
14});
15
16$rewriter = new Rewriter;
17$rewriter->addVisitor($visitor);
18$newDoc = $rewriter->rewrite($doc);
19
20// '<ul><li class="list-item">One</li><li class="list-item">Two</li></ul>'
21$newDoc->render();

You can also provide both enter and leave callbacks through the constructor:

1<?php
2
3use Forte\Facades\Forte;
4use Forte\Rewriting\CallbackVisitor;
5use Forte\Rewriting\NodePath;
6use Forte\Rewriting\Rewriter;
7
8$doc = Forte::parse('<section><p>Hello</p></section>');
9
10$visitor = new CallbackVisitor(
11 enter: function (NodePath $path) {
12 if ($path->isTag('section')) {
13 $path->addClass('prose');
14 }
15 },
16 leave: function (NodePath $path) {
17 if ($path->isTag('p')) {
18 $path->addClass('mb-4');
19 }
20 }
21);
22
23$rewriter = new Rewriter;
24$rewriter->addVisitor($visitor);
25$newDoc = $rewriter->rewrite($doc);
26
27// '<section class="prose"><p class="mb-4">Hello</p></section>'
28$newDoc->render();

The rewriteWith method on Document uses CallbackVisitor under the hood, making it the quickest way to apply a one-off transformation:

1<?php
2
3use Forte\Facades\Forte;
4use Forte\Rewriting\NodePath;
5
6$doc = Forte::parse('<div class="old">Hello</div>');
7
8$newDoc = $doc->rewriteWith(function (NodePath $path) {
9 if ($path->isTag('div')) {
10 $path->addClass('new');
11 }
12});
13
14// '<div class="old new">Hello</div>'
15$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:

1<?php
2
3use Forte\Rewriting\Builders\Builder;
4use Forte\Rewriting\NodePath;
5use Forte\Rewriting\Visitor;
6
7class MyRewriter extends Visitor
8{
9 public function enter(NodePath $path): void
10 {
11 if (! $path->isElement() || ! $path->hasAttribute('class')) {
12 return;
13 }
14
15 $path->wrapIn(
16 Builder::directive('foreach', '($users as $user)'),
17 Builder::directive('endforeach')
18 );
19 }
20}

Applying it to a document:

1<?php
2
3use Forte\Facades\Forte;
4use Forte\Rewriting\Rewriter;
5
6$blade = <<<'BLADE'
7<div class="mt-4">
8 @if ($count > 0)
9 <p>Hello, world.</p>
10 @endif
11</div>
12BLADE;
13
14$doc = Forte::parse($blade);
15
16$rewriter = new Rewriter;
17$rewriter->addVisitor(new MyRewriter);
18$newDoc = $rewriter->rewrite($doc);
19
20echo $newDoc->render();

would result in the following output (formatting has been applied to the output):

1@foreach ($users as $user)
2 <div class="mt-4">
3 @if ($count > 0)
4 <p>Hello, world.</p>
5 @endif
6 </div>
7@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:

1$path->node(); // Node (the current node)
2$path->parent(); // ?Node (parent node)
3$path->parentPath(); // ?NodePath (parent's path)
4$path->document(); // Document (the source document)

Position and depth information is also available:

1$path->indexInParent(); // int (position among siblings)
2$path->nodeIndex(); // int (index in flat storage)
3$path->depth(); // int (nesting depth, 0 for root)
4$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:

1$path->isTag('div'); // is element with tag name 'div'?
2$path->isDirectiveNamed('if'); // is directive/block named 'if'?
3$path->isElement(); // bool
4$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:

1$path->siblings(); // array<Node> (siblings excluding self)
2$path->previousSibling(); // ?Node
3$path->nextSibling(); // ?Node

You can also walk up the tree to find ancestor nodes:

1$path->ancestors(); // array<Node> (root-to-parent order)
2$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:

1$path->hasAttribute('data-id'); // bool
2$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:

1// Replace with a builder
2$path->replaceWith(Builder::element('section')->text('New content'));
3
4// Replace with raw source
5$path->replaceWith('<span>Replaced</span>');
6
7// Replace with multiple nodes
8$path->replaceWith([
9 Builder::comment('Replaced element'),
10 Builder::element('div')->text('New'),
11]);

#Removing Nodes

To remove a node and all of its children from the document, call remove on the node path:

1$path->remove();

#Inserting Nodes

You can insert new nodes adjacent to the current node without replacing it:

1// Insert before this node
2$path->insertBefore(Builder::comment('Before'));
3$path->insertBefore('<hr>');
4
5// Insert after this node
6$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:

1$path->wrapWith(Builder::element('div')->class('wrapper'));
2$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:

1$path->wrapIn(
2 Builder::directive('if', '($show)'),
3 Builder::directive('endif')
4);

For more complex transformations, surroundWith replaces the current node and inserts nodes before and after the replacement:

1$path->surroundWith(
2 Builder::text('Before '),
3 Builder::element('span')->text('Middle'),
4 Builder::text(' After')
5);

#Modifying Children

When working with container nodes like elements or directive blocks, you can modify their children directly:

1// Replace all children
2$path->replaceChildren(Builder::text('New content only'));
3
4// Prepend children
5$path->prependChildren(Builder::comment('Added at start'));
6
7// Append children
8$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:

1// Set/overwrite an attribute
2$path->setAttribute('data-id', '42');
3
4// Remove a static attribute
5$path->removeAttribute('style');
6
7// Remove a bound Blade attribute
8$path->removeAttribute(':class');
9
10// Rename the tag
11$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:

1<?php
2
3use Forte\Facades\Forte;
4use Forte\Rewriting\NodePath;
5
6$doc = Forte::parse('<div class="hidden">Hello</div>');
7
8$newDoc = $doc->rewriteWith(function (NodePath $path) {
9 if ($path->isTag('div')) {
10 $path->addClass('active');
11 $path->removeClass('hidden');
12 $path->toggleClass('visible');
13 $path->toggleClass('active', true); // Force add (no-op, already present)
14 $path->toggleClass('hidden', false); // Force remove (no-op, already removed)
15 }
16});
17
18$newDoc->render(); // '<div class="active visible">Hello</div>'

You can also query an element's current classes:

1<?php
2
3use Forte\Facades\Forte;
4use Forte\Rewriting\NodePath;
5
6$doc = Forte::parse('<div class="container mx-auto">Hello</div>');
7
8$doc->rewriteWith(function (NodePath $path) {
9 if ($path->isTag('div')) {
10 $path->hasClass('container'); // true
11 $path->hasClass('hidden'); // false
12 $path->getClasses(); // ['container', 'mx-auto']
13 }
14});

#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:

1$path->skipChildren();
2$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.

1<?php
2
3use Forte\Rewriting\Builders\Builder;
4
5Builder::element('div')->class('wrapper')->text('Hello');
6Builder::directive('if', '($condition)');
7Builder::echo('$name'); // {{ $name }}
8Builder::comment('A note'); // <!-- A note -->
9Builder::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:

1<?php
2
3use Forte\Facades\Forte;
4
5$newDoc = Forte::parse('....')
6 ->apply(
7 new RewriterOne,
8 new RewriterTwo,
9 new RewriterThree
10 );

#Applying Multiple Rewriters

If you'd like to create reusable custom rewriter pipelines, you may use the RewritePipeline class:

1<?php
2
3use Forte\Rewriting\RewritePipeline;
4use Forte\Facades\Forte;
5
6$pipeline = new RewritePipeline(
7 new RewriterOne,
8 new RewriterTwo,
9 new RewriterThree
10);
11
12$newDoc = $pipeline->rewrite(
13 Forte::parse('...')
14);

#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:

1<?php
2
3use Forte\Facades\Forte;
4
5Forte::app()
6 ->apply(
7 new RewriterOne,
8 new RewriterTwo,
9 new RewriterThree
10 );

#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:

1<?php
2
3use Forte\Facades\Forte;
4use Forte\Rewriting\RewriteBuilder;
5
6$doc = Forte::parse('<div><span>text</span></div>');
7
8$newDoc = $doc->rewrite(function (RewriteBuilder $builder) {
9 $builder->findAll('div')->addClass('container');
10 $builder->findAll('span')->setAttribute('data-new', 'true');
11});
12
13$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:

1<?php
2
3$builder->find('div'); // Selection (first div)
4$builder->findAll('div'); // Selection (all divs)

You can also select nodes using XPath expressions:

1<?php
2
3$builder->xpath('//div[@class]'); // Selection (first match)
4$builder->xpathAll('//div[@class]'); // Selection (all matches)

For direct node selection when you already have a reference, use select or selectAll:

1<?php
2
3$builder->select($node); // Selection (single node)
4$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:

1<?php
2
3use Forte\Facades\Forte;
4use Forte\Rewriting\RewriteBuilder;
5
6$doc = Forte::parse('<div class="old"><p>Keep</p><span>Remove</span></div>');
7
8$newDoc = $doc->rewrite(function (RewriteBuilder $builder) {
9 $builder->find('div')->removeClass('old')->addClass('new');
10 $builder->findAll('span')->remove();
11});
12
13$newDoc->render(); // '<div class="new"><p>Keep</p></div>'

The full set of mutation methods on Selection:

1<?php
2
3$selection->addClass('name'); // add a CSS class
4$selection->removeClass('name'); // remove a CSS class
5$selection->setAttribute('key', 'value'); // set an attribute
6$selection->removeAttribute('key'); // remove an attribute
7$selection->remove(); // remove selected nodes
8$selection->replaceWith('<new>content</new>'); // replace with content
9$selection->wrapWith('div.wrapper'); // wrap in an element
10$selection->insertBefore('<!-- before -->'); // insert before
11$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:

1<?php
2
3use Forte\Facades\Forte;
4use Forte\Rewriting\RewriteBuilder;
5
6$doc = Forte::parse('<span>content</span>');
7
8$newDoc = $doc->rewrite(function (RewriteBuilder $builder) {
9 $builder->find('span')->wrapWith('div.wrapper');
10});
11
12$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:

1<?php
2
3use Forte\Rewriting\NodePath;
4use Forte\Rewriting\Visitor;
5
6class AddContainerClass extends Visitor
7{
8 public function enter(NodePath $path): void
9 {
10 if ($path->isTag('div')) {
11 $path->addClass('container');
12 }
13 }
14}

#Replace Deprecated Tags

This visitor replaces deprecated HTML tags with their modern equivalents, adding appropriate utility classes where needed:

1<?php
2
3use Forte\Rewriting\NodePath;
4use Forte\Rewriting\Visitor;
5
6class ReplaceDeprecatedTags extends Visitor
7{
8 public function enter(NodePath $path): void
9 {
10 if ($path->isTag('center')) {
11 $path->renameTag('div');
12 $path->addClass('text-center');
13 }
14
15 if ($path->isTag('font')) {
16 $path->renameTag('span');
17 }
18 }
19}

#Remove All HTML Comments

This visitor strips all HTML comments from the document:

1<?php
2
3use Forte\Rewriting\NodePath;
4use Forte\Rewriting\Visitor;
5
6class RemoveComments extends Visitor
7{
8 public function enter(NodePath $path): void
9 {
10 if ($path->isComment()) {
11 $path->remove();
12 }
13 }
14}

#Wrap Components in Auth Checks

This visitor wraps admin panel components in authorization checks using Blade's @can directive:

1<?php
2
3use Forte\Rewriting\Builders\Builder;
4use Forte\Rewriting\NodePath;
5use Forte\Rewriting\Visitor;
6
7class WrapInAuthChecks extends Visitor
8{
9 public function enter(NodePath $path): void
10 {
11 if ($path->isTag('x-admin-panel')) {
12 $path->wrapIn(
13 Builder::directive('can', "('admin')"),
14 Builder::directive('endcan')
15 );
16 }
17 }
18}

#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:

1<?php
2
3use Forte\Rewriting\NodePath;
4use Forte\Rewriting\Visitor;
5
6class StripBladeAttributes extends Visitor
7{
8 public function enter(NodePath $path): void
9 {
10 if ($element = $path->asElement()) {
11 foreach ($element->attributes() as $attr) {
12 if ($attr->isBound()) {
13 $path->removeAttribute($attr->rawName());
14 }
15 }
16 }
17 }
18}

#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