Rewriting

Rewrite Passes

Forte ships with a set of ready-to-use rewrite passes for common template transformations. Each pass implements the AstRewriter interface and can be applied directly to a document using the apply method, or composed together using a RewritePipeline.

All element passes accept a tag name pattern as their first argument. Patterns support * wildcards and are matched using Str::is(), so div matches only <div>, h* matches <h1> through <h6>, and * matches every element.

#Element Passes

#AddClass

The AddClass pass appends a CSS class to every element that matches the given pattern:

1<?php
2
3use Forte\Facades\Forte;
4use Forte\Rewriting\Passes\Elements\AddClass;
5
6$doc = Forte::parse('<div>content</div>');
7$result = $doc->apply(new AddClass('div', 'active'));
8
9// '<div class="active">content</div>'
10$result->render();

When applied to elements that already have a class attribute, the new class is appended to the existing list:

1<?php
2
3use Forte\Facades\Forte;
4use Forte\Rewriting\Passes\Elements\AddClass;
5
6$doc = Forte::parse('<div class="mt-4">content</div>');
7$result = $doc->apply(new AddClass('div', 'active'));
8
9// '<div class="mt-4 active">content</div>'
10$result->render();

#RemoveClass

The RemoveClass pass removes a CSS class from matching elements. If the class is not present, the element is left unchanged:

1<?php
2
3use Forte\Facades\Forte;
4use Forte\Rewriting\Passes\Elements\RemoveClass;
5
6$doc = Forte::parse('<div class="mt-4 hidden">content</div>');
7$result = $doc->apply(new RemoveClass('div', 'hidden'));
8
9// '<div class="mt-4">content</div>'
10$result->render();

#RenameTag

Use RenameTag to change the tag name of matching elements. You can provide a string for a direct replacement, or a callback that receives the current tag name and returns the new one:

1<?php
2
3use Forte\Facades\Forte;
4use Forte\Rewriting\Passes\Elements\RenameTag;
5
6$doc = Forte::parse('<div>content</div>');
7$result = $doc->apply(new RenameTag('div', 'section'));
8
9// '<section>content</section>'
10$result->render();

With a callback, you can compute the new name dynamically. This is useful when renaming elements that follow a naming convention:

1<?php
2
3use Forte\Facades\Forte;
4use Forte\Rewriting\Passes\Elements\RenameTag;
5
6$doc = Forte::parse('<old-card>content</old-card>');
7$result = $doc->apply(new RenameTag('old-*', fn ($tag) => str_replace('old-', 'new-', $tag)));
8
9// '<new-card>content</new-card>'
10$result->render();

#SetAttribute

The SetAttribute pass sets an attribute on every matching element. If the attribute already exists, its value is overwritten:

1<?php
2
3use Forte\Facades\Forte;
4use Forte\Rewriting\Passes\Elements\SetAttribute;
5
6$doc = Forte::parse('<img src="photo.jpg">');
7$result = $doc->apply(new SetAttribute('img', 'loading', 'lazy'));
8
9// '<img src="photo.jpg" loading="lazy">'
10$result->render();

#RemoveAttributes

The RemoveAttributes pass removes all attributes matching the given name patterns from matching elements. Attribute patterns also support * wildcards:

1<?php
2
3use Forte\Facades\Forte;
4use Forte\Rewriting\Passes\Elements\RemoveAttributes;
5
6$doc = Forte::parse('<div data-testid="card" data-cy="wrapper" class="mt-4">content</div>');
7$result = $doc->apply(new RemoveAttributes('div', ['data-*']));
8
9// '<div class="mt-4">content</div>'
10$result->render();

#WrapElements

The WrapElements pass wraps each matching element inside a new parent element. You can optionally provide attributes for the wrapper:

1<?php
2
3use Forte\Facades\Forte;
4use Forte\Rewriting\Passes\Elements\WrapElements;
5
6$doc = Forte::parse('<img src="photo.jpg">');
7$result = $doc->apply(new WrapElements('img', 'figure', ['class' => 'media']));
8
9// '<figure class="media"><img src="photo.jpg"></figure>'
10$result->render();

#UnwrapElements

The UnwrapElements pass replaces matching elements with their children, effectively removing the element tag while keeping its content. This is the inverse of WrapElements:

1<?php
2
3use Forte\Facades\Forte;
4use Forte\Rewriting\Passes\Elements\UnwrapElements;
5
6$doc = Forte::parse('<div class="wrapper"><p>content</p></div>');
7$result = $doc->apply(new UnwrapElements('div'));
8
9// '<p>content</p>'
10$result->render();

#Directive Passes

Directive passes target Blade directives by name. Like element passes, they accept a name pattern with * wildcard support.

#RenameDirective

The RenameDirective pass renames matching directives. It handles both standalone directives and block directive pairs (including their @end counterpart):

