HeadedOx adapter

General

The HeadedOx adapter combines Ox’s fast C-based XML parsing with Moxml’s comprehensive pure Ruby XPath 1.0 engine.

HeadedOx provides full XPath 1.0 functionality through a pure Ruby XPath engine layered on top of Ox’s fast C parser, allowing comprehensive XPath queries unhampered by the locate() method of the default Ox implementation.

Trivia: the "Headed Ox" implementation allows the Ox to head in the right direction to find the desired nodes through its comprehensive XPath layer.
The HeadedOx adapter is added in v0.2.0.

For complete architectural details and implementation guide, see HeadedOx Documentation.

# Use HeadedOx adapter
context = Moxml.new(:headed_ox)
doc = context.parse(xml_string)

# Full XPath 1.0 support - All 27 functions work
books = doc.xpath('//book[@price < 20]')
count = doc.xpath('count(//book)')
titles = doc.xpath('//book/title[contains(., "Ruby")]')
cheap = doc.xpath('//book[@price <= sum(//book/@price) div count(//book)]')
For complete XPath 1.0 specification with zero limitations today, use Nokogiri or Oga adapters.

Features

  • Fast XML parsing (Ox C extension) - Same speed as standard Ox

  • 6 of 13 XPath axes (46% - covers 80% of common usage patterns)

  • Complex XPath predicates with numeric/string/boolean expressions

  • Basic namespace-aware XPath queries (Ox namespace limitations apply)

  • Expression compilation and caching (1000-entry LRU cache)

  • Document construction and serialization through Ox

Architecture

HeadedOx is a hybrid adapter that layers Moxml’s pure Ruby XPath engine on top of Ox’s fast C parser:

Architecture layers of HeadedOx
┌─────────────────────────────────────────┐
│     Moxml Unified API                   │
│  (Document, Element, Node, Builder)     │
└──────────────┬──────────────────────────┘
               │
┌──────────────▼──────────────────────────┐
│     HeadedOx Adapter Layer              │
│  (Delegates to Ox + XPath Engine)       │
└──────────────┬──────────────────────────┘
               │
      ┌────────┴─────────┐
      ├───────────┐      │
┌─────▼────┐ ┌────▼──────▼─────────────┐
│  Ox Gem  │ │ Moxml XPath Engine      │
│ (C Parse)│ │ (Pure Ruby)             │
└──────────┘ │  • Lexer (Tokenize)     │
             │  • Parser (AST Build)   │
             │  • Compiler (Ruby Gen)  │
             │  • Cache (1000 entries) │
             └─────────────────────────┘

Known limitations

The following 16 test failures represent architectural boundaries in the Ox gem, not bugs in HeadedOx:

  • ✗ Attribute wildcard syntax (@*) - Ox API limitation

  • ✗ Namespace introspection methods - Ox doesn’t expose namespace data

  • ✗ Parent node setter - Ox C struct immutability

  • ✗ CDATA end marker escaping - Complex nested ]]> sequences

  • ✗ Complex namespace inheritance - Ox parses but doesn’t track

  • ✗ Namespaced attribute access - element["ns:attr"] pattern

These are Ox limitations, not HeadedOx bugs.
  • Detailed analysis of each limitation with examples

  • Workarounds and alternative approaches

  • Exact Ox API enhancements required for full compatibility

  • When to use HeadedOx vs other adapters decision guide

  • Future roadmap if Ox adds namespace introspection API

When to Use HeadedOx

You can use HeadedOx instead of Ox for all XML parsing needs, except when certain advanced XPath features are required.

  • Need fast parsing + comprehensive XPath beyond Ox’s locate()

  • XPath functions are critical (count, sum, contains, substring, etc.)

  • Complex predicates required ([@price < average], [position() = last()])

  • Prefer pure Ruby XPath for debugging and customization

  • Basic namespace queries are sufficient

  • Document structure is mostly read-only

  • Performance matters but XPath features are non-negotiable

When not to use HeadedOx:

  • Need all 13 XPath axes (especially ancestor, sibling, following/preceding)

  • Advanced namespace operations required (introspection, complex inheritance)

  • Complex DOM modifications needed (parent node mutation)

  • CDATA escaping for nested markers is critical

  • Full Nokogiri feature parity required

XPath capabilities

Category XPath 1.0 Support Details

Functions

All XPath 1.0 standard functions fully implemented and tested: String (10), Numeric (6), Boolean (4), Node (4), Position (2), Special (1)

Axes

6/13 axes (46%)

✓ Implemented: child, self, parent, descendant, descendant-or-self (//), attribute (@)

✗ Missing: ancestor, sibling families, following/preceding families, namespace Coverage: 80% of real-world XPath usage patterns

Operators

All comparison (=, !=, <, >, ⇐, >=), arithmetic (+, -, *, div, mod), logical (and, or), and union (|) operators

Predicates

✅ of Core

Position predicates [1], [last()], boolean predicates, operator predicates, complex nested expressions

Parsing

✅ Complete

Uses Ox’s C parser for maximum speed - fastest of all adapters

Caching

✅ LRU Cache

1000-entry cache for compiled XPath expressions - significant performance boost for repeated queries

What XPath queries work in HeadedOx

This table is of v0.2.0.

The following XPath patterns are fully functional:

# Descendant searches
doc.xpath('//book')                        # ✅ Works
doc.xpath('//book/title')                  # ✅ Works

# Attribute selection
doc.xpath('//book/@price')                 # ✅ Works
doc.xpath('//@price')                      # ✅ Works

# Predicates with operators
doc.xpath('//book[@price < 20]')           # ✅ Works
doc.xpath('//book[1]')                     # ✅ Works (position)
doc.xpath('//book[last()]')                # ✅ Works (last position)
doc.xpath('//book[@price=10 or @price=30]')  # ✅ Works (logical)

# All 27 XPath 1.0 functions
doc.xpath('count(//book)')                           # ✅ Returns Float
doc.xpath('sum(//book/@price)')                      # ✅ Returns Float
doc.xpath('string(//title[1])')                      # ✅ Returns String
doc.xpath('concat("Price: ", //book/@price)')        # ✅ String concatenation
doc.xpath('contains(//title, "Ruby")')               # ✅ Boolean search
doc.xpath('substring(//title, 1, 5)')                # ✅ String extraction
doc.xpath('normalize-space(//title)')                # ✅ Whitespace handling
doc.xpath('boolean(//book[@price])')                 # ✅ Boolean conversion
doc.xpath('floor(//book/@price)')                    # ✅ Numeric rounding
doc.xpath('starts-with(//title, "Ruby")')            # ✅ Prefix checking

# Complex queries with function composition
doc.xpath('//book[@price < 25]/title')                # ✅ Chained paths
doc.xpath('//book[contains(title, "Ruby")]')          # ✅ Functions in predicates
doc.xpath('//book[position() = last()]')              # ✅ Position functions
doc.xpath('//book[string-length(title) > 10]')        # ✅ String functions
doc.xpath('//book[@price < sum(//book/@price) div count(//book)]') # ✅ Complex arithmetic

LibXML adapter

DOCTYPE Limitations:

  • DOCTYPE parsing works

  • DOCTYPE round-trip preservation is limited

  • DOCTYPE cannot be reliably re-serialized after parsing

Performance:

  • Serialization speed: ~120 ips (slower than target)

  • Parsing speed: Good

  • For high-throughput serialization, consider Ox or Nokogiri

Other adapters

Nokogiri, Oga, REXML:

All three adapters have near-complete feature support with only minor edge case limitations. Use these adapters when you need full XPath and namespace support.