AST

Traversal

Forte provides a variety of methods for navigating, querying, and iterating over nodes in a parsed document. Most traversal methods are available on both the Document and individual Node instances.

#Iterating Documents

You can iterate a document's root-level nodes with children (iterable) or getChildren (array):

1<?php
2
3use Forte\Facades\Forte;
4
5$doc = Forte::parse('<div>Hello</div><p>World</p>');
6
7foreach ($doc->children() as $child) {
8 // each root-level node
9}
10
11$children = $doc->getChildren(); // array<Node>

For quick access to the boundaries of the document:

1<?php
2
3$first = $doc->firstChild(); // ?Node
4$last = $doc->lastChild(); // ?Node

You can also check the size and content of the document:

1<?php
2
3$doc->childCount(); // int
4$doc->filled(); // true if document has non-whitespace content
5$doc->blank(); // true if document is empty or whitespace-only

#Walking the AST

Use walk for a depth-first traversal of the entire document tree. It calls the provided callback for each node and returns the document instance for chaining:

1<?php
2
3use Forte\Ast\Node;
4
5$doc->walk(function (Node $node) {
6 echo $node->kind() . "\n";
7})->render();

You may also call walk on any individual node to traverse only that subtree:

1<?php
2
3use Forte\Ast\Node;
4
5$element->walk(function (Node $node) {
6 // called for $element and all its descendants
7});

To collect all nodes below a given node without a callback, use descendants (lazy iterable) or getDescendants (array):

1<?php
2
3foreach ($node->descendants() as $descendant) {
4 // every node below $node
5}
6
7$all = $node->getDescendants(); // array<Node>

#Traversal Options

The walk, allOfType, and descendants methods accept a TraversalOptions parameter that controls which nodes are included. By default, internal nodes (like attributes and tag names) are excluded and synthetic nodes (created by rewriters) are included:

1<?php
2
3use Forte\Ast\TraversalOptions;
4
5// Default behavior: skip internal nodes, include synthetic nodes
6$options = TraversalOptions::defaults();
7
8// Include internal nodes (attributes, tag names, etc.)
9$deep = TraversalOptions::deep();
10
11$custom = new TraversalOptions(
12 includeInternal: true,
13 includeSynthetic: false,
14 includeTrivia: true,
15 maxDepth: 3,
16);

Pass options as the second argument to walk or allOfType. You can also pass true as a shorthand for TraversalOptions::deep():

1<?php
2
3use Forte\Ast\Node;
4
5// Walk including internal nodes
6$doc->walk(function (Node $node) {
7 // visits attributes, tag names, etc.
8}, TraversalOptions::deep());
9
10// Shorthand: pass true for deep traversal
11$doc->walk(function (Node $node) {
12 // same as above
13}, true);

#Finding Nodes

When you need a specific node rather than a full traversal, find returns the first match and findAll returns all matches:

1<?php
2
3use Forte\Ast\EchoNode;
4
5$firstEcho = $doc->find(fn(Node $n) => $n instanceof EchoNode); // ?Node
6$allEchoes = $doc->findAll(fn(Node $n) => $n instanceof EchoNode); // array<Node>

Dedicated finder methods are available for each major node type. Each has a singular variant (first match) and a plural variant (LazyCollection):

1<?php
2
3// Elements
4$doc->findElementByName('div'); // ?ElementNode
5$doc->findElementsByName('div'); // LazyCollection<ElementNode>
6
7// Components (full tag name including prefix)
8$doc->findComponentByName('x-alert'); // ?ComponentNode
9$doc->findComponentsByName('x-alert'); // LazyCollection<ComponentNode>
10
11// Standalone directives
12$doc->findDirectiveByName('include'); // ?DirectiveNode
13$doc->findDirectivesByName('include'); // LazyCollection<DirectiveNode>
14
15// Block directives
16$doc->findBlockDirectiveByName('if'); // ?DirectiveBlockNode
17$doc->findBlockDirectivesByName('if'); // LazyCollection<DirectiveBlockNode>

For editor integrations and cursor-aware tooling, you can locate the deepest node at a given source position:

1<?php
2
3$doc->findNodeAtOffset(42); // ?Node (by byte offset)
4$doc->findNodeAtPosition(3, 10); // ?Node (by line and column, 1-indexed)

#Lazy Collection Queries

For memory-efficient querying across the full document tree, Forte provides accessors that return LazyCollection instances for each node type:

1<?php
2
3$doc->elements; // all ElementNode instances
4$doc->components; // all ComponentNode instances
5$doc->directives; // all unpaired DirectiveNode instances
6$doc->blockDirectives; // all DirectiveBlockNode instances
7$doc->echoes; // all EchoNode instances
8$doc->rawEchoes; // raw echoes only ({!! !!})
9$doc->escapedEchoes; // escaped echoes only ({{ }})
10$doc->tripleEchoes; // triple echoes only ({{{ }}})
11$doc->comments; // all CommentNode and BladeCommentNode instances
12$doc->bladeComments; // Blade comments only ({{-- --}})
13$doc->htmlComments; // HTML comments only (<!-- -->)
14$doc->phpBlocks; // all PhpBlockNode instances
15$doc->phpTags; // all PhpTagNode instances
16$doc->text; // all TextNode instances

#Filtering

1<?php
2
3$divs = $doc->elements->filter(fn($el) => $el->is('div'));
4
5$includes = $doc->directives->filter(fn($d) => $d->nameText() === 'include');
6
7$matches = $doc->rawEchoes->filter(fn($e) => str_contains($e->expression(), '$html'));

#First Match

1<?php
2
3$firstDiv = $doc->elements->first(fn($el) => $el->is('div'));
4$firstIf = $doc->blockDirectives->first(fn($b) => $b->isIf());

#Counting

1<?php
2
3$elementCount = $doc->elements->count();
4$componentCount = $doc->components->count();
5$rawEchoCount = $doc->rawEchoes->count();

#Mapping

1<?php
2
3$tagNames = $doc->elements->map(fn($el) => $el->tagNameText())->all();
4// ['div', 'h1', 'p', 'span', ...]
5
6$directiveNames = $doc->directives->map(fn($d) => $d->nameText())->unique()->all();
7// ['include', 'csrf', 'props', ...]

#Existence Checks

1<?php
2
3$hasComponents = $doc->components->isNotEmpty();
4$hasRawEchoes = $doc->rawEchoes->isNotEmpty();

#Navigating the Tree

#Parent and Ancestor Navigation

Every node tracks its position in the tree. Use getParent to move up one level:

1<?php
2
3$parent = $node->getParent(); // ?Node

To walk the full ancestor chain from the immediate parent up to the root:

1<?php
2
3foreach ($node->ancestors() as $ancestor) {
4 // yields parent first, then grandparent, up to root
5}
6
7$ancestors = $node->getAncestors(); // array<Node> (parent-to-root order)

Ordering difference
The Node::ancestors() method yields in parent-to-root order. The NodePath::ancestors() method used in Rewriters returns an array in the opposite order (root-to-parent).

When you need the nearest ancestor matching a condition, use closest (predicate) or closestOfType (class name):

1<?php
2
3use Forte\Ast\Elements\ElementNode;
4use Forte\Ast\DirectiveBlockNode;
5
6$nearestDiv = $node->closest(fn($n) => $n instanceof ElementNode && $n->is('div'));
7$nearestBlock = $node->closestOfType(DirectiveBlockNode::class);

To check containment without retrieving the ancestor:

1<?php
2
3$isInsideForeach = $node->hasAncestorWhere(
4 fn($n) => $n instanceof DirectiveBlockNode && $n->isForeach()
5);

You can also check how deeply nested a node is (root-level nodes have depth 0):

1<?php
2
3$node->depth(); // int

#Sibling Navigation

Move horizontally through the tree with nextSibling and previousSibling:

1<?php
2
3$next = $node->nextSibling(); // ?Node
4$prev = $node->previousSibling(); // ?Node

To collect multiple siblings at once, use the directional or full sibling accessors:

1<?php
2
3$all = $node->getSiblings(); // array<Node>
4$after = $node->getNextSiblings(); // array<Node>
5$before = $node->getPreviousSiblings(); // array<Node>

You may also find siblings matching a predicate or of a specific type:

1<?php
2
3use Forte\Ast\Elements\ElementNode;
4
5$nextElement = $node->nextSiblingOfType(ElementNode::class); // ?Node
6$prevElement = $node->previousSiblingOfType(ElementNode::class); // ?Node
7
8$nextDiv = $node->nextSiblingWhere(
9 fn($n) => $n instanceof ElementNode && $n->is('div')
10);

Position predicates help identify boundary nodes:

1<?php
2
3$node->isFirstChild(); // bool
4$node->isLastChild(); // bool
5$node->isOnlyChild(); // bool

#Children Lookups

Any node with children supports predicate-based and type-based child lookups.

Use firstChildWhere and lastChildWhere to find children matching a predicate:

1<?php
2
3use Forte\Ast\Elements\ElementNode;
4
5$firstDiv = $node->firstChildWhere(
6 fn($n) => $n instanceof ElementNode && $n->is('div')
7);
8
9$lastDiv = $node->lastChildWhere(
10 fn($n) => $n instanceof ElementNode && $n->is('div')
11);

To collect all matching children:

1<?php
2
3$divs = $node->getChildrenWhere(
4 fn($n) => $n instanceof ElementNode && $n->is('div')
5);

You can also filter children by class name directly:

1<?php
2
3use Forte\Ast\EchoNode;
4
5$firstEcho = $node->firstChildOfType(EchoNode::class); // ?Node
6$allEchoes = $node->getChildrenOfType(EchoNode::class); // array<Node>

Shortcut methods are available for common node types:

1<?php
2
3$node->firstDirective(); // ?DirectiveNode
4$node->firstDirectiveBlock(); // ?DirectiveBlockNode
5$node->firstElement(); // ?ElementNode
6$node->firstText(); // ?TextNode
7$node->firstEcho(); // ?EchoNode
8
9$node->getDirectiveChildren(); // array<DirectiveNode>
10$node->getDirectiveBlockChildren(); // array<DirectiveBlockNode>
11$node->getElementChildren(); // array<ElementNode>
12$node->getTextChildren(); // array<TextNode>
13$node->getEchoChildren(); // array<EchoNode>

#Text Content

These methods extract plain text from a node's children. This is useful for reading the visible text content of elements:

Use textContent for the first text child, or trimmedTextContent for a whitespace-trimmed version:

1<?php
2
3$node->textContent(); // ?string
4$node->trimmedTextContent(); // ?string

To concatenate text from all text children:

1<?php
2
3$node->allTextContent(); // string
4$node->allTrimmedTextContent(); // string

#Tree Predicates

These methods help you understand a node's role in the tree structure. Use contains to check whether a node has another as a descendant:

1<?php
2
3$node->contains($otherNode); // bool

Leaf and root checks identify boundary positions:

1<?php
2
3$node->isLeaf(); // bool
4$node->isRoot(); // bool

To distinguish parsed nodes from those created by rewriters:

1<?php
2
3$node->isSynthetic(); // bool

#Node Collections

The nodes method on any node returns its children as a NodeCollection. The document's getRootNodes method does the same for root-level nodes. NodeCollection extends Laravel's Collection with node-specific filtering methods.

#Type Filtering

Filter a collection down to a specific node type:

1<?php
2
3$collection = $node->nodes();
4
5$collection->elements(); // ElementNode only
6$collection->directives(); // DirectiveNode only
7$collection->blockDirectives(); // DirectiveBlockNode only
8$collection->echoes(); // EchoNode only
9$collection->comments(); // CommentNode and BladeCommentNode
10$collection->components(); // ComponentNode only
11$collection->phpBlocks(); // PhpBlockNode only
12$collection->text(); // TextNode only
13$collection->ofType(EchoNode::class); // filter by any class

#Position Filtering

Narrow results to nodes at a specific source location. This is useful for editor integrations and range-based operations:

1<?php
2
3$collection->onLine(5); // nodes that span line 5
4$collection->startingOnLine(5); // nodes starting on line 5
5$collection->endingOnLine(5); // nodes ending on line 5
6$collection->containingOffset(42); // nodes containing byte offset 42
7$collection->betweenOffsets(10, 50); // nodes within an offset range

#Tree Filtering

Filter by structural role in the tree:

1<?php
2
3$collection->roots(); // root-level nodes only
4$collection->leaves(); // nodes with no children
5$collection->withChildren(); // nodes that have children

#Specific Queries

Query by directive or element name directly:

1<?php
2
3$collection->whereDirectiveName('include'); // directives named "include"
4$collection->whereElementIs('div'); // elements with tag name "div"

#Iterating Internal Children

Some nodes contain internal structural children that are not visited during a normal tree walk. These include element attributes, directive block structure, and component slots.

#Element Attributes

The attributes method on an ElementNode returns an Attributes collection. Individual attributes may be accessed by name:

1<?php
2
3use Forte\Ast\Elements\ElementNode;
4
5$element = $doc->findElementByName('div');
6
7$element->hasAttribute('class'); // bool
8$element->getAttribute('class'); // ?string
9$element->getClass(); // ?string (shorthand)
10$element->getId(); // ?string (shorthand)
11$element->getAttributes(); // array<Attribute>

You can filter attributes by type using the Attributes collection:

1<?php
2
3$attrs = $element->attributes();
4
5$attrs->static(); // standard HTML attributes
6$attrs->bound(); // Blade bound attributes (:name)
7$attrs->escaped(); // Blade escaped attributes (::name)
8$attrs->boolean(); // boolean attributes (no value)
9$attrs->complex(); // interpolated/expression attributes
10$attrs->simple(); // simple key-value attributes
11$attrs->bladeConstructs(); // embedded Blade constructs
12
13$attrs->whereNameIs('class'); // filter by exact name
14$attrs->whereNameMatches('/^data-/'); // filter by name pattern
15$attrs->exceptNames(['class', 'id']); // exclude specific names
16$attrs->onlyNames(['class', 'id']); // include only specific names

#Directive Block Structure

A DirectiveBlockNode contains an opening directive, optional intermediate directives, and a closing directive. These structural children are accessible directly:

1<?php
2
3use Forte\Ast\DirectiveBlockNode;
4
5$block = $doc->findBlockDirectiveByName('if');
6
7$block->startDirective(); // ?DirectiveNode (@if)
8$block->endDirective(); // ?DirectiveNode (@endif)
9$block->intermediateDirectives(); // iterable<DirectiveNode> (@elseif, @else)
10$block->hasIntermediates(); // bool

Block directives also expose type-check methods for common directive types:

1<?php
2
3$block->isIf(); // bool
4$block->isForeach(); // bool
5$block->isForelse(); // bool
6$block->isSection(); // bool
7$block->isSwitch(); // bool
8$block->isUnless(); // bool
9$block->isPush(); // bool
10$block->isOnce(); // bool
11$block->isVerbatim(); // bool

#Component Slots

A ComponentNode may contain named and default slots. The slot methods provide access to this internal structure:

1<?php
2
3use Forte\Ast\Components\ComponentNode;
4
5$component = $doc->findComponentByName('x-card');
6
7$component->hasSlots(); // bool
8$component->hasSlot('header'); // bool
9$component->hasDefaultSlot(); // bool
10$component->slot('header'); // ?SlotNode
11$component->getSlots(); // array<SlotNode>
12$component->getNamedSlots(); // array<SlotNode>
13$component->getDefaultSlot(); // array<Node> (non-slot children)
14$component->defaultSlotContent(); // string (rendered default slot)

Component identity metadata is also available:

1<?php
2
3$component->getComponentName(); // string (name without prefix)
4$component->getPrefix(); // string (e.g. "x-")
5$component->getType(); // string (e.g. "blade", "livewire")
6$component->isSlot(); // bool
7$component->getSlotName(); // ?string

#See also