1<?php
2
3use Forte\Facades\Forte;
4use Forte\Rewriting\Passes\Directives\RenameDirective;
5
6$doc = Forte::parse('@include("header")');
7$result = $doc->apply(new RenameDirective('include', 'livewire'));
8
9// '@livewire("header")'
10$result->render();

Block directives are renamed as a pair:

1<?php
2
3use Forte\Facades\Forte;
4use Forte\Rewriting\Passes\Directives\RenameDirective;
5
6$doc = Forte::parse('@once <script>alert(1)</script> @endonce');
7$result = $doc->apply(new RenameDirective('once', 'pushOnce'));
8
9// '@pushOnce <script>alert(1)</script> @endpushOnce'
10$result->render();

You can also pass a callback for dynamic renaming:

1<?php
2
3use Forte\Facades\Forte;
4use Forte\Rewriting\Passes\Directives\RenameDirective;
5
6$doc = Forte::parse('@pushOnce("scripts")');
7$result = $doc->apply(new RenameDirective('push*', fn ($name) => str_replace('push', 'stack', $name)));
8
9// '@stackOnce("scripts")'
10$result->render();

#RemoveDirective

The RemoveDirective pass removes matching directives from the document. For block directives, the entire block (including its children) is removed:

1<?php
2
3use Forte\Facades\Forte;
4use Forte\Rewriting\Passes\Directives\RemoveDirective;
5
6$doc = Forte::parse('<div>@include("sidebar")<p>content</p></div>');
7$result = $doc->apply(new RemoveDirective('include'));
8
9// '<div><p>content</p></div>'
10$result->render();

#WrapDirective

The WrapDirective pass wraps matching directives inside a new HTML element. Like WrapElements, you can provide a tag name and optional attributes for the wrapper:

1<?php
2
3use Forte\Facades\Forte;
4use Forte\Rewriting\Passes\Directives\WrapDirective;
5
6$doc = Forte::parse('@yield("sidebar")');
7$result = $doc->apply(new WrapDirective('yield', 'aside', ['class' => 'sidebar']));
8
9// '<aside class="sidebar">@yield("sidebar")</aside>'
10$result->render();

#Instrumentation

Forte includes a dedicated Instrumentation pass for injecting tracking markers around nodes. It supports HTML comment markers, data attribute injection, custom callbacks, and metadata decoding. See Instrumentation for the full API reference.

#Composing Passes

You can apply multiple passes in sequence using the apply method or a RewritePipeline. Passes execute in order, and each operates on the document produced by the previous pass:

1<?php
2
3use Forte\Facades\Forte;
4use Forte\Rewriting\Passes\Elements\AddClass;
5use Forte\Rewriting\Passes\Elements\RenameTag;
6use Forte\Rewriting\Passes\Elements\RemoveAttributes;
7use Forte\Rewriting\RewritePipeline;
8
9$pipeline = new RewritePipeline(
10 new RenameTag('center', 'div'),
11 new AddClass('div', 'text-center'),
12 new RemoveAttributes('*', ['style', 'bgcolor']),
13);
14
15$doc = Forte::parse('<center style="color: red">content</center>');
16$result = $pipeline->rewrite($doc);
17
18// '<div class="text-center">content</div>'
19$result->render();

You can also build pipelines incrementally with the add method, and check how many passes are queued with count:

1<?php
2
3use Forte\Rewriting\Passes\Elements\AddClass;
4use Forte\Rewriting\Passes\Elements\SetAttribute;
5use Forte\Rewriting\RewritePipeline;
6
7$pipeline = new RewritePipeline;
8$pipeline->add(new AddClass('img', 'responsive'));
9$pipeline->add(new SetAttribute('img', 'loading', 'lazy'));
10
11$pipeline->count(); // 2

RewritePipeline itself implements AstRewriter, so pipelines are composable. You can nest a pipeline inside another pipeline or pass it anywhere a single rewriter is accepted.

#Custom Element Passes

To create your own element pass, extend the ElementPass base class and implement the applyToElement method. The base class handles pattern matching and visitor setup:

1<?php
2
3use Forte\Ast\Elements\ElementNode;
4use Forte\Rewriting\NodePath;
5use Forte\Rewriting\Passes\Elements\ElementPass;
6
7readonly class AddLazyLoading extends ElementPass
8{
9 protected function applyToElement(NodePath $path, ElementNode $element): void
10 {
11 if (! $element->getAttribute('loading')) {
12 $path->setAttribute('loading', 'lazy');
13 }
14 }
15}

Apply it the same way as any built-in pass:

1<?php
2
3use Forte\Facades\Forte;
4
5$doc = Forte::parse('<img src="hero.jpg"><img src="thumb.jpg" loading="eager">');
6$result = $doc->apply(new AddLazyLoading('img'));

For passes that need to handle directives or other node types, implement the AstRewriter interface directly. The Custom Rewrite Passes article covers this in depth, and the Rewriters article covers the visitor pattern in detail.

#See also