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

<?php
use Forte\Facades\Forte;
$doc = Forte::parse('<div>Hello</div><p>World</p>');
foreach ($doc->children() as $child) {
// each root-level node
}
$children = $doc->getChildren(); // array<Node>

For quick access to the boundaries of the document:

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

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

<?php
$doc->childCount(); // int
$doc->filled(); // true if document has non-whitespace content
$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:

<?php
use Forte\Ast\Node;
$doc->walk(function (Node $node) {
echo $node->kind() . "\n";
})->render();

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

<?php
use Forte\Ast\Node;
$element->walk(function (Node $node) {
// called for $element and all its descendants
});

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

<?php
foreach ($node->descendants() as $descendant) {
// every node below $node
}
$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:

<?php
use Forte\Ast\TraversalOptions;
// Default behavior: skip internal nodes, include synthetic nodes
$options = TraversalOptions::defaults();
// Include internal nodes (attributes, tag names, etc.)
$deep = TraversalOptions::deep();
$custom = new TraversalOptions(
includeInternal: true,
includeSynthetic: false,
includeTrivia: true,
maxDepth: 3,
);

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

<?php
use Forte\Ast\Node;
// Walk including internal nodes
$doc->walk(function (Node $node) {
// visits attributes, tag names, etc.
}, TraversalOptions::deep());
// Shorthand: pass true for deep traversal
$doc->walk(function (Node $node) {
// same as above
}, true);

#Finding Nodes

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

<?php
use Forte\Ast\EchoNode;
$firstEcho = $doc->find(fn(Node $n) => $n instanceof EchoNode); // ?Node
$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):

<?php
// Elements
$doc->findElementByName('div'); // ?ElementNode
$doc->findElementsByName('div'); // LazyCollection<ElementNode>
// Components (full tag name including prefix)
$doc->findComponentByName('x-alert'); // ?ComponentNode
$doc->findComponentsByName('x-alert'); // LazyCollection<ComponentNode>
// Standalone directives
$doc->findDirectiveByName('include'); // ?DirectiveNode
$doc->findDirectivesByName('include'); // LazyCollection<DirectiveNode>
// Block directives
$doc->findBlockDirectiveByName('if'); // ?DirectiveBlockNode
$doc->findBlockDirectivesByName('if'); // LazyCollection<DirectiveBlockNode>

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

<?php
$doc->findNodeAtOffset(42); // ?Node (by byte offset)
$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:

<?php
$doc->elements; // all ElementNode instances
$doc->components; // all ComponentNode instances
$doc->directives; // all unpaired DirectiveNode instances
$doc->blockDirectives; // all DirectiveBlockNode instances
$doc->echoes; // all EchoNode instances
$doc->rawEchoes; // raw echoes only ({!! !!})
$doc->escapedEchoes; // escaped echoes only ({{ }})
$doc->tripleEchoes; // triple echoes only ({{{ }}})
$doc->comments; // all CommentNode and BladeCommentNode instances
$doc->bladeComments; // Blade comments only ({{-- --}})
$doc->htmlComments; // HTML comments only (<!-- -->)
$doc->phpBlocks; // all PhpBlockNode instances
$doc->phpTags; // all PhpTagNode instances
$doc->text; // all TextNode instances

#Filtering

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

#First Match

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

#Counting

<?php
$elementCount = $doc->elements->count();
$componentCount = $doc->components->count();
$rawEchoCount = $doc->rawEchoes->count();

#Mapping

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

#Existence Checks

<?php
$hasComponents = $doc->components->isNotEmpty();
$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:

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

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

<?php
foreach ($node->ancestors() as $ancestor) {
// yields parent first, then grandparent, up to root
}
$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):

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

To check containment without retrieving the ancestor:

<?php
$isInsideForeach = $node->hasAncestorWhere(
fn($n) => $n instanceof DirectiveBlockNode && $n->isForeach()
);

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

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

#Sibling Navigation

Move horizontally through the tree with nextSibling and previousSibling:

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

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

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

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

<?php
use Forte\Ast\Elements\ElementNode;
$nextElement = $node->nextSiblingOfType(ElementNode::class); // ?Node
$prevElement = $node->previousSiblingOfType(ElementNode::class); // ?Node
$nextDiv = $node->nextSiblingWhere(
fn($n) => $n instanceof ElementNode && $n->is('div')
);

