Parsing

Documents

The Document class is the entry point for working with Forte. It parses a Blade template into an AST and provides the interface for querying, traversing, and transforming the result.

#Parsing Templates

The Document::parse method accepts a template string and returns a Document instance:

1<?php
2
3use Forte\Ast\Document\Document;
4
5$doc = Document::parse('<div>{{ $name }}</div>');

The Forte facade provides a convenient alternative. The parseFile method reads a file from disk and sets the file path automatically:

1<?php
2
3use Forte\Facades\Forte;
4
5$doc = Forte::parse($template);
6$doc = Forte::parseFile('/path/to/template.blade.php');

#Parser Options

The parse method accepts an optional ParserOptions instance for configuring the parser. The withComponentPrefix method registers additional component prefixes. The withAllDirectives method enables accept-all mode, where any @word pattern is treated as a directive:

1<?php
2
3use Forte\Parser\ParserOptions;
4use Forte\Ast\Document\Document;
5
6$options = ParserOptions::make()
7 ->withComponentPrefix('my-')
8 ->withAllDirectives();
9
10$doc = Document::parse($template, $options);

You may also sync directives from Laravel's Blade compiler (this is done automatically when using the Forte facade):

1<?php
2
3$options = ParserOptions::make()
4 ->syncLaravelDirectives();

#Rendering

The render method converts the document back to a string. For unmodified documents, it reproduces the original source exactly. After rewriting, it produces the transformed output:

1<?php
2
3echo $doc->render();

Document implements Stringable, so you can use it directly in string contexts:

1<?php
2
3echo $doc;
4echo (string) $doc;

#Accessing Source

The source method returns the original template text. This always returns the original, even after rewriting:

1<?php
2
3$doc->source(); // string (original template)

The getText method extracts a substring by absolute byte offsets. The getTextBetween method extracts text between two nodes:

1<?php
2
3$doc->getText(0, 10); // first 10 bytes of source
4$doc->getTextBetween($nodeA, $nodeB); // text between two nodes

#Line Access

The getLines method splits the source into an array of lines. The getLine method returns a single line by 1-based line number:

1<?php
2
3$doc->getLines(); // array<string>
4$doc->getLine(1); // string (first line)
5$doc->getLineCount(); // int

The getLineExcerpt method returns a small window of lines around a given line number, which is useful for error reporting:

1<?php
2
3$excerpt = $doc->getLineExcerpt(5, 2); // lines 3-7, keyed by 1-based line number

#File Path

The setFilePath and getFilePath methods associate a file path with the document. Forte::parseFile sets this automatically:

1<?php
2
3$doc->setFilePath('/path/to/template.blade.php');
4$doc->getFilePath(); // "/path/to/template.blade.php" or null

#Iterating Children

The children method returns an iterable of the document's root-level nodes. The getChildren method returns the same nodes as an array:

1<?php
2
3foreach ($doc->children() as $child) {
4 // each root-level node
5}
6
7$children = $doc->getChildren(); // array<Node>

Document implements IteratorAggregate and Countable, so you can iterate and count directly:

1<?php
2
3foreach ($doc as $node) {
4 // same as $doc->children()
5}
6
7count($doc); // number of root-level nodes

The firstChild and lastChild methods return the first and last root-level nodes:

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

The getChildAt method returns a root child by its zero-based index:

1<?php
2
3$doc->getChildAt(0); // ?Node (same as firstChild)
4$doc->getChildAt(2); // ?Node (third root child)

The childCount method returns the number of root-level children:

1<?php
2
3$doc->childCount(); // int

#Predicate-Based Child Access

The firstChildWhere method returns the first root child matching a callback. The firstChildOfType method filters by class:

1<?php
2
3use Forte\Ast\Elements\ElementNode;
4
5$firstDiv = $doc->firstChildWhere(
6 fn($n) => $n instanceof ElementNode && $n->is('div')
7);
8
9$firstElement = $doc->firstChildOfType(ElementNode::class);

The childrenOfType method returns an iterable of all root children matching a class. The getChildrenOfType method returns them as an array:

1<?php
2
3$elements = $doc->getChildrenOfType(ElementNode::class); // array<ElementNode>

#Node Collections

The getRootNodes method returns the root children as a NodeCollection, which extends Laravel's Collection with node-specific filtering:

1<?php
2
3$roots = $doc->getRootNodes(); // NodeCollection
4
5$roots->elements(); // ElementNode only
6$roots->directives(); // DirectiveNode only
7$roots->onLine(5); // nodes spanning line 5
8$roots->containingOffset(42); // nodes containing byte offset 42

#Content Checks

