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

Overview

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

libleptris provides DOM parsing, a native XPath 1.0 engine, SAX, and serialization. It has no first-class XML declaration or programmatic DOCTYPE, so those live in CustomizedLeptris value objects stored through NativeAttachment.

Defined Under Namespace

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

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.

"1.9.32"
HTML_PARSE_SUPPORTED =

leptris_parse_html_string shipped in bindings 1.9.80 (libleptris 1.9.75, engine #659) as Leptris::XML.parse_html.

Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.80")
ATTR_RESULT_NATIVE =

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.

Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.105")
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_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")
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).

NATIVE_READ_LAYER &&
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.193.4")
BLANK_TEXT_RE =

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

/\A[ \t\r\n]*\z/
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, 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

actual_native, 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_generationObject (readonly)

Returns the value of attribute serialize_generation.



447
448
449
# File 'lib/moxml/adapter/leptris.rb', line 447

def serialize_generation
  @serialize_generation
end

Class Method Details

.add_child(parent, child) ⇒ Object



1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
# File 'lib/moxml/adapter/leptris.rb', line 1464

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



1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
# File 'lib/moxml/adapter/leptris.rb', line 1517

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



1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
# File 'lib/moxml/adapter/leptris.rb', line 1492

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.



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

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)


1887
1888
1889
1890
1891
1892
1893
# File 'lib/moxml/adapter/leptris.rb', line 1887

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



1743
1744
1745
1746
1747
1748
1749
# File 'lib/moxml/adapter/leptris.rb', line 1743

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

.attachmentsObject



576
577
578
# File 'lib/moxml/adapter/leptris.rb', line 576

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

.attr_matches?(attr_name) ⇒ Boolean

Returns:

  • (Boolean)


907
908
909
# File 'lib/moxml/adapter/leptris.rb', line 907

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

.attribute_element(attr) ⇒ Object



1412
1413
1414
# File 'lib/moxml/adapter/leptris.rb', line 1412

def attribute_element(attr)
  attr.element
end

.attribute_name(attr) ⇒ Object



1416
1417
1418
1419
1420
1421
1422
# File 'lib/moxml/adapter/leptris.rb', line 1416

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

.attributes(element) ⇒ Object



1407
1408
1409
1410
# File 'lib/moxml/adapter/leptris.rb', line 1407

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).



627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
# File 'lib/moxml/adapter/leptris.rb', line 627

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)


588
589
590
# File 'lib/moxml/adapter/leptris.rb', line 588

def bare_get_qname_safe?
  true
end

.bare_set_qname_safe?Boolean

Returns:

  • (Boolean)


584
585
586
# File 'lib/moxml/adapter/leptris.rb', line 584

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).



1278
1279
1280
1281
1282
1283
1284
1285
1286
# File 'lib/moxml/adapter/leptris.rb', line 1278

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].



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

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).



1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
# File 'lib/moxml/adapter/leptris.rb', line 1293

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_generationObject



451
452
453
# File 'lib/moxml/adapter/leptris.rb', line 451

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.



1268
1269
1270
1271
1272
1273
1274
# File 'lib/moxml/adapter/leptris.rb', line 1268

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



1653
1654
1655
1656
1657
# File 'lib/moxml/adapter/leptris.rb', line 1653

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



1142
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
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
# File 'lib/moxml/adapter/leptris.rb', line 1142

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



1664
1665
1666
1667
1668
# File 'lib/moxml/adapter/leptris.rb', line 1664

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)


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

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



838
839
840
# File 'lib/moxml/adapter/leptris.rb', line 838

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

.create_native_cdata(content, owner_doc = nil) ⇒ Object



863
864
865
# File 'lib/moxml/adapter/leptris.rb', line 863

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

.create_native_comment(content, owner_doc = nil) ⇒ Object



867
868
869
# File 'lib/moxml/adapter/leptris.rb', line 867

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

.create_native_declaration(version, encoding, standalone) ⇒ Object



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

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

.create_native_doctype(name, external_id, system_id) ⇒ Object



876
877
878
# File 'lib/moxml/adapter/leptris.rb', line 876

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.



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

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



884
885
886
887
888
889
890
# File 'lib/moxml/adapter/leptris.rb', line 884

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



924
925
926
927
# File 'lib/moxml/adapter/leptris.rb', line 924

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



871
872
873
874
# File 'lib/moxml/adapter/leptris.rb', line 871

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



855
856
857
858
859
860
861
# File 'lib/moxml/adapter/leptris.rb', line 855

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



899
900
901
# File 'lib/moxml/adapter/leptris.rb', line 899

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.



1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
# File 'lib/moxml/adapter/leptris.rb', line 1090

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.



394
395
396
397
398
399
400
401
402
# File 'lib/moxml/adapter/leptris.rb', line 394

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