Position predicates help identify boundary nodes:

<?php
$node->isFirstChild(); // bool
$node->isLastChild(); // bool
$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:

<?php
use Forte\Ast\Elements\ElementNode;
$firstDiv = $node->firstChildWhere(
fn($n) => $n instanceof ElementNode && $n->is('div')
);
$lastDiv = $node->lastChildWhere(
fn($n) => $n instanceof ElementNode && $n->is('div')
);

To collect all matching children:

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

You can also filter children by class name directly:

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

Shortcut methods are available for common node types:

<?php
$node->firstDirective(); // ?DirectiveNode
$node->firstDirectiveBlock(); // ?DirectiveBlockNode
$node->firstElement(); // ?ElementNode
$node->firstText(); // ?TextNode
$node->firstEcho(); // ?EchoNode
$node->getDirectiveChildren(); // array<DirectiveNode>
$node->getDirectiveBlockChildren(); // array<DirectiveBlockNode>
$node->getElementChildren(); // array<ElementNode>
$node->getTextChildren(); // array<TextNode>
$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:

<?php
$node->textContent(); // ?string
$node->trimmedTextContent(); // ?string

To concatenate text from all text children:

<?php
$node->allTextContent(); // string
$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:

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

Leaf and root checks identify boundary positions:

<?php
$node->isLeaf(); // bool
$node->isRoot(); // bool

To distinguish parsed nodes from those created by rewriters:

<?php
$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:

<?php
$collection = $node->nodes();
$collection->elements(); // ElementNode only
$collection->directives(); // DirectiveNode only
$collection->blockDirectives(); // DirectiveBlockNode only
$collection->echoes(); // EchoNode only
$collection->comments(); // CommentNode and BladeCommentNode
$collection->components(); // ComponentNode only
$collection->phpBlocks(); // PhpBlockNode only
$collection->text(); // TextNode only
$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:

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

#Tree Filtering

Filter by structural role in the tree:

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

#Specific Queries

Query by directive or element name directly:

<?php
$collection->whereDirectiveName('include'); // directives named "include"
$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:

<?php
use Forte\Ast\Elements\ElementNode;
$element = $doc->findElementByName('div');
$element->hasAttribute('class'); // bool
$element->getAttribute('class'); // ?string
$element->getClass(); // ?string (shorthand)
$element->getId(); // ?string (shorthand)
$element->getAttributes(); // array<Attribute>

You can filter attributes by type using the Attributes collection:

<?php
$attrs = $element->attributes();
$attrs->static(); // standard HTML attributes
$attrs->bound(); // Blade bound attributes (:name)
$attrs->escaped(); // Blade escaped attributes (::name)
$attrs->boolean(); // boolean attributes (no value)
$attrs->complex(); // interpolated/expression attributes
$attrs->simple(); // simple key-value attributes
$attrs->bladeConstructs(); // embedded Blade constructs
$attrs->whereNameIs('class'); // filter by exact name
$attrs->whereNameMatches('/^data-/'); // filter by name pattern
$attrs->exceptNames(['class', 'id']); // exclude specific names
$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:

<?php
use Forte\Ast\DirectiveBlockNode;
$block = $doc->findBlockDirectiveByName('if');
$block->startDirective(); // ?DirectiveNode (@if)
$block->endDirective(); // ?DirectiveNode (@endif)
$block->intermediateDirectives(); // iterable<DirectiveNode> (@elseif, @else)
$block->hasIntermediates(); // bool

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

<?php
$block->isIf(); // bool
$block->isForeach(); // bool
$block->isForelse(); // bool
$block->isSection(); // bool
$block->isSwitch(); // bool
$block->isUnless(); // bool
$block->isPush(); // bool
$block->isOnce(); // bool
$block->isVerbatim(); // bool

#Component Slots

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

<?php
use Forte\Ast\Components\ComponentNode;
$component = $doc->findComponentByName('x-card');
$component->hasSlots(); // bool
$component->hasSlot('header'); // bool
$component->hasDefaultSlot(); // bool
$component->slot('header'); // ?SlotNode
$component->getSlots(); // array<SlotNode>
$component->getNamedSlots(); // array<SlotNode>
$component->getDefaultSlot(); // array<Node> (non-slot children)
$component->defaultSlotContent(); // string (rendered default slot)

Component identity metadata is also available:

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

#See also