moxml 0.5.35 → 0.5.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f80e7825989e8653fcc4037f6e6a538ca28da7c6637d6bb941985bcc98d4dabf
4
- data.tar.gz: 974854c0f42279c86e404e48920a2fd6e92cebbac22a7c6781bae023fa017fde
3
+ metadata.gz: 9d5b785cac911c3289013e8ad7f3a41a0608498b6389098a80b58a6693f1a063
4
+ data.tar.gz: 73f7938818ebcc02059f1396d776e9db477f924ceefcce3680b14a2c3c26aca7
5
5
  SHA512:
6
- metadata.gz: 7749417f017a166425fb18e896e6c0346475dd78cb32792fe725153512b64c384f60cf026e5d8503e7fb0878f22a3e6f92221126f98f08550a7b58ba1e65e2a4
7
- data.tar.gz: 6545711a0dec77522f2965c5239c7cb8632878170e0fd6ea024be4d64515e49cd26550cf40b9dab7dda1ed2faa5e98caa0e076e0fda6781f7cc64463c520fd59
6
+ metadata.gz: 00ac1feeeb5e1f4e407d4ccdb3afb29580f85300c1daca678de547079db791488d7d792d15b95381ae3488fe6e1f9e73f782376af6a09078fd4101f523218e93
7
+ data.tar.gz: be6be6adfe05ad94ecc36b1dfea8f295683478243d46301f261a6a9caecb09ac59b836342f79e124f75855c4864757a341687ef51377c9aaa252d7316fb5f141
@@ -253,6 +253,30 @@ read cache materializes lazily). The remaining cold-walk gap is
253
253
  binding-side (its per-node Ruby wrapper construction) — tracked
254
254
  upstream as the TypedData work.
255
255
 
256
+ === Native read layer (leptris >= 1.9.162.6)
257
+
258
+ The leptris adapter adopts the binding's opt-in native read layer
259
+ (leptris-ruby#185): TypedData node wrappers with C-bound hot reads
260
+ and bulk children construction — one wrapper-cache round-trip for a
261
+ whole child list, zero per-node FFI. Element natives minted from
262
+ `#root` and `children` are native-layer nodes; the C tree is shared
263
+ with the binding, so writes made through the bridged binding nodes
264
+ (a `to_binding` wrap over the same pointer) are visible to native
265
+ reads. Engines without the layer, or ruby-platform installs without
266
+ the compiled bundle, stay on the binding path unchanged.
267
+
268
+ | path | binding path | native layer |
269
+ | Consumer pipeline (parse + typed walk) | 6112µs (0.50x nokogiri) | **3380µs (1.00-1.04x)** |
270
+ | Pipeline allocations | 13564 | **9813 (0.69x nokogiri)** |
271
+ | Cold walk (parse + mint) | 738µs | **509µs** |
272
+ | Raw walk (binding vs native, upstream's fixture) | 64.3µs/doc | 22.8µs/doc |
273
+
274
+ Entity-marker documents fall back to the binding children path (the
275
+ marker split materializes TextSegments); the wrapper's
276
+ `entity_bearing?` memo decides, so the check costs no per-call
277
+ climb. `Node#==` and the adapter protocol's `same_node?` compare
278
+ C-node addresses — two wrapper classes over one node compare equal.
279
+
256
280
  === Prefer bulk paths for per-node conversion
257
281
 
258
282
  Per-node Ruby iteration pays the wrapper tax per element. When the
@@ -116,6 +116,13 @@ module Moxml
116
116
  )
117
117
  end
118
118
 
119
+ # Protocol-level native equality; engines with more than one
120
+ # wrapper class over one C node override (leptris native
121
+ # read layer). Shared adapter examples compare through this.
122
+ def same_node?(one, other)
123
+ one == other
124
+ end
125
+
119
126
  # Backends without a native subtree digest answer nil —
120
127
  # the wrapper contract Node#digest gates on (issue #173).
121
128
  def digest(*)
@@ -272,7 +279,7 @@ namespace_validation_mode: :strict)
272
279
  # Marker-tracking adapters override this so the post-serialize
