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