Extensions
Extension DOM Mapping
Forte's XPath engine converts the AST into a DOM tree before evaluating queries. Extension nodes participate in this conversion through a three-tier system. You can control how your extension nodes appear in XPath by naming them, providing a custom mapper, or relying on the generic fallback.
#Naming Extension Nodes
The simplest way to make extension nodes queryable is to register a domElement name when you define the node kind. Pass the name as the fifth argument to NodeKindRegistry::register():
<?php
use Forte\Extensions\AbstractExtension;
use Forte\Parser\NodeKindRegistry;
class MarkerExtension extends AbstractExtension
{
// ...
protected function registerKinds(NodeKindRegistry $registry): void
{
$this->markerKind = $registry->register(
$this->id(), // namespace
'Marker', // name
MarkerNode::class,
'Marker', // label
'marker' // domElement: becomes <forte:marker>
);
$this->nodeKinds[] = $this->markerKind;
}
}
The registerKind() helper on AbstractExtension does not expose the domElement parameter
Call $registry->register() directly when you need a named DOM element, and remember to add the returned ID to $this->nodeKinds[] so that canHandle() and doHandle() still work.
The domElement value becomes the local name of the Forte-namespaced element. A registration with 'marker' produces <forte:marker> in the DOM tree, queryable as //forte:marker.
#The Three-Tier DOM Conversion
When the DomMapper encounters an extension node, it checks three sources in order:
| Priority | Source | Result |
|---|---|---|
| 1 | Custom ExtensionDomMapper |
Whatever DOM element your mapper creates |
| 2 | Named domElement in NodeKindRegistry |
<forte:{domElement}> with children preserved |
| 3 | Generic fallback | <forte:extension kind="N" name="Label"> |
The first match wins. A custom mapper always takes precedence over a named element, and a named element always takes precedence over the generic fallback.
#Querying Extension Nodes
Once your extension registers a domElement, you can query nodes with standard XPath expressions using the forte: prefix:
<?php
use Forte\Facades\Forte;
use Forte\Parser\ParserOptions;
$ext = new MarkerExtension;
$options = ParserOptions::withExtensions($ext);
$doc = Forte::parse('Hello ~world~ there', $options);
$results = $doc->xpath('//forte:marker');
count($results); // 1
$results->first()->markerName(); // "world"
Extension nodes work with the full XPath syntax. Predicates, axes, and compound expressions all apply. You can combine extension queries with built-in node queries in the same expression:
<?php
// Find markers inside @if blocks
$doc->xpath('//forte:if//forte:marker');
// Find markers that are direct children of a div
$doc->xpath('//div/forte:marker');
#Custom DOM Mapping
For full control over the DOM conversion, implement the ExtensionDomMapper interface and register it with DomMapper::registerExtensionMapper(). Your mapper receives the AST node, the DOMDocument, and the DomMapper instance:
<?php
use DOMDocument;
use DOMElement;
use Forte\Ast\Node;
use Forte\Querying\DomMapper;
use Forte\Querying\ExtensionDomMapper;
class MarkerDomMapper implements ExtensionDomMapper
{
public function toDOM(Node $node, DOMDocument $dom, DomMapper $mapper): DOMElement
{
$element = $dom->createElementNS(DomMapper::FORTE_NAMESPACE, 'forte:custom-marker');
$element->setAttribute('type', 'highlight');
$element->setAttribute(DomMapper::INDEX_ATTR, (string) $node->index());
return $element;
}
}
Register the mapper using the node's kind ID. Custom mappers override named domElement registrations. Once registered, the mapper takes full responsibility for the DOM conversion:
<?php
use Forte\Facades\Forte;
use Forte\Parser\ParserOptions;
use Forte\Querying\DomMapper;
$ext = new MarkerExtension;
$options = ParserOptions::withExtensions($ext);
Forte::parse('~init~', $options); // trigger registration
DomMapper::registerExtensionMapper($ext->getMarkerKind(), new MarkerDomMapper);
$doc = Forte::parse('Check ~this~ out', $options);
$results = $doc->xpath('//forte:custom-marker[@type="highlight"]');
count($results); // 1
Use DomMapper::clearExtensionMappers() to remove all registered custom mappers, for example during test teardown.
#ExtensionDomMapper Interface
| Method | Parameters | Description |
|---|---|---|
toDOM() |
Node $node, DOMDocument $dom, DomMapper $mapper |
Convert an AST node to a DOM element |
Your mapper must return a DOMElement created from the provided $dom document. Use DomMapper::FORTE_NAMESPACE with createElementNS() to keep your elements in the Forte namespace, and set DomMapper::INDEX_ATTR so the XPath engine can map results back to AST nodes.
#DomMapper Constants
The DomMapper class exposes constants for building Forte-namespaced DOM elements:
| Constant | Value | Purpose |
|---|---|---|
FORTE_NAMESPACE |
'https://fortephp.com/ns/blade' |
XML namespace URI for all Forte elements |
FORTE_PREFIX |
'forte' |
Namespace prefix used in XPath expressions |
INDEX_ATTR |
'data-forte-idx' |
Attribute linking DOM elements back to AST node indices |
Always use these constants rather than hard-coding values. They keep your extension compatible with future changes to the Forte namespace.
#NodeKindRegistry Parameters
The register() method on NodeKindRegistry accepts six parameters:
| Parameter | Type | Description |
|---|---|---|
$namespace |
string |
Extension namespace, typically your extension's id() |
$name |
string |
Kind name (e.g., 'Marker', 'Shortcode') |
$nodeClass |
?string |
Custom Node subclass for this kind, or null for GenericNode |
$label |
?string |
Human-readable label (defaults to $name) |
$domElement |
?string |
DOM element name for XPath (e.g., 'marker' → <forte:marker>) |
$category |
?string |
Optional category (e.g., 'Attribute' for attribute extension kinds) |
The method returns an int kind ID that you store and reference during tree building.
#Node Metadata
Extension nodes support the same metadata and tagging API as all other nodes. For the complete method reference, see Basic Nodes > Node Metadata.
Here is a quick example using data and tags on extension nodes:
<?php
use Forte\Facades\Forte;
use Forte\Parser\ParserOptions;
$ext = new MarkerExtension;
$options = ParserOptions::withExtensions($ext);
$doc = Forte::parse('Hello ~world~ there', $options);
$marker = $doc->findAll(fn ($n) => $n instanceof MarkerNode)[0];
$marker->setData('category', 'greeting');
$marker->getData('category'); // "greeting"
$marker->hasData('category'); // true
$marker->getData('missing', 'fallback'); // "fallback"
$marker->hasData('missing'); // false
$marker->removeData('category');
$marker->hasData('category'); // false
<?php
use Forte\Facades\Forte;
use Forte\Parser\ParserOptions;
$ext = new MarkerExtension;
$options = ParserOptions::withExtensions($ext);
$doc = Forte::parse('~alpha~ and ~beta~', $options);
$markers = $doc->findAll(fn ($n) => $n instanceof MarkerNode);
$markers[0]->tag('important');
$markers[1]->tag('important');
$markers[1]->tag('reviewed');
$markers[0]->hasTag('important'); // true
$markers[1]->hasTag('reviewed'); // true
$markers[0]->hasTag('reviewed'); // false
$tagged = $doc->findNodesByTag('important');
count($tagged); // 2
$markers[0]->untag('important');
$markers[0]->hasTag('important'); // false
count($doc->findNodesByTag('important')); // 1
#See also
- Parser Extensions: Build custom lexer tokens, AST nodes, and attribute parsers
- Basic Nodes: Full node metadata API reference (setData, getData, tag, untag, etc.)
- XPath Queries: Query documents with XPath expressions
- Traversal: Navigate and query the document tree