1710
1711
1712
# File 'lib/moxml/adapter/leptris.rb', line 1710

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

.doctype_name(node) ⇒ Object



1706
1707
1708
# File 'lib/moxml/adapter/leptris.rb', line 1706

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

.doctype_system_id(node) ⇒ Object



1714
1715
1716
# File 'lib/moxml/adapter/leptris.rb', line 1714

def doctype_system_id(node)
  node.system_id
end

.document(node) ⇒ Object



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

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



1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
# File 'lib/moxml/adapter/leptris.rb', line 1126

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



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
# File 'lib/moxml/adapter/leptris.rb', line 1938

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



892
893
894
895
896
897
# File 'lib/moxml/adapter/leptris.rb', line 892

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)


596
597
598
# File 'lib/moxml/adapter/leptris.rb', line 596

def expanded_attr_reads?
  true
end

.expanded_attr_value(element, uri, local) ⇒ Object



609
610
611
612
613
614
615
616
617
# File 'lib/moxml/adapter/leptris.rb', line 609

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)


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

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



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

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



1449
1450
1451
1452
# File 'lib/moxml/adapter/leptris.rb', line 1449

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

.has_declaration?(native_doc, _wrapper) ⇒ Boolean

Returns:

  • (Boolean)


912
913
914
915
916
917
# File 'lib/moxml/adapter/leptris.rb', line 912

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



1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
# File 'lib/moxml/adapter/leptris.rb', line 1628

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)


742
743
744
745
746
747
748
749
# File 'lib/moxml/adapter/leptris.rb', line 742

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)


751
752
753
754
755
756
757
758
# File 'lib/moxml/adapter/leptris.rb', line 751

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



1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
# File 'lib/moxml/adapter/leptris.rb', line 1395

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



1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
# File 'lib/moxml/adapter/leptris.rb', line 1000

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



1701
1702
1703
1704
# File 'lib/moxml/adapter/leptris.rb', line 1701

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

.namespace_prefix(namespace) ⇒ Object



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

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



1695
1696
1697
1698
1699
# File 'lib/moxml/adapter/leptris.rb', line 1695

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)


423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
# File 'lib/moxml/adapter/leptris.rb', line 423

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)


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

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)


1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
# File 'lib/moxml/adapter/leptris.rb', line 1868

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)


645
646
647
# File 'lib/moxml/adapter/leptris.rb', line 645

def native_identity_stable?
  true
end

.native_inclusive10(native) ⇒ Object



600
601
602
603
604
605
606
607
# File 'lib/moxml/adapter/leptris.rb', line 600

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)


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

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



1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
# File 'lib/moxml/adapter/leptris.rb', line 1753

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



1347
1348
1349
1350
1351
1352
# File 'lib/moxml/adapter/leptris.rb', line 1347

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



1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
# File 'lib/moxml/adapter/leptris.rb', line 1101

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



1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
# File 'lib/moxml/adapter/leptris.rb', line 1055

def node_type(node)
  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



1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
# File 'lib/moxml/adapter/leptris.rb', line 1223

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



649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
# File 'lib/moxml/adapter/leptris.rb', line 649

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_errors(native_doc) ⇒ Object



834
835
836
# File 'lib/moxml/adapter/leptris.rb', line 834

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.



784
785
786
787
788
789
790
791
792
# File 'lib/moxml/adapter/leptris.rb', line 784

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.



723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
# File 'lib/moxml/adapter/leptris.rb', line 723

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



760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
# File 'lib/moxml/adapter/leptris.rb', line 760

def parse_html(html, _options = {}, _context = nil)
  unless HTML_PARSE_SUPPORTED
    raise Moxml::AdapterError.new(
      "HTML parsing requires leptris >= 1.9.80 (have #{::Leptris::VERSION})",
      adapter: name, operation: "parse_html",
    )
  end

  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



540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
# File 'lib/moxml/adapter/leptris.rb', line 540

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 = if NATIVE_PLAN_ROWS
           ::Leptris::XML::Native.snapshot_rows(doc,
                                                root_binding.c_address)
         else
           doc.snapshot(root_binding)
         end
  rows.each do |row|
    if row.is_a?(::Array)
      yield(row[0], row[1], row[2], row[3])
    else
      next unless row[:kind] == "element"

      yield(row[:name], row[:attrs].flatten, row[:text], row[:depth])
    end
  end
  true
end

.prefix_part(name) ⇒ Object



1045
1046
1047
# File 'lib/moxml/adapter/leptris.rb', line 1045

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)


1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
# File 'lib/moxml/adapter/leptris.rb', line 1924

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



1354
1355
1356
1357
1358
1359
1360
1361
# File 'lib/moxml/adapter/leptris.rb', line 1354

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



