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():
1<?php
2
3use Forte\Extensions\AbstractExtension;
4use Forte\Parser\NodeKindRegistry;
5
6class MarkerExtension extends AbstractExtension
7{
8 // ...
9
10 protected function registerKinds(NodeKindRegistry $registry): void
11 {
12 $this->markerKind = $registry->register(
13 $this->id(), // namespace
14 'Marker', // name
15 MarkerNode::class,
16 'Marker', // label
17 'marker' // domElement: becomes <forte:marker>
18 );
19 $this->nodeKinds[] = $this->markerKind;
20 }
21}
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:
1<?php
2
3use Forte\Facades\Forte;
4use Forte\Parser\ParserOptions;
5
6$ext = new MarkerExtension;
7$options = ParserOptions::withExtensions($ext);
8$doc = Forte::parse('Hello ~world~ there', $options);
9
10$results = $doc->xpath('//forte:marker');
11
12count($results); // 1
13$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:
1<?php
2
3// Find markers inside @if blocks
4$doc->xpath('//forte:if//forte:marker');
5
6// Find markers that are direct children of a div
7$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:
1<?php
2
3use DOMDocument;
4use DOMElement;
5use Forte\Ast\Node;
6use Forte\Querying\DomMapper;
7use Forte\Querying\ExtensionDomMapper;
8
9class MarkerDomMapper implements ExtensionDomMapper
10{
11 public function toDOM(Node $node, DOMDocument $dom, DomMapper $mapper): DOMElement
12 {
13 $element = $dom->createElementNS(DomMapper::FORTE_NAMESPACE, 'forte:custom-marker');
14 $element->setAttribute('type', 'highlight');
15 $element->setAttribute(DomMapper::INDEX_ATTR, (string) $node->index());
16
17 return $element;
18 }
19}
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:
1<?php
2
3use Forte\Facades\Forte;
4use Forte\Parser\ParserOptions;
5use Forte\Querying\DomMapper;
6
7$ext = new MarkerExtension;
8$options = ParserOptions::withExtensions($ext);
9Forte::parse('~init~', $options); // trigger registration
10
11DomMapper::registerExtensionMapper($ext->getMarkerKind(), new MarkerDomMapper);
12
13$doc = Forte::parse('Check ~this~ out', $options);
14$results = $doc->xpath('//forte:custom-marker[@type="highlight"]');
15
16count($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:
1<?php
2
3use Forte\Facades\Forte;
4use Forte\Parser\ParserOptions;
5
6$ext = new MarkerExtension;
7$options = ParserOptions::withExtensions($ext);
8$doc = Forte::parse('Hello ~world~ there', $options);
9
10$marker = $doc->findAll(fn ($n) => $n instanceof MarkerNode)[0];
11
12$marker->setData('category', 'greeting');
13
14$marker->getData('category'); // "greeting"
15$marker->hasData('category'); // true
16$marker->getData('missing', 'fallback'); // "fallback"
17$marker->hasData('missing'); // false
18
19$marker->removeData('category');
20
21$marker->hasData('category'); // false
1<?php
2
3use Forte\Facades\Forte;
4use Forte\Parser\ParserOptions;
5
6$ext = new MarkerExtension;
7$options = ParserOptions::withExtensions($ext);
8$doc = Forte::parse('~alpha~ and ~beta~', $options);
9
10$markers = $doc->findAll(fn ($n) => $n instanceof MarkerNode);
11$markers[0]->tag('important');
12$markers[1]->tag('important');
13$markers[1]->tag('reviewed');
14
15$markers[0]->hasTag('important'); // true
16$markers[1]->hasTag('reviewed'); // true
17$markers[0]->hasTag('reviewed'); // false
18
19$tagged = $doc->findNodesByTag('important');
20
21count($tagged); // 2
22
23$markers[0]->untag('important');
24
25$markers[0]->hasTag('important'); // false
26count($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