273
280
  # restore can skip its full-output scans on marker-free
274
281
  # documents; the default stays conservative.
275
- def entity_bearing?(_native)
282
+ def entity_bearing?(_native, _doc = nil)
276
283
  true
277
284
  end
278
285
 
@@ -8,7 +8,16 @@ module Moxml
8
8
  # the ER builder path flips it, and the split/restore scans
9
9
  # consult it. Customized natives only exist as split products
10
10
  # of marker-bearing text, so they always report true.
11
- def entity_bearing?(native)
11
+ # doc is the binding document when the wrapper could resolve
12
+ # it through its parent chain; the C climb is the fallback.
13
+ def entity_bearing?(native, doc = nil)
14
+ if NATIVE_READ_LAYER && native.is_a?(::Leptris::XML::NativeNode)
15
+ doc ||= doc_for(native)
16
+ return false if doc.nil?
17
+
18
+ return attachments.get(doc, :entity_markers) != false
19
+ end
20
+
12
21
  case native
13
22
  when CustomizedLeptris::Declaration, CustomizedLeptris::Doctype,
14
23
  CustomizedLeptris::EntityReference, CustomizedLeptris::TextSegment,
@@ -30,6 +30,7 @@ module Moxml
30
30
  EMPTY_ELEMENT_RE = %r{<([A-Za-z_][\w.:-]*+)((?:"[^"]*+"|'[^']*+'|[^<>"'/]++)*+)/>}
31
31
 
32
32
  def serialize(node, options = {})
33
+ node = to_binding(node) if NATIVE_READ_LAYER
33
34
  # Entity restoration belongs to the wrapper layer
34
35
  # (Node#to_xml runs adapter.restore_entities for every
35
36
  # adapter); doing it here scanned the output a second time.
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "ffi"
4
+
3
5
  return if RUBY_ENGINE == "opal"
4
6
 
5
7
  require "stringio"
@@ -40,6 +42,87 @@ module Moxml
40
42
  DIGEST_SUPPORTED =
41
43
  Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.99")
42
44
 
45
+ # Opt-in native read layer (leptris-ruby#185, bindings >= 1.9.162.6):
46
+ # TypedData node wrappers with C-bound hot reads and bulk
47
+ # children construction. Minted from #root downward; the C
48
+ # tree is shared with the binding, so writes made through the
49
+ # bridged binding nodes are visible to native reads. Installs
50
+ # without the compiled bundle (ruby-platform gem) simply stay
51
+ # on the binding path.
52
+ begin
53
+ require "leptris/xml/native_layer"
54
+ rescue LoadError, StandardError
55
+ nil
56
+ end
57
+ NATIVE_READ_LAYER =
58
+ defined?(::Leptris::XML::NativeNode) &&
59
+ Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.162.6")
60
+
61
+ # The binding's scratch-buffer growth truncates bulk children
62
+ # at 512 on 1.9.163.x (leptris-ruby#202) — the engine copies
63
+ # any buffer size correctly and its count query is exact, so
64
+ # moxml fetches with its own grow-to-fit scratch (correct on
65
+ # every binding version; the binding's own Node#children is
66
+ # not trusted for large lists).
67
+
68
+ if NATIVE_READ_LAYER
69
+ # root NativeNode -> binding document (recorded at #root mint;
70
+ # NativeNode exposes no document accessor).
71
+ @native_doc_roots = ObjectSpace::WeakMap.new
72
+
73
+ class << self
74
+ def record_native_doc(root_native, doc)
75
+ @native_doc_roots[root_native] = doc
76
+ end
77
+
78
+ # Protocol-level node equality: one engine can hand out
79
+ # two wrapper classes over the same C node (native layer +
80
+ # binding), so raw == is not identity across the seam.
81
+ def same_node?(one, other)
82
+ return true if one.equal?(other)
83
+
84
+ if NATIVE_READ_LAYER
85
+ native_one = one.is_a?(::Leptris::XML::NativeNode)
86
+ native_other = other.is_a?(::Leptris::XML::NativeNode)
87
+ if native_one || native_other
88
+ address_one = native_one ? one.address : one.c_ptr.address
89
+ address_other = native_other ? other.address : other.c_ptr.address
90
+ return address_one == address_other
91
+ end
92
+ end
93
+
94
+ one == other
95
+ end
96
+
97
+ # Binding node for any native: identity for binding nodes,
98
+ # a Node.wrap over the shared C pointer for NativeNodes.
99
+ def to_binding(node)
100
+ return node unless NATIVE_READ_LAYER &&
101
+ node.is_a?(::Leptris::XML::NativeNode)
102
+
103
+ doc = doc_for(node)
104
+ ptr = ::FFI::Pointer.new(node.address)
105
+ return ::Leptris::XML::Node.wrap(ptr, doc) if doc
106
+
107
+ ::Leptris::XML::Node.wrap(ptr, nil)
108
+ end
109
+
110
+ # Binding document for a NativeNode subtree: climb parents
111
+ # to the root (C-bound reads) and look the doc up in the
112
+ # root registry. Detached subtrees answer nil.
113
+ def doc_for(node)
114
+ current = node
115
+ current = current.parent while current&.parent
116
+ @native_doc_roots[current]
117
+ end
118
+ end
119
+ end
120
+
121
+ # Binding node for any native: identity for binding nodes, a
122
+ # Node.wrap over the shared C pointer for NativeNodes (used by
123
+ # the write/serialize/namespace paths the native layer does
124
+ # not carry).
125
+
43
126
  # Native C14N delegation probe: the engine's C14N must
44
127
  # byte-match the Ruby reference (ported from canon) on a
45
128
  # namespace-sorting + attributes + comments shape before
@@ -191,7 +274,14 @@ module Moxml
191
274
  # document-plus-attachment probe here cost more than the
192
275
  # read itself).
193
276
  def bare_attr_value(element, name)
194
- element[name.to_s]
277
+ value = element[name.to_s]
278
+ if NATIVE_READ_LAYER &&
279
+ element.is_a?(::Leptris::XML::NativeNode) &&
280
+ value.is_a?(String)
281
+ return value.dup.force_encoding(Encoding::UTF_8)
282
+ end
283
+
284
+ value
195
285
  end
196
286
 
197
287
  def native_identity_stable?
@@ -428,10 +518,12 @@ module Moxml
428
518
  end
429
519
 
430
520
  def create_native_namespace(element, prefix, uri)
521
+ element = to_binding(element) if NATIVE_READ_LAYER
431
522
  element.add_namespace_definition(prefix, uri)
432
523
  end
433
524
 
434
525
  def set_namespace(node, namespace)
526
+ node = to_binding(node) if NATIVE_READ_LAYER
435
527
  return set_attribute_namespace(node, namespace) if node.is_a?(::Leptris::XML::Attr)
436
528
 
437
529
  element = node
@@ -498,6 +590,8 @@ module Moxml
498
590
  end
499
591
 
500
592
  def namespace(node)
593
+ node = to_binding(node) if NATIVE_READ_LAYER
594
+
501
595
  # The binding resolves Attr#namespace to the declaration but
502
596
  # without the prefix; when the qualified name carries one,
503
597
  # prefer the in-scope declaration that binds it so wrappers
@@ -549,6 +643,16 @@ module Moxml
549
643
  end
550
644
 
551
645
  def node_type(node)
646
+ if NATIVE_READ_LAYER &&
647
+ node.is_a?(::Leptris::XML::NativeNode)
648
+ type = node.node_type
649
+ # The native layer spells PIs :pi; the wrapper contract
650
+ # (node_type_map) says :processing_instruction.
651
+ return :processing_instruction if type == :pi
652
+
653
+ return type
654
+ end
655
+
552
656
  # Frequency-ordered: elements and text dominate every real
553
657
  # document, and Node.wrap dispatches here once per cold
554
658
  # wrap. CDATA must precede Text (CDATA < Text in the
@@ -574,6 +678,9 @@ module Moxml
574
678
  # lightweight Attr triples answer nil.
575
679
  def digest(node, drop_ws_text: false)
576
680
  return nil unless DIGEST_SUPPORTED
681
+ if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
682
+ return to_binding(node).digest(drop_ws: drop_ws_text)
683
+ end
577
684
  return nil unless node.is_a?(::Leptris::XML::Node) &&
578
685
  !node.is_a?(::Leptris::XML::ResultAttr)
579
686
 
@@ -584,10 +691,19 @@ module Moxml
584
691
  return node.root_name if node.is_a?(::Leptris::XML::DocType)
585
692
  return node.target if node.is_a?(CustomizedLeptris::DocumentPI)
586
693
 
694
+ if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
695
+ # Native strings arrive ASCII-8BIT; PIs expose no name
696
+ # through the native layer.
697
+ return to_binding(node).target.to_s if node.node_type == :pi
698
+
699
+ return node.name.dup.force_encoding(Encoding::UTF_8)
700
+ end
701
+
587
702
  node.name.to_s.dup.force_encoding("UTF-8")
588
703
  end
589
704
 
590
705
  def set_node_name(node, name)
706
+ node = to_binding(node) if NATIVE_READ_LAYER
591
707
  case node
592
708
  when ::Leptris::XML::ProcessingInstruction, CustomizedLeptris::DocumentPI then node.target = name
593
709
  else node.name = name
@@ -595,6 +711,7 @@ module Moxml
595
711
  end
596
712
 
597
713
  def duplicate_node(node)
714
+ node = to_binding(node) if NATIVE_READ_LAYER
598
715
  case node
599
716
  when CustomizedLeptris::Declaration
600
717
  CustomizedLeptris::Declaration.new(node.version, node.encoding, node.standalone)
@@ -609,12 +726,38 @@ module Moxml
609
726
  end
610
727
  end
611
728
 
612
- def children(node)
729
+ def children(node, entity_bearing: false)
730
+ if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
731
+ # Bulk C children through moxml's own scratch (see
732
+ # NATIVE_READ_LAYER note above). Entity-marker documents
733
+ # bridge each child for the marker split — the
734
+ # TextSegment reconstruction reads binding text nodes;
735
+ # the wrapper's memo supplies the flag, so deriving it
736
+ # here costs no C parent climb per call.
737
+ natives = bulk_native_children(node)
738
+ return natives unless entity_bearing
739
+
740
+ # Entity-marker documents: bridge for the marker split
741
+ # (TextSegment reconstruction reads binding text nodes).
742
+ doc = doc_for(node)
743
+ bound_children = natives.map { |child| to_binding(child) }
744
+ if doc && attachments.get(doc, :entity_markers) == false
745
+ return bound_children
746
+ end
747
+
748
+ return split_entity_markers(bound_children, to_binding(node))
749
+ end
613
750
  # Frequency-ordered: elements dominate every walk and paid
614
751
  # ten failed compares to reach the else arm.
615
752
  case node
616
753
  when ::Leptris::XML::Element
617
754
  natives = node.children.to_a
755
+ if NATIVE_READ_LAYER && natives.size == 512
756
+ # Binding scratch truncation (leptris-ruby#202): a
757
+ # full batch is ambiguous — refetch through moxml's
758
+ # own buffers to be sure.
759
+ natives = bulk_binding_children(node)
760
+ end
618
761
  # Parse records whether the preprocessed source held any
619
762
  # entity markers, and the ER builder path flips the flag
620
763
  # when it mints one. A false flag lets traversal skip the
@@ -643,6 +786,16 @@ module Moxml
643
786
  end
644
787
 
645
788
  def parent(node)
789
+ if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
790
+ parent = node.parent
791
+ return parent if parent
792
+
793
+ doc = doc_for(node)
794
+ return doc if doc&.root && doc.root.c_ptr.address == node.address
795
+
796
+ return nil
797
+ end
798
+
646
799
  # Frequency-ordered: elements dominate navigation and paid
647
800
  # three failed compares to reach the else arm.
648
801
  case node
@@ -657,6 +810,66 @@ module Moxml
657
810
  end
658
811
  end
659
812
 
813
+ # The binding-node twin of bulk_native_children (identity via
814
+ # the binding's wrap cache).
815
+ def bulk_binding_children(node)
816
+ copied, pointers = bulk_child_buffers(node.c_ptr)
817
+ return node.children.to_a if copied.zero?
818
+
819
+ doc = node.document
820
+ Array.new(copied) do |i|
821
+ ::Leptris::XML::Node.wrap(pointers.get_pointer(i * 8), doc)
822
+ end
823
+ end
824
+
825
+ # Bulk child fetch with moxml's own grow-to-fit thread-local
826
+ # scratch: count first (exact), size the buffers to it, wrap
827
+ # each pointer as a native-layer node. Correct on every
828
+ # binding version — the binding's own scratch truncates at
829
+ # 512 on 1.9.163.x (leptris-ruby#202).
830
+ def bulk_native_children(node)
831
+ copied, pointers = bulk_child_buffers(::FFI::Pointer.new(node.address))
832
+ return [] if copied.zero?
833
+
834
+ doc = doc_for(node)
835
+ return to_binding(node).children.to_a if doc.nil?
836
+
837
+ # Share the layer's per-document identity cache (keyed by
838
+ # node address): NativeNode.from alone mints fresh objects
839
+ # per call on 1.9.163.x, which would fork moxml wrappers.
840
+ cache = doc.native_cache
841
+ Array.new(copied) do |i|
842
+ child_ptr = pointers.get_pointer(i * 8)
843
+ cache[child_ptr.address] ||=
844
+ ::Leptris::XML::NativeNode.from(doc, child_ptr)
845
+ end
846
+ end
847
+
848
+ # Shared count-then-copy fetch over moxml's grow-to-fit
849
+ # thread-local scratch — the layer above the binding's
850
+ # (buggy on 1.9.163.x) own buffers. Returns [copied, pointers].
851
+ def bulk_child_buffers(ptr)
852
+ total = ::Leptris::XML::FFI.leptris_node_children_ex(ptr, nil, nil, 0)
853
+ return [0, nil] if total.zero?
854
+
855
+ scratch = (Thread.current[:moxml_leptris_children] ||= {})
856
+ pointers = scratch[:pointers]
857
+ if pointers.nil? || pointers.size / 8 < total
858
+ pointers&.free
859
+ pointers = scratch[:pointers] =
860
+ ::FFI::MemoryPointer.new(:pointer, total)
861
+ end
862
+ kinds = scratch[:kinds]
863
+ if kinds.nil? || kinds.size / 4 < total
864
+ kinds&.free
865
+ kinds = scratch[:kinds] = ::FFI::MemoryPointer.new(:int, total)
866
+ end
867
+ copied = ::Leptris::XML::FFI.leptris_node_children_ex(
868
+ ptr, pointers, kinds, total
869
+ )
870
+ [copied, pointers]
871
+ end
872
+
660
873
  # The binding reports the root element as parentless; the
661
874
  # moxml contract roots at the document.
662
875
  def root_parent(node)
@@ -664,14 +877,25 @@ module Moxml
664
877
  end
665
878
 
666
879
  def next_sibling(node)
880
+ return node.next_sibling if NATIVE_READ_LAYER &&
881
+ node.is_a?(::Leptris::XML::NativeNode)
882
+
667
883
  node.next_sibling if node.is_a?(::Leptris::XML::Node)
668
884
  end
669
885
 
670
886
  def previous_sibling(node)
887
+ # Bridged, not the native sibling walk: the native layer
888
+ # exposes next_sibling only, and its children batch
889
+ # truncates at 512 — the predecessor beyond that would be
890
+ # wrong.
891
+ node = to_binding(node)
671
892
  node.previous_sibling if node.is_a?(::Leptris::XML::Node)
672
893
  end
673
894
 
674
895
  def document(node)
896
+ return doc_for(node) if NATIVE_READ_LAYER &&
897
+ node.is_a?(::Leptris::XML::NativeNode)
898
+
675
899
  case node
676
900
  when ::Leptris::XML::Document then node
677
901
  when CustomizedLeptris::Declaration, CustomizedLeptris::Doctype then node.parent_doc
@@ -680,7 +904,15 @@ module Moxml
680
904
  end
681
905
 
682
906
  def root(document)
683
- document.root
907
+ r = document.root
908
+ return nil if r.nil?
909
+
910
+ if NATIVE_READ_LAYER
911
+ native = document.native_node
912
+ record_native_doc(native, document)
913
+ return native
914
+ end
915
+ r
684
916
  end
685
917
 
686
918
  def line_number(node)
@@ -691,6 +923,7 @@ module Moxml
691
923
  end
692
924
 
693
925
  def attributes(element)
926
+ element = to_binding(element)
694
927
  element.attribute_nodes
695
928
  end
696
929
 
@@ -707,6 +940,7 @@ module Moxml
707
940
  end
708
941
 
709
942
  def set_attribute(element, name, value)
943
+ element = to_binding(element) if NATIVE_READ_LAYER
710
944
  element[name.to_s] = value.to_s
711
945
  end
712
946
 
@@ -726,14 +960,17 @@ module Moxml
726
960
  end
727
961
 
728
962
  def get_attribute(element, name)
963
+ element = to_binding(element)
729
964
  element.attribute_nodes.find { |attr| attr.name == name.to_s }
730
965
  end
731
966
 
732
967
  def get_attribute_value(element, name)
968
+ element = to_binding(element)
733
969
  element[name.to_s]
734
970
  end
735
971
 
736
972
  def remove_attribute(element, name)
973
+ element = to_binding(element)
737
974
  element.remove_attribute(name.to_s)
738
975
  end
739
976
 
@@ -743,6 +980,10 @@ module Moxml
743
980
  end
744
981
 
745
982
  def add_child(parent, child)
983
+ if NATIVE_READ_LAYER
984
+ parent = to_binding(parent)
985
+ child = to_binding(child)
986
+ end
746
987
  case parent
747
988
  when ::Leptris::XML::Document then add_document_child(parent, child)
748
989
  else
@@ -759,6 +1000,7 @@ module Moxml
759
1000
  end
760
1001
 
761
1002
  def add_previous_sibling(node, new_node)
1003
+ node = to_binding(node) if NATIVE_READ_LAYER
762
1004
  # A PI inserted before the root lives at document level in
763
1005
  # libleptris's model, not in the element tree.
764
1006
  if new_node.is_a?(::Leptris::XML::ProcessingInstruction) &&
@@ -770,10 +1012,12 @@ module Moxml
770
1012
  end
771
1013
 
772
1014
  def add_next_sibling(node, new_node)
1015
+ node = to_binding(node) if NATIVE_READ_LAYER
773
1016
  node.add_next_sibling(new_node)
774
1017
  end
775
1018
 
776
1019
  def remove(node)
1020
+ node = to_binding(node) if NATIVE_READ_LAYER
777
1021
  case node
778
1022
  when CustomizedLeptris::Declaration
779
1023
  remove_declaration(node.parent_doc) if node.parent_doc
@@ -793,6 +1037,10 @@ module Moxml
793
1037
  end
794
1038
 
795
1039
  def replace(node, new_node)
1040
+ if NATIVE_READ_LAYER
1041
+ node = to_binding(node)
1042
+ new_node = to_binding(new_node)
1043
+ end
796
1044
  return node.replace(new_node) if node.is_a?(::Leptris::XML::Element)
797
1045
 
798
1046
  # libleptris only offers element-anchored insertion, so a
@@ -817,10 +1065,18 @@ module Moxml
817
1065
  end
818
1066
 
819
1067
  def replace_children(node, new_children)
1068
+ if NATIVE_READ_LAYER
1069
+ node = to_binding(node)
1070
+ new_children = new_children.map { |child| to_binding(child) }
1071
+ end
820
1072
  node.children = new_children
821
1073
  end
822
1074
 
823
1075
  def text_content(node)
1076
+ if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
1077
+ return node.content.dup.force_encoding(Encoding::UTF_8)
1078
+ end
1079
+
824
1080
  # Frequency-ordered: elements dominate reads; they paid
825
1081
  # three failed compares to reach the else arm. The
826
1082
  # duplicated branch bodies are the point.
@@ -837,6 +1093,7 @@ module Moxml
837
1093
  end
838
1094
 
839
1095
  def inner_text(node)
1096
+ node = to_binding(node) if NATIVE_READ_LAYER
840
1097
  # moxml semantic: direct text children only — no descendant
841
1098
  # text (that is #text), no comments. Entity references
842
1099
  # contribute their serialized form.
@@ -851,6 +1108,7 @@ module Moxml
851
1108
  end
852
1109
 
853
1110
  def set_text_content(node, content)
1111
+ node = to_binding(node) if NATIVE_READ_LAYER
854
1112
  case node
855
1113
  when ::Leptris::XML::Document
856
1114
  node.root&.content = content.to_s
@@ -899,6 +1157,7 @@ module Moxml
899
1157
  end
900
1158
 
901
1159
  def namespace_definitions(element)
1160
+ element = to_binding(element) if NATIVE_READ_LAYER
902
1161
  element.namespace_definitions
903
1162
  end
904
1163
 
@@ -929,6 +1188,7 @@ module Moxml
929
1188
  NATIVE_GATE_CACHE = XPath::Cache.new(100)
930
1189
 
931
1190
  def xpath(node, expression, namespaces = {})
1191
+ node = to_binding(node) if NATIVE_READ_LAYER
932
1192
  native = native_xpath(node, expression, namespaces)
933
1193
  return native unless native.nil?
934
1194
 
@@ -312,7 +312,7 @@ module Moxml
312
312
  # Native documents are read-only, do nothing for them
313
313
  end
314
314
 
315
- def children(node)
315
+ def children(node, **_entity_bearing)
316
316
  native_node = unpatch_node(node)
317
317
  return [] unless native_node
318
318
 
@@ -248,7 +248,7 @@ module Moxml
248
248
  node.name = name
249
249
  end
250
250
 
251
- def children(node)
251
+ def children(node, **_entity_bearing)
252
252
  node.children
253
253
  end
254
254
 
@@ -232,7 +232,7 @@ module Moxml
232
232
  node.name = name
233
233
  end
234
234
 
235
- def children(node)
235
+ def children(node, **_entity_bearing)
236
236
  all_children = []
237
237
 
238
238
  if node.is_a?(::Oga::XML::Document)
@@ -298,7 +298,7 @@ module Moxml
298
298
  end
299
299
  end
300
300
 
301
- def children(node)
301
+ def children(node, **_entity_bearing)
302
302
  return [] unless node.is_a?(::Ox::Element) || node.is_a?(::Ox::Document)
303
303
 
304
304
  result = node.nodes || []
@@ -188,7 +188,7 @@ module Moxml
188
188
  end
189
189
  end
190
190
 
191
- def children(node)
191
+ def children(node, **_entity_bearing)
192
192
  return [] unless node.is_a?(::REXML::Parent)
193
193
 
194
194
  # Return all children preserving whitespace text nodes
data/lib/moxml/c14n.rb CHANGED
@@ -66,7 +66,7 @@ module Moxml
66
66
  return nil unless adapter == Moxml::Adapter::Leptris &&
67
67
  Moxml::Adapter::Leptris.native_c14n_byte_safe?
68
68
 
69
- node_or_xml.native.canonicalize(
69
+ Moxml::Adapter::Leptris.to_binding(node_or_xml.native).canonicalize(
70
70
  ::Leptris::XML::FFI::C14N_1_0, nil,
71
71
  mode: ::Leptris::XML::FFI::C14N_MODE_CANONICAL
72
72
  )
data/lib/moxml/node.rb CHANGED
@@ -54,7 +54,10 @@ module Moxml
54
54
 
55
55
  def children
56
56
  @children ||= begin
57
- natives = adapter.children(@native)
57
+ # The wrapper's entity memo decides the marker split; the
58
+ # adapter would otherwise re-derive it per call (a C parent
59
+ # climb on the native layer).
60
+ natives = adapter.children(@native, entity_bearing: entity_bearing?)
58
61
  natives = natives.map { adapter.patch_node(_1, @native) } if adapter.patches_children?
59
62
  NodeSet.new(natives, context, self)
60
63
  end
@@ -150,10 +153,24 @@ module Moxml
150
153
  @entity_bearing
151
154
  else
152
155
  @entity_bearing_gen = gen
153
- @entity_bearing = adapter.entity_bearing?(@native)
156
+ @entity_bearing = adapter.entity_bearing?(@native, document_native_for_markers)
154
157
  end
155
158
  end
156
159
 
160
+ # Binding document through the wrapper parent chain — pure Ruby
161
+ # attr reads; the native-layer fallback climbs C parents per
162
+ # call (the doc_for walk was the largest wrapper-layer cost on
163
+ # the consumer pipeline after the native adoption).
164
+ def document_native_for_markers
165
+ node = self
166
+ while node
167
+ return node.native if node.document?
168
+
169
+ node = node.parent_node
170
+ end
171
+ nil
172
+ end
173
+
157
174
  def xpath(expression, namespaces = {})
158
175
  result = adapter.xpath(@native, expression, namespaces)
159
176
  # Adapter contract: Array<native> | LazyNodeSet | scalar.
@@ -384,7 +401,11 @@ module Moxml
384
401
  end
385
402
 
386
403
  def ==(other)
387
- self.class == other.class && @native == other.native
404
+ # Native equality goes through the adapter: engines with a
405
+ # native read layer hand out two wrapper classes over one C
406
+ # node, and raw native == is false across that seam.
407
+ self.class == other.class &&
408
+ adapter.same_node?(@native, other.native)
388
409
  end
389
410
 
390
411
  TYPES.each do |node_type|
@@ -447,7 +468,7 @@ module Moxml
447
468
  # Internal: Set the parent node for cache invalidation tracking.
448
469
  # Called by NodeSet, Document, Element when establishing parent-child
449
470
  # relationships. Public to allow cross-class usage within Moxml internals.
450
- attr_writer :parent_node
471
+ attr_accessor :parent_node
451
472
 
452
473
  def adapter
453
474
  # A context's adapter object is fixed for its lifetime; the
data/lib/moxml/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- VERSION = "0.5.35"
4
+ VERSION = "0.5.36"
5
5
  end
@@ -43,7 +43,7 @@ RSpec.shared_examples "Moxml::NodeSet" do
43
43
  it "compares nodes" do
44
44
  xpath_results = doc.xpath("//child")
45
45
  element_children = doc.root.children.grep(Moxml::Element)
46
- expect(xpath_results.map(&:native)).to eq(element_children.map(&:native))
46
+ expect(xpath_results.to_a).to eq(element_children)
47
47
  end
48
48
  end
49
49
 
@@ -79,22 +79,20 @@ RSpec.shared_examples "xml adapter" do
79
79
  first = children[0]
80
80
  second = children[1]
81
81
 
82
- expect(described_class.next_sibling(first)).to eq(second)
83
- expect(described_class.previous_sibling(second)).to eq(first)
82
+ expect(described_class.same_node?(described_class.next_sibling(first), second)).to be(true)
83
+ expect(described_class.same_node?(described_class.previous_sibling(second), first)).to be(true)
84
84
  end
85
85
 
86
86
  it "adds child" do
87
87
  element = described_class.create_element("new")
88
88
  described_class.add_child(root, element)
89
- expect(described_class.children(root).last).to eq(element)
89
+ expect(described_class.same_node?(described_class.children(root).last, element)).to be(true)
90
90
  end
91
91
 
92
92
  it "adds text child" do
93
93
  described_class.add_child(root, "text")
94
- # a workaround for Rexml until we convert tests to work with Moxml wrappers
95
94
  last_child = described_class.children(root).last
96
- last_text = last_child.respond_to?(:text) ? last_child.text : last_child.to_s
97
- expect(last_text).to eq("text")
95
+ expect(described_class.text_content(last_child)).to eq("text")
98
96
  end
99
97
  end
100
98
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moxml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.35
4
+ version: 0.5.36
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.