1675
1676
1677
1678
1679
# File 'lib/moxml/adapter/leptris.rb', line 1675

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



1049
1050
1051
1052
1053
# File 'lib/moxml/adapter/leptris.rb', line 1049

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



368
369
370
# File 'lib/moxml/adapter/leptris.rb', line 368

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

.remove(node) ⇒ Object



1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
# File 'lib/moxml/adapter/leptris.rb', line 1549

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 CustomizedLeptris::EntityReference
    marker_text_for(node.parent, node.name)&.unlink
  when CustomizedLeptris::DocumentPI
    raise Moxml::NotImplementedError.new(
      "libleptris has no document-level PI removal",
      adapter: :leptris,
      feature: :remove,
    )
  else
    node.unlink
  end
end

.remove_attribute(element, name) ⇒ Object



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

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

.remove_attribute_native(attr) ⇒ Object



1459
1460
1461
1462
# File 'lib/moxml/adapter/leptris.rb', line 1459

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

.remove_declaration(native_doc) ⇒ Object



919
920
921
922
# File 'lib/moxml/adapter/leptris.rb', line 919

def remove_declaration(native_doc)
  attachments.delete(native_doc, :declaration)
  attachments.delete(native_doc, :had_source_declaration)
end

.replace(node, new_node) ⇒ Object



1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
# File 'lib/moxml/adapter/leptris.rb', line 1569

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



1597
1598
1599
1600
1601
1602
1603
# File 'lib/moxml/adapter/leptris.rb', line 1597

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.



1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
# File 'lib/moxml/adapter/leptris.rb', line 1034

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



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

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.



1343
1344
1345
# File 'lib/moxml/adapter/leptris.rb', line 1343

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)


375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
# File 'lib/moxml/adapter/leptris.rb', line 375

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



1965
1966
1967
1968
1969
1970
1971
# File 'lib/moxml/adapter/leptris.rb', line 1965

def sax_parse(xml, handler)
  bridge = LeptrisSAXBridge.new(handler)
  xml_string = xml.is_a?(IO) || xml.is_a?(::StringIO) ? xml.read : xml.to_s
  ::Leptris::XML::SAX::Parser.new(bridge).parse(xml_string)
rescue ::Leptris::XML::ParseError, ::Leptris::XML::Error => e
  handler.on_error(Moxml::ParseError.new(e.message))
end

.selects_attribute_results?(ast) ⇒ Boolean

Returns:

  • (Boolean)


1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
# File 'lib/moxml/adapter/leptris.rb', line 1906

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



1424
1425
1426
1427
# File 'lib/moxml/adapter/leptris.rb', line 1424

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



1429
1430
1431
1432
1433
1434
1435
1436
1437
# File 'lib/moxml/adapter/leptris.rb', line 1429

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.



985
986
987
988
989
990
991
992
993
994
995
996
997
998
# File 'lib/moxml/adapter/leptris.rb', line 985

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



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

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

.set_cdata_content(node, content) ⇒ Object



1659
1660
1661
1662
# File 'lib/moxml/adapter/leptris.rb', line 1659

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



1670
1671
1672
1673
# File 'lib/moxml/adapter/leptris.rb', line 1670

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



903
904
905
# File 'lib/moxml/adapter/leptris.rb', line 903

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



929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
# File 'lib/moxml/adapter/leptris.rb', line 929

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



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

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



1681
1682
1683
1684
# File 'lib/moxml/adapter/leptris.rb', line 1681

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



580
581
582
# File 'lib/moxml/adapter/leptris.rb', line 580

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

.set_text_content(node, content) ⇒ Object



1643
1644
1645
1646
1647
1648
1649
1650
1651
# File 'lib/moxml/adapter/leptris.rb', line 1643

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.



1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
# File 'lib/moxml/adapter/leptris.rb', line 1251

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



1389
1390
1391
1392
1393
# File 'lib/moxml/adapter/leptris.rb', line 1389

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.



802
803
804
805
806
807
808
# File 'lib/moxml/adapter/leptris.rb', line 802

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



810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
# File 'lib/moxml/adapter/leptris.rb', line 810

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



1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
# File 'lib/moxml/adapter/leptris.rb', line 1605

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.



150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/moxml/adapter/leptris.rb', line 150

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)


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

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.



356
357
358
359
360
361
362
363
364
365
366
# File 'lib/moxml/adapter/leptris.rb', line 356

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|
    yield Moxml::Node.wrap(node, context) if entering && depth.positive?
  end
end

.wrap_native(node, type, _context) ⇒ Object



329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
# File 'lib/moxml/adapter/leptris.rb', line 329

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



1735
1736
1737
1738
1739
1740
1741
# File 'lib/moxml/adapter/leptris.rb', line 1735

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