Class: Moxml::Adapter::Leptris

Inherits:
Base
  • Object
show all
Extended by:
DocumentParts, Markers, Materialize, Serialize
Defined in:
lib/moxml/adapter/leptris.rb,
lib/moxml/adapter/leptris/markers.rb,
lib/moxml/adapter/leptris/serialize.rb,
lib/moxml/adapter/leptris/sax_bridge.rb,
lib/moxml/adapter/leptris/materialize.rb,
lib/moxml/adapter/leptris/document_parts.rb,
lib/moxml/adapter/leptris/sax_record_replay.rb

Overview

Adapter over the leptris FFI binding (libleptris C library).

libleptris provides DOM parsing, a native XPath 1.0 engine, SAX, and serialization. Its document-level creation surfaces exist (set_doctype 1.9.176/#212, declaration setters #1094) but are SET-ONLY — no unset for declaration, DOCTYPE, or document PIs — while the moxml facade contract makes those parts removable nodes. CustomizedLeptris value objects therefore remain the lifecycle record, attached through NativeAttachment.

Defined Under Namespace

Modules: DocumentParts, Identity, Markers, Materialize, Serialize Classes: LeptrisSAXBridge, SaxRecordReplay

Constant Summary collapse

MINIMUM_BINDING_VERSION =

The binding floor (issue #149): 1.9.32 carried the traverse fix (leptris-ruby#89) and made built documents reflect their parts immediately (leptris-ruby#91); the document node (1.9.26), DTDATTR (1.9.8), and the batch child-pointer read (1.7.0) surfaces predate it. Older bindings are not eligible for the default (see Config.leptris_preferred_available?) and the adapter no longer carries accommodation paths for them. Raised 1.9.32 -> 1.9.194.1 (2026-09-20): every known consumer resolves a modern binding (canon itself pins ~> 1.9.193), and the sub-1.9.194 accommodation paths are deleted.

"1.9.194.1"
ATTR_RESULT_NATIVE =

leptris_parse_html_string shipped in bindings 1.9.80 (libleptris 1.9.75, engine #659) as Leptris::XML.parse_html. Attribute-node xpath results carry proper wrappers since 1.9.105 (leptris-ruby#153: ResultAttr with name/value); before that the native gate routed them to the Ruby engine.

true
DIGEST_SUPPORTED =

leptris_node_digest shipped in bindings 1.9.99 (libleptris 1.9.99, engine #869): on-demand Merkle subtree hash, zero cost when unused. Below it, Node#digest answers nil.

Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.99")
NATIVE_XPATH_SEAMS =

1.9.163.7 added the XPath seams (TODO.perf/15+16): Searchable#at_xpath materializes nodeset entry 0 and frees the handle in one C dispatch — no NodeSet container, no AutoPointer (2.8x on repeat at_xpath) — and CompiledXPath#eval_ptrs runs the compiled handle against raw pointers, skipping the per-call EvaluationContext (~2us on every multi-result query).

defined?(::Leptris::XML::Native) &&
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.163.7")
NATIVE_READ_LAYER =
defined?(::Leptris::XML::NativeNode) &&
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.162.6")
NATIVE_STRINGS_UTF8 =

1.9.163.2's native layer returns UTF-8, unfrozen strings (1.9.162.x answered ASCII-8BIT — the reads retagged with a dup+force_encoding per call).

defined?(::Leptris::XML::NativeNode) &&
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.163.2")
NATIVE_MUTATIONS_COHERENT =

1.9.163.5 (leptris-ruby#204/#208): native mutations are version-coherent with the binding (both surfaces' memos drop on mutation) — the builder factories and native add_child are adoptable end-to-end; NativeNode grew a document accessor and line numbers.

defined?(::Leptris::XML::NativeNode) &&
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.163.5")
NN =
::Leptris::XML::NativeNode
NN_ATTRIBUTE =
NN.instance_method(:attribute)
NN_NAME =
NN.instance_method(:name)
NN_PARENT =
NN.instance_method(:parent)
NN_NEXT_SIBLING =
NN.instance_method(:next_sibling)
NN_CONTENT =
NN.instance_method(:content)
NN_CHILDREN =
NN.instance_method(:children)
NN_NODE_TYPE =
NN.instance_method(:node_type)
NN_DOCUMENT =
NN.instance_method(:document)
NN_ADD_CHILD =
NN.instance_method(:add_child)
NATIVE_SOURCE_POSITION =

1.9.181 (lockstep): Node#source_position — col_start, col_end from the engine's source-position API (upstream #1124); created nodes report zeros.

Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.181")
NATIVE_ENTITY_REFS =

Entity-reference preservation (leptris-ruby#212 / upstream #1094): 1.9.177 ships ParseOptions.keep_entity_refs plus first-class EntityReference nodes — the marker machinery becomes bypassable when the Context's entity_mode is :keep.

defined?(::Leptris::XML::EntityReference) &&
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.177")
NATIVE_BULK_FIXED =

1.9.163.2 moved the native bulk children into C (Native.bulk_children) and fixed the 512 truncation (leptris-ruby#202). The 162.6-163.1 window carries the binding-side truncation, so moxml fetches with its own count-then-copy scratch there; newer bindings use their C bulk path (version-memoized, cheaper than a per-call count+copy).

defined?(::Leptris::XML::NativeNode) &&
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.163.2")
NATIVE_SAX_RECORDS =

Bulk SAX record drain (leptris#1298): leptris_sax_records_parse

  • Leptris::XML::SAX::Records — one crossing per document, handlers replay or walk the table. Ships in binding 1.9.242.0: 1.9.240/241 carry the drain WITHOUT the duplicate-attribute bail (moxml#281 — the record table has no error channel, so a fatal dup-attr document parses with on_error never firing), so the gate names the first release where the drain is CORRECT, not merely present. Dev floor pins ~> 1.9.238; older bindings take the callback Parser below.
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.242")
NATIVE_IDENTITY =
NATIVE_MUTATIONS_COHERENT &&
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.174.4")
NATIVE_KLASS_CHILDREN =

leptris-ruby#246 klass-propagating reads: the read family (children/element_children/next_sibling/parent) mints the mapped subclass per kind, so children arrive already carrying the contract — wrap_native's is_a? guard short-circuits and the per-visit from() mint (the second allocation of the pair) drops out. Kind order is the C layer's node-type ints: element, text, comment, cdata, pi.

NATIVE_IDENTITY &&
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.193.2")
NATIVE_C_WALK =
defined?(::Leptris::XML::NativeNode) &&
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.174.6")
BINDING_FAST_WRAP =

Fast mint for walk-yielded binding nodes (#312): one class-keyed hash answers wrapper class AND type — the generic wrap path's node_type probe, wrap_native detour, type map, and tap all drop out. Identity rides the

{
  ::Leptris::XML::Element => [Moxml::Wrappers::Element, :element],
  ::Leptris::XML::Text => [Moxml::Wrappers::Text, :text],
  ::Leptris::XML::CDATA => [Moxml::Wrappers::Cdata, :cdata],
  ::Leptris::XML::Comment => [Moxml::Wrappers::Comment, :comment],
  ::Leptris::XML::ProcessingInstruction =>
    [Moxml::Wrappers::ProcessingInstruction, :processing_instruction],
}.freeze
PREFIXED_ATTR_PREDICATES_NATIVE =

leptris-ruby#103: prefixed attribute tests inside predicates stopped resolving through the document's in-scope declarations on released 1.9.37–1.9.39; 1.9.40 (engine 1.9.14+) restored the fallback. Older bindings keep the Ruby-engine routing in native_expression?.

Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.40")
LIBXML2_LAYOUT_PARITY =

leptris/leptris#636 (leptris-ruby 1.9.42): libxml2-compatible pretty-print — child-PI lines, DOCTYPE internal-subset layout, no stray trailing newline after non-ASCII text, and the indent unit (to_xml's indent_text takes the unit string). Older floor bindings treat indent_text as a display-form boolean, so the passthrough and the parity pins gate on this.

Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.42")
INDENT_UNIT_SUPPORTED =

leptris-ruby#109 (1.9.45): Element#to_xml takes the indent-unit string, so moxml's per-child document composition can carry indent_text: through. Text-bearing leaves still emit spaces until the engine fix (leptris/leptris#658); the boolean display form stays document-level and is never forwarded (the element face raises on it).

Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.45")
ELEMENT_UNIT_COMMENTS =

leptris-ruby#115 (fixed 1.9.50): the element unit path keeps child comments.

Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.50")
ENGINE_NOBLANKS_SAFE =

leptris/leptris#677: the engine's DROP_WS_TEXT trims boundary whitespace of non-blank text nodes. Probed at load rather than version-gated so the C fix is adopted the moment a fixed binding installs — no moxml release needed. While the probe fails, noblanks drops blanks moxml-side after parse.

begin
  doc = ::Leptris::XML::Document.parse(
    "<r> x</r>", options: ::Leptris::XML::ParseOptions.noblanks
  )
  text = doc.root.children.first
  text.is_a?(::Leptris::XML::Text) && text.content == " x"
rescue StandardError
  false
end
ENGINE_MULTI_DECL_SUBSET_OK =

leptris/leptris#687: the engine's subset serializer mangles every internal-subset declaration after the first. Probed for the same reason — once fixed, the whole-document fast path serves multi-declaration subsets too.

begin
  doc = ::Leptris::XML::Document.parse(
    %(<?xml version="1.0"?><!DOCTYPE r [<!ELEMENT r (#PCDATA)><!ATTLIST e a CDATA "d">]><r/>),
  )
  doc.to_xml.include?("<!ATTLIST")
rescue StandardError
  false
end
NATIVE_PLAN_ROWS =

Plan row stream (Moxml::Plan): the engine's one-pass C snapshot is pre-order with the first text child on the row — exactly the plan's shape, with no wrapper minting at all. Marker-bearing documents stay on the generic path (the bulk stream has no marker split).

true
NATIVE_PLAN_STRUCTS =

leptris-ruby#272 (binding 1.9.201.1): Native.plan_structs — the C struct executor behind Moxml::StructPlan. The floor is the varargs-mint fix: 1.9.197.1–1.9.201.0 seed unmapped struct members with garbage (GC aborts, corrupted data); StructPlan falls back to its spec-pinned Moxml::Plan path below the fix. The probe guards the version gate: a numerically newer lockstep can publish without the face (1.9.199.0 shipped without it), and the gate alone would NoMethodError.

Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.201.1") &&
        (!defined?(::Leptris::XML::Native) ||
::Leptris::XML::Native.respond_to?(:plan_structs))
NATIVE_PLAN_TYPED =

leptris-ruby#275 (binding 1.9.204, the remove half of the document-level surfaces): remove_pi, clear_declaration, remove_doctype complete the set-only creation entries. Attached DOCTYPEs materialize as native nodes, document-PI removal works, and declaration state mirrors through. The typed executor face (leptris-ruby: typed plan scalars — [slot, tag] spec entries cast in C, no Ruby String for numeric/boolean slots). Capability-probed, not version-gated.

!defined?(::Leptris::XML::Native) ||
::Leptris::XML::Native.respond_to?(:plan_structs_typed?)
NATIVE_DOC_PARTS =
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.204.0") &&
::Leptris::XML::Document.method_defined?(:remove_doctype) &&
::Leptris::XML::Document.method_defined?(:clear_declaration)
BLANK_TEXT_RE =

XML whitespace exactly — \v and \f are not XML space.

/\A[ \t\r\n]*\z/
DIAG_KINDS =

leptris#1200 recover diagnostics (binding FFI >= the 1.9.206 surface adoption; probed, not version-gated — lockstep trains have shipped gems without faces).

%i[
  invalid not_allowed_anywhere not_allowed_here
  not_allowed_yet incomplete missing_required_attr
  attr_not_allowed attr_value_invalid
  char_content_invalid recover
].freeze
VISIT_NODE_TYPES =

Walk-minted binding nodes carry the engine's kind int in replaces the class-case chain on the per-wrap hot path.

%i[element text comment cdata
processing_instruction doctype].freeze
NATIVE_XPATH_CACHE =

XPath prefers libleptris' native C engine (three orders of magnitude faster than the Ruby engine) for document-context queries, with a conservative gate: the Ruby engine handles element-context evaluation (native relative-context semantics are not contract-verified), variable references, and expressions whose results are attribute nodes (native returns those without name accessors). Both engines resolve expression prefixes against document-declared namespaces.

XPath::Cache.new(100)
NATIVE_GATE_CACHE =

The gate decision is expression-intrinsic; re-walking the cached AST three times per xpath call cost ~23% of repeated selective queries, so the boolean caches beside it.

XPath::Cache.new(100)
PARENT_AXIS_CACHE =

Root-context queries alone take the parent-axis guard; the AST walk is per-expression, so the verdict caches too.

XPath::Cache.new(100)

Constants included from Serialize

Serialize::ELEMENT_DEFAULT_KWARGS, Serialize::ELEMENT_EXPAND_KWARGS, Serialize::EMPTY_ELEMENT_RE, Serialize::EXPAND_EMPTY_NATIVE, Serialize::LITERAL_REGIONS, Serialize::RAW_AMP_GUARD_ACTIVE, Serialize::RAW_AMP_RE, Serialize::RAW_LT_GUARD_ACTIVE, Serialize::RAW_LT_RE, Serialize::RAW_LT_TRIGGER_RE, Serialize::TRAILING_NL_STRIP_ACTIVE

Constants included from DocumentParts

DocumentParts::DOCUMENT_ATTACHMENT_KEYS, DocumentParts::DOCUMENT_CHILD_PTRS

Constants included from Materialize

Materialize::EMPTY_FIELDS

Constants included from XmlUtils

XmlUtils::VALID_ELEMENT_NAMES

Class Attribute Summary collapse

Class Method Summary collapse

Methods included from Serialize

native_expand?, next_literal_region, normalize_markup, normalize_serialization, opener_at_offset, raw_serialize, serialize

Methods included from DocumentParts

add_document_child, assemble_document_children, default_declaration_xml, document_has_declaration?, fast_document_output, format_internal_subset, free_document, marker_text_for, markup_decl_end, mirror_declaration_native, native_doctype_xml, serialize_document

Methods included from Markers

entity_bearing?, split_entity_markers

Methods included from Materialize

bulk_materialize?, emit_element_fields, materialize_fields, walk_fields

Methods inherited from Base

bulk_materialize?, children_accepts_entity_flag?, create_cdata, create_comment, create_declaration, create_doctype, create_element, create_entity_reference, create_namespace, create_processing_instruction, create_text, decode_entities, entity_bearing?, free_document, in_scope_namespaces, materialize_fields, patch_node, patches_children?, preprocess_entities, restore_entities, sax_supported?, wrappers_recyclable?

Methods included from XmlUtils

#encode_entities, #normalize_xml_value, #validate_comment_content, #validate_declaration_encoding, #validate_declaration_standalone, #validate_declaration_version, #validate_element_name, #validate_entity_reference_name, #validate_pi_target, #validate_prefix, #validate_uri

Class Attribute Details

.serialize_generation ⇒ Object (readonly)

Returns the value of attribute serialize_generation.



458
459
460
# File 'lib/moxml/adapter/leptris.rb', line 458

def serialize_generation
  @serialize_generation
end

Class Method Details

.actual_native(child_native, parent_native) ⇒ Object



1599
1600
1601
1602
1603
1604
1605
1606
# File 'lib/moxml/adapter/leptris.rb', line 1599

def actual_native(child_native, parent_native)
  if NATIVE_DOC_PARTS && child_native.is_a?(CustomizedLeptris::Doctype) &&
      parent_native.is_a?(::Leptris::XML::Document)
    attached = attachments.get(parent_native, :doctype)
    return attached if attached.is_a?(::Leptris::XML::DocType)
  end
  child_native
end

.add_child(parent, child) ⇒ Object



1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
# File 'lib/moxml/adapter/leptris.rb', line 1608

def add_child(parent, child)
  if NATIVE_READ_LAYER && !(NATIVE_MUTATIONS_COHERENT &&
           parent.is_a?(::Leptris::XML::NativeNode) &&
           child.is_a?(::Leptris::XML::NativeNode))
    parent = to_binding(parent)
    child = to_binding(child)
  end
  case parent
  when ::Leptris::XML::Document then add_document_child(parent, child)
  else
    if child.is_a?(CustomizedLeptris::EntityReference)
      marker = parent.document.create_text_node("#{Entity::MARKER}#{child.name};")
      parent.add_child(marker)
      attachments.set(parent.document, :entity_markers, true)
      bump_serialize_generation
      return child
    end
    if NATIVE_MUTATIONS_COHERENT && parent.is_a?(NN) && child.is_a?(NN)
      doc = NN_DOCUMENT.bind_call(parent)
      child = doc.create_text_node(child) if child.is_a?(String)
      return NN_ADD_CHILD.bind_call(parent, child)
    end

    child = parent.document.create_text_node(child) if child.is_a?(String)
    parent.add_child(child)
  end
end

.add_next_sibling(node, new_node) ⇒ Object



1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
# File 'lib/moxml/adapter/leptris.rb', line 1661

def add_next_sibling(node, new_node)
  if NATIVE_READ_LAYER
    node = to_binding(node)
    new_node = to_binding(new_node)
  end
  return node.add_next_sibling(new_node) if node.is_a?(::Leptris::XML::Element)

  # Non-element receivers (Text/Comment/CDATA): the binding's
  # method is Element-only, but the C insert anchors on any
  # node — route through the FFI entry directly, keeping the
  # binding's namespace-lift adoption for element inserts
  # (issue #245).
  adopt_for_sibling_insert(new_node, node)
  ::Leptris::XML::FFI.check_status(
    ::Leptris::XML::FFI.leptris_element_insert_after(
      node.c_ptr, new_node.c_ptr
    ),
  )
  new_node
end

.add_previous_sibling(node, new_node) ⇒ Object



1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
# File 'lib/moxml/adapter/leptris.rb', line 1636

def add_previous_sibling(node, new_node)
  if NATIVE_READ_LAYER
    node = to_binding(node)
    new_node = to_binding(new_node)
  end
  # A PI inserted before the root lives at document level in
  # libleptris's model, not in the element tree.
  if new_node.is_a?(::Leptris::XML::ProcessingInstruction) &&
      node.document&.root == node
    node.document.add_pi(new_node.target, new_node.content.to_s)
    return new_node
  end
  return node.add_previous_sibling(new_node) if node.is_a?(::Leptris::XML::Element)

  # Non-element receivers: raw-FFI anchor (issue #245, same
  # shape as add_next_sibling).
  adopt_for_sibling_insert(new_node, node)
  ::Leptris::XML::FFI.check_status(
    ::Leptris::XML::FFI.leptris_element_insert_before(
      node.c_ptr, new_node.c_ptr
    ),
  )
  new_node
end

.adopt_for_sibling_insert(new_node, anchor) ⇒ Object

The binding's add_*_ sibling methods lift an adopted element's namespace declarations into the target scope; the raw-FFI path above must preserve that.



1685
1686
1687
1688
1689
1690
1691
# File 'lib/moxml/adapter/leptris.rb', line 1685

def adopt_for_sibling_insert(new_node, anchor)
  return unless new_node.is_a?(::Leptris::XML::Element)
  return if ::Leptris::XML::Element.skip_adoption_lift?(new_node)

  scope = anchor.parent.is_a?(::Leptris::XML::Element) ? anchor.parent.namespaces : {}
  ::Leptris::XML::Element.lift_namespaces_for_adoption(new_node, scope)
end

.ast_contains_type?(ast, type) ⇒ Boolean

Returns:

  • (Boolean)


2050
2051
2052
2053
2054
2055
2056
# File 'lib/moxml/adapter/leptris.rb', line 2050

def ast_contains_type?(ast, type)
  return true if ast.type == type

  ast.children.any? do |child|
    child.is_a?(XPath::AST::Node) && ast_contains_type?(child, type)
  end
end

.at_xpath(node, expression, namespaces = {}) ⇒ Object



1906
1907
1908
1909
1910
1911
1912
# File 'lib/moxml/adapter/leptris.rb', line 1906

def at_xpath(node, expression, namespaces = {})
  native = native_xpath(node, expression, namespaces, first_only: true)
  return native unless native.nil?

  result = engine_xpath(node, expression, namespaces)
  result.is_a?(Array) ? result.first : result
end

.attachments ⇒ Object



629
630
631
# File 'lib/moxml/adapter/leptris.rb', line 629

def attachments
  @attachments ||= Moxml::NativeAttachment.new
end

.attr_matches?(attr_name) ⇒ Boolean

Returns:

  • (Boolean)


986
987
988
# File 'lib/moxml/adapter/leptris.rb', line 986

def attr_matches?(attr_name)
  %w[version encoding standalone].include?(attr_name.to_s)
end

.attribute_element(attr) ⇒ Object



1547
1548
1549
# File 'lib/moxml/adapter/leptris.rb', line 1547

def attribute_element(attr)
  attr.element
end

.attribute_name(attr) ⇒ Object



1551
1552
1553
1554
1555
1556
1557
# File 'lib/moxml/adapter/leptris.rb', line 1551

def attribute_name(attr)
  # leptris Attr names are qualified; the wrapper composes the
  # prefix, so expose the local part.
  return attr.name.split(":", 2)[1] if attr.name.include?(":")

  attr.name.to_s.dup.force_encoding("UTF-8")
end

.attribute_pairs(element) ⇒ Object

One C crossing per element on bindings with the bulk face (leptris-ruby#278): interned names, fresh values, zero Attr objects. Face-probed, not version-gated — the floor (1.9.194.1) admits bindings without it, which take the base default over #attributes.



1540
1541
1542
1543
1544
1545
# File 'lib/moxml/adapter/leptris.rb', line 1540

def attribute_pairs(element)
  binding_element = to_binding(element)
  return super unless binding_element.respond_to?(:attribute_pairs)

  binding_element.attribute_pairs
end

.attributes(element) ⇒ Object



1530
1531
1532
1533
# File 'lib/moxml/adapter/leptris.rb', line 1530

def attributes(element)
  element = to_binding(element)
  element.attribute_nodes
end

.bare_attr_value(element, name) ⇒ Object

Fast bare-name read: the binding call plus marker restoration when the document carries them (entity-free documents — the common case — skip the scan; parentless iterparse elements are never bearing). Raw qualified-name read; the entity-restore decision is the wrapper's (Element#[] rides its generation memo — the document-plus-attachment probe here cost more than the read itself).



680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
# File 'lib/moxml/adapter/leptris.rb', line 680

def bare_attr_value(element, name)
  # Exact-name C read: bare names target no-namespace
  # attributes (the documented contract — a bare "y" never
  # resolves a "p:y" sibling).
  value = if NATIVE_READ_LAYER && element.is_a?(NN)
            NN_ATTRIBUTE.bind_call(element, name.to_s)
          else
            element[name.to_s]
          end
  if !NATIVE_STRINGS_UTF8 && NATIVE_READ_LAYER &&
      element.is_a?(::Leptris::XML::NativeNode) &&
      value.is_a?(String)
    return value.dup.force_encoding(Encoding::UTF_8)
  end

  value
end

.bare_get_qname_safe? ⇒ Boolean

Returns:

  • (Boolean)


641
642
643
# File 'lib/moxml/adapter/leptris.rb', line 641

def bare_get_qname_safe?
  true
end

.bare_set_qname_safe? ⇒ Boolean

Returns:

  • (Boolean)


637
638
639
# File 'lib/moxml/adapter/leptris.rb', line 637

def bare_set_qname_safe?
  true
end

.bulk_binding_children(node) ⇒ Object

The binding-node twin of bulk_native_children (identity via the binding's wrap cache).



1375
1376
1377
1378
1379
1380
1381
1382
1383
# File 'lib/moxml/adapter/leptris.rb', line 1375

def bulk_binding_children(node)
  copied, pointers = bulk_child_buffers(node.c_ptr)
  return node.children.to_a if copied.zero?

  doc = node.document
  Array.new(copied) do |i|
    ::Leptris::XML::Node.wrap(pointers.get_pointer(i * 8), doc)
  end
end

.bulk_child_buffers(ptr) ⇒ Object

Shared count-then-copy fetch over moxml's grow-to-fit thread-local scratch — the layer above the binding's (buggy on 1.9.163.x) own buffers. Returns [copied, pointers].



1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
# File 'lib/moxml/adapter/leptris.rb', line 1416

def bulk_child_buffers(ptr)
  total = ::Leptris::XML::FFI.leptris_node_children_ex(ptr, nil, nil, 0)
  return [0, nil] if total.zero?

  scratch = (Thread.current[:moxml_leptris_children] ||= {})
  pointers = scratch[:pointers]
  if pointers.nil? || pointers.size / 8 < total
    pointers&.free
    pointers = scratch[:pointers] =
      ::FFI::MemoryPointer.new(:pointer, total)
  end
  kinds = scratch[:kinds]
  if kinds.nil? || kinds.size / 4 < total
    kinds&.free
    kinds = scratch[:kinds] = ::FFI::MemoryPointer.new(:int, total)
  end
  copied = ::Leptris::XML::FFI.leptris_node_children_ex(
    ptr, pointers, kinds, total
  )
  [copied, pointers]
end

.bulk_native_children(node) ⇒ Object

Bulk child fetch with moxml's own grow-to-fit thread-local scratch: count first (exact), size the buffers to it, wrap each pointer as a native-layer node. Correct on every binding version — the binding's own scratch truncates at 512 on 1.9.163.x (leptris-ruby#202).



1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
# File 'lib/moxml/adapter/leptris.rb', line 1390

def bulk_native_children(node)
  copied, pointers = bulk_child_buffers(::FFI::Pointer.new(node.address))
  return [] if copied.zero?

  doc = doc_for(node)
  if doc.nil?
    # Unregistered native (adapter-level use outside #root):
    # the 163.2 binding's bulk needs a document, so walk
    # siblings instead of crashing through a doc-less wrap.
    return sibling_walk_children(node)
  end

  # Share the layer's per-document identity cache (keyed by
  # node address): NativeNode.from alone mints fresh objects
  # per call on 1.9.163.x, which would fork moxml wrappers.
  cache = doc.native_cache
  Array.new(copied) do |i|
    child_ptr = pointers.get_pointer(i * 8)
    cache[child_ptr.address] ||=
      ::Leptris::XML::NativeNode.from(doc, child_ptr)
  end
end

.bump_serialize_generation ⇒ Object



462
463
464
# File 'lib/moxml/adapter/leptris.rb', line 462

def self.bump_serialize_generation
  @serialize_generation += 1
end

.canonical_native(doc, node) ⇒ Object

Native twin for a binding node through the document's address-keyed native cache — minting on miss so every accessor converges on one native per node (issue #219). Binding natives (Attribute/Attr, synthetic wrappers) and docless nodes pass through unchanged.



1365
1366
1367
1368
1369
1370
1371
# File 'lib/moxml/adapter/leptris.rb', line 1365

def canonical_native(doc, node)
  return node unless node.is_a?(::Leptris::XML::Element)

  cache = doc.native_cache
  cache[node.c_ptr.address] ||=
    ::Leptris::XML::NativeNode.from(doc, node.c_ptr)
end

.cdata_content(node) ⇒ Object



1816
1817
1818
1819
1820
# File 'lib/moxml/adapter/leptris.rb', line 1816

def cdata_content(node)
  return NN_CONTENT.bind_call(node) if NATIVE_READ_LAYER && node.is_a?(NN)

  node.content
end

.children(node, entity_bearing: false) ⇒ Object



1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
# File 'lib/moxml/adapter/leptris.rb', line 1239

def children(node, entity_bearing: false)
  # NATIVE_READ_LAYER gates the whole keep-path: it consults
  # NN_DOCUMENT (defined only with the native layer), and
  # native-less installs answer through the binding case
  # branch below, whose children already carry first-class
  # EntityReference nodes (issue #240).
  if NATIVE_READ_LAYER && NATIVE_ENTITY_REFS &&
      native_keep_entity_refs?(node)
    return to_binding(node).children.to_a
  end

  if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
    # Bulk C children through moxml's own scratch (see
    # NATIVE_READ_LAYER note above). Entity-marker documents
    # bridge each child for the marker split — the
    # TextSegment reconstruction reads binding text nodes;
    # the wrapper's memo supplies the flag, so deriving it
    # here costs no C parent climb per call.
    natives = if NATIVE_BULK_FIXED
                NN_CHILDREN.bind_call(node).to_a
              else
                bulk_native_children(node)
              end
    return natives unless entity_bearing

    # Entity-marker documents: bridge for the marker split
    # (TextSegment reconstruction reads binding text nodes).
    doc = doc_for(node)
    bound_children = natives.map { |child| to_binding(child) }
    if doc && attachments.get(doc, :entity_markers) == false
      return bound_children
    end

    return split_entity_markers(bound_children, to_binding(node))
  end
  # Frequency-ordered: elements dominate every walk and paid
  # ten failed compares to reach the else arm.
  case node
  when ::Leptris::XML::Element
    natives = node.children.to_a
    if NATIVE_READ_LAYER && natives.size == 512
      # Binding scratch truncation (leptris-ruby#202): a
      # full batch is ambiguous — refetch through moxml's
      # own buffers to be sure.
      natives = bulk_binding_children(node)
    end
    # Parse records whether the preprocessed source held any
    # entity markers, and the ER builder path flips the flag
    # when it mints one. A false flag lets traversal skip the
    # marker split — including the per-text content fetch that
    # dominates cold children cost. Cross-document moves of
    # marker-bearing text into an entity-free document degrade
    # to literal text.
    return natives if node.document.nil? ||
      attachments.get(node.document, :entity_markers) == false

    split_entity_markers(natives, node)
  when ::Leptris::XML::Document
    assemble_document_children(node)
  when CustomizedLeptris::Declaration, CustomizedLeptris::Doctype,
       CustomizedLeptris::EntityReference, CustomizedLeptris::TextSegment,
       CustomizedLeptris::DocumentPI,
       # Terminal node kinds pay an FFI round trip for an empty
       # list; unfiltered recursions visit every text node.
       ::Leptris::XML::Text, ::Leptris::XML::Comment,
       ::Leptris::XML::CDATA, ::Leptris::XML::ProcessingInstruction,
       ::Leptris::XML::Attr
    []
  else
    node.children.to_a
  end
end

.comment_content(node) ⇒ Object



1827
1828
1829
1830
1831
# File 'lib/moxml/adapter/leptris.rb', line 1827

def comment_content(node)
  return NN_CONTENT.bind_call(node) if NATIVE_READ_LAYER && node.is_a?(NN)

  node.content
end

.contains_parent_axis?(ast) ⇒ Boolean

Returns:

  • (Boolean)


2019
2020
2021
2022
2023
2024
2025
2026
# File 'lib/moxml/adapter/leptris.rb', line 2019

def contains_parent_axis?(ast)
  return true if ast.type == :parent
  return true if ast.type == :axis && ast.children.first == "parent"

  ast.children.any? do |child|
    child.is_a?(XPath::AST::Node) && contains_parent_axis?(child)
  end
end

.create_document(_native_doc = nil) ⇒ Object



917
918
919
# File 'lib/moxml/adapter/leptris.rb', line 917

def create_document(_native_doc = nil)
  ::Leptris::XML::Document.create
end

.create_element_with_attrs(parent, name, attrs) ⇒ Object

Single-crossing construction (#312/#1344 consumer face): create + attach under parent + all attributes in ONE Ruby->C call (create_child is create+attach; the attr loop is C->C dlsym). Requires parent binding + the face; callers fall back to create_element + set_attribute + add_child when absent.



1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
# File 'lib/moxml/adapter/leptris.rb', line 1510

def create_element_with_attrs(parent, name, attrs)
  binding_parent = to_binding(parent)
  doc = binding_parent.document or return nil
  return nil unless ::Leptris::XML::Native.respond_to?(:create_element_with_attrs)

  flat = attrs.flatten
  addr = ::Leptris::XML::Native.create_element_with_attrs(
    doc.c_address, binding_parent.c_address, name.to_s, flat
  )
  return nil if addr.nil? || addr.zero?

  # The binding's own wrap produces the proper binding
  # Element (kind class, @c_address, @document); canonical
  # convergence (#219) hands every accessor — children
  # included — the SAME native, so the wrapper identity
  # holds across later traversals.
  canonical_native(doc,
                   ::Leptris::XML::Node.wrap(::FFI::Pointer.new(addr), doc))
end

.create_native_cdata(content, owner_doc = nil) ⇒ Object



942
943
944
# File 'lib/moxml/adapter/leptris.rb', line 942

def create_native_cdata(content, owner_doc = nil)
  (owner_doc || create_document).create_cdata(content)
end

.create_native_comment(content, owner_doc = nil) ⇒ Object



946
947
948
# File 'lib/moxml/adapter/leptris.rb', line 946

def create_native_comment(content, owner_doc = nil)
  (owner_doc || create_document).create_comment(content)
end

.create_native_declaration(version, encoding, standalone) ⇒ Object



959
960
961
# File 'lib/moxml/adapter/leptris.rb', line 959

def create_native_declaration(version, encoding, standalone)
  CustomizedLeptris::Declaration.new(version, encoding, standalone)
end

.create_native_doctype(name, external_id, system_id) ⇒ Object



955
956
957
# File 'lib/moxml/adapter/leptris.rb', line 955

def create_native_doctype(name, external_id, system_id)
  CustomizedLeptris::Doctype.new(name, external_id, system_id)
end

.create_native_element(name, owner_doc = nil) ⇒ Object

Native builder factories: one C call and a TypedData wrap. Adopted from 1.9.163.5, where native mutations became version-coherent (leptris-ruby#204/#208) — the earlier rejection (stale binding memos + per-attach bridge cost) no longer applies: add_child carries a native fast path.



926
927
928
929
930
931
932
# File 'lib/moxml/adapter/leptris.rb', line 926

def create_native_element(name, owner_doc = nil)
  if NATIVE_MUTATIONS_COHERENT && owner_doc
    return owner_doc.native_create_element(name.to_s)
  end

  (owner_doc || create_document).create_element(name.to_s)
end

.create_native_entity_reference(name, owner_doc = nil) ⇒ Object



963
964
965
966
967
968
969
# File 'lib/moxml/adapter/leptris.rb', line 963

def create_native_entity_reference(name, owner_doc = nil)
  if NATIVE_ENTITY_REFS && owner_doc
    return owner_doc.create_entity_reference(name.to_s)
  end

  CustomizedLeptris::EntityReference.new(name)
end

.create_native_namespace(element, prefix, uri) ⇒ Object



1006
1007
1008
1009
# File 'lib/moxml/adapter/leptris.rb', line 1006

def create_native_namespace(element, prefix, uri)
  element = to_binding(element) if NATIVE_READ_LAYER
  element.add_namespace_definition(prefix, uri)
end

.create_native_processing_instruction(target, content) ⇒ Object



950
951
952
953
# File 'lib/moxml/adapter/leptris.rb', line 950

def create_native_processing_instruction(target, content)
  doc = create_document
  doc.create_processing_instruction(target.to_s, content.to_s)
end

.create_native_text(content, owner_doc = nil) ⇒ Object



934
935
936
937
938
939
940
# File 'lib/moxml/adapter/leptris.rb', line 934

def create_native_text(content, owner_doc = nil)
  if NATIVE_MUTATIONS_COHERENT && owner_doc
    return owner_doc.native_create_text(content)
  end

  (owner_doc || create_document).create_text_node(content)
end

.declaration_attribute(declaration, attr_name) ⇒ Object



978
979
980
# File 'lib/moxml/adapter/leptris.rb', line 978

def declaration_attribute(declaration, attr_name)
  declaration.public_send(attr_name) if attr_matches?(attr_name)
end

.digest(node, drop_ws_text: false) ⇒ Object

Subtree digest over the binding node (issue #173). Only binding Nodes carry a C node handle: the synthetic wrappers (declarations, doctypes, entity markers) and the lightweight Attr triples answer nil.



1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
# File 'lib/moxml/adapter/leptris.rb', line 1187

def digest(node, drop_ws_text: false)
  return nil unless DIGEST_SUPPORTED
  if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
    return to_binding(node).digest(drop_ws: drop_ws_text)
  end
  return nil unless node.is_a?(::Leptris::XML::Node) &&
    !node.is_a?(::Leptris::XML::ResultAttr)

  node.digest(drop_ws: drop_ws_text)
end

.doc_for(node) ⇒ Object

Binding document for a NativeNode subtree: climb parents to the root (C-bound reads) and look the doc up in the root registry. Detached subtrees answer nil.



405
406
407
408
409
410
411
412
413
# File 'lib/moxml/adapter/leptris.rb', line 405

def doc_for(node)
  # The 1.9.163.5 native layer exposes the owning document
  # directly; the root climb serves older bindings.
  return NN_DOCUMENT.bind_call(node) if NATIVE_MUTATIONS_COHERENT

  current = node
  current = current.parent while current&.parent
  @native_doc_roots[current]
end

.doctype_external_id(node) ⇒ Object



1873
1874
1875
# File 'lib/moxml/adapter/leptris.rb', line 1873

def doctype_external_id(node)
  node.is_a?(CustomizedLeptris::Doctype) ? node.external_id : node.public_id
end

.doctype_name(node) ⇒ Object



1869
1870
1871
# File 'lib/moxml/adapter/leptris.rb', line 1869

def doctype_name(node)
  node.is_a?(CustomizedLeptris::Doctype) ? node.name : node.root_name
end

.doctype_system_id(node) ⇒ Object



1877
1878
1879
# File 'lib/moxml/adapter/leptris.rb', line 1877

def doctype_system_id(node)
  node.system_id
end

.document(node) ⇒ Object



1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
# File 'lib/moxml/adapter/leptris.rb', line 1460

def document(node)
  return doc_for(node) if NATIVE_READ_LAYER &&
    node.is_a?(::Leptris::XML::NativeNode)

  case node
  when ::Leptris::XML::Document then node
  when CustomizedLeptris::Declaration, CustomizedLeptris::Doctype then node.parent_doc
  else node.document
  end
end

.duplicate_node(node) ⇒ Object



1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
# File 'lib/moxml/adapter/leptris.rb', line 1223

def duplicate_node(node)
  node = to_binding(node) if NATIVE_READ_LAYER
  case node
  when CustomizedLeptris::Declaration
    CustomizedLeptris::Declaration.new(node.version, node.encoding, node.standalone)
  when CustomizedLeptris::Doctype
    CustomizedLeptris::Doctype.new(node.name, node.external_id, node.system_id)
  when CustomizedLeptris::EntityReference
    CustomizedLeptris::EntityReference.new(node.name)
  when CustomizedLeptris::DocumentPI
    CustomizedLeptris::DocumentPI.new(node.target, node.data, node.parent_doc)
  else
    node.dup
  end
end

.engine_xpath(node, expression, namespaces = {}) ⇒ Object



2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
# File 'lib/moxml/adapter/leptris.rb', line 2101

def engine_xpath(node, expression, namespaces = {})
  unless node.is_a?(Moxml::Node)
    node = Moxml::Node.wrap(node, Context.new(:leptris))
  end

  ast = XPath::Parser.parse(expression)
  proc = XPath::Compiler.compile_with_cache(ast, namespaces: namespaces)
  result = proc.call(node)

  case result
  when Array, NodeSet
    nodes = result.is_a?(NodeSet) ? result.to_a : result
    seen = {}.compare_by_identity
    nodes.map { |n| n.is_a?(Moxml::Node) ? n.native : n }
      .select do |native|
        if seen.key?(native)
          false
        else
          seen[native] = true
          true
        end
      end
  else
    result
  end
end

.entity_reference_name(node) ⇒ Object



971
972
973
974
975
976
# File 'lib/moxml/adapter/leptris.rb', line 971

def entity_reference_name(node)
  return node.name if NATIVE_ENTITY_REFS &&
    node.is_a?(::Leptris::XML::EntityReference)

  node.name if node.is_a?(CustomizedLeptris::EntityReference)
end

.expanded_attr_reads? ⇒ Boolean

Expanded-name (URI + local) attribute VALUE lookup on the engine — the same match rule as the resolver (xmlns declarations invisible, no-namespace never matches a prefixed name), without materializing the attribute list.

Returns:

  • (Boolean)


649
650
651
# File 'lib/moxml/adapter/leptris.rb', line 649

def expanded_attr_reads?
  true
end

.expanded_attr_value(element, uri, local) ⇒ Object



662
663
664
665
666
667
668
669
670
# File 'lib/moxml/adapter/leptris.rb', line 662

def expanded_attr_value(element, uri, local)
  # Bridge first: identity-mode wrappers hand a NativeNode
  # (no attribute_ns); binding elements pass through as-is.
  # The class-receiver is_a? this method replaced was always
  # false — element.adapter IS the adapter class — so the
  # branch sat dead since identity mode and the binding-only
  # assumption went unnoticed (issue #242).
  to_binding(element).attribute_ns(uri, local)
end

.expression_uses_parent_axis?(expression) ⇒ Boolean

Returns:

  • (Boolean)


2012
2013
2014
2015
2016
2017
# File 'lib/moxml/adapter/leptris.rb', line 2012

def expression_uses_parent_axis?(expression)
  ast = XPath::Parser.parse_with_cache(expression)
  contains_parent_axis?(ast)
rescue XPath::SyntaxError
  true
end

.get_attribute(element, name) ⇒ Object



1579
1580
1581
1582
# File 'lib/moxml/adapter/leptris.rb', line 1579

def get_attribute(element, name)
  element = to_binding(element)
  element.attribute_nodes.find { |attr| attr.name == name.to_s }
end

.get_attribute_value(element, name) ⇒ Object



1584
1585
1586
1587
# File 'lib/moxml/adapter/leptris.rb', line 1584

def get_attribute_value(element, name)
  element = to_binding(element)
  element[name.to_s]
end

.has_declaration?(native_doc, _wrapper) ⇒ Boolean

Returns:

  • (Boolean)


991
992
993
994
995
996
# File 'lib/moxml/adapter/leptris.rb', line 991

def has_declaration?(native_doc, _wrapper)
  return true if attachments.get(native_doc, :declaration)

  attachments.key?(native_doc, :had_source_declaration) &&
    attachments.get(native_doc, :had_source_declaration)
end

.inner_text(node) ⇒ Object



1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
# File 'lib/moxml/adapter/leptris.rb', line 1791

def inner_text(node)
  node = to_binding(node) if NATIVE_READ_LAYER
  # moxml semantic: direct text children only — no descendant
  # text (that is #text), no comments. Entity references
  # contribute their serialized form.
  children(node).filter_map do |child|
    case child
    when CustomizedLeptris::EntityReference then "&#{child.name};"
    when ::Leptris::XML::CDATA then child.content
    when ::Leptris::XML::Text, CustomizedLeptris::TextSegment
      child.content.to_s.dup.force_encoding("UTF-8")
    end
  end.join
end

.iterparse(xml, mode = :top_level, _context = nil, &block) ⇒ Object

Raises:

  • (ArgumentError)


795
796
797
798
799
800
801
802
# File 'lib/moxml/adapter/leptris.rb', line 795

def iterparse(xml, mode = :top_level, _context = nil, &block)
  raise ArgumentError, "iterparse requires a block" unless block

  ctx = _context || Context.new(:leptris)
  ::Leptris::XML::Iterparse.parse(xml, mode: mode) do |element|
    yield(Node.wrap(element, ctx))
  end
end

.iterparse_file(path, mode = :top_level, _context = nil, &block) ⇒ Object

Raises:

  • (ArgumentError)


804
805
806
807
808
809
810
811
# File 'lib/moxml/adapter/leptris.rb', line 804

def iterparse_file(path, mode = :top_level, _context = nil, &block)
  raise ArgumentError, "iterparse_file requires a block" unless block

  ctx = _context || Context.new(:leptris)
  ::Leptris::XML::Iterparse.parse_file(path, mode: mode) do |element|
    yield(Node.wrap(element, ctx))
  end
end

.line_number(node) ⇒ Object



1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
# File 'lib/moxml/adapter/leptris.rb', line 1492

def line_number(node)
  if NATIVE_MUTATIONS_COHERENT &&
      node.is_a?(::Leptris::XML::NativeNode)
    line = node.line
    return line.nil? || line.zero? ? nil : line
  end
  return nil unless node.is_a?(::Leptris::XML::Node)

  line = node.line
  line.zero? ? nil : line
end

.namespace(node) ⇒ Object



1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
# File 'lib/moxml/adapter/leptris.rb', line 1082

def namespace(node)
  node = to_binding(node) if NATIVE_READ_LAYER

  # The binding resolves Attr#namespace to the declaration but
  # without the prefix; when the qualified name carries one,
  # prefer the in-scope declaration that binds it so wrappers
  # keep prefix and uri.
  if node.is_a?(::Leptris::XML::Attr) &&
      (prefix = node.prefix || prefix_part(node.name))
    resolved = resolve_prefix_ns(node.element, prefix)
    return resolved if resolved
  end

  ns = node.namespace
  return ns unless ns.nil?

  # Set-side namespaces (created attributes, renamed elements)
  # carry a prefix the native resolver did not bind; resolve
  # through the scope chain so wrapper-level access stays
  # XML-correct.
  owner = node.is_a?(::Leptris::XML::Attr) ? node.element : node
  prefix = if node.is_a?(::Leptris::XML::Attr)
             node.prefix || prefix_part(node.name)
           else
             prefix_part(node.name)
           end
  return nil if prefix.nil?

  resolve_prefix_ns(owner, prefix)
end

.namespace_definitions(element) ⇒ Object



1864
1865
1866
1867
# File 'lib/moxml/adapter/leptris.rb', line 1864

def namespace_definitions(element)
  element = to_binding(element) if NATIVE_READ_LAYER
  element.namespace_definitions
end

.namespace_prefix(namespace) ⇒ Object



1849
1850
1851
1852
1853
1854
1855
1856
# File 'lib/moxml/adapter/leptris.rb', line 1849

def namespace_prefix(namespace)
  # Attr#namespace is the bare URI string in leptris 1.9+ (the
  # C model: a namespace handle IS the URI); attributes carry
  # their prefix on the Attr itself.
  return nil if namespace.is_a?(String)

  namespace.prefix
end

.namespace_uri(namespace) ⇒ Object



1858
1859
1860
1861
1862
# File 'lib/moxml/adapter/leptris.rb', line 1858

def namespace_uri(namespace)
  return namespace if namespace.is_a?(String)

  namespace.href
end

.native_c14n_byte_safe? ⇒ Boolean

Native C14N delegation probe: the engine's C14N must byte-match the Ruby reference (ported from canon) on a namespace-sorting + attributes + comments shape before Moxml::C14n hands the default path to it. The engine currently emits namespace declarations in document order instead of lexicographic (filed leptris/leptris#881); the probe auto-adopts the native path once a fixed build lands — no moxml release needed. Lazy, not load-time: the probe exercises the wrapper layer (Document/serialize/C14n), which is circular while THIS adapter file is still loading — the load-time form rescued to false on every build and masked a landed engine fix.

Returns:

  • (Boolean)


434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
# File 'lib/moxml/adapter/leptris.rb', line 434

def self.native_c14n_byte_safe?
  return @native_c14n_byte_safe unless @native_c14n_byte_safe.nil?

  @native_c14n_byte_safe = begin
    probe_xml = %(<?xml version="1.0"?><doc xmlns:p="urn:p" xmlns="urn:d" b="2" a="1"><e p:x="v" z="w">t &amp; u</e><!-- c --></doc>)
    native_doc = ::Leptris::XML::Document.parse(probe_xml)
    native = native_doc.root.canonicalize(
      ::Leptris::XML::FFI::C14N_1_0, nil,
      mode: ::Leptris::XML::FFI::C14N_MODE_CANONICAL
    )
    wrapper = Moxml::Wrappers::Document.new(native_doc, Moxml::Context.new(:leptris))
    reference = Moxml::C14n::Inclusive10.new.canonicalize(wrapper.root)
    native == reference
  rescue StandardError
    false
  end
end

.native_context_node?(node) ⇒ Boolean

Returns:

  • (Boolean)


2003
2004
2005
2006
2007
2008
2009
2010
# File 'lib/moxml/adapter/leptris.rb', line 2003

def native_context_node?(node)
  # Parentless elements (iterparse yields) have no document
  # handle for the compiled eval — the Ruby engine owns them.
  return false if node.is_a?(::Leptris::XML::Element) && node.document.nil?

  node.is_a?(::Leptris::XML::Document) ||
    node.is_a?(::Leptris::XML::Element)
end

.native_expression?(expression) ⇒ Boolean

Document-context queries without variable references, attribute-node results, or the nokogiri-compat xmlns: reserved prefix convention.

Returns:

  • (Boolean)


2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
# File 'lib/moxml/adapter/leptris.rb', line 2031

def native_expression?(expression)
  NATIVE_GATE_CACHE.get_or_set(expression) do
    ast = XPath::Parser.parse_with_cache(expression)
    next false if ast_contains_type?(ast, :variable)
    next false if uses_xmlns_prefix?(ast)
    next false if !PREFIXED_ATTR_PREDICATES_NATIVE && prefixed_attribute_test?(ast)

    # Attribute-node results are native since 1.9.105
    # (leptris-ruby#153: ResultAttr wrappers with name/value);
    # older bindings returned generic Node wrappers whose
    # #name raised, so the Ruby engine owned them.
    next true if selects_attribute_results?(ast) && ATTR_RESULT_NATIVE

    !selects_attribute_results?(ast)
  end
rescue XPath::SyntaxError
  false
end

.native_identity_stable? ⇒ Boolean

Returns:

  • (Boolean)


698
699
700
# File 'lib/moxml/adapter/leptris.rb', line 698

def native_identity_stable?
  true
end

.native_inclusive10(native) ⇒ Object



653
654
655
656
657
658
659
660
# File 'lib/moxml/adapter/leptris.rb', line 653

def native_inclusive10(native)
  return nil unless native_c14n_byte_safe?

  to_binding(native).canonicalize(
    ::Leptris::XML::FFI::C14N_1_0, nil,
    mode: ::Leptris::XML::FFI::C14N_MODE_CANONICAL
  )
end

.native_keep_entity_refs?(node) ⇒ Boolean

Returns:

  • (Boolean)


1312
1313
1314
1315
1316
1317
1318
# File 'lib/moxml/adapter/leptris.rb', line 1312

def native_keep_entity_refs?(node)
  return false unless NATIVE_READ_LAYER &&
    node.is_a?(::Leptris::XML::NativeNode)

  doc = NN_DOCUMENT.bind_call(node)
  attachments.get(doc, :keep_entity_refs) == true
end

.native_xpath(node, expression, namespaces, first_only: false) ⇒ Array, ...

Returns native results, or nil when the query must run on the Ruby engine.

Returns:

  • (Array, Object, nil) —

    native results, or nil when the query must run on the Ruby engine



1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
# File 'lib/moxml/adapter/leptris.rb', line 1916

def native_xpath(node, expression, namespaces, first_only: false)
  # Canonical natives (the #219 unification) are bare TypedData
  # wrappers — no Searchable methods, no document handle for
  # the gates below. The memoized bridge recovers the binding
  # identity; without it every element-context query fell to
  # the Ruby engine (a silent native-path loss since #219).
  if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
    node = to_binding(node)
  end
  return nil unless native_context_node?(node)
  return nil unless native_expression?(expression)
  # The binding reports the root element parentless while moxml
  # roots it at the document, so parent-axis queries from the
  # root element keep the Ruby engine.
  if node.is_a?(::Leptris::XML::Element) &&
      node.document&.root.equal?(node) &&
      PARENT_AXIS_CACHE.get_or_set(expression) do
        expression_uses_parent_axis?(expression)
      end
    return nil
  end

  compiled = NATIVE_XPATH_CACHE.get_or_set(expression) do
    ::Leptris::XML::XPath.compile(expression)
  end
  document = node.is_a?(::Leptris::XML::Document) ? node : node.document

  if namespaces && !namespaces.empty?
    # Namespace-bound evaluation has no raw-pointer entry —
    # Searchable resolves the ns set for both result shapes.
    result = if first_only
               node.at_xpath(expression, namespaces)
             else
               compiled.eval(node, namespaces)
             end
    return case result
           when ::Leptris::XML::NodeSet
             first_only ? result.first : result.extend(Moxml::LazyNodeSet)
           else
             result
           end
  end

  unless NATIVE_XPATH_SEAMS
    result = compiled.eval(node)
    return case result
           when ::Leptris::XML::NodeSet
             first_only ? result.first : result.extend(Moxml::LazyNodeSet)
           else
             result
           end
  end

  # eval_ptrs against raw pointers — the per-call
  # EvaluationContext that #eval builds costs ~2us of Ruby
  # on every query (leptris-ruby TODO.perf/15).
  doc_ptr = node.is_a?(::Leptris::XML::Document) ? node.c_ptr : document.c_ptr
  context_ptr = node.is_a?(::Leptris::XML::Document) ? nil : node.c_ptr
  result_ptr = compiled.eval_ptrs(doc_ptr, context_ptr)
  return nil if result_ptr.null?

  if first_only
    # The single-result seam: nodeset entry 0 materializes
    # and frees in one C dispatch — no NodeSet container, no
    # AutoPointer (leptris-ruby TODO.perf/16). The module
    # object back means a scalar, whose wrap (and free)
    # stays with wrap_xpath_first_result.
    return ::Leptris::XML::Searchable.wrap_xpath_first_result(
      document, result_ptr
    )
  end

  # The binding's #to_a mints one Ruby wrapper per result
  # node; hand the native set through so .size/.first stay
  # native-side (LazyNodeSet holds it unmaterialized).
  # Scalars pass through unwrapped-and-unextended.
  result = ::Leptris::XML::Searchable.wrap_xpath_result(
    document, result_ptr
  )
  result.is_a?(::Leptris::XML::NodeSet) ? result.extend(Moxml::LazyNodeSet) : result
rescue ::Leptris::XML::XPathError
  # Not supported by the native engine — the Ruby engine is a
  # full XPath 1.0 implementation, including Moxml's syntax
  # errors for invalid expressions.
  nil
end

.next_sibling(node) ⇒ Object



1444
1445
1446
1447
1448
1449
# File 'lib/moxml/adapter/leptris.rb', line 1444

def next_sibling(node)
  return NN_NEXT_SIBLING.bind_call(node) if NATIVE_READ_LAYER &&
    node.is_a?(::Leptris::XML::NativeNode)

  node.next_sibling if node.is_a?(::Leptris::XML::Node)
end

.node_name(node) ⇒ Object



1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
# File 'lib/moxml/adapter/leptris.rb', line 1198

def node_name(node)
  return node.root_name if node.is_a?(::Leptris::XML::DocType)
  return node.target if node.is_a?(CustomizedLeptris::DocumentPI)

  if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
    # PIs expose no name through the native layer.
    return to_binding(node).target.to_s if NN_NODE_TYPE.bind_call(node) == :pi

    name = NN_NAME.bind_call(node)
    return name if NATIVE_STRINGS_UTF8

    name.dup.force_encoding(Encoding::UTF_8)
  end

  node.name.to_s.dup.force_encoding("UTF-8")
end

.node_type(node) ⇒ Object



1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
# File 'lib/moxml/adapter/leptris.rb', line 1143

def node_type(node)
  if node.is_a?(::Leptris::XML::Node) &&
      node.instance_variable_defined?(:@node_type)
    kind = node.instance_variable_get(:@node_type)
    if kind.is_a?(Integer)
      mapped = VISIT_NODE_TYPES[kind]
      return mapped if mapped
    end
  end

  if NATIVE_READ_LAYER &&
      node.is_a?(::Leptris::XML::NativeNode)
    type = NN_NODE_TYPE.bind_call(node)
    # The native layer spells PIs :pi; the wrapper contract
    # (node_type_map) says :processing_instruction.
    return :processing_instruction if type == :pi

    return type
  end

  # Frequency-ordered: elements and text dominate every real
  # document, and Node.wrap dispatches here once per cold
  # wrap. CDATA must precede Text (CDATA < Text in the
  # binding).
  case node
  when ::Leptris::XML::Element then :element
  when ::Leptris::XML::CDATA then :cdata
  when ::Leptris::XML::Text, CustomizedLeptris::TextSegment then :text
  when ::Leptris::XML::Attr, ::Leptris::XML::ResultAttr then :attribute
  when ::Leptris::XML::Comment then :comment
  when ::Leptris::XML::ProcessingInstruction, CustomizedLeptris::DocumentPI then :processing_instruction
  when ::Leptris::XML::Document then :document
  when ::Leptris::XML::DocType, CustomizedLeptris::Doctype then :doctype
  when CustomizedLeptris::Declaration then :declaration
  when CustomizedLeptris::EntityReference then :entity_reference
  when ::Leptris::XML::EntityReference then :entity_reference if NATIVE_ENTITY_REFS
  else :unknown
  end
end

.parent(node) ⇒ Object



1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
# File 'lib/moxml/adapter/leptris.rb', line 1320

def parent(node)
  if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
    parent = NN_PARENT.bind_call(node)
    return parent if parent

    doc = doc_for(node)
    return doc if doc&.root && doc.root.c_ptr.address == node.address

    return nil
  end

  # Frequency-ordered: elements dominate navigation and paid
  # three failed compares to reach the else arm.
  case node
  when ::Leptris::XML::Element then root_parent(node)
  when ::Leptris::XML::Document then nil
  when CustomizedLeptris::Declaration, CustomizedLeptris::Doctype,
       CustomizedLeptris::DocumentPI then node.parent_doc
  when CustomizedLeptris::TextSegment, CustomizedLeptris::EntityReference then node.parent
  # Same body as the Element arm by design (frequency
  # ordering); the generic kinds are cold.
  else root_parent(node) # rubocop:disable Lint/DuplicateBranch
  end
end

.parse(xml, options = {}, _context = nil) ⇒ Object



702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
# File 'lib/moxml/adapter/leptris.rb', line 702

def parse(xml, options = {}, _context = nil)
  xml_string = xml.is_a?(IO) || xml.is_a?(StringIO) ? xml.read : xml.to_s
  # The marker flag rides preprocess's own `&` scan — no
  # second full-buffer probe (issue #132 parse-side note).
  entity_mode_keep =
    NATIVE_ENTITY_REFS &&
    (_context&.config&.entity_mode == :keep || options[:keep_entity_refs] == true)
  processed, entity_markers =
    if entity_mode_keep
      [xml_string, false]
    else
      Entity.preprocess_with_marker_flag(xml_string)
    end

  # readonly: true (issue #133): the binding memoizes reads and
  # refuses mutations — the parse-and-read lifecycle for
  # multi-pass consumers (comparison, diff, signature).
  # dtdattr: true opts into DTD ATTLIST default materialization
  # (off by default since libleptris 1.9.8, matching libxml2).
  native_doc = begin
    ::Leptris::XML::Document.parse(
      processed,
      readonly: options[:readonly] == true,
      options: parse_flags(options, context: _context),
    )
  rescue ::Leptris::XML::ParseError => e
    # libleptris has no recovery mode that survives unclosed
    # tags; non-strict callers get an empty document, matching
    # the Libxml adapter's non-strict behavior. The fatal
    # error rides the document as parse diagnostics (issue
    # #147) — otherwise nothing says why it came back empty.
    raise Moxml::ParseError.new(e.message) if options[:strict]

    recover_errors = [e.message]
    create_document
  end
  ctx = _context || Context.new(:leptris)
  doc = Wrappers::Document.new(native_doc, ctx)

  if options[:noblanks] && !ENGINE_NOBLANKS_SAFE
    # The engine's DROP_WS_TEXT trims boundary whitespace of
    # NON-blank text nodes at parse (leptris/leptris#677), so
    # the flag is not forwarded — blanks drop here instead.
    if options[:readonly] == true
      raise ArgumentError,
            "noblanks: true requires a mutable document (readonly: true freezes the tree at parse)"
    end

    strip_blank_text_nodes!(native_doc)
  end

  record_source_declaration(native_doc, processed)
  attachments.set(native_doc, :entity_markers, entity_markers)
  attachments.set(native_doc, :keep_entity_refs, entity_mode_keep)
  bump_serialize_generation
  attachments.set(native_doc, :parse_errors, recover_errors) if recover_errors

  doc
end

.parse_diagnostics(native_doc) ⇒ Object



894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
# File 'lib/moxml/adapter/leptris.rb', line 894

def parse_diagnostics(native_doc)
  ffi = ::Leptris::XML::FFI
  return [] unless native_doc.is_a?(::Leptris::XML::Document) &&
    ffi.respond_to?(:leptris_document_parse_diag_count)

  doc = native_doc.c_ptr
  count = ffi.leptris_document_parse_diag_count(doc)
  return [] if count.zero?

  kind_ptr = ::FFI::MemoryPointer.new(:int)
  diagnostics = []
  count.times do |i|
    buffer = ::FFI::MemoryPointer.new(512)
    next unless ffi.leptris_document_parse_diag(
      doc, i, kind_ptr, buffer, 512
    ) == 1

    kind = DIAG_KINDS[kind_ptr.read_int] || :unknown
    diagnostics << { kind: kind, message: buffer.read_string }
  end
  diagnostics
end

.parse_errors(native_doc) ⇒ Object



880
881
882
# File 'lib/moxml/adapter/leptris.rb', line 880

def parse_errors(native_doc)
  attachments.get(native_doc, :parse_errors) || NO_PARSE_ERRORS
end

.parse_flags(options, context: nil) ⇒ Object

nil when no parse flag is requested — the binding treats a nil options hash as plain defaults (matching libxml2/ Nokogiri semantics: blanks kept, no ATTLIST defaults). noblanks forwards only once the engine flag is libxml2-safe (see ENGINE_NOBLANKS_SAFE); otherwise it is moxml-side.



830
831
832
833
834
835
836
837
838
# File 'lib/moxml/adapter/leptris.rb', line 830

def parse_flags(options, context: nil)
  flags = 0
  flags |= ::Leptris::XML::ParseOptions::DTDATTR if options[:dtdattr] == true
  flags |= ::Leptris::XML::ParseOptions::KEEP_ENTITY_REFS if NATIVE_ENTITY_REFS &&
    (context&.config&.entity_mode == :keep ||
     options[:keep_entity_refs] == true)
  flags |= ::Leptris::XML::ParseOptions::NOBLANKS if options[:noblanks] == true && ENGINE_NOBLANKS_SAFE
  flags.zero? ? nil : ::Leptris::XML::ParseOptions.new(flags)
end

.parse_fragment(xml, _context = nil) ⇒ Object

Tolerant HTML4/5 parsing into the standard DOM (engine leptris/leptris#659): implied end tags, void elements, raw-text script/style, case-insensitive lowercased names, the HTML named-entity table, synthesized html/head/body. The engine decodes HTML entities directly into text — no marker pipeline, so the marker split scan is stood down. Incremental parse (libleptris v1.6/#586): yields completed elements as the parse runs; each prior subtree is released. Yielded elements are parentless (document nil) and valid only inside the block — the wrapper mirrors that lifetime. Wrapper-parse: the synthetic root's children are the fragment's top-level nodes (the append_xml / inner_xml= trick). Children come through the normal children path so entity-marker splitting applies.



776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
# File 'lib/moxml/adapter/leptris.rb', line 776

def parse_fragment(xml, _context = nil)
  doc = parse("<m>#{xml}</m>")
  # Lifetime seam (#245): the fragment's natives live in the
  # parse document's C tree, and the binding Document is the
  # sole owner with a GC finalizer. The parent_node chain
  # (fragment -> synthetic <m> -> Document wrapper) keeps the
  # owning wrappers strongly reachable from every returned
  # node; the wrappers are minted HERE and returned as-is —
  # re-wrapping at the caller would race the context map's
  # weak values and silently drop the chain.
  root_wrapper = doc.root
  root_wrapper.parent_node = doc
  children(root_wrapper.native).map do |child|
    wrapped = Node.wrap(child, doc.context)
    wrapped.parent_node = root_wrapper
    wrapped
  end
end

.parse_html(html, _options = {}, _context = nil) ⇒ Object



813
814
815
816
817
818
819
820
821
822
823
# File 'lib/moxml/adapter/leptris.rb', line 813

def parse_html(html, _options = {}, _context = nil)
  html_string = html.is_a?(IO) || html.is_a?(StringIO) ? html.read : html.to_s
  native_doc = begin
    ::Leptris::XML.parse_html(html_string)
  rescue ::Leptris::XML::ParseError => e
    raise Moxml::ParseError.new(e.message)
  end
  attachments.set(native_doc, :entity_markers, false)
  bump_serialize_generation
  Wrappers::Document.new(native_doc, _context || Context.new(:leptris))
end

.plan_rows(native) ⇒ Object



603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
# File 'lib/moxml/adapter/leptris.rb', line 603

def self.plan_rows(native)
  return nil unless NATIVE_READ_LAYER

  doc = if native.is_a?(::Leptris::XML::Document)
          native
        else
          doc_for(native)
        end
  return nil if doc.nil? || attachments.get(doc, :entity_markers)

  root_binding = if native.is_a?(::Leptris::XML::Document)
                   doc.root
                 else
                   to_binding(native)
                 end
  return nil unless root_binding

  rows = ::Leptris::XML::Native.snapshot_rows(doc,
                                              root_binding.c_address)
  rows.each do |row|
    yield(row[0], row[1], row[2], row[3])
  end
  true
end

.plan_structs(native, spec) ⇒ Object



582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
# File 'lib/moxml/adapter/leptris.rb', line 582

def self.plan_structs(native, spec)
  return nil unless NATIVE_PLAN_STRUCTS

  doc = if native.is_a?(::Leptris::XML::Document)
          native
        else
          doc_for(native)
        end
  return nil if doc.nil? || attachments.get(doc, :entity_markers)

  root_binding = if native.is_a?(::Leptris::XML::Document)
                   doc.root
                 else
                   to_binding(native)
                 end
  return nil unless root_binding

  ::Leptris::XML::Native.plan_structs(doc, root_binding.c_address,
                                      spec)
end

.prefix_part(name) ⇒ Object



1127
1128
1129
# File 'lib/moxml/adapter/leptris.rb', line 1127

def prefix_part(name)
  name.include?(":") ? name.split(":", 2)[0] : nil
end

.prefixed_attribute_test?(ast, parent_axis = nil) ⇒ Boolean

Released native engines 1.9.37–1.9.39 do not match prefixed attribute tests inside predicates (@p:kind='a'); the Ruby engine does. An attribute test is a :test whose parent axis is "attribute"; bare ones (namespace nil) stay native. Retired above PREFIXED_ATTR_PREDICATES_NATIVE (1.9.40+).

Returns:

  • (Boolean)


2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
# File 'lib/moxml/adapter/leptris.rb', line 2087

def prefixed_attribute_test?(ast, parent_axis = nil)
  if ast.type == :test && parent_axis == "attribute"
    ns = ast.value[:namespace]
    return true if ns && !ns.empty? && ns != "xmlns"
  end

  axis = ast.children.first if ast.type == :axis

  ast.children.any? do |child|
    child.is_a?(XPath::AST::Node) &&
      prefixed_attribute_test?(child, axis || (child.type == :axis ? nil : parent_axis))
  end
end

.previous_sibling(node) ⇒ Object



1451
1452
1453
1454
1455
1456
1457
1458
# File 'lib/moxml/adapter/leptris.rb', line 1451

def previous_sibling(node)
  # Bridged, not the native sibling walk: the native layer
  # exposes next_sibling only, and its children batch
  # truncates at 512 — the predecessor beyond that would be
  # wrong.
  node = to_binding(node)
  node.previous_sibling if node.is_a?(::Leptris::XML::Node)
end

.processing_instruction_content(node) ⇒ Object



1838
1839
1840
1841
1842
# File 'lib/moxml/adapter/leptris.rb', line 1838

def processing_instruction_content(node)
  return NN_CONTENT.bind_call(node) if NATIVE_READ_LAYER && node.is_a?(NN)

  node.content
end

.processing_instruction_target(node) ⇒ Object



1131
1132
1133
1134
1135
# File 'lib/moxml/adapter/leptris.rb', line 1131

def processing_instruction_target(node)
  return to_binding(node).target if NATIVE_READ_LAYER && node.is_a?(NN)

  node.target
end

.record_native_doc(root_native, doc) ⇒ Object



379
380
381
# File 'lib/moxml/adapter/leptris.rb', line 379

def record_native_doc(root_native, doc)
  @native_doc_roots[root_native] = doc
end

.remove(node) ⇒ Object



1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
# File 'lib/moxml/adapter/leptris.rb', line 1693

def remove(node)
  node = to_binding(node) if NATIVE_READ_LAYER
  case node
  when CustomizedLeptris::Declaration
    remove_declaration(node.parent_doc) if node.parent_doc
  when CustomizedLeptris::Doctype
    attachments.delete(node.parent_doc, :doctype) if node.parent_doc
  when ::Leptris::XML::DocType
    if NATIVE_DOC_PARTS
      doc = node.document || doc_for(node)
      if doc
        doc.remove_doctype
        attachments.delete(doc, :doctype)
      end
    end
  when CustomizedLeptris::EntityReference
    marker_text_for(node.parent, node.name)&.unlink
  when CustomizedLeptris::DocumentPI
    if NATIVE_DOC_PARTS && node.parent_doc
      pis = node.parent_doc.processing_instructions
      index = pis.index do |target, data|
        target == node.target && data == node.data
      end
      if index
        node.parent_doc.remove_pi(index)
        node.parent_doc = nil
      end
    else
      raise Moxml::NotImplementedError.new(
        "libleptris has no document-level PI removal",
        adapter: :leptris,
        feature: :remove,
      )
    end
  else
    node.unlink
  end
end

.remove_attribute(element, name) ⇒ Object



1589
1590
1591
1592
# File 'lib/moxml/adapter/leptris.rb', line 1589

def remove_attribute(element, name)
  element = to_binding(element)
  element.remove_attribute(name.to_s)
end

.remove_attribute_native(attr) ⇒ Object



1594
1595
1596
1597
# File 'lib/moxml/adapter/leptris.rb', line 1594

def remove_attribute_native(attr)
  attr.element.remove_attribute(attr.name)
  attr
end

.remove_declaration(native_doc) ⇒ Object



998
999
1000
1001
1002
1003
1004
# File 'lib/moxml/adapter/leptris.rb', line 998

def remove_declaration(native_doc)
  attachments.delete(native_doc, :declaration)
  attachments.delete(native_doc, :had_source_declaration)
  if NATIVE_DOC_PARTS && native_doc.is_a?(::Leptris::XML::Document)
    native_doc.clear_declaration
  end
end

.replace(node, new_node) ⇒ Object



1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
# File 'lib/moxml/adapter/leptris.rb', line 1732

def replace(node, new_node)
  if NATIVE_READ_LAYER
    node = to_binding(node)
    new_node = to_binding(new_node)
  end
  return node.replace(new_node) if node.is_a?(::Leptris::XML::Element)

  # libleptris only offers element-anchored insertion, so a
  # content node (text/comment/CDATA/PI) is replaced by
  # unlinking and re-inserting: after an element sibling when
  # one exists, else appended (end-of-list) to the parent.
  parent = node.parent
  unless parent
    raise Moxml::DocumentStructureError.new(
      "cannot replace a detached node",
    )
  end

  prev = node.previous_sibling
  node.unlink
  if prev.is_a?(::Leptris::XML::Element)
    prev.add_next_sibling(new_node)
  else
    parent.add_child(new_node)
  end
  new_node
end

.replace_children(node, new_children) ⇒ Object



1760
1761
1762
1763
1764
1765
1766
# File 'lib/moxml/adapter/leptris.rb', line 1760

def replace_children(node, new_children)
  if NATIVE_READ_LAYER
    node = to_binding(node)
    new_children = new_children.map { |child| to_binding(child) }
  end
  node.children = new_children
end

.resolve_prefix_ns(owner, prefix) ⇒ Object

Nearest in-scope declaration of prefix, walking owner then ancestors. Unbound prefix → nil. Returns the Namespace object so wrappers keep prefix and uri.



1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
# File 'lib/moxml/adapter/leptris.rb', line 1116

def resolve_prefix_ns(owner, prefix)
  current = owner
  while current.is_a?(::Leptris::XML::Element)
    hit = current.namespace_definitions.find { |ns| ns.prefix == prefix }
    return hit if hit

    current = current.parent
  end
  nil
end

.root(document) ⇒ Object



1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
# File 'lib/moxml/adapter/leptris.rb', line 1471

def root(document)
  r = document.root
  return nil if r.nil?

  if NATIVE_READ_LAYER
    # Through the same address-keyed cache as the children
    # path: NativeNode.from alone mints a fresh object per
    # call on 1.9.163.x, splitting the wrappers (issue #219).
    native = canonical_native(document, r)
    record_native_doc(native, document)
    return native
  end
  r
end

.root_parent(node) ⇒ Object

The binding reports the root element as parentless; the moxml contract roots at the document.



1440
1441
1442
# File 'lib/moxml/adapter/leptris.rb', line 1440

def root_parent(node)
  node.parent || (node.document&.root == node ? node.document : nil)
end

.same_node?(one, other) ⇒ Boolean

Protocol-level node equality: one engine can hand out two wrapper classes over the same C node (native layer + binding), so raw == is not identity across the seam.

Returns:

  • (Boolean)


386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
# File 'lib/moxml/adapter/leptris.rb', line 386

def same_node?(one, other)
  return true if one.equal?(other)

  if NATIVE_READ_LAYER
    native_one = one.is_a?(::Leptris::XML::NativeNode)
    native_other = other.is_a?(::Leptris::XML::NativeNode)
    if native_one || native_other
      address_one = native_one ? one.address : one.c_ptr.address
      address_other = native_other ? other.address : other.c_ptr.address
      return address_one == address_other
    end
  end

  one == other
end

.sax_parse(xml, handler) ⇒ Object



2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
# File 'lib/moxml/adapter/leptris.rb', line 2128

def sax_parse(xml, handler)
  bridge = LeptrisSAXBridge.new(handler)
  xml_string = xml.is_a?(IO) || xml.is_a?(::StringIO) ? xml.read : xml.to_s
  # Bulk record drain (leptris#1298, NATIVE_SAX_RECORDS):
  # one crossing per document. The handler decides the shape
  # polymorphically — Handler#on_sax_records answers
  # :unhandled, so classic handlers get the exact callback
  # stream replayed from the table; hot loops override and
  # walk the records (nil table = outside the drain subset
  # or pre-1.9.238 binding — callback path).
  table = NATIVE_SAX_RECORDS ? ::Leptris::XML::SAX::Records.open(xml_string) : nil
  if table
    begin
      return if handler.on_sax_records(table) != :unhandled

      SaxRecordReplay.new(bridge).run(table)
    ensure
      table.free
    end
  else
    ::Leptris::XML::SAX::Parser.new(bridge).parse(xml_string)
  end
rescue ::Leptris::XML::ParseError, ::Leptris::XML::Error => e
  handler.on_error(Moxml::ParseError.new(e.message))
end

.selects_attribute_results?(ast) ⇒ Boolean

Returns:

  • (Boolean)


2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
# File 'lib/moxml/adapter/leptris.rb', line 2069

def selects_attribute_results?(ast)
  case ast.type
  when :pipe, :union, :filter_expr
    ast.children.any? { |child| selects_attribute_results?(child) }
  when :absolute_path, :relative_path
    step = ast.children.last
    step = step.children.first if step.type == :step_with_predicates
    step.type == :axis && step.children.first == "attribute"
  else
    false
  end
end

.set_attribute(element, name, value) ⇒ Object



1559
1560
1561
1562
# File 'lib/moxml/adapter/leptris.rb', line 1559

def set_attribute(element, name, value)
  element = to_binding(element) if NATIVE_READ_LAYER
  element[name.to_s] = value.to_s
end

.set_attribute_name(attr, name) ⇒ Object



1564
1565
1566
1567
1568
1569
1570
1571
1572
# File 'lib/moxml/adapter/leptris.rb', line 1564

def set_attribute_name(attr, name)
  # Attr is an immutable value object; renames go through the
  # element and yield a fresh native, which the wrapper adopts.
  element = attr.element
  value = attr.value
  element.remove_attribute(attr.name)
  element[name.to_s] = value
  element.attribute_nodes.reverse.find { |candidate| candidate.name == name.to_s }
end

.set_attribute_namespace(attr, namespace) ⇒ Object

Attr is an immutable value object: namespace changes go through the owning element, recreating the attribute with a qualified name, and yield a fresh native for the wrapper.



1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
# File 'lib/moxml/adapter/leptris.rb', line 1067

def set_attribute_namespace(attr, namespace)
  element = attr.element
  local = attr.name.include?(":") ? attr.name.split(":", 2)[1] : attr.name
  value = attr.value
  element.remove_attribute(attr.name)
  prefix = namespace.is_a?(String) ? nil : namespace.prefix
  uri = namespace.is_a?(String) ? namespace : namespace.href
  qualified = prefix.nil? || prefix.empty? ? local : "#{prefix}:#{local}"
  element[qualified] = value
  if prefix && !prefix.empty? && !uri.to_s.empty? && !resolve_prefix_ns(element, prefix)
    element.add_namespace_definition(prefix, uri.to_s)
  end
  element.attribute_nodes.reverse.find { |candidate| candidate.name == qualified }
end

.set_attribute_value(attr, value) ⇒ Object



1574
1575
1576
1577
# File 'lib/moxml/adapter/leptris.rb', line 1574

def set_attribute_value(attr, value)
  attr.value = value.to_s
  attr
end

.set_cdata_content(node, content) ⇒ Object



1822
1823
1824
1825
# File 'lib/moxml/adapter/leptris.rb', line 1822

def set_cdata_content(node, content)
  node = to_binding(node) if NATIVE_READ_LAYER
  node.content = content.to_s
end

.set_comment_content(node, content) ⇒ Object



1833
1834
1835
1836
# File 'lib/moxml/adapter/leptris.rb', line 1833

def set_comment_content(node, content)
  node = to_binding(node) if NATIVE_READ_LAYER
  node.content = content.to_s
end

.set_declaration_attribute(declaration, attr_name, value) ⇒ Object



982
983
984
# File 'lib/moxml/adapter/leptris.rb', line 982

def set_declaration_attribute(declaration, attr_name, value)
  declaration.public_send("#{attr_name}=", value) if attr_matches?(attr_name)
end

.set_namespace(node, namespace) ⇒ Object



1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
# File 'lib/moxml/adapter/leptris.rb', line 1011

def set_namespace(node, namespace)
  node = to_binding(node) if NATIVE_READ_LAYER
  return set_attribute_namespace(node, namespace) if node.is_a?(::Leptris::XML::Attr)

  element = node
  if namespace.nil?
    # Supported un-prefix (#245, relaton migration): rename
    # the node to its local part (name = local) — the
    # serializer re-attaches the old prefix after namespace=
    # nil on prefixed elements until leptris-ruby#244 lands.
    # Nil-clear contract (issue #164): undeclare the default
    # namespace (xmlns="") and drop any name prefix — the
    # element then reports no namespace, matching the other
    # adapters. Elements that carried a PREFIX cannot fully
    # detach the engine's namespace link (leptris-ruby#132):
    # the name is unqualified and the undeclaration added, but
    # the serializer may re-attach the old prefix until the
    # engine grows a clear entry.
    element.name = element.name.split(":", 2)[-1] if element.name.include?(":")
    element.default_namespace = ""
    return element
  end
  prefix = namespace.is_a?(String) ? nil : namespace.prefix
  uri = namespace.is_a?(String) ? namespace : namespace.href
  if prefix.nil? || prefix.empty?
    element.default_namespace = uri.to_s
    # Drop any prefix from the element name: a default namespace
    # never applies to a prefixed name.
    element.name = element.name.split(":", 2)[-1] if element.name.include?(":")
  else
    # Name is the local part re-prefixed (a qname create leaves
    # "p:c"; re-joining without the split would double —
    # issue #208). Declare only when the prefix is not already
    # in scope: under a parent that binds p, a bare name set
    # is enough (leptris resolves through ancestors). Detached
    # elements still get the declaration so a standalone
    # serialize stays well-formed.
    local = element.name.split(":", 2)[-1]
    element.name = "#{prefix}:#{local}"
    already = resolve_prefix_ns(element, prefix)
    needs_decl = already.nil? || already.href.to_s != uri.to_s
    # Detached elements that will be attached under a declaring
    # parent must not carry their own declaration — that is
    # the create_element(qname) + namespace= + attach order
    # (issue #208). Standalone serialize of a still-detached
    # namespaced element is the add_namespace caller's job.
    if needs_decl && !element.parent.nil?
      element.add_namespace_definition(prefix, uri.to_s)
    end
  end
  element
end

.set_node_name(node, name) ⇒ Object



1215
1216
1217
1218
1219
1220
1221
# File 'lib/moxml/adapter/leptris.rb', line 1215

def set_node_name(node, name)
  node = to_binding(node) if NATIVE_READ_LAYER
  case node
  when ::Leptris::XML::ProcessingInstruction, CustomizedLeptris::DocumentPI then node.target = name
  else node.name = name
  end
end

.set_processing_instruction_content(node, content) ⇒ Object



1844
1845
1846
1847
# File 'lib/moxml/adapter/leptris.rb', line 1844

def set_processing_instruction_content(node, content)
  node = to_binding(node) if NATIVE_READ_LAYER
  node.data = content.to_s
end

.set_root(doc, element) ⇒ Object



633
634
635
# File 'lib/moxml/adapter/leptris.rb', line 633

def set_root(doc, element)
  to_binding(doc).root = to_binding(element)
end

.set_text_content(node, content) ⇒ Object



1806
1807
1808
1809
1810
1811
1812
1813
1814
# File 'lib/moxml/adapter/leptris.rb', line 1806

def set_text_content(node, content)
  node = to_binding(node) if NATIVE_READ_LAYER
  case node
  when ::Leptris::XML::Document
    node.root&.content = content.to_s
  else
    node.content = content.to_s
  end
end

.sibling_walk_children(node) ⇒ Object

Fallback for natives without a registered document: walk first_child/next_sibling at the FFI level and wrap binding nodes over the raw pointers.



1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
# File 'lib/moxml/adapter/leptris.rb', line 1348

def sibling_walk_children(node)
  ptr = ::FFI::Pointer.new(node.address)
  first = ::Leptris::XML::FFI.leptris_node_first_child(ptr)
  children = []
  current = first
  until current.null?
    children << ::Leptris::XML::Node.wrap(current, nil)
    current = ::Leptris::XML::FFI.leptris_node_next_sibling(current)
  end
  children
end

.source_position(node) ⇒ Object



1486
1487
1488
1489
1490
# File 'lib/moxml/adapter/leptris.rb', line 1486

def source_position(node)
  return nil unless NATIVE_SOURCE_POSITION

  to_binding(node).source_position
end

.strip_blank_text_nodes!(doc) ⇒ Object

libxml2's XML_PARSE_NOBLANKS semantics (issues #153/#156): drop WHOLLY-whitespace text nodes; the boundary spaces of text-bearing nodes stay (mixed content cannot be re-indented). Raw-pointer walk over the root subtree — the same batch child-pointer pattern the materializer uses.



848
849
850
851
852
853
854
# File 'lib/moxml/adapter/leptris.rb', line 848

def strip_blank_text_nodes!(doc)
  binding_ffi = ::Leptris::XML::FFI
  root = binding_ffi.leptris_document_root(doc.c_ptr)
  return unless root && !root.null?

  strip_blanks_under(root, binding_ffi, ::FFI::MemoryPointer.new(:pointer, 64), 64)
end

.strip_blanks_under(ptr, binding_ffi, buf, capacity) ⇒ Object



856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
# File 'lib/moxml/adapter/leptris.rb', line 856

def strip_blanks_under(ptr, binding_ffi, buf, capacity)
  count = binding_ffi.leptris_node_children(ptr, buf, capacity)
  while count == capacity
    capacity *= 4
    buf = ::FFI::MemoryPointer.new(:pointer, capacity)
    count = binding_ffi.leptris_node_children(ptr, buf, capacity)
  end

  # Snapshot before unlinking or recursing — both reuse the
  # buffer for the next level.
  children = buf.read_array_of_pointer(count)
  children.each do |child|
    case binding_ffi.leptris_node_get_type(child)
    when ::Leptris::XML::FFI::NODE_ELEMENT
      strip_blanks_under(child, binding_ffi, buf, capacity)
    when ::Leptris::XML::FFI::NODE_TEXT
      content = binding_ffi.leptris_text_node_get_content(child)
      next unless content.match?(BLANK_TEXT_RE)

      binding_ffi.check_status(binding_ffi.leptris_node_unlink(child))
    end
  end
end

.text_content(node) ⇒ Object



1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
# File 'lib/moxml/adapter/leptris.rb', line 1768

def text_content(node)
  if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
    content = NN_CONTENT.bind_call(node)
    return content if NATIVE_STRINGS_UTF8

    content.dup.force_encoding(Encoding::UTF_8)
  end

  # Frequency-ordered: elements dominate reads; they paid
  # three failed compares to reach the else arm. The
  # duplicated branch bodies are the point.
  case node
  when ::Leptris::XML::Element, ::Leptris::XML::Text
    node.content.to_s
  when ::Leptris::XML::Document then node.root ? node.root.content : ""
  when CustomizedLeptris::Declaration, CustomizedLeptris::Doctype,
       CustomizedLeptris::EntityReference
    ""
  when CustomizedLeptris::TextSegment then node.content
  else node.content.to_s # rubocop:disable Lint/DuplicateBranch
  end
end

.to_binding(node) ⇒ Object

native -> bridged binding node. The bridge object is identity-stable for the native's lifetime (the binding's per-document wrap cache), so repeat bridges — the write loop on a factory-created element, re-accessed attribute lists — skip the document resolution and pointer wrap. Binding node for any native: identity for binding nodes (and on installs without the native layer — the constant check short-circuits), a Node.wrap over the shared C pointer for NativeNodes.



164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/moxml/adapter/leptris.rb', line 164

def to_binding(node)
  return node unless NATIVE_READ_LAYER &&
    node.is_a?(::Leptris::XML::NativeNode)

  bridged = @binding_of[node]
  return bridged if bridged

  doc = doc_for(node)
  ptr = ::FFI::Pointer.new(node.address)
  bridged = ::Leptris::XML::Node.wrap(ptr, doc)
  @binding_of[node] = bridged unless bridged.nil?
  bridged
end

.uses_xmlns_prefix?(ast) ⇒ Boolean

xmlns:name is a nokogiri-compat convention addressing elements in the default namespace; only the Ruby engine implements it.

Returns:

  • (Boolean)


2061
2062
2063
2064
2065
2066
2067
# File 'lib/moxml/adapter/leptris.rb', line 2061

def uses_xmlns_prefix?(ast)
  return true if ast.type == :test && ast.value[:namespace] == "xmlns"

  ast.children.any? do |child|
    child.is_a?(XPath::AST::Node) && uses_xmlns_prefix?(child)
  end
end

.walk_descendants(native, context) ⇒ Object

One C visit walk (TODO.perf/27: post-order/abort state in the C callback, rb_yield direct — no FFI closure per node) instead of per-level children walks. Pre-order via entering; depth 0 is the receiver and stays unyielded.



339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/moxml/adapter/leptris.rb', line 339

def walk_descendants(native, context)
  return nil unless NATIVE_C_WALK

  root = to_binding(native)
  return nil unless root.is_a?(::Leptris::XML::Element) ||
    root.is_a?(::Leptris::XML::Document)

  root.visit do |node, entering, depth|
    if entering && depth.positive?
      yield wrap_binding_node(node, context)
    end
  end
end

.wrap_binding_node(node, context) ⇒ Object



367
368
369
370
371
372
373
374
375
376
377
# File 'lib/moxml/adapter/leptris.rb', line 367

def wrap_binding_node(node, context)
  cached = node.instance_variable_get(:@moxml_wrapper)
  return cached if cached

  entry = BINDING_FAST_WRAP[node.class]
  return nil unless entry

  wrapper = entry[0].new(node, context, self, entry[1])
  node.instance_variable_set(:@moxml_wrapper, wrapper)
  wrapper
end

.wrap_native(node, type, _context) ⇒ Object



312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/moxml/adapter/leptris.rb', line 312

def wrap_native(node, type, _context)
  return nil unless NATIVE_IDENTITY &&
    node.is_a?(::Leptris::XML::NativeNode)

  # from() registers in the shared address-keyed native
  # cache, so later canonical/bulk mints hand OUR instance
  # back — a native already carrying the contract IS the
  # wrapper; minting again would loop per access.
  return node if node.is_a?(::Moxml::Node)

  klass = Identity::TYPES[type]
  return nil unless klass

  doc = doc_for(node)
  return nil unless doc

  klass.from(doc, node)
end

.xpath(node, expression, namespaces = {}) ⇒ Object



1898
1899
1900
1901
1902
1903
1904
# File 'lib/moxml/adapter/leptris.rb', line 1898

def xpath(node, expression, namespaces = {})
  node = to_binding(node) if NATIVE_READ_LAYER
  native = native_xpath(node, expression, namespaces)
  return native unless native.nil?

  engine_xpath(node, expression, namespaces)
end