09

April 10, 2026 · View on GitHub

@nodable/flexible-xml-parser uses path-expression-matcher for all path-based features: stopNodes, skip.tags, exitIf, and value parser context.


Pattern Syntax

PatternMatches
'root.script'<script> as a direct child of <root>
'*.script'<script> with exactly one parent (any name)
'..script'<script> anywhere in the tree
'root..script'<script> anywhere inside <root>
'..div[class=code]'<div class="code"> anywhere
'root.item:first'First <item> under <root>
'root.item:nth(2)'Third <item> under <root> (0-indexed)
'soap::Envelope'<Envelope> with namespace soap

String vs Pre-compiled Expressions

Plain strings are automatically converted to Expression objects at parser construction time. For reusable parsers, pre-compile expressions to avoid re-parsing on every parse() call:

import { Expression } from 'path-expression-matcher';

const scriptExpr = new Expression('..script');  // compiled once

const parser = new XMLParser({
  tags: { stopNodes: [scriptExpr] },
});

// Reuse the same parser and expression for many documents
const r1 = parser.parse(html1);
const r2 = parser.parse(html2);

You can mix strings and Expression objects in the same array.


The ReadOnlyMatcher

A ReadOnlyMatcher is passed to every user-facing callback (value parsers, onStopNode, exitIf, output builder methods). It reflects the current parser position and lets you inspect the path safely without risk of mutating parser state.

Available methods

MethodReturns
matches(expression)boolean — does current path match?
getCurrentTag()Current tag name
getCurrentNamespace()Namespace prefix or undefined
getAttrValue(name)Attribute value on current node
hasAttr(name)boolean
getPosition()Child index of current node
getCounter()Occurrence count of this tag name at this level
getDepth()Nesting depth
toString()Dot-separated path string, e.g. "root.users.user"
toArray()Array of tag names

Mutating methods (push, pop, reset, etc.) throw TypeError if called on a ReadOnlyMatcher.


Matcher in Value Parser Context

Every value parser's parse(val, context) call includes a matcher on the context:

import { Expression } from 'path-expression-matcher';

const priceExpr = new Expression('..price');

class CurrencyParser {
  parse(val, context) {
    if (typeof val !== 'string') return val;
    if (!context?.matcher?.matches(priceExpr)) return val;
    return parseFloat(val.replace(/[$€£¥₹,]/g, ''));
  }
}

const parser = new XMLParser({
  tags: { valueParsers: [new CurrencyParser()] },
});

Matcher in OutputBuilder Methods

The matcher passed to addElement, closeElement, and addValue reflects the current position:

import { CompactBuilder } from '@nodable/compact-builder';
import { Expression } from 'path-expression-matcher';

const internalExpr = new Expression('..internal');

class FilteredBuilder extends CompactBuilder {
  constructor(...args) {
    super(...args);
    this._skipDepth = 0;
  }
  addElement(tag, matcher) {
    if (this._skipDepth > 0 || matcher.matches(internalExpr)) {
      this._skipDepth++;
      return;
    }
    super.addElement(tag, matcher);
  }
  closeElement(matcher) {
    if (this._skipDepth > 0) { this._skipDepth--; return; }
    super.closeElement(matcher);
  }
}

Attribute Conditions

Attribute conditions in path expressions work because the parser calls matcher.updateCurrent(rawAttrs) before the stop-node or skip-tag check runs. So '..div[class=code]' correctly matches <div class="code">:

const parser = new XMLParser({
  skip: { attributes: false },
  tags: {
    stopNodes: [new Expression('..div[class=code]')],
  },
});

parser.parse('<root><div class="code"><pre>raw</pre></div><div class="text"><p>parsed</p></div></root>');

➡ Next: 10 — TypeScript