The filled method returns true if the document contains non-whitespace content. The blank method returns the inverse:

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

#Walking the Tree

The walk method performs a depth-first traversal of the entire document, calling a callback for every node. It returns the document for chaining:

1<?php
2
3use Forte\Ast\Node;
4use Forte\Ast\Elements\ElementNode;
5
6$doc->walk(function (Node $node) {
7 if ($node instanceof ElementNode) {
8 echo $node->tagNameText() . "\n";
9 }
10});

You may chain walk with other operations:

1<?php
2
3$output = $doc->walk(function (Node $node) {
4 // inspect every node
5})->render();

#Finding and Querying Nodes

The Document class provides the same finder methods, lazy collection properties, and eager collection variants available on any node. For the complete reference, see Traversal.

Quick reference:

  • $doc->find(fn($n) => ...) / $doc->findAll(fn($n) => ...): predicate search
  • $doc->findElementByName('div') / $doc->findDirectiveByName('include'): find nodes by name
  • $doc->findNodeAtOffset(42) / $doc->findNodeAtPosition(3, 10): by-position finders
  • $doc->elements, $doc->components, $doc->directives, ...: LazyCollection accessors
  • $doc->getElements(), $doc->getComponents(), ...: eager NodeCollection variants

#Node Depth

The getNodeDepth method returns the nesting depth of any node. Root-level nodes have a depth of 0:

1<?php
2
3$depth = $doc->getNodeDepth($someNode); // 0 for root, 1 for first-level, etc.

#Rewriting

The document provides three ways to apply transformations. All three return a new Document instance with the changes applied.

The rewrite method creates a RewriteBuilder for declarative rewriting:

1<?php
2
3$newDoc = $doc->rewrite(function ($builder) {
4 $builder->xpath('//div')->remove();
5});

The rewriteWith method accepts a callback that receives a NodePath for each node:

1<?php
2
3use Forte\Rewriting\NodePath;
4
5$newDoc = $doc->rewriteWith(function (NodePath $path) {
6 // transform nodes
7});

The apply method accepts one or more AstRewriter instances:

1<?php
2
3$newDoc = $doc->apply(
4 $rewriterA,
5 $rewriterB
6);

#Diagnostics

The diagnostics method returns a DiagnosticBag containing any lexer or parser errors found during parsing:

1<?php
2
3$bag = $doc->diagnostics();
4
5$bag->hasErrors(); // bool
6$bag->hasWarnings(); // bool
7$bag->errors(); // Collection<Diagnostic>
8$bag->warnings(); // Collection<Diagnostic>

The hasErrors method on the document is a shorthand for checking if any errors were recorded:

1<?php
2
3if ($doc->hasErrors()) {
4 echo $doc->diagnostics()->format($doc->source());
5}

#Node Metadata

The document supports attaching arbitrary metadata and tags to nodes. This is useful for passing information between rewriter passes or for annotating nodes during analysis.

The setNodeData and getNodeData methods store and retrieve key-value metadata by node index:

1<?php
2
3$doc->setNodeData($node->index(), 'processed', true);
4$doc->getNodeData($node->index(), 'processed'); // true
5$doc->hasNodeData($node->index(), 'processed'); // true
6$doc->removeNodeData($node->index(), 'processed');

The tagNode and nodeHasTag methods provide a lightweight way to mark nodes without associated values:

1<?php
2
3$doc->tagNode($node->index(), 'needs-review');
4$doc->nodeHasTag($node->index(), 'needs-review'); // true
5$doc->untagNode($node->index(), 'needs-review');

The findNodesByTag method returns all nodes with a given tag:

1<?php
2
3$flagged = $doc->findNodesByTag('needs-review'); // array<Node>

#Fluent Conditionals

The when method executes a callback when a condition is truthy. You may provide an optional else branch:

1<?php
2
3$doc->when($shouldProcess, function (Document $doc) {
4 // process the document
5});
6
7$doc->when($condition,
8 fn($doc) => $doc->walk(...), // then
9 fn($doc) => ..., // else
10);

The unless method is the inverse, executing when the condition is falsy:

1<?php
2
3$doc->unless($isProduction, function (Document $doc) {
4 // debug processing
5});

The tap method executes a callback and returns the document, which is useful for side effects like logging:

1<?php
2
3$doc->tap(function (Document $doc) {
4 logger()->info('Document has ' . count($doc) . ' root nodes');
5});

#XPath Querying

The xpath method provides XPath-based querying through DOM mapping:

1<?php
2
3$wrapper = $doc->xpath('//div[@class="container"]');
4
5$wrapper->first(); // ?Node
6$wrapper->all(); // array<Node>
7$wrapper->count(); // int

#See also